Skip to content

Commit c7883db

Browse files
authored
R812: migrate Settings layouts to window width classes (#918)
Co-authored-by: ryacub <ryacub@users.noreply.github.com>
1 parent aff6567 commit c7883db

9 files changed

Lines changed: 257 additions & 72 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
2626
### Improved
2727
- The reader and player rotation controls now disable on Android 16 large screens, because the system ignores orientation requests there. The app shows a short explanation, and the stored orientation preference remains available for use on smaller screens.
2828
- Home navigation now uses a bottom bar at Compact width, a navigation rail at Medium width, and a permanent drawer at Expanded width.
29+
- Settings and player settings now use one-pane navigation at Compact and Medium widths and two-pane navigation at Expanded width, while retaining the selected destination when the window changes size.
2930

3031
### Fixed
3132
- Backup restore now prunes deleted-category IDs from Updates and Upcoming category filter preferences.

app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsMainScreen.kt

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -107,27 +107,38 @@ object SettingsMainScreen : Screen() {
107107
containerColor = containerColor,
108108
content = { contentPadding ->
109109
val state = rememberLazyListState()
110-
val indexSelected = if (twoPane) {
111-
items.indexOfFirst { it.screen::class == navigator.items.first()::class }
112-
.also {
113-
LaunchedEffect(Unit) {
114-
state.animateScrollToItem(it)
115-
if (it > 0) {
116-
// Lift scroll
117-
topBarState.contentOffset = topBarState.heightOffsetLimit
118-
}
119-
}
110+
val settingsItems = items
111+
val selectedScreenClass = if (twoPane) {
112+
navigator.items.asReversed()
113+
.asSequence()
114+
.map { it::class }
115+
.firstOrNull { screenClass ->
116+
settingsItems.any { it.screen::class == screenClass }
120117
}
121118
} else {
122119
null
123120
}
121+
val indexSelected = if (twoPane) {
122+
settingsItems.indexOfFirst { it.screen::class == selectedScreenClass }
123+
} else {
124+
null
125+
}
126+
if (indexSelected != null && indexSelected >= 0) {
127+
LaunchedEffect(selectedScreenClass) {
128+
state.animateScrollToItem(indexSelected)
129+
if (indexSelected > 0) {
130+
// Lift scroll
131+
topBarState.contentOffset = topBarState.heightOffsetLimit
132+
}
133+
}
134+
}
124135

125136
LazyColumn(
126137
state = state,
127138
contentPadding = contentPadding,
128139
) {
129140
itemsIndexed(
130-
items = items,
141+
items = settingsItems,
131142
key = { _, item ->
132143
val screenName = item.screen::class.qualifiedName
133144
?: item.screen::class.simpleName

app/src/main/java/eu/kanade/presentation/more/settings/screen/player/PlayerSettingsMainScreen.kt

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -109,20 +109,30 @@ class PlayerSettingsMainScreen(private val mainSettings: Boolean) : Screen() {
109109
containerColor = containerColor,
110110
content = { contentPadding ->
111111
val state = rememberLazyListState()
112-
val indexSelected = if (twoPane) {
113-
items.indexOfFirst { it.screen::class == navigator.items.first()::class }
114-
.also {
115-
LaunchedEffect(Unit) {
116-
state.animateScrollToItem(it)
117-
if (it > 0) {
118-
// Lift scroll
119-
topBarState.contentOffset = topBarState.heightOffsetLimit
120-
}
121-
}
112+
val selectedScreenClass = if (twoPane) {
113+
navigator.items.asReversed()
114+
.asSequence()
115+
.map { it::class }
116+
.firstOrNull { screenClass ->
117+
items.any { it.screen::class == screenClass }
122118
}
123119
} else {
124120
null
125121
}
122+
val indexSelected = if (twoPane) {
123+
items.indexOfFirst { it.screen::class == selectedScreenClass }
124+
} else {
125+
null
126+
}
127+
if (indexSelected != null && indexSelected >= 0) {
128+
LaunchedEffect(selectedScreenClass) {
129+
state.animateScrollToItem(indexSelected)
130+
if (indexSelected > 0) {
131+
// Lift scroll
132+
topBarState.contentOffset = topBarState.heightOffsetLimit
133+
}
134+
}
135+
}
126136

127137
LazyColumn(
128138
state = state,
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package eu.kanade.presentation.util
2+
3+
/** Removes the one-pane root while keeping the selected destination stack. */
4+
fun <T> settingsStackForTwoPane(
5+
screens: List<T>,
6+
isMainScreen: (T) -> Boolean,
7+
defaultScreen: T,
8+
): List<T> {
9+
if (screens.firstOrNull()?.let(isMainScreen) != true) return screens
10+
return if (screens.size == 1) listOf(defaultScreen) else screens.drop(1)
11+
}
12+
13+
/** Adds the one-pane root before a destination when the stack enters one-pane mode. */
14+
fun <T> settingsStackForSinglePane(
15+
screens: List<T>,
16+
isMainScreen: (T) -> Boolean,
17+
mainScreen: T,
18+
): List<T> {
19+
return if (screens.firstOrNull()?.let(isMainScreen) == true) {
20+
screens
21+
} else {
22+
listOf(mainScreen) + screens
23+
}
24+
}

app/src/main/java/eu/kanade/presentation/util/WindowWidthClass.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,21 @@ fun homeNavigationLayoutFor(windowWidthClass: WindowWidthClass): HomeNavigationL
3535
WindowWidthClass.Expanded -> HomeNavigationLayout.Drawer
3636
}
3737

38+
/** The navigation layout that Settings uses for a [WindowWidthClass]. */
39+
enum class SettingsNavigationLayout {
40+
SinglePane,
41+
TwoPane,
42+
}
43+
44+
fun settingsNavigationLayoutFor(
45+
windowWidthClass: WindowWidthClass,
46+
): SettingsNavigationLayout = when (windowWidthClass) {
47+
WindowWidthClass.Compact,
48+
WindowWidthClass.Medium,
49+
-> SettingsNavigationLayout.SinglePane
50+
WindowWidthClass.Expanded -> SettingsNavigationLayout.TwoPane
51+
}
52+
3853
/** Lower bound of [WindowWidthClass.Medium], in dp. */
3954
const val MEDIUM_WIDTH_BREAKPOINT_DP = WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND
4055

app/src/main/java/eu/kanade/tachiyomi/ui/setting/PlayerSettingsScreen.kt

Lines changed: 54 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ import androidx.compose.foundation.layout.systemBars
88
import androidx.compose.foundation.layout.windowInsetsPadding
99
import androidx.compose.runtime.Composable
1010
import androidx.compose.runtime.CompositionLocalProvider
11+
import androidx.compose.runtime.LaunchedEffect
12+
import androidx.compose.runtime.getValue
13+
import androidx.compose.runtime.mutableStateOf
14+
import androidx.compose.runtime.remember
15+
import androidx.compose.runtime.setValue
1116
import androidx.compose.ui.Modifier
1217
import cafe.adriel.voyager.navigator.LocalNavigator
1318
import cafe.adriel.voyager.navigator.Navigator
@@ -17,45 +22,73 @@ import eu.kanade.presentation.more.settings.screen.player.PlayerSettingsPlayerSc
1722
import eu.kanade.presentation.util.DefaultNavigatorScreenTransition
1823
import eu.kanade.presentation.util.LocalBackPress
1924
import eu.kanade.presentation.util.Screen
20-
import eu.kanade.presentation.util.isTabletUi
25+
import eu.kanade.presentation.util.SettingsNavigationLayout
26+
import eu.kanade.presentation.util.currentWindowWidthClass
27+
import eu.kanade.presentation.util.settingsNavigationLayoutFor
28+
import eu.kanade.presentation.util.settingsStackForSinglePane
29+
import eu.kanade.presentation.util.settingsStackForTwoPane
2130
import tachiyomi.presentation.core.components.TwoPanelBox
2231

2332
class PlayerSettingsScreen(private val mainSettings: Boolean) : Screen() {
2433
@Composable
2534
override fun Content() {
2635
val parentNavigator = LocalNavigator.currentOrThrow
27-
if (!isTabletUi()) {
28-
Navigator(
29-
screen = PlayerSettingsMainScreen(mainSettings),
30-
content = {
31-
val pop: () -> Unit = {
32-
if (it.canPop) {
33-
it.pop()
34-
} else {
35-
parentNavigator.pop()
36-
}
36+
val navigationLayout = settingsNavigationLayoutFor(currentWindowWidthClass())
37+
val twoPane = navigationLayout == SettingsNavigationLayout.TwoPane
38+
val mainScreen = remember(mainSettings) { PlayerSettingsMainScreen(mainSettings) }
39+
var previousNavigationLayout by remember { mutableStateOf<SettingsNavigationLayout?>(null) }
40+
Navigator(
41+
screen = if (twoPane) PlayerSettingsPlayerScreen else mainScreen,
42+
) { navigator ->
43+
LaunchedEffect(navigationLayout) {
44+
val previousLayout = previousNavigationLayout
45+
if (twoPane && previousLayout != SettingsNavigationLayout.TwoPane) {
46+
val screens = settingsStackForTwoPane(
47+
screens = navigator.items,
48+
isMainScreen = { it::class == mainScreen::class },
49+
defaultScreen = PlayerSettingsPlayerScreen,
50+
)
51+
if (screens != navigator.items) {
52+
navigator.replaceAll(screens)
3753
}
38-
CompositionLocalProvider(LocalBackPress provides pop) {
39-
DefaultNavigatorScreenTransition(navigator = it)
54+
} else if (!twoPane &&
55+
(previousLayout == null || previousLayout == SettingsNavigationLayout.TwoPane)
56+
) {
57+
val screens = settingsStackForSinglePane(
58+
screens = navigator.items,
59+
isMainScreen = { it::class == mainScreen::class },
60+
mainScreen = mainScreen,
61+
)
62+
if (screens != navigator.items) {
63+
navigator.replaceAll(screens)
4064
}
41-
},
42-
)
43-
} else {
44-
Navigator(
45-
screen = PlayerSettingsPlayerScreen,
46-
) {
65+
}
66+
previousNavigationLayout = navigationLayout
67+
}
68+
if (twoPane) {
4769
val insets = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal)
4870
TwoPanelBox(
4971
modifier = Modifier
5072
.windowInsetsPadding(insets)
5173
.consumeWindowInsets(insets),
5274
startContent = {
5375
CompositionLocalProvider(LocalBackPress provides parentNavigator::pop) {
54-
PlayerSettingsMainScreen(mainSettings).Content(twoPane = true)
76+
mainScreen.Content(twoPane = true)
5577
}
5678
},
57-
endContent = { DefaultNavigatorScreenTransition(navigator = it) },
79+
endContent = { DefaultNavigatorScreenTransition(navigator = navigator) },
5880
)
81+
} else {
82+
val pop: () -> Unit = {
83+
if (navigator.canPop) {
84+
navigator.pop()
85+
} else {
86+
parentNavigator.pop()
87+
}
88+
}
89+
CompositionLocalProvider(LocalBackPress provides pop) {
90+
DefaultNavigatorScreenTransition(navigator = navigator)
91+
}
5992
}
6093
}
6194
}

app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsScreen.kt

Lines changed: 57 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ import androidx.compose.foundation.layout.systemBars
88
import androidx.compose.foundation.layout.windowInsetsPadding
99
import androidx.compose.runtime.Composable
1010
import androidx.compose.runtime.CompositionLocalProvider
11+
import androidx.compose.runtime.LaunchedEffect
12+
import androidx.compose.runtime.getValue
13+
import androidx.compose.runtime.mutableStateOf
14+
import androidx.compose.runtime.remember
15+
import androidx.compose.runtime.setValue
1116
import androidx.compose.ui.Modifier
1217
import cafe.adriel.voyager.navigator.LocalNavigator
1318
import cafe.adriel.voyager.navigator.Navigator
@@ -20,7 +25,11 @@ import eu.kanade.presentation.more.settings.screen.about.AboutScreen
2025
import eu.kanade.presentation.util.DefaultNavigatorScreenTransition
2126
import eu.kanade.presentation.util.LocalBackPress
2227
import eu.kanade.presentation.util.Screen
23-
import eu.kanade.presentation.util.isTabletUi
28+
import eu.kanade.presentation.util.SettingsNavigationLayout
29+
import eu.kanade.presentation.util.currentWindowWidthClass
30+
import eu.kanade.presentation.util.settingsNavigationLayoutFor
31+
import eu.kanade.presentation.util.settingsStackForSinglePane
32+
import eu.kanade.presentation.util.settingsStackForTwoPane
2433
import tachiyomi.presentation.core.components.TwoPanelBox
2534

2635
class SettingsScreen(
@@ -32,36 +41,43 @@ class SettingsScreen(
3241
@Composable
3342
override fun Content() {
3443
val parentNavigator = LocalNavigator.currentOrThrow
35-
if (!isTabletUi()) {
36-
Navigator(
37-
screen = when (destination) {
38-
Destination.About.id -> AboutScreen
39-
Destination.DataAndStorage.id -> SettingsDataScreen
40-
Destination.Tracking.id -> SettingsTrackingScreen
41-
else -> SettingsMainScreen
42-
},
43-
content = {
44-
val pop: () -> Unit = {
45-
if (it.canPop) {
46-
it.pop()
47-
} else {
48-
parentNavigator.pop()
49-
}
44+
val navigationLayout = settingsNavigationLayoutFor(currentWindowWidthClass())
45+
val twoPane = navigationLayout == SettingsNavigationLayout.TwoPane
46+
var previousNavigationLayout by remember { mutableStateOf<SettingsNavigationLayout?>(null) }
47+
Navigator(
48+
screen = when (destination) {
49+
Destination.About.id -> AboutScreen
50+
Destination.DataAndStorage.id -> SettingsDataScreen
51+
Destination.Tracking.id -> SettingsTrackingScreen
52+
else -> if (twoPane) SettingsAppearanceScreen else SettingsMainScreen
53+
},
54+
) { navigator ->
55+
LaunchedEffect(navigationLayout) {
56+
val previousLayout = previousNavigationLayout
57+
if (twoPane && previousLayout != SettingsNavigationLayout.TwoPane) {
58+
val screens = settingsStackForTwoPane(
59+
screens = navigator.items,
60+
isMainScreen = { it::class == SettingsMainScreen::class },
61+
defaultScreen = SettingsAppearanceScreen,
62+
)
63+
if (screens != navigator.items) {
64+
navigator.replaceAll(screens)
5065
}
51-
CompositionLocalProvider(LocalBackPress provides pop) {
52-
DefaultNavigatorScreenTransition(navigator = it)
66+
} else if (!twoPane && destination == null &&
67+
(previousLayout == null || previousLayout == SettingsNavigationLayout.TwoPane)
68+
) {
69+
val screens = settingsStackForSinglePane(
70+
screens = navigator.items,
71+
isMainScreen = { it::class == SettingsMainScreen::class },
72+
mainScreen = SettingsMainScreen,
73+
)
74+
if (screens != navigator.items) {
75+
navigator.replaceAll(screens)
5376
}
54-
},
55-
)
56-
} else {
57-
Navigator(
58-
screen = when (destination) {
59-
Destination.About.id -> AboutScreen
60-
Destination.DataAndStorage.id -> SettingsDataScreen
61-
Destination.Tracking.id -> SettingsTrackingScreen
62-
else -> SettingsAppearanceScreen
63-
},
64-
) {
77+
}
78+
previousNavigationLayout = navigationLayout
79+
}
80+
if (twoPane) {
6581
val insets = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal)
6682
TwoPanelBox(
6783
modifier = Modifier
@@ -72,8 +88,19 @@ class SettingsScreen(
7288
SettingsMainScreen.Content(twoPane = true)
7389
}
7490
},
75-
endContent = { DefaultNavigatorScreenTransition(navigator = it) },
91+
endContent = { DefaultNavigatorScreenTransition(navigator = navigator) },
7692
)
93+
} else {
94+
val pop: () -> Unit = {
95+
if (navigator.canPop) {
96+
navigator.pop()
97+
} else {
98+
parentNavigator.pop()
99+
}
100+
}
101+
CompositionLocalProvider(LocalBackPress provides pop) {
102+
DefaultNavigatorScreenTransition(navigator = navigator)
103+
}
77104
}
78105
}
79106
}

0 commit comments

Comments
 (0)