Cachette devient Noxely, et gagne une page À propos (1.6.0)
Le nom change, et pas seulement à l'écran : identifiant d'application, paquet Kotlin, dépôt et dossier d'enregistrements suivent, pour qu'il ne subsiste pas deux noms selon l'endroit où l'on regarde. Android traite le nouvel identifiant comme une application distincte : une installation en 1.5.0 ne se mettra pas à jour seule et devra être remplacée. C'est le prix d'un renommage complet, assumé plutôt que contourné. Les enregistrements déjà faits ne sont pas perdus, étant dans le stockage partagé, et le scan continue d'écarter l'ancien dossier pour ne pas les compter deux fois. L'icône fournie est détournée de son fond blanc avant d'être déclinée : posée telle quelle, le masque du système aurait redécoupé un badge qui l'est déjà et laissé du blanc sur les bords. La couleur de fond est échantillonnée dans l'image, non choisie au jugé. La page À propos dit enfin ce que l'application fait, ce qu'elle garantit, avec quoi elle est construite et qui en répond. Sa version vient du manifeste et son journal du CHANGELOG du dépôt : ni l'un ni l'autre ne peut dériver. La licence n'étant pas déclarée, la pastille le signale en rouge au lieu d'en inventer une.
This commit is contained in:
1 parent
b9da43366b
commit
6f5adc687e
76 files changed
+809
-264
No files matched your search
+10
-3
@@ -8,11 +8,11 @@ plugins {
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "fr.tuxgyver.cachette"
|
||||
namespace = "fr.tuxgyver.noxely"
|
||||
compileSdk = 36
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "fr.tuxgyver.cachette"
|
||||
applicationId = "fr.tuxgyver.noxely"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
|
||||
@@ -33,7 +33,7 @@ android {
|
||||
""
|
||||
}
|
||||
buildConfigField("String", "GITEA_UPDATE_TOKEN", "\"$updateToken\"")
|
||||
buildConfigField("String", "UPDATE_API", "\"https://git.tuxtech.fr/api/v1/repos/tuxgyver/Cachette\"")
|
||||
buildConfigField("String", "UPDATE_API", "\"https://git.tuxtech.fr/api/v1/repos/tuxgyver/Noxely\"")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@@ -77,6 +77,13 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
/** Le journal affiché dans « À propos » est celui du dépôt, pas une copie qui dériverait. */
|
||||
val copyChangelog = tasks.register<Copy>("copyChangelogToAssets") {
|
||||
from(rootProject.file("CHANGELOG.md"))
|
||||
into(layout.projectDirectory.dir("src/main/assets"))
|
||||
}
|
||||
tasks.named("preBuild") { dependsOn(copyChangelog) }
|
||||
|
||||
dependencies {
|
||||
implementation(platform("androidx.compose:compose-bom:2025.10.00"))
|
||||
implementation("androidx.compose.ui:ui")
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
android:maxSdkVersion="32" />
|
||||
|
||||
<application
|
||||
android:name=".CachetteApp"
|
||||
android:name=".NoxelyApp"
|
||||
android:allowBackup="false"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
@@ -26,13 +26,13 @@
|
||||
android:roundIcon="@mipmap/ic_launcher"
|
||||
android:supportsRtl="true"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:theme="@style/Theme.Cachette">
|
||||
android:theme="@style/Theme.Noxely">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/Theme.Cachette"
|
||||
android:theme="@style/Theme.Noxely"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
@@ -50,7 +50,7 @@
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".offline.CachetteDownloadService"
|
||||
android:name=".offline.NoxelyDownloadService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync">
|
||||
<intent-filter>
|
||||
|
||||
+7
-7
@@ -1,4 +1,4 @@
|
||||
package fr.tuxgyver.cachette
|
||||
package fr.tuxgyver.noxely
|
||||
|
||||
import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
@@ -9,8 +9,8 @@ import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.content.ContextCompat
|
||||
import fr.tuxgyver.cachette.ui.CachetteRoot
|
||||
import fr.tuxgyver.cachette.ui.theme.CachetteTheme
|
||||
import fr.tuxgyver.noxely.ui.NoxelyRoot
|
||||
import fr.tuxgyver.noxely.ui.theme.NoxelyTheme
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
@@ -21,20 +21,20 @@ class MainActivity : ComponentActivity() {
|
||||
enableEdgeToEdge()
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val container = (application as CachetteApp).container
|
||||
val container = (application as NoxelyApp).container
|
||||
container.player.connect()
|
||||
askForNotificationsIfNeeded()
|
||||
|
||||
setContent {
|
||||
CachetteTheme {
|
||||
CachetteRoot(container)
|
||||
NoxelyTheme {
|
||||
NoxelyRoot(container)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
if (isFinishing) {
|
||||
(application as CachetteApp).container.player.release()
|
||||
(application as NoxelyApp).container.player.release()
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
+20
-20
@@ -1,25 +1,25 @@
|
||||
package fr.tuxgyver.cachette
|
||||
package fr.tuxgyver.noxely
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import fr.tuxgyver.cachette.library.ImportedStore
|
||||
import fr.tuxgyver.cachette.library.LibraryRepository
|
||||
import fr.tuxgyver.cachette.library.LocalMediaScanner
|
||||
import fr.tuxgyver.cachette.library.PlaylistStore
|
||||
import fr.tuxgyver.cachette.offline.DownloadTracker
|
||||
import fr.tuxgyver.cachette.offline.OfflineModule
|
||||
import fr.tuxgyver.cachette.playback.PlayerController
|
||||
import fr.tuxgyver.cachette.radio.RadioDirectory
|
||||
import fr.tuxgyver.cachette.sources.ArchiveOrgCatalog
|
||||
import fr.tuxgyver.cachette.sources.Catalog
|
||||
import fr.tuxgyver.cachette.sources.CatalogId
|
||||
import fr.tuxgyver.cachette.sources.CatalogItem
|
||||
import fr.tuxgyver.cachette.sources.WikimediaCatalog
|
||||
import fr.tuxgyver.cachette.sources.LibriVoxCatalog
|
||||
import fr.tuxgyver.cachette.sources.RssImporter
|
||||
import fr.tuxgyver.cachette.sources.UrlImporter
|
||||
import fr.tuxgyver.cachette.update.UpdateCheckWorker
|
||||
import fr.tuxgyver.cachette.update.UpdateRepository
|
||||
import fr.tuxgyver.noxely.library.ImportedStore
|
||||
import fr.tuxgyver.noxely.library.LibraryRepository
|
||||
import fr.tuxgyver.noxely.library.LocalMediaScanner
|
||||
import fr.tuxgyver.noxely.library.PlaylistStore
|
||||
import fr.tuxgyver.noxely.offline.DownloadTracker
|
||||
import fr.tuxgyver.noxely.offline.OfflineModule
|
||||
import fr.tuxgyver.noxely.playback.PlayerController
|
||||
import fr.tuxgyver.noxely.radio.RadioDirectory
|
||||
import fr.tuxgyver.noxely.sources.ArchiveOrgCatalog
|
||||
import fr.tuxgyver.noxely.sources.Catalog
|
||||
import fr.tuxgyver.noxely.sources.CatalogId
|
||||
import fr.tuxgyver.noxely.sources.CatalogItem
|
||||
import fr.tuxgyver.noxely.sources.WikimediaCatalog
|
||||
import fr.tuxgyver.noxely.sources.LibriVoxCatalog
|
||||
import fr.tuxgyver.noxely.sources.RssImporter
|
||||
import fr.tuxgyver.noxely.sources.UrlImporter
|
||||
import fr.tuxgyver.noxely.update.UpdateCheckWorker
|
||||
import fr.tuxgyver.noxely.update.UpdateRepository
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
@@ -67,7 +67,7 @@ class AppContainer(context: Context) {
|
||||
val updates = UpdateRepository(context, BuildConfig.VERSION_NAME)
|
||||
}
|
||||
|
||||
class CachetteApp : Application() {
|
||||
class NoxelyApp : Application() {
|
||||
|
||||
lateinit var container: AppContainer
|
||||
private set
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
package fr.tuxgyver.cachette.library
|
||||
package fr.tuxgyver.noxely.library
|
||||
|
||||
import android.content.Context
|
||||
import fr.tuxgyver.cachette.model.MediaEntry
|
||||
import fr.tuxgyver.noxely.model.MediaEntry
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
package fr.tuxgyver.cachette.library
|
||||
package fr.tuxgyver.noxely.library
|
||||
|
||||
import fr.tuxgyver.cachette.model.MediaEntry
|
||||
import fr.tuxgyver.noxely.model.MediaEntry
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
+9
-6
@@ -1,4 +1,4 @@
|
||||
package fr.tuxgyver.cachette.library
|
||||
package fr.tuxgyver.noxely.library
|
||||
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
@@ -8,9 +8,9 @@ import androidx.core.net.toUri
|
||||
import android.os.Build
|
||||
import android.provider.MediaStore
|
||||
import androidx.core.content.ContextCompat
|
||||
import fr.tuxgyver.cachette.model.MediaEntry
|
||||
import fr.tuxgyver.cachette.model.MediaKind
|
||||
import fr.tuxgyver.cachette.model.SourceKind
|
||||
import fr.tuxgyver.noxely.model.MediaEntry
|
||||
import fr.tuxgyver.noxely.model.MediaKind
|
||||
import fr.tuxgyver.noxely.model.SourceKind
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@@ -124,13 +124,16 @@ class LocalMediaScanner(private val context: Context) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Musique et podcasts, hors enregistrements de Cachette : ceux-ci sont déjà décrits par la
|
||||
* Musique et podcasts, hors enregistrements de Noxely : ceux-ci sont déjà décrits par la
|
||||
* bibliothèque, et les relire ici les afficherait en double.
|
||||
*/
|
||||
private fun audioSelection(): String {
|
||||
val base = "(${MediaStore.Audio.Media.IS_MUSIC} != 0 OR ${MediaStore.Audio.Media.IS_PODCAST} != 0)"
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
"$base AND ${MediaStore.Audio.Media.RELATIVE_PATH} NOT LIKE 'Music/Cachette%'"
|
||||
// L'ancien dossier reste exclu : les enregistrements d'avant le changement de nom
|
||||
// sont toujours décrits par la bibliothèque et apparaîtraient sinon en double.
|
||||
"$base AND ${MediaStore.Audio.Media.RELATIVE_PATH} NOT LIKE 'Music/Noxely%'" +
|
||||
" AND ${MediaStore.Audio.Media.RELATIVE_PATH} NOT LIKE 'Music/Cachette%'"
|
||||
} else {
|
||||
base
|
||||
}
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
package fr.tuxgyver.cachette.library
|
||||
package fr.tuxgyver.noxely.library
|
||||
|
||||
import android.content.Context
|
||||
import fr.tuxgyver.cachette.model.Playlist
|
||||
import fr.tuxgyver.noxely.model.Playlist
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package fr.tuxgyver.cachette.model
|
||||
package fr.tuxgyver.noxely.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package fr.tuxgyver.cachette.model
|
||||
package fr.tuxgyver.noxely.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
+6
-6
@@ -1,4 +1,4 @@
|
||||
package fr.tuxgyver.cachette.offline
|
||||
package fr.tuxgyver.noxely.offline
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.OptIn
|
||||
@@ -8,7 +8,7 @@ import androidx.media3.exoplayer.offline.Download
|
||||
import androidx.media3.exoplayer.offline.DownloadManager
|
||||
import androidx.media3.exoplayer.offline.DownloadRequest
|
||||
import androidx.media3.exoplayer.offline.DownloadService
|
||||
import fr.tuxgyver.cachette.model.MediaEntry
|
||||
import fr.tuxgyver.noxely.model.MediaEntry
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -110,7 +110,7 @@ class DownloadTracker(
|
||||
// Ré-ajouter la même requête relance un téléchargement en échec.
|
||||
DownloadService.sendAddDownload(
|
||||
context,
|
||||
CachetteDownloadService::class.java,
|
||||
NoxelyDownloadService::class.java,
|
||||
download.request,
|
||||
false,
|
||||
)
|
||||
@@ -129,15 +129,15 @@ class DownloadTracker(
|
||||
val request = DownloadRequest.Builder(entry.id, entry.uri.toUri())
|
||||
.setData(entry.title.toByteArray())
|
||||
.build()
|
||||
DownloadService.sendAddDownload(context, CachetteDownloadService::class.java, request, false)
|
||||
DownloadService.sendAddDownload(context, NoxelyDownloadService::class.java, request, false)
|
||||
}
|
||||
|
||||
fun remove(id: String) {
|
||||
DownloadService.sendRemoveDownload(context, CachetteDownloadService::class.java, id, false)
|
||||
DownloadService.sendRemoveDownload(context, NoxelyDownloadService::class.java, id, false)
|
||||
}
|
||||
|
||||
fun removeAll() {
|
||||
DownloadService.sendRemoveAllDownloads(context, CachetteDownloadService::class.java, false)
|
||||
DownloadService.sendRemoveAllDownloads(context, NoxelyDownloadService::class.java, false)
|
||||
}
|
||||
|
||||
private suspend fun reload() = withContext(Dispatchers.IO) {
|
||||
+5
-5
@@ -1,4 +1,4 @@
|
||||
package fr.tuxgyver.cachette.offline
|
||||
package fr.tuxgyver.noxely.offline
|
||||
|
||||
import android.app.Notification
|
||||
import androidx.annotation.OptIn
|
||||
@@ -9,15 +9,15 @@ import androidx.media3.exoplayer.offline.DownloadService
|
||||
import androidx.media3.exoplayer.scheduler.Scheduler
|
||||
import androidx.media3.exoplayer.workmanager.WorkManagerScheduler
|
||||
import androidx.media3.exoplayer.offline.DownloadNotificationHelper
|
||||
import fr.tuxgyver.cachette.CachetteApp
|
||||
import fr.tuxgyver.cachette.R
|
||||
import fr.tuxgyver.noxely.NoxelyApp
|
||||
import fr.tuxgyver.noxely.R
|
||||
|
||||
/**
|
||||
* Service de mise en cache. Il tourne en premier plan pendant les téléchargements et délègue la
|
||||
* reprise après coupure réseau ou redémarrage à WorkManager.
|
||||
*/
|
||||
@OptIn(UnstableApi::class)
|
||||
class CachetteDownloadService : DownloadService(
|
||||
class NoxelyDownloadService : DownloadService(
|
||||
NOTIFICATION_ID,
|
||||
DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL,
|
||||
CHANNEL_ID,
|
||||
@@ -29,7 +29,7 @@ class CachetteDownloadService : DownloadService(
|
||||
}
|
||||
|
||||
override fun getDownloadManager(): DownloadManager =
|
||||
(application as CachetteApp).container.offline.downloadManager
|
||||
(application as NoxelyApp).container.offline.downloadManager
|
||||
|
||||
override fun getScheduler(): Scheduler = WorkManagerScheduler(this, WORK_NAME)
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
package fr.tuxgyver.cachette.offline
|
||||
package fr.tuxgyver.noxely.offline
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.OptIn
|
||||
@@ -31,7 +31,7 @@ class OfflineModule(private val context: Context) {
|
||||
|
||||
private val httpDataSourceFactory: DataSource.Factory by lazy {
|
||||
DefaultHttpDataSource.Factory()
|
||||
.setUserAgent("Cachette/1.0 (Android)")
|
||||
.setUserAgent("Noxely/1.0 (Android)")
|
||||
.setAllowCrossProtocolRedirects(true)
|
||||
.setConnectTimeoutMs(20_000)
|
||||
.setReadTimeoutMs(30_000)
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
package fr.tuxgyver.cachette.playback
|
||||
package fr.tuxgyver.noxely.playback
|
||||
|
||||
import androidx.core.net.toUri
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import fr.tuxgyver.cachette.model.MediaEntry
|
||||
import fr.tuxgyver.noxely.model.MediaEntry
|
||||
|
||||
/**
|
||||
* Les `MediaItem` transmis par un contrôleur perdent leur URI en chemin : c'est le service qui la
|
||||
+5
-5
@@ -1,4 +1,4 @@
|
||||
package fr.tuxgyver.cachette.playback
|
||||
package fr.tuxgyver.noxely.playback
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
@@ -13,8 +13,8 @@ import androidx.media3.session.MediaSession
|
||||
import androidx.media3.session.MediaSessionService
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import fr.tuxgyver.cachette.CachetteApp
|
||||
import fr.tuxgyver.cachette.MainActivity
|
||||
import fr.tuxgyver.noxely.NoxelyApp
|
||||
import fr.tuxgyver.noxely.MainActivity
|
||||
|
||||
/** Lecture audio et vidéo en arrière-plan, avec notification et commandes système. */
|
||||
@OptIn(UnstableApi::class)
|
||||
@@ -24,7 +24,7 @@ class PlaybackService : MediaSessionService() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
val container = (application as CachetteApp).container
|
||||
val container = (application as NoxelyApp).container
|
||||
|
||||
val player = ExoPlayer.Builder(this)
|
||||
.setMediaSourceFactory(DefaultMediaSourceFactory(container.offline.playbackDataSourceFactory()))
|
||||
@@ -76,7 +76,7 @@ class PlaybackService : MediaSessionService() {
|
||||
controller: MediaSession.ControllerInfo,
|
||||
mediaItems: MutableList<MediaItem>,
|
||||
): ListenableFuture<MutableList<MediaItem>> {
|
||||
val library = (application as CachetteApp).container.library
|
||||
val library = (application as NoxelyApp).container.library
|
||||
val resolved = mediaItems.mapTo(mutableListOf()) { item ->
|
||||
library.entry(item.mediaId)?.toMediaItem() ?: item
|
||||
}
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
package fr.tuxgyver.cachette.playback
|
||||
package fr.tuxgyver.noxely.playback
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
@@ -6,8 +6,8 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.session.MediaController
|
||||
import androidx.media3.session.SessionToken
|
||||
import fr.tuxgyver.cachette.model.MediaEntry
|
||||
import fr.tuxgyver.cachette.model.MediaKind
|
||||
import fr.tuxgyver.noxely.model.MediaEntry
|
||||
import fr.tuxgyver.noxely.model.MediaKind
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
package fr.tuxgyver.cachette.radio
|
||||
package fr.tuxgyver.noxely.radio
|
||||
|
||||
import fr.tuxgyver.cachette.sources.openConnection
|
||||
import fr.tuxgyver.noxely.sources.openConnection
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.Json
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package fr.tuxgyver.cachette.radio
|
||||
package fr.tuxgyver.noxely.radio
|
||||
|
||||
/** Une station de radio et son flux direct. */
|
||||
data class RadioStation(
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package fr.tuxgyver.cachette.radio
|
||||
package fr.tuxgyver.noxely.radio
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
+11
-11
@@ -1,4 +1,4 @@
|
||||
package fr.tuxgyver.cachette.radio
|
||||
package fr.tuxgyver.noxely.radio
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
@@ -11,13 +11,13 @@ import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import androidx.core.app.NotificationCompat
|
||||
import fr.tuxgyver.cachette.CachetteApp
|
||||
import fr.tuxgyver.cachette.MainActivity
|
||||
import fr.tuxgyver.cachette.R
|
||||
import fr.tuxgyver.cachette.model.MediaEntry
|
||||
import fr.tuxgyver.cachette.model.MediaKind
|
||||
import fr.tuxgyver.cachette.model.SourceKind
|
||||
import fr.tuxgyver.cachette.sources.openConnection
|
||||
import fr.tuxgyver.noxely.NoxelyApp
|
||||
import fr.tuxgyver.noxely.MainActivity
|
||||
import fr.tuxgyver.noxely.R
|
||||
import fr.tuxgyver.noxely.model.MediaEntry
|
||||
import fr.tuxgyver.noxely.model.MediaKind
|
||||
import fr.tuxgyver.noxely.model.SourceKind
|
||||
import fr.tuxgyver.noxely.sources.openConnection
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -246,7 +246,7 @@ class RecordingService : Service() {
|
||||
val published = output.finish(title, elapsed)
|
||||
if (published != null) {
|
||||
val (uri, size) = published
|
||||
val container = (application as CachetteApp).container
|
||||
val container = (application as NoxelyApp).container
|
||||
val entry = MediaEntry(
|
||||
id = "rec:${uri.hashCode()}",
|
||||
source = SourceKind.RECORDING,
|
||||
@@ -343,8 +343,8 @@ class RecordingService : Service() {
|
||||
replace(Regex("[^\\p{L}\\p{N} _-]"), "").trim().replace(' ', '_').take(40).ifBlank { "radio" }
|
||||
|
||||
companion object {
|
||||
private const val ACTION_START = "fr.tuxgyver.cachette.RECORD_START"
|
||||
private const val ACTION_STOP = "fr.tuxgyver.cachette.RECORD_STOP"
|
||||
private const val ACTION_START = "fr.tuxgyver.noxely.RECORD_START"
|
||||
private const val ACTION_STOP = "fr.tuxgyver.noxely.RECORD_STOP"
|
||||
private const val EXTRA_ID = "id"
|
||||
private const val EXTRA_NAME = "name"
|
||||
private const val EXTRA_URL = "url"
|
||||
+4
-4
@@ -1,18 +1,18 @@
|
||||
package fr.tuxgyver.cachette.radio
|
||||
package fr.tuxgyver.noxely.radio
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.MediaStore
|
||||
import fr.tuxgyver.cachette.model.MediaEntry
|
||||
import fr.tuxgyver.noxely.model.MediaEntry
|
||||
import java.io.File
|
||||
import java.io.OutputStream
|
||||
|
||||
/**
|
||||
* Destination d'un enregistrement radio.
|
||||
*
|
||||
* À partir d'Android 10, le fichier est écrit dans `Music/Cachette` via MediaStore : il devient
|
||||
* À partir d'Android 10, le fichier est écrit dans `Music/Noxely` via MediaStore : il devient
|
||||
* visible de tous les lecteurs de l'appareil et survit à la désinstallation. En deçà, l'écriture
|
||||
* dans MediaStore réclamerait une permission de stockage que l'application n'a pas besoin de
|
||||
* demander par ailleurs, donc le fichier reste dans le dossier privé.
|
||||
@@ -100,7 +100,7 @@ class RecordingSink(
|
||||
replace(Regex("[^\\p{L}\\p{N} ._-]"), "").trim().take(80).ifBlank { "enregistrement" }
|
||||
|
||||
companion object {
|
||||
const val RELATIVE_PATH = "Music/Cachette"
|
||||
const val RELATIVE_PATH = "Music/Noxely"
|
||||
private const val MIN_USEFUL_BYTES = 16 * 1024L
|
||||
|
||||
/** Supprime un enregistrement, quel que soit l'endroit où il avait été écrit. */
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
package fr.tuxgyver.cachette.sources
|
||||
package fr.tuxgyver.noxely.sources
|
||||
|
||||
import fr.tuxgyver.cachette.model.MediaEntry
|
||||
import fr.tuxgyver.cachette.model.MediaKind
|
||||
import fr.tuxgyver.cachette.model.SourceKind
|
||||
import fr.tuxgyver.noxely.model.MediaEntry
|
||||
import fr.tuxgyver.noxely.model.MediaKind
|
||||
import fr.tuxgyver.noxely.model.SourceKind
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.Json
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
package fr.tuxgyver.cachette.sources
|
||||
package fr.tuxgyver.noxely.sources
|
||||
|
||||
import fr.tuxgyver.cachette.model.MediaEntry
|
||||
import fr.tuxgyver.noxely.model.MediaEntry
|
||||
|
||||
/** Un résultat de recherche : un album, un livre ou un morceau, pas encore un fichier. */
|
||||
data class CatalogItem(
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
package fr.tuxgyver.cachette.sources
|
||||
package fr.tuxgyver.noxely.sources
|
||||
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
@@ -19,5 +19,5 @@ internal fun openConnection(
|
||||
instanceFollowRedirects = true
|
||||
connectTimeout = 15_000
|
||||
readTimeout = readTimeoutMs
|
||||
setRequestProperty("User-Agent", "Cachette/1.1 (Android)")
|
||||
setRequestProperty("User-Agent", "Noxely/1.1 (Android)")
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
package fr.tuxgyver.cachette.sources
|
||||
package fr.tuxgyver.noxely.sources
|
||||
|
||||
import fr.tuxgyver.cachette.model.MediaEntry
|
||||
import fr.tuxgyver.noxely.model.MediaEntry
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
package fr.tuxgyver.cachette.sources
|
||||
package fr.tuxgyver.noxely.sources
|
||||
|
||||
import android.util.Xml
|
||||
import fr.tuxgyver.cachette.model.MediaEntry
|
||||
import fr.tuxgyver.cachette.model.MediaKind
|
||||
import fr.tuxgyver.cachette.model.SourceKind
|
||||
import fr.tuxgyver.noxely.model.MediaEntry
|
||||
import fr.tuxgyver.noxely.model.MediaKind
|
||||
import fr.tuxgyver.noxely.model.SourceKind
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
+4
-4
@@ -1,10 +1,10 @@
|
||||
package fr.tuxgyver.cachette.sources
|
||||
package fr.tuxgyver.noxely.sources
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import fr.tuxgyver.cachette.model.MediaEntry
|
||||
import fr.tuxgyver.cachette.model.MediaKind
|
||||
import fr.tuxgyver.cachette.model.SourceKind
|
||||
import fr.tuxgyver.noxely.model.MediaEntry
|
||||
import fr.tuxgyver.noxely.model.MediaKind
|
||||
import fr.tuxgyver.noxely.model.SourceKind
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
package fr.tuxgyver.cachette.sources
|
||||
package fr.tuxgyver.noxely.sources
|
||||
|
||||
import fr.tuxgyver.cachette.model.MediaEntry
|
||||
import fr.tuxgyver.cachette.model.MediaKind
|
||||
import fr.tuxgyver.cachette.model.SourceKind
|
||||
import fr.tuxgyver.noxely.model.MediaEntry
|
||||
import fr.tuxgyver.noxely.model.MediaKind
|
||||
import fr.tuxgyver.noxely.model.SourceKind
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.Json
|
||||
+25
-18
@@ -1,4 +1,4 @@
|
||||
package fr.tuxgyver.cachette.ui
|
||||
package fr.tuxgyver.noxely.ui
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
@@ -35,22 +35,23 @@ import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import fr.tuxgyver.cachette.AppContainer
|
||||
import fr.tuxgyver.cachette.model.MediaEntry
|
||||
import fr.tuxgyver.cachette.model.SourceKind
|
||||
import fr.tuxgyver.cachette.sources.CatalogId
|
||||
import fr.tuxgyver.cachette.ui.browse.CatalogBrowseScreen
|
||||
import fr.tuxgyver.cachette.ui.browse.CatalogItemScreen
|
||||
import fr.tuxgyver.cachette.ui.browse.SourceBrowseScreen
|
||||
import fr.tuxgyver.cachette.ui.library.LibraryScreen
|
||||
import fr.tuxgyver.cachette.ui.offline.DownloadsScreen
|
||||
import fr.tuxgyver.cachette.ui.player.MiniPlayer
|
||||
import fr.tuxgyver.cachette.ui.radio.RadioScreen
|
||||
import fr.tuxgyver.cachette.ui.playlists.AddToPlaylistSheet
|
||||
import fr.tuxgyver.cachette.ui.playlists.PlaylistDetailScreen
|
||||
import fr.tuxgyver.cachette.ui.playlists.PlaylistsScreen
|
||||
import fr.tuxgyver.cachette.ui.player.NowPlayingScreen
|
||||
import fr.tuxgyver.cachette.ui.sources.SourcesScreen
|
||||
import fr.tuxgyver.noxely.AppContainer
|
||||
import fr.tuxgyver.noxely.model.MediaEntry
|
||||
import fr.tuxgyver.noxely.model.SourceKind
|
||||
import fr.tuxgyver.noxely.sources.CatalogId
|
||||
import fr.tuxgyver.noxely.ui.about.AboutScreen
|
||||
import fr.tuxgyver.noxely.ui.browse.CatalogBrowseScreen
|
||||
import fr.tuxgyver.noxely.ui.browse.CatalogItemScreen
|
||||
import fr.tuxgyver.noxely.ui.browse.SourceBrowseScreen
|
||||
import fr.tuxgyver.noxely.ui.library.LibraryScreen
|
||||
import fr.tuxgyver.noxely.ui.offline.DownloadsScreen
|
||||
import fr.tuxgyver.noxely.ui.player.MiniPlayer
|
||||
import fr.tuxgyver.noxely.ui.radio.RadioScreen
|
||||
import fr.tuxgyver.noxely.ui.playlists.AddToPlaylistSheet
|
||||
import fr.tuxgyver.noxely.ui.playlists.PlaylistDetailScreen
|
||||
import fr.tuxgyver.noxely.ui.playlists.PlaylistsScreen
|
||||
import fr.tuxgyver.noxely.ui.player.NowPlayingScreen
|
||||
import fr.tuxgyver.noxely.ui.sources.SourcesScreen
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
private enum class Tab(val route: String, val label: String, val icon: ImageVector) {
|
||||
@@ -65,9 +66,10 @@ private const val CATALOG_ROUTE = "catalog/{catalog}"
|
||||
private const val CATALOG_ITEM_ROUTE = "catalog/{catalog}/item"
|
||||
private const val PLAYLIST_ROUTE = "playlist/{playlist}"
|
||||
private const val RADIO_ROUTE = "radio"
|
||||
private const val ABOUT_ROUTE = "about"
|
||||
|
||||
@Composable
|
||||
fun CachetteRoot(container: AppContainer) {
|
||||
fun NoxelyRoot(container: AppContainer) {
|
||||
val navController = rememberNavController()
|
||||
val backStackEntry by navController.currentBackStackEntryAsState()
|
||||
val currentRoute = backStackEntry?.destination?.route
|
||||
@@ -145,6 +147,10 @@ fun CachetteRoot(container: AppContainer) {
|
||||
)
|
||||
}
|
||||
|
||||
composable(ABOUT_ROUTE) {
|
||||
AboutScreen(onBack = navController::popBackStack, contentPadding = padding)
|
||||
}
|
||||
|
||||
composable(RADIO_ROUTE) {
|
||||
RadioScreen(
|
||||
container = container,
|
||||
@@ -174,6 +180,7 @@ fun CachetteRoot(container: AppContainer) {
|
||||
onBrowseSource = { navController.navigate("browse/${it.name}") },
|
||||
onBrowseCatalog = { navController.navigate("catalog/${it.name}") },
|
||||
onBrowseRadio = { navController.navigate(RADIO_ROUTE) },
|
||||
onOpenAbout = { navController.navigate(ABOUT_ROUTE) },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,355 @@
|
||||
package fr.tuxgyver.noxely.ui.about
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ScrollableTabRow
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import fr.tuxgyver.noxely.R
|
||||
|
||||
private val TABS = listOf(
|
||||
"⭐ Fonctions",
|
||||
"🛡️ Garanties",
|
||||
"🧱 Technique",
|
||||
"🕓 Journal",
|
||||
"👤 Auteur",
|
||||
)
|
||||
|
||||
/**
|
||||
* Page « À propos » : ce que fait l'application, ce qu'elle garantit, avec quoi elle est faite,
|
||||
* ce qui a changé et qui en répond. L'en-tête ne défile pas ; seul l'onglet courant défile.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AboutScreen(onBack: () -> Unit, contentPadding: PaddingValues) {
|
||||
var tab by rememberSaveable { mutableIntStateOf(0) }
|
||||
val listPadding = PaddingValues(bottom = contentPadding.calculateBottomPadding())
|
||||
|
||||
Column(Modifier.fillMaxSize()) {
|
||||
TopAppBar(
|
||||
title = { Text("À propos") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = "Retour")
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
Identity()
|
||||
|
||||
ScrollableTabRow(selectedTabIndex = tab, edgePadding = 16.dp) {
|
||||
TABS.forEachIndexed { index, label ->
|
||||
Tab(
|
||||
selected = tab == index,
|
||||
onClick = { tab = index },
|
||||
text = { Text(label, style = MaterialTheme.typography.labelLarge) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
when (tab) {
|
||||
0 -> FeaturesTab(listPadding)
|
||||
1 -> GuaranteesTab(listPadding)
|
||||
2 -> StackTab(listPadding)
|
||||
3 -> ChangelogTab(listPadding)
|
||||
else -> AuthorTab(listPadding)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Identity() {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(R.mipmap.ic_launcher_foreground),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.size(80.dp)
|
||||
.clip(RoundedCornerShape(20.dp)),
|
||||
)
|
||||
Spacer(Modifier.size(6.dp))
|
||||
Text(AppInfo.NAME, style = MaterialTheme.typography.headlineSmall)
|
||||
Text(
|
||||
text = AppInfo.TAGLINE,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Spacer(Modifier.size(10.dp))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Pill("v${AppInfo.VERSION}", MaterialTheme.colorScheme.primary)
|
||||
// Rouge tant qu'aucune licence n'est déclarée : une garantie qui manque doit se voir.
|
||||
Pill(
|
||||
text = AppInfo.LICENSE,
|
||||
color = if (AppInfo.LICENSE_DEFINED) MaterialTheme.colorScheme.secondary
|
||||
else MaterialTheme.colorScheme.error,
|
||||
)
|
||||
Pill("Hors ligne", Color(0xFF2E7D32))
|
||||
}
|
||||
Spacer(Modifier.size(12.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Pill(text: String, color: Color) {
|
||||
Surface(color = color.copy(alpha = 0.14f), shape = CircleShape) {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = color,
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 5.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FeaturesTab(padding: PaddingValues) {
|
||||
LazyColumn(
|
||||
contentPadding = padding,
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
) {
|
||||
items(AppInfo.FEATURES) { (title, description) ->
|
||||
Column {
|
||||
Text(title, style = MaterialTheme.typography.titleSmall)
|
||||
Text(
|
||||
text = description,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
item { Spacer(Modifier.size(16.dp)) }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GuaranteesTab(padding: PaddingValues) {
|
||||
LazyColumn(
|
||||
contentPadding = padding,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
) {
|
||||
item {
|
||||
Text(
|
||||
text = AppInfo.GUARANTEES_INTRO,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
Spacer(Modifier.size(4.dp))
|
||||
}
|
||||
items(AppInfo.GUARANTEES) { guarantee ->
|
||||
Row {
|
||||
Text("•", color = MaterialTheme.colorScheme.secondary)
|
||||
Spacer(Modifier.width(10.dp))
|
||||
Text(
|
||||
text = guarantee,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
item { Spacer(Modifier.size(16.dp)) }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StackTab(padding: PaddingValues) {
|
||||
LazyColumn(
|
||||
contentPadding = padding,
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
) {
|
||||
items(AppInfo.STACK) { (layer, tools) ->
|
||||
Row(verticalAlignment = Alignment.Top) {
|
||||
Text(
|
||||
text = layer,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
modifier = Modifier.width(116.dp),
|
||||
)
|
||||
Text(
|
||||
text = tools,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
item { Spacer(Modifier.size(16.dp)) }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChangelogTab(padding: PaddingValues) {
|
||||
val context = LocalContext.current
|
||||
val lines = remember { readChangelog(context) }
|
||||
|
||||
LazyColumn(
|
||||
contentPadding = padding,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
) {
|
||||
if (lines.isEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
text = "Journal indisponible.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
items(lines) { line ->
|
||||
when (line) {
|
||||
is ChangelogLine.Version -> {
|
||||
Spacer(Modifier.size(8.dp))
|
||||
Text(
|
||||
text = line.text,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
|
||||
is ChangelogLine.Section -> Text(
|
||||
text = line.text,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
)
|
||||
|
||||
is ChangelogLine.Bullet -> Row {
|
||||
Text("•", color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
text = emphasised(line.text),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
item { Spacer(Modifier.size(16.dp)) }
|
||||
}
|
||||
}
|
||||
|
||||
/** Le fichier n'emploie que du gras : inutile d'embarquer un analyseur Markdown complet. */
|
||||
@Composable
|
||||
private fun emphasised(text: String) = buildAnnotatedString {
|
||||
var rest = text
|
||||
while (true) {
|
||||
val open = rest.indexOf("**")
|
||||
if (open < 0) break
|
||||
val close = rest.indexOf("**", open + 2)
|
||||
if (close < 0) break
|
||||
append(rest.substring(0, open))
|
||||
withStyle(SpanStyle(fontWeight = FontWeight.SemiBold)) {
|
||||
append(rest.substring(open + 2, close))
|
||||
}
|
||||
rest = rest.substring(close + 2)
|
||||
}
|
||||
append(rest)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AuthorTab(padding: PaddingValues) {
|
||||
LazyColumn(
|
||||
contentPadding = padding,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
) {
|
||||
item {
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
shape = MaterialTheme.shapes.large,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Column(Modifier.padding(20.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.size(52.dp)
|
||||
.clip(CircleShape),
|
||||
) {
|
||||
Surface(color = MaterialTheme.colorScheme.primary, shape = CircleShape) {
|
||||
Box(Modifier.size(52.dp), contentAlignment = Alignment.Center) {
|
||||
Text(
|
||||
text = AppInfo.AUTHOR.take(1).uppercase(),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.width(16.dp))
|
||||
Column {
|
||||
Text(AppInfo.AUTHOR, style = MaterialTheme.typography.titleMedium)
|
||||
Text(
|
||||
text = AppInfo.AUTHOR_ROLE,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.size(18.dp))
|
||||
Text("Dépôt", style = MaterialTheme.typography.labelMedium)
|
||||
Text(
|
||||
text = AppInfo.REPOSITORY,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
|
||||
Spacer(Modifier.size(18.dp))
|
||||
Text(
|
||||
text = AppInfo.AUTHORSHIP,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.size(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package fr.tuxgyver.noxely.ui.about
|
||||
|
||||
import fr.tuxgyver.noxely.BuildConfig
|
||||
|
||||
/**
|
||||
* Contenu de la page « À propos », séparé de son rendu.
|
||||
*
|
||||
* Tout ce qui est écrit ici se vérifie dans le dépôt : une fonction décrite existe, une garantie
|
||||
* annoncée est tenue par du code. La version n'est pas recopiée mais lue du manifeste.
|
||||
*/
|
||||
object AppInfo {
|
||||
|
||||
const val NAME = "Noxely"
|
||||
const val TAGLINE = "Mettez la musique de côté pendant que vous avez du réseau."
|
||||
val VERSION: String get() = BuildConfig.VERSION_NAME
|
||||
|
||||
const val REPOSITORY = "https://git.tuxtech.fr/tuxgyver/Noxely"
|
||||
const val AUTHOR = "Johnny"
|
||||
const val AUTHOR_ROLE = "Conception, direction et supervision"
|
||||
const val AUTHORSHIP =
|
||||
"Application développée avec l'assistance de l'IA, sous la direction et la supervision de Johnny."
|
||||
|
||||
/** Aucune licence n'est déclarée dans le dépôt : la pastille le signale plutôt que d'en inventer une. */
|
||||
const val LICENSE = "Licence à définir"
|
||||
const val LICENSE_DEFINED = false
|
||||
|
||||
/** Ce que fait l'application, du point de vue de qui l'utilise. */
|
||||
val FEATURES = listOf(
|
||||
"Écouter sans réseau" to
|
||||
"Ce que vous mettez de côté reste lisible dans le métro, en avion ou forfait épuisé. " +
|
||||
"Audio et vidéo, avec les commandes au casque et sur l'écran verrouillé.",
|
||||
"Choisir piste par piste" to
|
||||
"Un album ou un livre affiche ses pistes avec leur durée et leur taille. Vous cochez " +
|
||||
"ce que vous emportez, et le bouton annonce le poids exact avant de lancer.",
|
||||
"Essayer avant de garder" to
|
||||
"Un appui sur une piste la joue en ligne, sans rien mettre en cache. On décide après.",
|
||||
"Puiser dans des catalogues libres" to
|
||||
"Internet Archive, Underground (netlabels et Free Music Archive), LibriVox et " +
|
||||
"Wikimedia Commons, sans compte ni clé, avec filtrage par langue.",
|
||||
"Écouter et enregistrer la radio" to
|
||||
"Les stations les plus écoutées, ce qu'elles diffusent à l'instant, et " +
|
||||
"l'enregistrement d'un direct — qui prend le nom du morceau annoncé.",
|
||||
"Guetter un morceau" to
|
||||
"Vous nommez un titre : l'enregistrement se déclenche dès que la station l'annonce, " +
|
||||
"et s'arrête à la fin du morceau.",
|
||||
"Organiser en playlists" to
|
||||
"Des listes nommées, alimentées d'un appui long, lues à la suite.",
|
||||
"Rester à jour" to
|
||||
"L'application surveille les publications du dépôt, télécharge la nouvelle version " +
|
||||
"en Wi-Fi et propose de l'installer.",
|
||||
)
|
||||
|
||||
/** Chaque promesse est tenue par du code, et dit où. */
|
||||
val GUARANTEES_INTRO =
|
||||
"Noxely est un outil personnel : rien n'y transite par un service tiers, et rien ne s'y " +
|
||||
"télécharge sans un geste de votre part."
|
||||
|
||||
val GUARANTEES = listOf(
|
||||
"Aucun compte, aucune inscription, aucune télémétrie : le projet n'embarque aucune " +
|
||||
"bibliothèque d'analyse d'audience.",
|
||||
"Les seules connexions sortantes sont celles des adresses que vous ajoutez et des " +
|
||||
"catalogues que vous interrogez.",
|
||||
"Le cache ne contient que ce que vous avez demandé à garder : rien n'y est écrit pendant " +
|
||||
"une écoute en ligne, et rien n'en est effacé sans votre geste.",
|
||||
"Le contenu mis en cache est exclu des sauvegardes dans le nuage et des transferts " +
|
||||
"d'appareil à appareil.",
|
||||
"Un téléchargement interrompu par une panne de serveur repart seul, trois fois, en " +
|
||||
"espaçant les tentatives ; sans réseau, il attend son retour.",
|
||||
"Les catalogues proposés ne présentent que des œuvres libres de droits ou du domaine " +
|
||||
"public ; les éléments en accès restreint sont écartés avant même d'être affichés.",
|
||||
"Le jeton qui sert à vérifier les mises à jour est en lecture seule et ne peut rien publier.",
|
||||
)
|
||||
|
||||
/** Couche → outils. Six lignes suffisent. */
|
||||
val STACK = listOf(
|
||||
"Interface" to "Jetpack Compose, Material 3, thèmes clair et sombre",
|
||||
"Lecture" to "Media3 / ExoPlayer, MediaSessionService, notification système",
|
||||
"Hors connexion" to "Cache Media3 non évinçant, WorkManager pour la reprise",
|
||||
"Bibliothèque" to "MediaStore pour l'appareil, fichiers JSON pour les imports",
|
||||
"Réseau" to "HttpURLConnection, kotlinx.serialization, flux ICY pour la radio",
|
||||
"Publication" to "APK signé, releases Gitea, mise à jour intégrée",
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package fr.tuxgyver.noxely.ui.about
|
||||
|
||||
import android.content.Context
|
||||
|
||||
/** Une ligne du journal, telle qu'elle sera rendue. */
|
||||
sealed interface ChangelogLine {
|
||||
data class Version(val text: String) : ChangelogLine
|
||||
data class Section(val text: String) : ChangelogLine
|
||||
data class Bullet(val text: String) : ChangelogLine
|
||||
}
|
||||
|
||||
/**
|
||||
* Lit le CHANGELOG livré dans les ressources.
|
||||
*
|
||||
* Le fichier est celui du dépôt, recopié dans les assets à la compilation : le journal affiché
|
||||
* ne peut donc pas diverger de celui que la publication utilise pour ses notes de version.
|
||||
* Seules les formes réellement employées par le fichier sont interprétées — inutile d'embarquer
|
||||
* un analyseur Markdown complet pour trois motifs. Le préambule qui précède la première version
|
||||
* est ignoré : il décrit la convention du fichier, pas l'histoire du logiciel.
|
||||
*/
|
||||
fun readChangelog(context: Context, limitVersions: Int = 6): List<ChangelogLine> {
|
||||
val raw = runCatching {
|
||||
context.assets.open("CHANGELOG.md").bufferedReader().readText()
|
||||
}.getOrNull() ?: return emptyList()
|
||||
|
||||
val lines = mutableListOf<ChangelogLine>()
|
||||
var versions = 0
|
||||
|
||||
for (line in raw.lineSequence()) {
|
||||
val trimmed = line.trim()
|
||||
when {
|
||||
trimmed.startsWith("## ") -> {
|
||||
versions++
|
||||
if (versions > limitVersions) break
|
||||
lines += ChangelogLine.Version(trimmed.removePrefix("## ").replace("[", "").replace("]", ""))
|
||||
}
|
||||
|
||||
trimmed.startsWith("### ") ->
|
||||
lines += ChangelogLine.Section(trimmed.removePrefix("### "))
|
||||
|
||||
trimmed.startsWith("- ") && versions > 0 ->
|
||||
lines += ChangelogLine.Bullet(trimmed.removePrefix("- "))
|
||||
|
||||
// Une puce peut courir sur plusieurs lignes : on la recolle à la précédente.
|
||||
trimmed.isNotEmpty() && versions > 0 && lines.lastOrNull() is ChangelogLine.Bullet -> {
|
||||
val previous = lines.removeAt(lines.lastIndex) as ChangelogLine.Bullet
|
||||
lines += ChangelogLine.Bullet("${previous.text} $trimmed")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return lines
|
||||
}
|
||||
+7
-7
@@ -1,4 +1,4 @@
|
||||
package fr.tuxgyver.cachette.ui.browse
|
||||
package fr.tuxgyver.noxely.ui.browse
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
@@ -47,12 +47,12 @@ import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import fr.tuxgyver.cachette.AppContainer
|
||||
import fr.tuxgyver.cachette.model.MediaKind
|
||||
import fr.tuxgyver.cachette.sources.CatalogId
|
||||
import fr.tuxgyver.cachette.sources.CatalogItem
|
||||
import fr.tuxgyver.cachette.ui.components.Artwork
|
||||
import fr.tuxgyver.cachette.ui.components.formatBytes
|
||||
import fr.tuxgyver.noxely.AppContainer
|
||||
import fr.tuxgyver.noxely.model.MediaKind
|
||||
import fr.tuxgyver.noxely.sources.CatalogId
|
||||
import fr.tuxgyver.noxely.sources.CatalogItem
|
||||
import fr.tuxgyver.noxely.ui.components.Artwork
|
||||
import fr.tuxgyver.noxely.ui.components.formatBytes
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
+6
-6
@@ -1,4 +1,4 @@
|
||||
package fr.tuxgyver.cachette.ui.browse
|
||||
package fr.tuxgyver.noxely.ui.browse
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -40,11 +40,11 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import fr.tuxgyver.cachette.AppContainer
|
||||
import fr.tuxgyver.cachette.model.MediaEntry
|
||||
import fr.tuxgyver.cachette.sources.CatalogId
|
||||
import fr.tuxgyver.cachette.ui.components.formatBytes
|
||||
import fr.tuxgyver.cachette.ui.components.formatDuration
|
||||
import fr.tuxgyver.noxely.AppContainer
|
||||
import fr.tuxgyver.noxely.model.MediaEntry
|
||||
import fr.tuxgyver.noxely.sources.CatalogId
|
||||
import fr.tuxgyver.noxely.ui.components.formatBytes
|
||||
import fr.tuxgyver.noxely.ui.components.formatDuration
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package fr.tuxgyver.cachette.ui.browse
|
||||
package fr.tuxgyver.noxely.ui.browse
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.slideInVertically
|
||||
+5
-5
@@ -1,4 +1,4 @@
|
||||
package fr.tuxgyver.cachette.ui.browse
|
||||
package fr.tuxgyver.noxely.ui.browse
|
||||
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -35,10 +35,10 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import fr.tuxgyver.cachette.AppContainer
|
||||
import fr.tuxgyver.cachette.model.MediaEntry
|
||||
import fr.tuxgyver.cachette.model.SourceKind
|
||||
import fr.tuxgyver.cachette.ui.components.MediaRow
|
||||
import fr.tuxgyver.noxely.AppContainer
|
||||
import fr.tuxgyver.noxely.model.MediaEntry
|
||||
import fr.tuxgyver.noxely.model.SourceKind
|
||||
import fr.tuxgyver.noxely.ui.components.MediaRow
|
||||
|
||||
/**
|
||||
* Parcours d'une source déjà présente dans la bibliothèque : on y choisit, un par un ou en lot,
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
package fr.tuxgyver.cachette.ui.components
|
||||
package fr.tuxgyver.noxely.ui.components
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build
|
||||
@@ -28,7 +28,7 @@ import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.core.net.toUri
|
||||
import coil.compose.AsyncImage
|
||||
import fr.tuxgyver.cachette.model.MediaKind
|
||||
import fr.tuxgyver.noxely.model.MediaKind
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package fr.tuxgyver.cachette.ui.components
|
||||
package fr.tuxgyver.noxely.ui.components
|
||||
|
||||
import java.util.Locale
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
package fr.tuxgyver.cachette.ui.components
|
||||
package fr.tuxgyver.noxely.ui.components
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
@@ -32,8 +32,8 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import fr.tuxgyver.cachette.model.MediaEntry
|
||||
import fr.tuxgyver.cachette.offline.OfflineState
|
||||
import fr.tuxgyver.noxely.model.MediaEntry
|
||||
import fr.tuxgyver.noxely.offline.OfflineState
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
+8
-8
@@ -1,4 +1,4 @@
|
||||
package fr.tuxgyver.cachette.ui.library
|
||||
package fr.tuxgyver.noxely.ui.library
|
||||
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
@@ -40,12 +40,12 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import fr.tuxgyver.cachette.AppContainer
|
||||
import fr.tuxgyver.cachette.library.LocalMediaScanner
|
||||
import fr.tuxgyver.cachette.model.MediaEntry
|
||||
import fr.tuxgyver.cachette.model.MediaKind
|
||||
import fr.tuxgyver.cachette.offline.OfflineState
|
||||
import fr.tuxgyver.cachette.ui.components.MediaRow
|
||||
import fr.tuxgyver.noxely.AppContainer
|
||||
import fr.tuxgyver.noxely.library.LocalMediaScanner
|
||||
import fr.tuxgyver.noxely.model.MediaEntry
|
||||
import fr.tuxgyver.noxely.model.MediaKind
|
||||
import fr.tuxgyver.noxely.offline.OfflineState
|
||||
import fr.tuxgyver.noxely.ui.components.MediaRow
|
||||
|
||||
private enum class LibraryFilter(val label: String) {
|
||||
ALL("Tout"),
|
||||
@@ -201,7 +201,7 @@ private fun PermissionCard(onGrant: () -> Unit) {
|
||||
Text("Accéder à vos médias", style = MaterialTheme.typography.titleMedium)
|
||||
Spacer(Modifier.size(6.dp))
|
||||
Text(
|
||||
"Cachette lit la musique et les vidéos déjà présentes sur l'appareil. Rien n'est envoyé ailleurs.",
|
||||
"Noxely lit la musique et les vidéos déjà présentes sur l'appareil. Rien n'est envoyé ailleurs.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
Spacer(Modifier.size(14.dp))
|
||||
+7
-7
@@ -1,4 +1,4 @@
|
||||
package fr.tuxgyver.cachette.ui.offline
|
||||
package fr.tuxgyver.noxely.ui.offline
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -37,12 +37,12 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import fr.tuxgyver.cachette.AppContainer
|
||||
import fr.tuxgyver.cachette.model.MediaEntry
|
||||
import fr.tuxgyver.cachette.model.SourceKind
|
||||
import fr.tuxgyver.cachette.radio.RecordingSink
|
||||
import fr.tuxgyver.cachette.ui.components.MediaRow
|
||||
import fr.tuxgyver.cachette.ui.components.formatBytes
|
||||
import fr.tuxgyver.noxely.AppContainer
|
||||
import fr.tuxgyver.noxely.model.MediaEntry
|
||||
import fr.tuxgyver.noxely.model.SourceKind
|
||||
import fr.tuxgyver.noxely.radio.RecordingSink
|
||||
import fr.tuxgyver.noxely.ui.components.MediaRow
|
||||
import fr.tuxgyver.noxely.ui.components.formatBytes
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
package fr.tuxgyver.cachette.ui.player
|
||||
package fr.tuxgyver.noxely.ui.player
|
||||
|
||||
import androidx.compose.foundation.basicMarquee
|
||||
import androidx.compose.foundation.clickable
|
||||
@@ -26,8 +26,8 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import fr.tuxgyver.cachette.playback.PlayerUiState
|
||||
import fr.tuxgyver.cachette.ui.components.Artwork
|
||||
import fr.tuxgyver.noxely.playback.PlayerUiState
|
||||
import fr.tuxgyver.noxely.ui.components.Artwork
|
||||
|
||||
@Composable
|
||||
fun MiniPlayer(
|
||||
+7
-7
@@ -1,4 +1,4 @@
|
||||
package fr.tuxgyver.cachette.ui.player
|
||||
package fr.tuxgyver.noxely.ui.player
|
||||
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.compose.foundation.background
|
||||
@@ -54,12 +54,12 @@ import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.ui.PlayerView
|
||||
import fr.tuxgyver.cachette.model.MediaEntry
|
||||
import fr.tuxgyver.cachette.model.MediaKind
|
||||
import fr.tuxgyver.cachette.offline.OfflineState
|
||||
import fr.tuxgyver.cachette.playback.PlayerUiState
|
||||
import fr.tuxgyver.cachette.ui.components.Artwork
|
||||
import fr.tuxgyver.cachette.ui.components.formatDuration
|
||||
import fr.tuxgyver.noxely.model.MediaEntry
|
||||
import fr.tuxgyver.noxely.model.MediaKind
|
||||
import fr.tuxgyver.noxely.offline.OfflineState
|
||||
import fr.tuxgyver.noxely.playback.PlayerUiState
|
||||
import fr.tuxgyver.noxely.ui.components.Artwork
|
||||
import fr.tuxgyver.noxely.ui.components.formatDuration
|
||||
|
||||
@Composable
|
||||
fun NowPlayingScreen(
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
package fr.tuxgyver.cachette.ui.playlists
|
||||
package fr.tuxgyver.noxely.ui.playlists
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -35,8 +35,8 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import fr.tuxgyver.cachette.AppContainer
|
||||
import fr.tuxgyver.cachette.model.MediaEntry
|
||||
import fr.tuxgyver.noxely.AppContainer
|
||||
import fr.tuxgyver.noxely.model.MediaEntry
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/** Choix de la playlist d'accueil, avec création à la volée si aucune ne convient. */
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
package fr.tuxgyver.cachette.ui.playlists
|
||||
package fr.tuxgyver.noxely.ui.playlists
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -33,9 +33,9 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import fr.tuxgyver.cachette.AppContainer
|
||||
import fr.tuxgyver.cachette.model.MediaEntry
|
||||
import fr.tuxgyver.cachette.ui.components.MediaRow
|
||||
import fr.tuxgyver.noxely.AppContainer
|
||||
import fr.tuxgyver.noxely.model.MediaEntry
|
||||
import fr.tuxgyver.noxely.ui.components.MediaRow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/** Le contenu d'une playlist, dans l'ordre d'ajout. */
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
package fr.tuxgyver.cachette.ui.playlists
|
||||
package fr.tuxgyver.noxely.ui.playlists
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -39,7 +39,7 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import fr.tuxgyver.cachette.AppContainer
|
||||
import fr.tuxgyver.noxely.AppContainer
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
+14
-14
@@ -1,4 +1,4 @@
|
||||
package fr.tuxgyver.cachette.ui.radio
|
||||
package fr.tuxgyver.noxely.ui.radio
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
@@ -57,17 +57,17 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import fr.tuxgyver.cachette.AppContainer
|
||||
import fr.tuxgyver.cachette.model.MediaEntry
|
||||
import fr.tuxgyver.cachette.model.MediaKind
|
||||
import fr.tuxgyver.cachette.model.SourceKind
|
||||
import fr.tuxgyver.cachette.radio.RADIO_GENRES
|
||||
import fr.tuxgyver.cachette.radio.RadioStation
|
||||
import fr.tuxgyver.cachette.radio.Recorder
|
||||
import fr.tuxgyver.cachette.radio.RecordingService
|
||||
import fr.tuxgyver.cachette.ui.components.Artwork
|
||||
import fr.tuxgyver.cachette.ui.components.formatBytes
|
||||
import fr.tuxgyver.cachette.ui.components.formatDuration
|
||||
import fr.tuxgyver.noxely.AppContainer
|
||||
import fr.tuxgyver.noxely.model.MediaEntry
|
||||
import fr.tuxgyver.noxely.model.MediaKind
|
||||
import fr.tuxgyver.noxely.model.SourceKind
|
||||
import fr.tuxgyver.noxely.radio.RADIO_GENRES
|
||||
import fr.tuxgyver.noxely.radio.RadioStation
|
||||
import fr.tuxgyver.noxely.radio.Recorder
|
||||
import fr.tuxgyver.noxely.radio.RecordingService
|
||||
import fr.tuxgyver.noxely.ui.components.Artwork
|
||||
import fr.tuxgyver.noxely.ui.components.formatBytes
|
||||
import fr.tuxgyver.noxely.ui.components.formatDuration
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
@@ -258,7 +258,7 @@ fun RadioScreen(
|
||||
onDismiss = { scheduleFor = null },
|
||||
onConfirm = { wanted ->
|
||||
RecordingService.start(context, station, whenTitleContains = wanted)
|
||||
onMessage("Cachette guette « $wanted » sur ${station.name}")
|
||||
onMessage("Noxely guette « $wanted » sur ${station.name}")
|
||||
scheduleFor = null
|
||||
},
|
||||
)
|
||||
@@ -293,7 +293,7 @@ private fun ScheduleDialog(
|
||||
text = {
|
||||
Column {
|
||||
Text(
|
||||
text = "Cachette écoutera ${station.name} et déclenchera l'enregistrement dès " +
|
||||
text = "Noxely écoutera ${station.name} et déclenchera l'enregistrement dès " +
|
||||
"qu'un titre contenant ce texte sera annoncé, puis s'arrêtera à la fin du morceau.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
+18
-4
@@ -1,4 +1,4 @@
|
||||
package fr.tuxgyver.cachette.ui.sources
|
||||
package fr.tuxgyver.noxely.ui.sources
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -17,6 +17,7 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight
|
||||
import androidx.compose.material.icons.automirrored.rounded.MenuBook
|
||||
import androidx.compose.material.icons.rounded.Headphones
|
||||
import androidx.compose.material.icons.rounded.Info
|
||||
import androidx.compose.material.icons.rounded.LibraryMusic
|
||||
import androidx.compose.material.icons.rounded.Public
|
||||
import androidx.compose.material.icons.rounded.Radio
|
||||
@@ -49,9 +50,9 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import fr.tuxgyver.cachette.AppContainer
|
||||
import fr.tuxgyver.cachette.model.SourceKind
|
||||
import fr.tuxgyver.cachette.sources.CatalogId
|
||||
import fr.tuxgyver.noxely.AppContainer
|
||||
import fr.tuxgyver.noxely.model.SourceKind
|
||||
import fr.tuxgyver.noxely.sources.CatalogId
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
@@ -62,6 +63,7 @@ fun SourcesScreen(
|
||||
onBrowseSource: (SourceKind) -> Unit,
|
||||
onBrowseCatalog: (CatalogId) -> Unit,
|
||||
onBrowseRadio: () -> Unit,
|
||||
onOpenAbout: () -> Unit,
|
||||
) {
|
||||
val entries by container.library.entries.collectAsStateWithLifecycle()
|
||||
var sheet by remember { mutableStateOf<ImportKind?>(null) }
|
||||
@@ -176,6 +178,18 @@ fun SourcesScreen(
|
||||
UpdateCard(container)
|
||||
}
|
||||
|
||||
item {
|
||||
SourceCard(
|
||||
icon = Icons.Rounded.Info,
|
||||
title = "À propos",
|
||||
status = "Version ${fr.tuxgyver.noxely.BuildConfig.VERSION_NAME}",
|
||||
description = "Ce que fait Noxely, ce qu'elle garantit, avec quoi elle est faite et qui en répond.",
|
||||
actionLabel = "Ouvrir",
|
||||
onAction = onOpenAbout,
|
||||
onOpen = onOpenAbout,
|
||||
)
|
||||
}
|
||||
|
||||
unavailableSourceCards(
|
||||
listOf(
|
||||
UnavailableSource(
|
||||
+6
-6
@@ -1,4 +1,4 @@
|
||||
package fr.tuxgyver.cachette.ui.sources
|
||||
package fr.tuxgyver.noxely.ui.sources
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -24,10 +24,10 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import fr.tuxgyver.cachette.AppContainer
|
||||
import fr.tuxgyver.cachette.BuildConfig
|
||||
import fr.tuxgyver.cachette.update.UpdateInfo
|
||||
import fr.tuxgyver.cachette.update.UpdateNotifier
|
||||
import fr.tuxgyver.noxely.AppContainer
|
||||
import fr.tuxgyver.noxely.BuildConfig
|
||||
import fr.tuxgyver.noxely.update.UpdateInfo
|
||||
import fr.tuxgyver.noxely.update.UpdateNotifier
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
|
||||
@@ -82,7 +82,7 @@ fun UpdateCard(container: AppContainer) {
|
||||
Text(
|
||||
text = when (current) {
|
||||
UpdateUiState.Idle ->
|
||||
"Cachette regarde chaque jour si une nouvelle version a été publiée, et la télécharge en Wi-Fi."
|
||||
"Noxely regarde chaque jour si une nouvelle version a été publiée, et la télécharge en Wi-Fi."
|
||||
UpdateUiState.Checking -> "Vérification en cours…"
|
||||
UpdateUiState.UpToDate -> "Vous avez déjà la dernière version."
|
||||
is UpdateUiState.Available -> "Version ${current.info.version} disponible."
|
||||
+6
-6
@@ -1,4 +1,4 @@
|
||||
package fr.tuxgyver.cachette.ui.theme
|
||||
package fr.tuxgyver.noxely.ui.theme
|
||||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
@@ -57,7 +57,7 @@ private val DarkColors = darkColorScheme(
|
||||
outlineVariant = Color(0xFF3A3048),
|
||||
)
|
||||
|
||||
private val CachetteShapes = Shapes(
|
||||
private val NoxelyShapes = Shapes(
|
||||
extraSmall = RoundedCornerShape(8.dp),
|
||||
small = RoundedCornerShape(14.dp),
|
||||
medium = RoundedCornerShape(20.dp),
|
||||
@@ -65,7 +65,7 @@ private val CachetteShapes = Shapes(
|
||||
extraLarge = RoundedCornerShape(36.dp),
|
||||
)
|
||||
|
||||
private val CachetteTypography = Typography().run {
|
||||
private val NoxelyTypography = Typography().run {
|
||||
copy(
|
||||
displaySmall = displaySmall.copy(fontWeight = FontWeight.Bold, letterSpacing = (-0.5).sp),
|
||||
headlineMedium = headlineMedium.copy(fontWeight = FontWeight.Bold, letterSpacing = (-0.4).sp),
|
||||
@@ -77,7 +77,7 @@ private val CachetteTypography = Typography().run {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CachetteTheme(
|
||||
fun NoxelyTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
@@ -87,8 +87,8 @@ fun CachetteTheme(
|
||||
val colors = if (darkTheme) DarkColors else LightColors
|
||||
MaterialTheme(
|
||||
colorScheme = colors,
|
||||
shapes = CachetteShapes,
|
||||
typography = CachetteTypography,
|
||||
shapes = NoxelyShapes,
|
||||
typography = NoxelyTypography,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
package fr.tuxgyver.cachette.update
|
||||
package fr.tuxgyver.noxely.update
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.Constraints
|
||||
@@ -8,7 +8,7 @@ import androidx.work.NetworkType
|
||||
import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
import fr.tuxgyver.cachette.CachetteApp
|
||||
import fr.tuxgyver.noxely.NoxelyApp
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
@@ -21,7 +21,7 @@ class UpdateCheckWorker(
|
||||
) : CoroutineWorker(context, params) {
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
val repository = (applicationContext as CachetteApp).container.updates
|
||||
val repository = (applicationContext as NoxelyApp).container.updates
|
||||
val update = runCatching { repository.checkForUpdate() }.getOrNull()
|
||||
?: return Result.success()
|
||||
val apk = runCatching { repository.downloadApk(update) }.getOrNull()
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
package fr.tuxgyver.cachette.update
|
||||
package fr.tuxgyver.noxely.update
|
||||
|
||||
import android.Manifest
|
||||
import android.app.NotificationChannel
|
||||
@@ -12,7 +12,7 @@ import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import fr.tuxgyver.cachette.R
|
||||
import fr.tuxgyver.noxely.R
|
||||
import java.io.File
|
||||
|
||||
/** Prévient que la mise à jour est téléchargée et ouvre l'installateur système d'un appui. */
|
||||
@@ -38,7 +38,7 @@ object UpdateNotifier {
|
||||
|
||||
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_offline)
|
||||
.setContentTitle("Cachette $version est prête")
|
||||
.setContentTitle("Noxely $version est prête")
|
||||
.setContentText("Appuyez pour installer la mise à jour.")
|
||||
.setContentIntent(pending)
|
||||
.setAutoCancel(true)
|
||||
@@ -73,7 +73,7 @@ object UpdateNotifier {
|
||||
CHANNEL_ID,
|
||||
"Mises à jour",
|
||||
NotificationManager.IMPORTANCE_DEFAULT,
|
||||
).apply { description = "Disponibilité d'une nouvelle version de Cachette" }
|
||||
).apply { description = "Disponibilité d'une nouvelle version de Noxely" }
|
||||
context.getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
package fr.tuxgyver.cachette.update
|
||||
package fr.tuxgyver.noxely.update
|
||||
|
||||
import android.content.Context
|
||||
import fr.tuxgyver.cachette.BuildConfig
|
||||
import fr.tuxgyver.noxely.BuildConfig
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.Json
|
||||
@@ -103,7 +103,7 @@ class UpdateRepository(
|
||||
instanceFollowRedirects = true
|
||||
connectTimeout = 15_000
|
||||
readTimeout = 60_000
|
||||
setRequestProperty("User-Agent", "Cachette/$currentVersion (Android)")
|
||||
setRequestProperty("User-Agent", "Noxely/$currentVersion (Android)")
|
||||
setRequestProperty("Authorization", "token ${BuildConfig.GITEA_UPDATE_TOKEN}")
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<!-- Note de musique posée sur une flèche descendante : « mis de côté, hors ligne ». -->
|
||||
<path
|
||||
android:fillColor="#FFB79CFF"
|
||||
android:pathData="M64,28 L64,58.5 a10,10 0 1,1 -6,-9.2 L58,37.5 L44,40.6 L44,64 a10,10 0 1,1 -6,-9.2 L38,35.8 L64,28 Z" />
|
||||
<path
|
||||
android:fillColor="#FF7CE7D6"
|
||||
android:pathData="M54,70 L54,80 L47,80 L54,88 L61,80 L54,80 Z" />
|
||||
</vector>
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 80 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 134 KiB |
@@ -1,5 +1,5 @@
|
||||
<resources>
|
||||
<style name="Theme.Cachette" parent="android:Theme.Material.NoActionBar">
|
||||
<style name="Theme.Noxely" parent="android:Theme.Material.NoActionBar">
|
||||
<item name="android:windowBackground">@color/splash_background</item>
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<resources>
|
||||
<color name="splash_background">#FFF7F2FA</color>
|
||||
<color name="ic_launcher_background">#FF1B1033</color>
|
||||
<color name="ic_launcher_background">#FF0E9CC7</color>
|
||||
</resources>
|
||||
@@ -1,4 +1,4 @@
|
||||
<resources>
|
||||
<string name="app_name">Cachette</string>
|
||||
<string name="app_name">Noxely</string>
|
||||
<string name="download_channel_name">Téléchargements</string>
|
||||
</resources>
|
||||
@@ -1,5 +1,5 @@
|
||||
<resources>
|
||||
<style name="Theme.Cachette" parent="android:Theme.Material.Light.NoActionBar">
|
||||
<style name="Theme.Noxely" parent="android:Theme.Material.Light.NoActionBar">
|
||||
<item name="android:windowBackground">@color/splash_background</item>
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
|
||||
Reference in new issue
Block a user