This commit is contained in:
@@ -304,6 +304,9 @@ data class Settings(
|
||||
val updateBaseUrl: String? = null,
|
||||
val updateRepo: String? = null,
|
||||
val updateToken: String? = null,
|
||||
// Device-level: Back-button behaviour belongs to the television and its remote, not
|
||||
// to the signed-in viewer, so it deliberately does not travel through UserPreferences.
|
||||
val confirmExitMemby: Boolean = false,
|
||||
// Show each item's Emby "Logo" image in place of the plain-text title.
|
||||
val showTitleLogo: Boolean = true,
|
||||
// Slide up a "next up" banner near the end of an episode and roll into the next one.
|
||||
@@ -487,6 +490,7 @@ class SettingsStore(private val context: Context) {
|
||||
val UPDATE_BASE_URL = stringPreferencesKey("update_base_url")
|
||||
val UPDATE_REPO = stringPreferencesKey("update_repo")
|
||||
val UPDATE_TOKEN = stringPreferencesKey("update_token")
|
||||
val CONFIRM_EXIT_MEMBY = booleanPreferencesKey("confirm_exit_memby")
|
||||
val SHOW_TITLE_LOGO = booleanPreferencesKey("show_title_logo")
|
||||
val AUTO_PLAY_NEXT = booleanPreferencesKey("auto_play_next_episode")
|
||||
val SHOW_TEN_MINUTE_REMINDER = booleanPreferencesKey("show_ten_minute_reminder")
|
||||
@@ -570,6 +574,10 @@ class SettingsStore(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setConfirmExitMemby(enabled: Boolean) {
|
||||
context.dataStore.edit { it[Keys.CONFIRM_EXIT_MEMBY] = enabled }
|
||||
}
|
||||
|
||||
/**
|
||||
* Ids of alerts already shown on this TV. The gateway keeps offering an alert for as
|
||||
* long as it is current, so without this an "aired" banner would return every poll —
|
||||
@@ -1344,6 +1352,7 @@ class SettingsStore(private val context: Context) {
|
||||
updateBaseUrl = preferences[Keys.UPDATE_BASE_URL],
|
||||
updateRepo = preferences[Keys.UPDATE_REPO],
|
||||
updateToken = preferences[Keys.UPDATE_TOKEN],
|
||||
confirmExitMemby = preferences[Keys.CONFIRM_EXIT_MEMBY] ?: false,
|
||||
showTitleLogo = preferences[Keys.SHOW_TITLE_LOGO] ?: true,
|
||||
autoPlayNextEpisode = preferences[Keys.AUTO_PLAY_NEXT] ?: true,
|
||||
showTenMinuteReminder = preferences[Keys.SHOW_TEN_MINUTE_REMINDER] ?: true,
|
||||
|
||||
@@ -232,6 +232,7 @@ private fun AppRoot(onCloseSettings: () -> Unit) {
|
||||
var appUpdate by remember { mutableStateOf<GatewayUpdate?>(null) }
|
||||
var initialUpdateCheckComplete by remember { mutableStateOf(false) }
|
||||
var dismissedUpdateVersion by rememberSaveable { mutableStateOf<String?>(null) }
|
||||
var confirmingExit by rememberSaveable { mutableStateOf(false) }
|
||||
// SettingsStore starts eagerly in Application.onCreate. Reuse its in-memory value when
|
||||
// Android recreates this activity after the viewer returns from the TV home screen;
|
||||
// starting from null needlessly painted "Opening Memby..." while the replayed value
|
||||
@@ -415,7 +416,9 @@ private fun AppRoot(onCloseSettings: () -> Unit) {
|
||||
!InstallPermission.granted(LocalContext.current) ->
|
||||
InstallPermissionScreen(onContinue = { installPermissionHandled = true })
|
||||
loaded.isSignedIn -> {
|
||||
BackHandler(onBack = onCloseSettings)
|
||||
BackHandler {
|
||||
if (loaded.confirmExitMemby) confirmingExit = true else onCloseSettings()
|
||||
}
|
||||
key(loaded.userId, loaded.serverUrl) {
|
||||
HomeScreen(settings = loaded)
|
||||
}
|
||||
@@ -446,6 +449,69 @@ private fun AppRoot(onCloseSettings: () -> Unit) {
|
||||
)
|
||||
else -> FirstRunScreen(onGetStarted = { startingFirstRun = true })
|
||||
}
|
||||
if (confirmingExit) {
|
||||
ExitMembyConfirmation(
|
||||
onStay = { confirmingExit = false },
|
||||
onExit = onCloseSettings,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExitMembyConfirmation(
|
||||
onStay: () -> Unit,
|
||||
onExit: () -> Unit,
|
||||
) {
|
||||
val stayFocus = remember { FocusRequester() }
|
||||
val exitFocus = remember { FocusRequester() }
|
||||
BackHandler(onBack = onStay)
|
||||
LaunchedEffect(Unit) {
|
||||
kotlinx.coroutines.delay(16L)
|
||||
runCatching { stayFocus.requestFocus() }
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.zIndex(20f)
|
||||
.background(Color.Black.copy(alpha = 0.82f)),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.width(480.dp)
|
||||
.background(Color(0xFF20262B), RoundedCornerShape(18.dp))
|
||||
.padding(32.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(18.dp),
|
||||
) {
|
||||
Text(
|
||||
"Close Memby?",
|
||||
color = Color.White,
|
||||
fontSize = 26.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
Text(
|
||||
"Choose Stay to keep browsing, or close Memby and return to your TV.",
|
||||
color = Color(0xFFBCC4CA),
|
||||
fontSize = 16.sp,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(14.dp)) {
|
||||
Button(
|
||||
onClick = onStay,
|
||||
modifier = Modifier
|
||||
.focusRequester(stayFocus)
|
||||
.focusProperties { right = exitFocus },
|
||||
) { Text("Stay in Memby") }
|
||||
Button(
|
||||
onClick = onExit,
|
||||
modifier = Modifier
|
||||
.focusRequester(exitFocus)
|
||||
.focusProperties { left = stayFocus },
|
||||
) { Text("Close Memby") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -208,6 +208,7 @@ internal data class SettingsPanelState(
|
||||
val showCardMetadata: Boolean = true,
|
||||
val showRatingsStrip: Boolean = true,
|
||||
val hideWatchedMovies: Boolean = false,
|
||||
val confirmExitMemby: Boolean = false,
|
||||
val welcomeQuoteStyle: String = WelcomeQuoteStyle.NEUTRAL.value,
|
||||
/**
|
||||
* The viewer's own colour scheme, and the ones the gateway says they may pick between.
|
||||
@@ -264,6 +265,7 @@ internal data class SettingsPanelActions(
|
||||
val onShowCardMetadataChanged: (Boolean) -> Unit = {},
|
||||
val onShowRatingsStripChanged: (Boolean) -> Unit = {},
|
||||
val onHideWatchedMoviesChanged: (Boolean) -> Unit = {},
|
||||
val onConfirmExitMembyChanged: (Boolean) -> Unit = {},
|
||||
val onWelcomeQuoteStyleChanged: (String) -> Unit = {},
|
||||
val onThemeChanged: (String) -> Unit = {},
|
||||
val onPageSelected: (SettingsPage) -> Unit = {},
|
||||
@@ -312,6 +314,7 @@ fun SettingsSheet(
|
||||
var showCardMetadata by rememberSaveable { mutableStateOf(settings.showHomeCardMetadata) }
|
||||
var showRatingsStrip by rememberSaveable { mutableStateOf(settings.showRatingsStrip) }
|
||||
var hideWatchedMovies by rememberSaveable { mutableStateOf(settings.hideWatchedMovies) }
|
||||
var confirmExitMemby by rememberSaveable { mutableStateOf(settings.confirmExitMemby) }
|
||||
var welcomeQuoteStyle by rememberSaveable { mutableStateOf(settings.welcomeQuoteStyle) }
|
||||
// The resolved theme and the schemes on offer both come from the gateway, through the
|
||||
// sync that owns them. Collected here rather than in SettingsPanelContent so the content
|
||||
@@ -375,6 +378,7 @@ fun SettingsSheet(
|
||||
settings.showHomeCardMetadata,
|
||||
settings.showRatingsStrip,
|
||||
settings.hideWatchedMovies,
|
||||
settings.confirmExitMemby,
|
||||
settings.autoPlayNextEpisode,
|
||||
settings.showTenMinuteReminder,
|
||||
settings.seekIntervalSeconds,
|
||||
@@ -395,6 +399,7 @@ fun SettingsSheet(
|
||||
showCardMetadata = settings.showHomeCardMetadata
|
||||
showRatingsStrip = settings.showRatingsStrip
|
||||
hideWatchedMovies = settings.hideWatchedMovies
|
||||
confirmExitMemby = settings.confirmExitMemby
|
||||
welcomeQuoteStyle = settings.welcomeQuoteStyle
|
||||
}
|
||||
|
||||
@@ -421,6 +426,7 @@ fun SettingsSheet(
|
||||
showCardMetadata = showCardMetadata,
|
||||
showRatingsStrip = showRatingsStrip,
|
||||
hideWatchedMovies = hideWatchedMovies,
|
||||
confirmExitMemby = confirmExitMemby,
|
||||
welcomeQuoteStyle = welcomeQuoteStyle,
|
||||
themeId = settings.themeId,
|
||||
themeOptions = availableThemes.map { theme ->
|
||||
@@ -510,6 +516,10 @@ fun SettingsSheet(
|
||||
hideWatchedMovies = it
|
||||
scope.launch { store.setHideWatchedMovies(it) }
|
||||
},
|
||||
onConfirmExitMembyChanged = {
|
||||
confirmExitMemby = it
|
||||
scope.launch { store.setConfirmExitMemby(it) }
|
||||
},
|
||||
onThemeChanged = { chosen ->
|
||||
// No local echo: what is on screen is the palette the gateway resolves, and it
|
||||
// arrives through ThemeSync a moment later. Painting optimistically here would
|
||||
@@ -850,6 +860,13 @@ internal fun SettingsPanelContent(
|
||||
checked = state.showRatingsStrip,
|
||||
onCheckedChange = actions.onShowRatingsStripChanged,
|
||||
)
|
||||
SettingDivider()
|
||||
SettingsToggleRow(
|
||||
title = "Confirm before closing Memby",
|
||||
description = "Ask before the Back button closes the app.",
|
||||
checked = state.confirmExitMemby,
|
||||
onCheckedChange = actions.onConfirmExitMembyChanged,
|
||||
)
|
||||
if (state.hiddenHomeRowCount > 0) {
|
||||
SettingDivider()
|
||||
SettingsActionRow(
|
||||
|
||||
@@ -185,9 +185,17 @@ class UserPreferencesTest {
|
||||
token = "session-token",
|
||||
ringColorHex = "FF0000",
|
||||
rotationIntervalSeconds = 45,
|
||||
confirmExitMemby = true,
|
||||
).toUserPreferences().encode().toString()
|
||||
|
||||
listOf("Living room", "device-1", "gitea-secret", "git.example", "session-token")
|
||||
listOf(
|
||||
"Living room",
|
||||
"device-1",
|
||||
"gitea-secret",
|
||||
"git.example",
|
||||
"session-token",
|
||||
"confirmExitMemby",
|
||||
)
|
||||
.forEach { assertTrue("$it reached the wire", it !in encoded) }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user