Onglet Radio sur le serveur, bouton de synchronisation sur le mobile (2.5.0)
Le serveur gagne un onglet Radio : les stations de l'annuaire, écoutables en direct dans le navigateur ou enregistrables — jusqu'à l'arrêt ou pour une durée choisie —, puis réécoutables depuis l'interface. Quatre enregistrements en parallèle au plus ; si le flux tombe, il est rouvert et le fichier continue. Les enregistrements rejoignent la bibliothèque : le mobile les reçoit. Un redémarrage du service clôt proprement les captures en cours, et celles restées « en cours » après un plantage sont récupérées au démarrage suivant. La copie de flux sans blocs ICY, propre au guet, est extraite pour être partagée. Côté mobile, la synchronisation manuelle n'était qu'un petit lien dans l'onglet Connexion : elle a maintenant un vrai bouton dans l'onglet Synchronisation. Deux passages ne se chevauchent plus, et une erreur inattendue s'affiche au lieu de fermer l'application. Le code Kotlin n'a pas été compilé sur cette machine (pas de JDK) : il a été relu, mais reste à construire. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N6pmSE168yjyf8vgV8mZx6
This commit is contained in:
1 parent
7810bef8a3
commit
5b5e90247f
15 files changed
+593
-48
No files matched your search
@@ -21,6 +21,7 @@ import fr.tuxgyver.noxely.model.SourceKind
|
||||
import fr.tuxgyver.noxely.radio.RecordingSink
|
||||
import fr.tuxgyver.noxely.sources.openConnection
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
@@ -95,7 +96,21 @@ class SyncClient(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun sync(): SyncResult = withContext(Dispatchers.IO) {
|
||||
/** Un seul passage à la fois : la synchronisation planifiée et le bouton ne se chevauchent pas. */
|
||||
private val running = Mutex()
|
||||
|
||||
/**
|
||||
* Récupère ce qui est prêt sur le serveur.
|
||||
*
|
||||
* Deux passages simultanés téléchargeraient les mêmes fichiers deux fois : le second est donc
|
||||
* refusé, avec un message clair plutôt qu'un doublon dans la bibliothèque.
|
||||
*/
|
||||
suspend fun sync(): SyncResult {
|
||||
if (!running.tryLock()) return SyncResult(0, 0, "Synchronisation déjà en cours")
|
||||
return try { runSync() } finally { running.unlock() }
|
||||
}
|
||||
|
||||
private suspend fun runSync(): SyncResult = withContext(Dispatchers.IO) {
|
||||
val config = settings.config.value
|
||||
if (!config.isConfigured) return@withContext SyncResult(0, 0, "Serveur non configuré")
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import fr.tuxgyver.noxely.AppContainer
|
||||
import fr.tuxgyver.noxely.BuildConfig
|
||||
import fr.tuxgyver.noxely.sync.SyncResult
|
||||
import fr.tuxgyver.noxely.ui.sources.UpdateCard
|
||||
import kotlinx.coroutines.launch
|
||||
import java.text.SimpleDateFormat
|
||||
@@ -84,6 +85,19 @@ fun ServerScreen(
|
||||
var busy by remember { mutableStateOf(false) }
|
||||
var probe by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
// Le bouton de l'onglet Synchronisation et celui de Connexion font exactement la même chose.
|
||||
val runSync: () -> Unit = {
|
||||
if (!busy) {
|
||||
busy = true
|
||||
scope.launch {
|
||||
val result = runCatching { container.sync.sync() }
|
||||
.getOrElse { SyncResult(0, 0, it.message ?: "Synchronisation impossible") }
|
||||
busy = false
|
||||
onMessage(result.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(config.baseUrl, config.token) {
|
||||
if (url.isBlank()) url = config.baseUrl
|
||||
if (token.isBlank()) token = config.token
|
||||
@@ -191,14 +205,7 @@ fun ServerScreen(
|
||||
if (busy) {
|
||||
CircularProgressIndicator(strokeWidth = 2.dp, modifier = Modifier.size(22.dp))
|
||||
} else if (config.isConfigured) {
|
||||
TextButton(onClick = {
|
||||
busy = true
|
||||
scope.launch {
|
||||
val result = container.sync.sync()
|
||||
busy = false
|
||||
onMessage(result.message)
|
||||
}
|
||||
}) { Text("Synchroniser") }
|
||||
TextButton(onClick = runSync) { Text("Synchroniser") }
|
||||
}
|
||||
}
|
||||
probe?.let {
|
||||
@@ -249,6 +256,38 @@ fun ServerScreen(
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Column {
|
||||
Button(
|
||||
onClick = runSync,
|
||||
enabled = config.isConfigured && !busy,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
if (busy) {
|
||||
CircularProgressIndicator(
|
||||
strokeWidth = 2.dp,
|
||||
modifier = Modifier.size(18.dp),
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
)
|
||||
Spacer(Modifier.width(10.dp))
|
||||
Text("Synchronisation en cours…")
|
||||
} else {
|
||||
Icon(Icons.Rounded.CloudSync, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||
Spacer(Modifier.width(10.dp))
|
||||
Text("Synchroniser maintenant")
|
||||
}
|
||||
}
|
||||
if (!config.isConfigured) {
|
||||
Text(
|
||||
text = "Renseignez d'abord le serveur dans l'onglet Connexion.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = 6.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
else -> {
|
||||
|
||||
Reference in new issue
Block a user