mirror of
https://github.com/chmouel/liseur.git
synced 2026-10-19 09:13:36 +00:00
feat: Show settings beside their pages on large screens
On a tablet, an unfolded foldable or any window wide enough for two panes, Settings now stays on the left and the page chosen from it opens on the right, with a placeholder until one is chosen. This uses Material's list-detail scene strategy for Navigation 3. Choosing another row replaces the open page instead of stacking it, Back closes the page and then Settings, and Settings' own arrow leaves Settings altogether. A page shown beside the list drops its back arrow; a page nested inside one, such as a server being edited, keeps it. Narrow windows are unchanged. The server screen is only a pane when opened from Settings; from the library it remains a screen of its own, and the licences stay full screen. On e-paper the panes switch without motion.
This commit is contained in:
parent
5fdde2414e
commit
9a648f280d
24 changed files with 288 additions and 104 deletions
|
|
@ -1156,6 +1156,13 @@ Key decisions:
|
|||
previewed. ViewModels stay activity-scoped and shared between screens;
|
||||
there is deliberately no per-entry ViewModel decorator. The reader is a
|
||||
separate activity and does not use it.
|
||||
- On a wide window Settings is a list pane and the page chosen from it a
|
||||
detail pane, through Material's `ListDetailSceneStrategy` (Navigation 3
|
||||
scenes, not `ListDetailPaneScaffold`). Back pops one route at a time
|
||||
(`PopLatest`); choosing another row replaces the open page
|
||||
(`openDetail`). Pages use `PaneBackButton`, which hides the arrow while
|
||||
the list is beside them; a page nested inside one wraps itself in
|
||||
`NestedPage` to keep its arrow. On e-paper the panes get no motion.
|
||||
|
||||
## Implementation invariants
|
||||
|
||||
|
|
|
|||
|
|
@ -211,6 +211,7 @@ dependencies {
|
|||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.navigation3.runtime)
|
||||
implementation(libs.androidx.navigation3.ui)
|
||||
implementation(libs.compose.material3.adaptive.navigation3)
|
||||
implementation(libs.androidx.fragment.compose)
|
||||
|
||||
implementation(platform(libs.compose.bom))
|
||||
|
|
|
|||
|
|
@ -16,6 +16,10 @@ import androidx.activity.compose.LocalActivity
|
|||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
|
||||
import androidx.compose.material3.adaptive.navigation.BackNavigationBehavior
|
||||
import androidx.compose.material3.adaptive.navigation3.ListDetailSceneStrategy
|
||||
import androidx.compose.material3.adaptive.navigation3.rememberListDetailSceneStrategy
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
|
|
@ -66,6 +70,7 @@ import com.chmouel.liseur.ui.settings.ServerAccountViewModel
|
|||
import com.chmouel.liseur.ui.settings.AboutScreen
|
||||
import com.chmouel.liseur.ui.settings.AppLocales
|
||||
import com.chmouel.liseur.ui.settings.LicencesScreen
|
||||
import com.chmouel.liseur.ui.settings.SettingsPagePlaceholder
|
||||
import com.chmouel.liseur.ui.settings.SettingsScreen
|
||||
import com.chmouel.liseur.ui.settings.SettingsBackupScreen
|
||||
import com.chmouel.liseur.ui.settings.ReadingAppearanceScreen
|
||||
|
|
@ -95,6 +100,7 @@ import kotlinx.coroutines.Job
|
|||
import androidx.compose.animation.ContentTransform
|
||||
import androidx.compose.animation.EnterTransition
|
||||
import androidx.compose.animation.ExitTransition
|
||||
import androidx.compose.animation.core.snap
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
|
|
@ -111,8 +117,11 @@ import androidx.navigationevent.compose.rememberNavigationEventDispatcherOwner
|
|||
import com.chmouel.liseur.ui.navigation.LaunchStack
|
||||
import com.chmouel.liseur.ui.navigation.Route
|
||||
import com.chmouel.liseur.ui.navigation.RouteBackStackSaver
|
||||
import com.chmouel.liseur.ui.navigation.closeSettings
|
||||
import com.chmouel.liseur.ui.navigation.contentKey
|
||||
import com.chmouel.liseur.ui.navigation.isSettingsPage
|
||||
import com.chmouel.liseur.ui.navigation.launchStack
|
||||
import com.chmouel.liseur.ui.navigation.openDetail
|
||||
import com.chmouel.liseur.ui.navigation.pop
|
||||
import com.chmouel.liseur.ui.navigation.push
|
||||
|
||||
|
|
@ -244,6 +253,7 @@ class MainActivity : ComponentActivity() {
|
|||
private const val SOURCE_URL = "https://github.com/chmouel/liseur"
|
||||
private const val SPONSOR_URL = "https://github.com/sponsors/chmouel"
|
||||
|
||||
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
|
||||
@Composable
|
||||
private fun LiseurApp(
|
||||
settings: AppSettings,
|
||||
|
|
@ -295,11 +305,41 @@ private fun LiseurApp(
|
|||
if (backStack.lastOrNull() == Route.SettingsBackup) settingsZipBackup.close()
|
||||
backStack.pop()
|
||||
}
|
||||
// A page chosen from Settings takes the place of the one open beside it.
|
||||
val openPage: (Route) -> Unit = { page ->
|
||||
if (backStack.lastOrNull() == Route.SettingsBackup && page != Route.SettingsBackup) {
|
||||
settingsZipBackup.close()
|
||||
}
|
||||
backStack.openDetail(page)
|
||||
}
|
||||
// Settings' own arrow leaves Settings, page beside it and all.
|
||||
val closeSettings: () -> Unit = {
|
||||
if (Route.SettingsBackup in backStack) settingsZipBackup.close()
|
||||
backStack.closeSettings()
|
||||
}
|
||||
val eInk = LocalEInk.current
|
||||
// Settings beside its pages on a wide window. Back pops one route at a
|
||||
// time: the default would skip Settings, whose list-and-placeholder
|
||||
// layout looks the same to the scaffold as list-and-page.
|
||||
val listDetail = rememberListDetailSceneStrategy<Route>(
|
||||
backNavigationBehavior = BackNavigationBehavior.PopLatest,
|
||||
)
|
||||
// NavDisplay's transitions do not reach the panes, which animate on
|
||||
// their own; on e-paper they are told not to.
|
||||
val paneMotion = if (eInk) {
|
||||
ListDetailSceneStrategy.paneAnimation(
|
||||
enterTransition = EnterTransition.None,
|
||||
exitTransition = ExitTransition.None,
|
||||
boundsAnimationSpec = snap(),
|
||||
)
|
||||
} else {
|
||||
emptyMap()
|
||||
}
|
||||
|
||||
NavDisplay(
|
||||
backStack = backStack,
|
||||
onBack = back,
|
||||
sceneStrategies = listOf(listDetail),
|
||||
entryDecorators = listOf(
|
||||
rememberSaveableStateHolderNavEntryDecorator(),
|
||||
rememberTopEntryBackDecorator(backStack),
|
||||
|
|
@ -322,7 +362,19 @@ private fun LiseurApp(
|
|||
defaultPredictivePopTransitionSpec()
|
||||
},
|
||||
entryProvider = { route ->
|
||||
NavEntry(route, contentKey = route.contentKey) {
|
||||
val below = backStack.getOrNull(backStack.indexOf(route) - 1)
|
||||
val pane = when {
|
||||
route == Route.Settings -> ListDetailSceneStrategy.listPane(
|
||||
detailPlaceholder = { SettingsPagePlaceholder() },
|
||||
) + paneMotion
|
||||
// The server screen is also opened from the library, and
|
||||
// there it is a screen of its own.
|
||||
route.isSettingsPage &&
|
||||
(route != Route.ServerAccount || below == Route.Settings) ->
|
||||
ListDetailSceneStrategy.detailPane() + paneMotion
|
||||
else -> emptyMap()
|
||||
}
|
||||
NavEntry(route, contentKey = route.contentKey, metadata = pane) {
|
||||
when (route) {
|
||||
Route.Library -> androidx.compose.runtime.key(libraryLaunchId) {
|
||||
LibraryRoute(
|
||||
|
|
@ -411,20 +463,20 @@ private fun LiseurApp(
|
|||
dynamicColorAvailable = dynamicColorAvailable,
|
||||
onThemeMode = { scope.launch { repository.setThemeMode(it) } },
|
||||
onDynamicColor = { scope.launch { repository.setDynamicColor(it) } },
|
||||
onOpenAccount = { backStack.push(Route.ServerAccount) },
|
||||
onOpenReadingAppearance = { backStack.push(Route.ReadingAppearance) },
|
||||
onOpenReadingNavigation = { backStack.push(Route.ReadingNavigation) },
|
||||
onOpenSettingsBackup = { backStack.push(Route.SettingsBackup) },
|
||||
onOpenHiddenBooks = { backStack.push(Route.HiddenBooks) },
|
||||
onOpenAccount = { openPage(Route.ServerAccount) },
|
||||
onOpenReadingAppearance = { openPage(Route.ReadingAppearance) },
|
||||
onOpenReadingNavigation = { openPage(Route.ReadingNavigation) },
|
||||
onOpenSettingsBackup = { openPage(Route.SettingsBackup) },
|
||||
onOpenHiddenBooks = { openPage(Route.HiddenBooks) },
|
||||
libraryFolders = library.libraryFolders,
|
||||
onRemoveFolder = { library.removeFolder(it) },
|
||||
server = context.container.remoteAccount.server,
|
||||
onOpenAbout = { backStack.push(Route.About) },
|
||||
onBack = back,
|
||||
onOpenAbout = { openPage(Route.About) },
|
||||
onBack = closeSettings,
|
||||
flavorReadingRows = {
|
||||
context.container.readAloud.SettingsEntry(onClick = { backStack.push(Route.ReadAloud) })
|
||||
context.container.translate.SettingsEntry(onClick = { backStack.push(Route.Translation) })
|
||||
ServicesEntry(onClick = { backStack.push(Route.Services) })
|
||||
context.container.readAloud.SettingsEntry(onClick = { openPage(Route.ReadAloud) })
|
||||
context.container.translate.SettingsEntry(onClick = { openPage(Route.Translation) })
|
||||
ServicesEntry(onClick = { openPage(Route.Services) })
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import androidx.compose.foundation.text.KeyboardActions
|
|||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
|
||||
import androidx.compose.material.icons.outlined.Add
|
||||
import androidx.compose.material.icons.outlined.Check
|
||||
import androidx.compose.material.icons.outlined.Dns
|
||||
|
|
@ -92,6 +91,8 @@ import com.chmouel.liseur.tts.SpeechServerPresets
|
|||
import com.chmouel.liseur.tts.onLeaving
|
||||
import com.chmouel.liseur.ui.contentWidthCap
|
||||
import com.chmouel.liseur.ui.settings.ConnectionRow
|
||||
import com.chmouel.liseur.ui.settings.NestedPage
|
||||
import com.chmouel.liseur.ui.settings.PaneBackButton
|
||||
import com.chmouel.liseur.ui.settings.RowDivider
|
||||
import com.chmouel.liseur.ui.settings.SettingsGroup
|
||||
import com.chmouel.liseur.ui.windowWidth
|
||||
|
|
@ -135,16 +136,19 @@ internal fun ServicesScreen(connections: ServerConnections, accounts: ServiceAcc
|
|||
editing?.let { opened ->
|
||||
val id = drafts[draft] ?: opened.takeIf { it != NEW_SERVER }
|
||||
BackHandler { editing = null }
|
||||
key(draft) {
|
||||
ServerScreen(
|
||||
connections = connections,
|
||||
draft = draft,
|
||||
id = id,
|
||||
server = servers.readable?.firstOrNull { it.id == id },
|
||||
usedByReadAloud = id != null && id == readAloud,
|
||||
usedByTranslation = id != null && id == translation,
|
||||
onBack = { editing = null },
|
||||
)
|
||||
// A page inside this one: its arrow is the only way back out.
|
||||
NestedPage {
|
||||
key(draft) {
|
||||
ServerScreen(
|
||||
connections = connections,
|
||||
draft = draft,
|
||||
id = id,
|
||||
server = servers.readable?.firstOrNull { it.id == id },
|
||||
usedByReadAloud = id != null && id == readAloud,
|
||||
usedByTranslation = id != null && id == translation,
|
||||
onBack = { editing = null },
|
||||
)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -214,11 +218,7 @@ private fun SettingsPage(
|
|||
topBar = {
|
||||
LargeTopAppBar(
|
||||
title = { Text(title) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Outlined.ArrowBack, contentDescription = stringResource(R.string.back))
|
||||
}
|
||||
},
|
||||
navigationIcon = { PaneBackButton(onBack) },
|
||||
actions = { actions() },
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,13 +16,11 @@ import androidx.compose.foundation.rememberScrollState
|
|||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
|
||||
import androidx.compose.material.icons.outlined.Check
|
||||
import androidx.compose.material.icons.outlined.Translate
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LargeTopAppBar
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
|
|
@ -69,6 +67,8 @@ import com.chmouel.liseur.ui.BusyIndicator
|
|||
import com.chmouel.liseur.ui.LiseurModalBottomSheet
|
||||
import com.chmouel.liseur.ui.contentWidthCap
|
||||
import com.chmouel.liseur.ui.settings.ConnectionRow
|
||||
import com.chmouel.liseur.ui.settings.NestedPage
|
||||
import com.chmouel.liseur.ui.settings.PaneBackButton
|
||||
import com.chmouel.liseur.ui.settings.SettingsGroup
|
||||
import com.chmouel.liseur.ui.windowWidth
|
||||
import java.text.NumberFormat
|
||||
|
|
@ -110,7 +110,13 @@ internal fun TranslationSettingsScreen(feature: ServiceTranslate, onBack: () ->
|
|||
// Opened straight onto the Services page, back leaves altogether.
|
||||
val leave = if (services) onBack else ({ managing = false })
|
||||
BackHandler(onBack = leave)
|
||||
feature.ServicesPage(onBack = leave)
|
||||
// Opened from this screen, the page needs its own way back to it;
|
||||
// opened straight onto it, leaving it leaves the screen.
|
||||
if (services) {
|
||||
feature.ServicesPage(onBack = leave)
|
||||
} else {
|
||||
NestedPage { feature.ServicesPage(onBack = leave) }
|
||||
}
|
||||
return
|
||||
}
|
||||
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
|
||||
|
|
@ -126,11 +132,7 @@ internal fun TranslationSettingsScreen(feature: ServiceTranslate, onBack: () ->
|
|||
topBar = {
|
||||
LargeTopAppBar(
|
||||
title = { Text(stringResource(R.string.translation_settings_title)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Outlined.ArrowBack, contentDescription = stringResource(R.string.back))
|
||||
}
|
||||
},
|
||||
navigationIcon = { PaneBackButton(onBack) },
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ import androidx.compose.foundation.text.KeyboardActions
|
|||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.outlined.VolumeUp
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material.icons.filled.Stop
|
||||
|
|
@ -100,6 +99,8 @@ import com.chmouel.liseur.providers.ServiceSheet
|
|||
import com.chmouel.liseur.ui.LiseurModalBottomSheet
|
||||
import com.chmouel.liseur.ui.contentWidthCap
|
||||
import com.chmouel.liseur.ui.settings.ConnectionRow
|
||||
import com.chmouel.liseur.ui.settings.NestedPage
|
||||
import com.chmouel.liseur.ui.settings.PaneBackButton
|
||||
import com.chmouel.liseur.ui.settings.SettingsGroup
|
||||
import com.chmouel.liseur.ui.windowWidth
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
|
@ -142,7 +143,7 @@ internal fun ReadAloudSettingsScreen(feature: SpeechReadAloud, onBack: () -> Uni
|
|||
var managing by rememberSaveable { mutableStateOf(false) }
|
||||
if (managing) {
|
||||
BackHandler { managing = false }
|
||||
feature.ServicesPage(onBack = { managing = false })
|
||||
NestedPage { feature.ServicesPage(onBack = { managing = false }) }
|
||||
return
|
||||
}
|
||||
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
|
||||
|
|
@ -157,11 +158,7 @@ internal fun ReadAloudSettingsScreen(feature: SpeechReadAloud, onBack: () -> Uni
|
|||
topBar = {
|
||||
LargeTopAppBar(
|
||||
title = { Text(stringResource(R.string.read_aloud_settings_title)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Outlined.ArrowBack, contentDescription = stringResource(R.string.back))
|
||||
}
|
||||
},
|
||||
navigationIcon = { PaneBackButton(onBack) },
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -113,6 +113,43 @@ fun MutableList<Route>.pop() {
|
|||
if (size > 1) removeAt(lastIndex)
|
||||
}
|
||||
|
||||
/** Pages opened from Settings, shown beside it on a wide window. */
|
||||
val Route.isSettingsPage: Boolean
|
||||
get() = when (this) {
|
||||
Route.ReadingAppearance, Route.ReadingNavigation, Route.ReadAloud,
|
||||
Route.Translation, Route.Services, Route.SettingsBackup,
|
||||
Route.HiddenBooks, Route.About, Route.ServerAccount,
|
||||
-> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a Settings page in place of whatever page is open beside Settings.
|
||||
*
|
||||
* Choosing another row on a wide window swaps the page rather than piling
|
||||
* it on top of the last one, so Back still returns to Settings in one step.
|
||||
* Choosing the row already open does nothing, keeping its place. Without
|
||||
* Settings on the stack, it is an ordinary [push].
|
||||
*/
|
||||
fun MutableList<Route>.openDetail(route: Route) {
|
||||
if (lastOrNull() == route) return
|
||||
val settings = lastIndexOf(Route.Settings)
|
||||
if (settings >= 0) {
|
||||
while (size > settings + 1) removeAt(lastIndex)
|
||||
}
|
||||
add(route)
|
||||
}
|
||||
|
||||
/** Closes Settings and any page open beside it. */
|
||||
fun MutableList<Route>.closeSettings() {
|
||||
val settings = lastIndexOf(Route.Settings)
|
||||
if (settings <= 0) {
|
||||
pop()
|
||||
return
|
||||
}
|
||||
while (size > settings) removeAt(lastIndex)
|
||||
}
|
||||
|
||||
/** The stack a widget or shortcut launch starts from. */
|
||||
fun launchStack(target: LaunchStack): List<Route> = when (target) {
|
||||
LaunchStack.LIBRARY -> listOf(Route.Library)
|
||||
|
|
|
|||
|
|
@ -20,11 +20,9 @@ import androidx.compose.foundation.rememberScrollState
|
|||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
|
||||
import androidx.compose.material.icons.outlined.FavoriteBorder
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
|
|
@ -109,14 +107,7 @@ fun AboutScreen(
|
|||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.about_title)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Outlined.ArrowBack,
|
||||
contentDescription = stringResource(R.string.back),
|
||||
)
|
||||
}
|
||||
},
|
||||
navigationIcon = { PaneBackButton(onBack) },
|
||||
)
|
||||
},
|
||||
) { padding ->
|
||||
|
|
|
|||
|
|
@ -9,10 +9,7 @@ import androidx.compose.foundation.layout.widthIn
|
|||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LargeTopAppBar
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
|
|
@ -55,14 +52,7 @@ fun HiddenBooksScreen(
|
|||
topBar = {
|
||||
LargeTopAppBar(
|
||||
title = { Text(stringResource(R.string.hidden_books)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Outlined.ArrowBack,
|
||||
contentDescription = stringResource(R.string.back),
|
||||
)
|
||||
}
|
||||
},
|
||||
navigationIcon = { PaneBackButton(onBack) },
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
package com.chmouel.liseur.ui.settings
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
|
||||
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
|
||||
import androidx.compose.material3.adaptive.layout.PaneAdaptedValue
|
||||
import androidx.compose.material3.adaptive.navigation3.LocalListDetailSceneScope
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.chmouel.liseur.R
|
||||
|
||||
/**
|
||||
* The back arrow of a page opened from Settings.
|
||||
*
|
||||
* On a wide window the page sits beside the Settings list, and an arrow
|
||||
* there would point at a list that is already on screen, so it is left
|
||||
* out; Back still closes the page. A page inside the page — a server being
|
||||
* edited, the services a feature manages — clears the scene scope around
|
||||
* itself and keeps its arrow, since only that arrow leads back out of it.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
|
||||
@Composable
|
||||
fun PaneBackButton(onBack: () -> Unit) {
|
||||
val listShown = LocalListDetailSceneScope.current
|
||||
?.scaffoldTransitionScope
|
||||
?.scaffoldStateTransition
|
||||
?.targetState
|
||||
?.get(ListDetailPaneScaffoldRole.List) == PaneAdaptedValue.Expanded
|
||||
if (listShown) return
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Outlined.ArrowBack,
|
||||
contentDescription = stringResource(R.string.back),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Gives a page nested inside a Settings page its own back arrow. */
|
||||
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
|
||||
@Composable
|
||||
fun NestedPage(content: @Composable () -> Unit) {
|
||||
CompositionLocalProvider(
|
||||
LocalListDetailSceneScope provides null,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
|
@ -12,10 +12,7 @@ import androidx.compose.foundation.rememberScrollState
|
|||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LargeTopAppBar
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
|
|
@ -113,14 +110,7 @@ fun ReadingAppearanceScreen(
|
|||
topBar = {
|
||||
LargeTopAppBar(
|
||||
title = { Text(stringResource(R.string.settings_reading_appearance)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Outlined.ArrowBack,
|
||||
contentDescription = stringResource(R.string.back),
|
||||
)
|
||||
}
|
||||
},
|
||||
navigationIcon = { PaneBackButton(onBack) },
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -13,13 +13,10 @@ import androidx.compose.foundation.text.KeyboardActions
|
|||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LargeTopAppBar
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.MenuAnchorType
|
||||
|
|
@ -96,14 +93,7 @@ fun ReadingNavigationScreen(
|
|||
topBar = {
|
||||
LargeTopAppBar(
|
||||
title = { Text(stringResource(R.string.settings_reading_navigation)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Outlined.ArrowBack,
|
||||
contentDescription = stringResource(R.string.back),
|
||||
)
|
||||
}
|
||||
},
|
||||
navigationIcon = { PaneBackButton(onBack) },
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ import androidx.compose.foundation.selection.toggleable
|
|||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.outlined.OpenInNew
|
||||
import androidx.compose.material.icons.outlined.CheckCircle
|
||||
import androidx.compose.material.icons.outlined.CloudOff
|
||||
|
|
@ -175,14 +174,7 @@ fun ServerAccountScreen(
|
|||
topBar = {
|
||||
LargeTopAppBar(
|
||||
title = { Text(stringResource(R.string.server_title)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Outlined.ArrowBack,
|
||||
contentDescription = stringResource(R.string.back),
|
||||
)
|
||||
}
|
||||
},
|
||||
navigationIcon = { PaneBackButton(onBack) },
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.widthIn
|
|||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
|
||||
import androidx.compose.material.icons.outlined.FileOpen
|
||||
import androidx.compose.material.icons.outlined.FileUpload
|
||||
import androidx.compose.material.icons.outlined.FolderOpen
|
||||
|
|
@ -18,7 +17,6 @@ import com.chmouel.liseur.data.library.BookExportResult
|
|||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LargeTopAppBar
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
|
|
@ -109,14 +107,7 @@ fun SettingsBackupScreen(
|
|||
topBar = {
|
||||
LargeTopAppBar(
|
||||
title = { Text(stringResource(R.string.settings_backup_title)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Outlined.ArrowBack,
|
||||
contentDescription = stringResource(R.string.back),
|
||||
)
|
||||
}
|
||||
},
|
||||
navigationIcon = { PaneBackButton(onBack) },
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
package com.chmouel.liseur.ui.settings
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.chmouel.liseur.R
|
||||
|
||||
/**
|
||||
* What fills the page side of Settings on a wide window before a page is
|
||||
* chosen: a quiet line saying what the space is for.
|
||||
*/
|
||||
@Composable
|
||||
fun SettingsPagePlaceholder() {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.safeDrawingPadding()
|
||||
.padding(32.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Outlined.Settings,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(48.dp),
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.settings_page_placeholder),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -746,6 +746,7 @@
|
|||
<string name="footer_field_empty">留空</string>
|
||||
<string name="settings_reading">阅读</string>
|
||||
<string name="settings_reading_navigation">阅读与导航</string>
|
||||
<string name="settings_page_placeholder">选择一项设置,在这里打开。</string>
|
||||
<string name="settings_reading_navigation_paged">分页模式。翻页区域:%1$s</string>
|
||||
<string name="settings_reading_navigation_scrolling">滚动阅读。</string>
|
||||
<string name="settings_screen">屏幕</string>
|
||||
|
|
|
|||
|
|
@ -730,6 +730,7 @@
|
|||
|
||||
<string name="settings_reading">Lesen</string>
|
||||
<string name="settings_reading_navigation">Lesen & Navigation</string>
|
||||
<string name="settings_page_placeholder">Wähle eine Einstellung, um sie hier zu öffnen.</string>
|
||||
<string name="settings_reading_navigation_paged">Seitenweise. Umblätterseiten: %1$s</string>
|
||||
<string name="settings_reading_navigation_scrolling">Durch Scrollen lesen.</string>
|
||||
<string name="settings_screen">Bildschirm</string>
|
||||
|
|
|
|||
|
|
@ -719,6 +719,7 @@
|
|||
<string name="footer_field_empty">Nada aquí</string>
|
||||
<string name="settings_reading">Lectura</string>
|
||||
<string name="settings_reading_navigation">Lectura y navegación</string>
|
||||
<string name="settings_page_placeholder">Elige un ajuste para abrirlo aquí.</string>
|
||||
<string name="settings_reading_navigation_paged">Por páginas. Lados de paso: %1$s</string>
|
||||
<string name="settings_reading_navigation_scrolling">Leer desplazando.</string>
|
||||
<string name="settings_screen">Pantalla</string>
|
||||
|
|
|
|||
|
|
@ -719,6 +719,7 @@
|
|||
<string name="footer_field_empty">Rien ici</string>
|
||||
<string name="settings_reading">Lecture</string>
|
||||
<string name="settings_reading_navigation">Lecture et navigation</string>
|
||||
<string name="settings_page_placeholder">Choisissez un réglage pour l\'ouvrir ici.</string>
|
||||
<string name="settings_reading_navigation_paged">Paginé. Côtés pour tourner la page : %1$s</string>
|
||||
<string name="settings_reading_navigation_scrolling">Lecture par défilement.</string>
|
||||
<string name="settings_screen">Écran</string>
|
||||
|
|
|
|||
|
|
@ -719,6 +719,7 @@
|
|||
<string name="footer_field_empty">Niente qui</string>
|
||||
<string name="settings_reading">Lettura</string>
|
||||
<string name="settings_reading_navigation">Lettura e navigazione</string>
|
||||
<string name="settings_page_placeholder">Scegli un\'impostazione per aprirla qui.</string>
|
||||
<string name="settings_reading_navigation_paged">A pagine. Lati di svolta: %1$s</string>
|
||||
<string name="settings_reading_navigation_scrolling">Lettura a scorrimento.</string>
|
||||
<string name="settings_screen">Schermo</string>
|
||||
|
|
|
|||
|
|
@ -769,6 +769,7 @@
|
|||
<string name="footer_field_empty">Ничего</string>
|
||||
<string name="settings_reading">Чтение</string>
|
||||
<string name="settings_reading_navigation">Чтение и навигация</string>
|
||||
<string name="settings_page_placeholder">Выберите настройку, чтобы открыть её здесь.</string>
|
||||
<string name="settings_reading_navigation_paged">Постранично. Стороны перелистывания: %1$s</string>
|
||||
<string name="settings_reading_navigation_scrolling">Чтение прокруткой.</string>
|
||||
<string name="settings_screen">Экран</string>
|
||||
|
|
|
|||
|
|
@ -754,6 +754,7 @@
|
|||
|
||||
<string name="settings_reading">Reading</string>
|
||||
<string name="settings_reading_navigation">Reading & navigation</string>
|
||||
<string name="settings_page_placeholder">Choose a setting to open it here.</string>
|
||||
<string name="settings_reading_navigation_paged">Paged. Page-turn sides: %1$s</string>
|
||||
<string name="settings_reading_navigation_scrolling">Read by scrolling.</string>
|
||||
<string name="settings_screen">Screen</string>
|
||||
|
|
|
|||
|
|
@ -77,4 +77,35 @@ class RoutesTest {
|
|||
assertNotEquals(a.contentKey, b.contentKey)
|
||||
assertEquals("Settings", Route.Settings.contentKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun anotherSettingsPageReplacesTheOpenOne() {
|
||||
val stack = mutableListOf(Route.Library, Route.Settings, Route.About, Route.Licences)
|
||||
stack.openDetail(Route.HiddenBooks)
|
||||
assertEquals(listOf(Route.Library, Route.Settings, Route.HiddenBooks), stack)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun theOpenSettingsPageIsLeftAlone() {
|
||||
val stack = mutableListOf(Route.Library, Route.Settings, Route.About, Route.Licences)
|
||||
stack.openDetail(Route.Licences)
|
||||
assertEquals(listOf(Route.Library, Route.Settings, Route.About, Route.Licences), stack)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun withoutSettingsAPageIsSimplyPushed() {
|
||||
val stack = mutableListOf<Route>(Route.Library)
|
||||
stack.openDetail(Route.ServerAccount)
|
||||
assertEquals(listOf(Route.Library, Route.ServerAccount), stack)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun closingSettingsClosesThePageBesideIt() {
|
||||
val stack = mutableListOf(Route.Library, Route.Settings, Route.About, Route.Licences)
|
||||
stack.closeSettings()
|
||||
assertEquals(listOf(Route.Library), stack)
|
||||
val alone = mutableListOf(Route.Library, Route.Settings)
|
||||
alone.closeSettings()
|
||||
assertEquals(listOf(Route.Library), alone)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ jsoup = "1.23.2"
|
|||
coroutines = "1.11.0"
|
||||
reorderable = "3.1.0"
|
||||
navigation3 = "1.2.0"
|
||||
adaptiveNavigation3 = "1.3.0"
|
||||
robolectric = "4.17"
|
||||
junit = "4.13.2"
|
||||
|
||||
|
|
@ -47,6 +48,7 @@ compose-material-icons-extended = { module = "androidx.compose.material:material
|
|||
reorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "reorderable" }
|
||||
androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "navigation3" }
|
||||
androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "navigation3" }
|
||||
compose-material3-adaptive-navigation3 = { module = "androidx.compose.material3.adaptive:adaptive-navigation3", version.ref = "adaptiveNavigation3" }
|
||||
readium-shared = { module = "org.readium.kotlin-toolkit:readium-shared", version.ref = "readium" }
|
||||
readium-streamer = { module = "org.readium.kotlin-toolkit:readium-streamer", version.ref = "readium" }
|
||||
readium-navigator = { module = "org.readium.kotlin-toolkit:readium-navigator", version.ref = "readium" }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue