mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-26 22:42:17 +00:00
feat(simkl): explain sync cadence
This commit is contained in:
parent
b4eba6e0c5
commit
b817eb5b14
5 changed files with 138 additions and 1 deletions
|
|
@ -1139,6 +1139,12 @@
|
|||
<string name="settings_simkl_authorization_expired">The Simkl sign-in request expired. Start again.</string>
|
||||
<string name="settings_simkl_sign_in_failed">Could not complete Simkl sign in. Please try again.</string>
|
||||
<string name="settings_simkl_sync_now">Sync now</string>
|
||||
<string name="settings_simkl_sync_info_action">How syncing works</string>
|
||||
<string name="settings_simkl_sync_info_title">How Simkl syncing works</string>
|
||||
<string name="settings_simkl_sync_info_description">Nuvio automatically checks Simkl at most once every %1$d minutes. Changes made through another app or the Simkl website may take that long to appear.</string>
|
||||
<string name="settings_simkl_sync_info_activity">Each refresh first checks whether Simkl reports any changes and downloads only the updates. This follows Simkl’s API rules, reduces unnecessary data use, and helps keep the service stable.</string>
|
||||
<string name="settings_simkl_sync_info_manual">Changes made in Nuvio are sent to Simkl immediately. Use Sync now whenever you want Nuvio to check for remote changes sooner.</string>
|
||||
<string name="settings_simkl_sync_info_docs">Read the Simkl sync guide</string>
|
||||
<string name="settings_simkl_authorization_revoked">Simkl authorization expired or was revoked. Connect again.</string>
|
||||
<string name="tracking_source_simkl">Simkl</string>
|
||||
<string name="tracking_library_source_simkl_selected">Simkl watchlist selected</string>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,106 @@
|
|||
package com.nuvio.app.features.settings
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.BasicAlertDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
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.platform.LocalUriHandler
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.nuvio.app.features.simkl.SIMKL_AUTOMATIC_REFRESH_INTERVAL_MINUTES
|
||||
import nuvio.composeapp.generated.resources.Res
|
||||
import nuvio.composeapp.generated.resources.action_close
|
||||
import nuvio.composeapp.generated.resources.settings_simkl_sync_info_activity
|
||||
import nuvio.composeapp.generated.resources.settings_simkl_sync_info_description
|
||||
import nuvio.composeapp.generated.resources.settings_simkl_sync_info_docs
|
||||
import nuvio.composeapp.generated.resources.settings_simkl_sync_info_manual
|
||||
import nuvio.composeapp.generated.resources.settings_simkl_sync_info_title
|
||||
import nuvio.composeapp.generated.resources.settings_trakt_failed_open_browser
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
internal fun SimklSyncInfoDialog(onDismiss: () -> Unit) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
var browserError by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
BasicAlertDialog(onDismissRequest = onDismiss) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(Res.string.settings_simkl_sync_info_title),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(
|
||||
Res.string.settings_simkl_sync_info_description,
|
||||
SIMKL_AUTOMATIC_REFRESH_INTERVAL_MINUTES,
|
||||
),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(Res.string.settings_simkl_sync_info_activity),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(Res.string.settings_simkl_sync_info_manual),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
if (browserError) {
|
||||
Text(
|
||||
text = stringResource(Res.string.settings_trakt_failed_open_browser),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
browserError = false
|
||||
runCatching { uriHandler.openUri(SIMKL_SYNC_GUIDE_URL) }
|
||||
.onFailure { browserError = true }
|
||||
},
|
||||
) {
|
||||
Text(stringResource(Res.string.settings_simkl_sync_info_docs))
|
||||
}
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringResource(Res.string.action_close))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val SIMKL_SYNC_GUIDE_URL = "https://api.simkl.org/guides/sync"
|
||||
|
|
@ -15,6 +15,7 @@ import androidx.compose.foundation.lazy.LazyListScope
|
|||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Check
|
||||
import androidx.compose.material.icons.rounded.Info
|
||||
import androidx.compose.material3.BasicAlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
|
|
@ -99,6 +100,7 @@ import nuvio.composeapp.generated.resources.settings_simkl_open_login
|
|||
import nuvio.composeapp.generated.resources.settings_simkl_sign_in_description
|
||||
import nuvio.composeapp.generated.resources.settings_simkl_sign_in_failed
|
||||
import nuvio.composeapp.generated.resources.settings_simkl_sync_now
|
||||
import nuvio.composeapp.generated.resources.settings_simkl_sync_info_action
|
||||
import nuvio.composeapp.generated.resources.settings_simkl_visit
|
||||
import nuvio.composeapp.generated.resources.tracking_library_source_simkl_selected
|
||||
import nuvio.composeapp.generated.resources.tracking_source_simkl
|
||||
|
|
@ -791,6 +793,7 @@ private fun SimklConnectionCard(
|
|||
SimklSyncRepository.state
|
||||
}.collectAsStateWithLifecycle()
|
||||
val scope = rememberCoroutineScope()
|
||||
var showSyncInfo by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
ProviderConnectionCard(
|
||||
isTablet = isTablet,
|
||||
|
|
@ -813,6 +816,7 @@ private fun SimklConnectionCard(
|
|||
openLoginLabel = stringResource(Res.string.settings_simkl_open_login),
|
||||
disconnectLabel = stringResource(Res.string.settings_simkl_disconnect),
|
||||
syncLabel = stringResource(Res.string.settings_simkl_sync_now),
|
||||
infoLabel = stringResource(Res.string.settings_simkl_sync_info_action),
|
||||
isSyncing = syncState.isLoading,
|
||||
missingCredentialsMessage = stringResource(Res.string.settings_simkl_missing_credentials),
|
||||
errorMessage = simklErrorMessage(uiState.error) ?: syncState.errorMessage,
|
||||
|
|
@ -829,8 +833,13 @@ private fun SimklConnectionCard(
|
|||
SimklSyncRepository.refresh(TrackingRefreshIntent.USER_INITIATED)
|
||||
}
|
||||
},
|
||||
onInfoRequested = { showSyncInfo = true },
|
||||
onDisconnect = SimklAuthRepository::onDisconnectRequested,
|
||||
)
|
||||
|
||||
if (showSyncInfo) {
|
||||
SimklSyncInfoDialog(onDismiss = { showSyncInfo = false })
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -848,6 +857,7 @@ private fun ProviderConnectionCard(
|
|||
openLoginLabel: String,
|
||||
disconnectLabel: String,
|
||||
syncLabel: String? = null,
|
||||
infoLabel: String? = null,
|
||||
isSyncing: Boolean = false,
|
||||
missingCredentialsMessage: String,
|
||||
statusMessage: String? = null,
|
||||
|
|
@ -858,6 +868,7 @@ private fun ProviderConnectionCard(
|
|||
onResumeAuthorization: () -> String?,
|
||||
onCancelAuthorization: () -> Unit,
|
||||
onSyncRequested: (() -> Unit)? = null,
|
||||
onInfoRequested: (() -> Unit)? = null,
|
||||
onDisconnect: () -> Unit,
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
|
|
@ -907,6 +918,17 @@ private fun ProviderConnectionCard(
|
|||
}
|
||||
}
|
||||
}
|
||||
if (infoLabel != null && onInfoRequested != null) {
|
||||
TextButton(onClick = onInfoRequested) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Info,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
Text(infoLabel)
|
||||
}
|
||||
}
|
||||
Button(
|
||||
onClick = onDisconnect,
|
||||
enabled = !isLoading && !isSyncing,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@ import kotlinx.atomicfu.atomic
|
|||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
internal const val SIMKL_AUTOMATIC_REFRESH_INTERVAL_MS = 15L * 60L * 1_000L
|
||||
internal const val SIMKL_AUTOMATIC_REFRESH_INTERVAL_MINUTES = 15
|
||||
internal const val SIMKL_AUTOMATIC_REFRESH_INTERVAL_MS =
|
||||
SIMKL_AUTOMATIC_REFRESH_INTERVAL_MINUTES * 60L * 1_000L
|
||||
|
||||
internal fun shouldRunSimklRefresh(
|
||||
intent: TrackingRefreshIntent,
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ class SimklRefreshPolicyTest {
|
|||
fun `automatic refresh is throttled for fifteen minutes`() {
|
||||
val lastCheckedAt = 1_000L
|
||||
|
||||
assertEquals(15, SIMKL_AUTOMATIC_REFRESH_INTERVAL_MINUTES)
|
||||
assertFalse(
|
||||
shouldRunSimklRefresh(
|
||||
intent = TrackingRefreshIntent.AUTOMATIC,
|
||||
|
|
|
|||
Loading…
Reference in a new issue