Преглед изворни кода

Merge Voyager screens (#8656)

* Merge Voyager screens

* cleanups
Ivan Iskandar пре 2 година
родитељ
комит
3d66eaea83
67 измењених фајлова са 1176 додато и 1987 уклоњено
  1. 2 4
      app/build.gradle.kts
  2. 45 45
      app/src/main/java/eu/kanade/presentation/components/MangaBottomActionMenu.kt
  3. 48 0
      app/src/main/java/eu/kanade/presentation/components/NavigationBar.kt
  4. 59 0
      app/src/main/java/eu/kanade/presentation/components/NavigationRail.kt
  5. 9 1
      app/src/main/java/eu/kanade/presentation/components/Scaffold.kt
  6. 1 4
      app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt
  7. 0 3
      app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt
  8. 4 11
      app/src/main/java/eu/kanade/presentation/more/MoreScreen.kt
  9. 144 0
      app/src/main/java/eu/kanade/presentation/more/NewUpdateScreen.kt
  10. 12 7
      app/src/main/java/eu/kanade/presentation/more/settings/screen/AboutScreen.kt
  11. 0 3
      app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt
  12. 5 6
      app/src/main/java/eu/kanade/presentation/util/Navigator.kt
  13. 2 2
      app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt
  14. 2 2
      app/src/main/java/eu/kanade/tachiyomi/glance/UpdatesGridGlanceWidget.kt
  15. 0 86
      app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/BaseController.kt
  16. 0 49
      app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/ComposeController.kt
  17. 0 25
      app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/ConductorExtensions.kt
  18. 0 119
      app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/DialogController.kt
  19. 0 46
      app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/OneWayFadeChangeHandler.kt
  20. 0 3
      app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/RootController.kt
  21. 0 27
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseController.kt
  22. 22 4
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt
  23. 0 2
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaScreen.kt
  24. 2 7
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt
  25. 0 34
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchController.kt
  26. 14 20
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt
  27. 0 17
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesFilterController.kt
  28. 4 4
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesFilterScreen.kt
  29. 7 8
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesTab.kt
  30. 0 69
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt
  31. 8 24
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt
  32. 0 25
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchController.kt
  33. 8 9
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt
  34. 0 17
      app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryController.kt
  35. 1 8
      app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryScreen.kt
  36. 0 15
      app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadController.kt
  37. 3 3
      app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadQueueScreen.kt
  38. 0 26
      app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryController.kt
  39. 5 0
      app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryScreenModel.kt
  40. 39 7
      app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryTab.kt
  41. 288 0
      app/src/main/java/eu/kanade/tachiyomi/ui/home/HomeScreen.kt
  42. 0 53
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt
  43. 6 6
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt
  44. 99 89
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt
  45. 173 410
      app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
  46. 0 24
      app/src/main/java/eu/kanade/tachiyomi/ui/main/WhatsNewDialogController.kt
  47. 0 34
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt
  48. 26 42
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt
  49. 0 18
      app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreController.kt
  50. 39 15
      app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreTab.kt
  51. 0 62
      app/src/main/java/eu/kanade/tachiyomi/ui/more/NewUpdateDialogController.kt
  52. 41 0
      app/src/main/java/eu/kanade/tachiyomi/ui/more/NewUpdateScreen.kt
  53. 2 2
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt
  54. 0 37
      app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt
  55. 2 10
      app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsScreen.kt
  56. 0 13
      app/src/main/java/eu/kanade/tachiyomi/ui/stats/StatsController.kt
  57. 3 5
      app/src/main/java/eu/kanade/tachiyomi/ui/stats/StatsScreen.kt
  58. 0 13
      app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesController.kt
  59. 34 8
      app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt
  60. 7 0
      app/src/main/java/eu/kanade/tachiyomi/util/Constants.kt
  61. 0 196
      app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiBottomNavigationView.kt
  62. 0 53
      app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiChangeHandlerFrameLayout.kt
  63. 2 2
      app/src/main/res/drawable/anim_more_enter.xml
  64. 0 80
      app/src/main/res/layout-sw720dp/main_activity.xml
  65. 0 68
      app/src/main/res/layout/main_activity.xml
  66. 7 5
      gradle/libs.versions.toml
  67. 1 0
      i18n/src/main/res/values/strings.xml

+ 2 - 4
app/build.gradle.kts

@@ -267,15 +267,12 @@ dependencies {
         exclude(group = "androidx.viewpager", module = "viewpager")
     }
     implementation(libs.insetter)
-    implementation(libs.markwon)
+    implementation(libs.bundles.richtext)
     implementation(libs.aboutLibraries.compose)
     implementation(libs.cascade)
     implementation(libs.bundles.voyager)
     implementation(libs.wheelpicker)
 
-    // Conductor
-    implementation(libs.conductor)
-
     // FlowBinding
     implementation(libs.flowbinding.android)
 
@@ -328,6 +325,7 @@ tasks {
         kotlinOptions.freeCompilerArgs += listOf(
             "-opt-in=coil.annotation.ExperimentalCoilApi",
             "-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi",
+            "-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
             "-opt-in=androidx.compose.material.ExperimentalMaterialApi",
             "-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
             "-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",

+ 45 - 45
app/src/main/java/eu/kanade/presentation/components/MangaBottomActionMenu.kt

@@ -2,6 +2,7 @@ package eu.kanade.presentation.components
 
 import androidx.compose.animation.AnimatedVisibility
 import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.tween
 import androidx.compose.animation.expandVertically
 import androidx.compose.animation.fadeIn
 import androidx.compose.animation.fadeOut
@@ -16,10 +17,10 @@ import androidx.compose.foundation.layout.WindowInsets
 import androidx.compose.foundation.layout.WindowInsetsSides
 import androidx.compose.foundation.layout.asPaddingValues
 import androidx.compose.foundation.layout.navigationBars
-import androidx.compose.foundation.layout.navigationBarsPadding
 import androidx.compose.foundation.layout.only
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.windowInsetsPadding
 import androidx.compose.foundation.shape.ZeroCornerSize
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.outlined.BookmarkAdd
@@ -95,7 +96,11 @@ fun MangaBottomActionMenu(
             }
             Row(
                 modifier = Modifier
-                    .padding(WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues())
+                    .padding(
+                        WindowInsets.navigationBars
+                            .only(WindowInsetsSides.Bottom)
+                            .asPaddingValues(),
+                    )
                     .padding(horizontal = 8.dp, vertical = 12.dp),
             ) {
                 if (onBookmarkClicked != null) {
@@ -213,16 +218,16 @@ private fun RowScope.Button(
 fun LibraryBottomActionMenu(
     visible: Boolean,
     modifier: Modifier = Modifier,
-    onChangeCategoryClicked: (() -> Unit)?,
-    onMarkAsReadClicked: (() -> Unit)?,
-    onMarkAsUnreadClicked: (() -> Unit)?,
+    onChangeCategoryClicked: () -> Unit,
+    onMarkAsReadClicked: () -> Unit,
+    onMarkAsUnreadClicked: () -> Unit,
     onDownloadClicked: ((DownloadAction) -> Unit)?,
-    onDeleteClicked: (() -> Unit)?,
+    onDeleteClicked: () -> Unit,
 ) {
     AnimatedVisibility(
         visible = visible,
-        enter = expandVertically(expandFrom = Alignment.Bottom),
-        exit = shrinkVertically(shrinkTowards = Alignment.Bottom),
+        enter = expandVertically(animationSpec = tween(delayMillis = 300)),
+        exit = shrinkVertically(animationSpec = tween()),
     ) {
         val scope = rememberCoroutineScope()
         Surface(
@@ -244,36 +249,33 @@ fun LibraryBottomActionMenu(
             }
             Row(
                 modifier = Modifier
-                    .navigationBarsPadding()
+                    .windowInsetsPadding(
+                        WindowInsets.navigationBars
+                            .only(WindowInsetsSides.Bottom),
+                    )
                     .padding(horizontal = 8.dp, vertical = 12.dp),
             ) {
-                if (onChangeCategoryClicked != null) {
-                    Button(
-                        title = stringResource(R.string.action_move_category),
-                        icon = Icons.Outlined.Label,
-                        toConfirm = confirm[0],
-                        onLongClick = { onLongClickItem(0) },
-                        onClick = onChangeCategoryClicked,
-                    )
-                }
-                if (onMarkAsReadClicked != null) {
-                    Button(
-                        title = stringResource(R.string.action_mark_as_read),
-                        icon = Icons.Outlined.DoneAll,
-                        toConfirm = confirm[1],
-                        onLongClick = { onLongClickItem(1) },
-                        onClick = onMarkAsReadClicked,
-                    )
-                }
-                if (onMarkAsUnreadClicked != null) {
-                    Button(
-                        title = stringResource(R.string.action_mark_as_unread),
-                        icon = Icons.Outlined.RemoveDone,
-                        toConfirm = confirm[2],
-                        onLongClick = { onLongClickItem(2) },
-                        onClick = onMarkAsUnreadClicked,
-                    )
-                }
+                Button(
+                    title = stringResource(R.string.action_move_category),
+                    icon = Icons.Outlined.Label,
+                    toConfirm = confirm[0],
+                    onLongClick = { onLongClickItem(0) },
+                    onClick = onChangeCategoryClicked,
+                )
+                Button(
+                    title = stringResource(R.string.action_mark_as_read),
+                    icon = Icons.Outlined.DoneAll,
+                    toConfirm = confirm[1],
+                    onLongClick = { onLongClickItem(1) },
+                    onClick = onMarkAsReadClicked,
+                )
+                Button(
+                    title = stringResource(R.string.action_mark_as_unread),
+                    icon = Icons.Outlined.RemoveDone,
+                    toConfirm = confirm[2],
+                    onLongClick = { onLongClickItem(2) },
+                    onClick = onMarkAsUnreadClicked,
+                )
                 if (onDownloadClicked != null) {
                     var downloadExpanded by remember { mutableStateOf(false) }
                     Button(
@@ -292,15 +294,13 @@ fun LibraryBottomActionMenu(
                         )
                     }
                 }
-                if (onDeleteClicked != null) {
-                    Button(
-                        title = stringResource(R.string.action_delete),
-                        icon = Icons.Outlined.Delete,
-                        toConfirm = confirm[4],
-                        onLongClick = { onLongClickItem(4) },
-                        onClick = onDeleteClicked,
-                    )
-                }
+                Button(
+                    title = stringResource(R.string.action_delete),
+                    icon = Icons.Outlined.Delete,
+                    toConfirm = confirm[4],
+                    onLongClick = { onLongClickItem(4) },
+                    onClick = onDeleteClicked,
+                )
             }
         }
     }

+ 48 - 0
app/src/main/java/eu/kanade/presentation/components/NavigationBar.kt

@@ -0,0 +1,48 @@
+package eu.kanade.presentation.components
+
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.windowInsetsPadding
+import androidx.compose.foundation.selection.selectableGroup
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.NavigationBarDefaults
+import androidx.compose.material3.contentColorFor
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+
+/**
+ * M3 Navbar with no horizontal spacer
+ *
+ * @see [androidx.compose.material3.NavigationBar]
+ */
+@Composable
+fun NavigationBar(
+    modifier: Modifier = Modifier,
+    containerColor: Color = NavigationBarDefaults.containerColor,
+    contentColor: Color = MaterialTheme.colorScheme.contentColorFor(containerColor),
+    tonalElevation: Dp = NavigationBarDefaults.Elevation,
+    windowInsets: WindowInsets = NavigationBarDefaults.windowInsets,
+    content: @Composable RowScope.() -> Unit,
+) {
+    androidx.compose.material3.Surface(
+        color = containerColor,
+        contentColor = contentColor,
+        tonalElevation = tonalElevation,
+        modifier = modifier,
+    ) {
+        Row(
+            modifier = Modifier
+                .fillMaxWidth()
+                .windowInsetsPadding(windowInsets)
+                .height(80.dp)
+                .selectableGroup(),
+            content = content,
+        )
+    }
+}

+ 59 - 0
app/src/main/java/eu/kanade/presentation/components/NavigationRail.kt

@@ -0,0 +1,59 @@
+package eu.kanade.presentation.components
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.foundation.layout.windowInsetsPadding
+import androidx.compose.foundation.selection.selectableGroup
+import androidx.compose.material3.NavigationRailDefaults
+import androidx.compose.material3.contentColorFor
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+
+/**
+ * Center-aligned M3 Navigation rail
+ *
+ * @see [androidx.compose.material3.NavigationRail]
+ */
+@Composable
+fun NavigationRail(
+    modifier: Modifier = Modifier,
+    containerColor: Color = NavigationRailDefaults.ContainerColor,
+    contentColor: Color = contentColorFor(containerColor),
+    header: @Composable (ColumnScope.() -> Unit)? = null,
+    windowInsets: WindowInsets = NavigationRailDefaults.windowInsets,
+    content: @Composable ColumnScope.() -> Unit,
+) {
+    androidx.compose.material3.Surface(
+        color = containerColor,
+        contentColor = contentColor,
+        modifier = modifier,
+        tonalElevation = 3.dp,
+    ) {
+        Column(
+            Modifier
+                .fillMaxHeight()
+                .windowInsetsPadding(windowInsets)
+                .widthIn(min = 80.dp)
+                .padding(vertical = 4.dp)
+                .selectableGroup(),
+            horizontalAlignment = Alignment.CenterHorizontally,
+            verticalArrangement = Arrangement.spacedBy(space = 4.dp, alignment = Alignment.CenterVertically),
+        ) {
+            if (header != null) {
+                header()
+                Spacer(Modifier.height(8.dp))
+            }
+            content()
+        }
+    }
+}

+ 9 - 1
app/src/main/java/eu/kanade/presentation/components/Scaffold.kt

@@ -16,11 +16,14 @@
 
 package eu.kanade.presentation.components
 
+import androidx.compose.foundation.layout.MutableWindowInsets
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.WindowInsets
 import androidx.compose.foundation.layout.asPaddingValues
 import androidx.compose.foundation.layout.calculateEndPadding
 import androidx.compose.foundation.layout.calculateStartPadding
+import androidx.compose.foundation.layout.exclude
+import androidx.compose.foundation.layout.withConsumedWindowInsets
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.ScaffoldDefaults
@@ -31,6 +34,7 @@ import androidx.compose.material3.rememberTopAppBarState
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.remember
 import androidx.compose.runtime.staticCompositionLocalOf
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
@@ -67,6 +71,7 @@ import kotlin.math.max
  * * Remove height constraint for expanded app bar
  * * Also take account of fab height when providing inner padding
  * * Fixes for fab and snackbar horizontal placements when [contentWindowInsets] is used
+ * * Handle consumed window insets
  *
  * @param modifier the [Modifier] to be applied to this scaffold
  * @param topBar top app bar of the screen, typically a [SmallTopAppBar]
@@ -103,9 +108,12 @@ fun Scaffold(
     contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets,
     content: @Composable (PaddingValues) -> Unit,
 ) {
+    // Tachiyomi: Handle consumed window insets
+    val remainingWindowInsets = remember { MutableWindowInsets() }
     androidx.compose.material3.Surface(
         modifier = Modifier
             .nestedScroll(topBarScrollBehavior.nestedScrollConnection)
+            .withConsumedWindowInsets { remainingWindowInsets.insets = contentWindowInsets.exclude(it) }
             .then(modifier),
         color = containerColor,
         contentColor = contentColor,
@@ -116,7 +124,7 @@ fun Scaffold(
             bottomBar = bottomBar,
             content = content,
             snackbar = snackbarHost,
-            contentWindowInsets = contentWindowInsets,
+            contentWindowInsets = remainingWindowInsets,
             fab = floatingActionButton,
         )
     }

+ 1 - 4
app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt

@@ -20,7 +20,6 @@ import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.res.stringResource
-import eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView
 import kotlinx.coroutines.launch
 
 @Composable
@@ -88,9 +87,7 @@ fun TabbedScreen(
                 verticalAlignment = Alignment.Top,
             ) { page ->
                 tabs[page].content(
-                    TachiyomiBottomNavigationView.withBottomNavPadding(
-                        PaddingValues(bottom = contentPadding.calculateBottomPadding()),
-                    ),
+                    PaddingValues(bottom = contentPadding.calculateBottomPadding()),
                     snackbarHostState,
                 )
             }

+ 0 - 3
app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt

@@ -5,7 +5,6 @@ import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.outlined.DeleteSweep
 import androidx.compose.material3.Icon
 import androidx.compose.material3.IconButton
-import androidx.compose.material3.ScaffoldDefaults
 import androidx.compose.material3.SnackbarHost
 import androidx.compose.material3.SnackbarHostState
 import androidx.compose.runtime.Composable
@@ -21,7 +20,6 @@ import eu.kanade.presentation.history.components.HistoryContent
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.ui.history.HistoryScreenModel
 import eu.kanade.tachiyomi.ui.history.HistoryState
-import eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView
 import java.util.Date
 
 @Composable
@@ -55,7 +53,6 @@ fun HistoryScreen(
             )
         },
         snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
-        contentWindowInsets = TachiyomiBottomNavigationView.withBottomNavInset(ScaffoldDefaults.contentWindowInsets),
     ) { contentPadding ->
         state.list.let {
             if (it == null) {

+ 4 - 11
app/src/main/java/eu/kanade/presentation/more/MoreScreen.kt

@@ -1,10 +1,7 @@
 package eu.kanade.presentation.more
 
 import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.WindowInsets
-import androidx.compose.foundation.layout.asPaddingValues
-import androidx.compose.foundation.layout.navigationBars
-import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.foundation.layout.systemBarsPadding
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.outlined.CloudOff
 import androidx.compose.material.icons.outlined.GetApp
@@ -29,8 +26,7 @@ import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
 import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.ui.more.DownloadQueueState
-import eu.kanade.tachiyomi.ui.more.MoreController
-import eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView
+import eu.kanade.tachiyomi.util.Constants
 
 @Composable
 fun MoreScreen(
@@ -50,10 +46,7 @@ fun MoreScreen(
     val uriHandler = LocalUriHandler.current
 
     ScrollbarLazyColumn(
-        modifier = Modifier.statusBarsPadding(),
-        contentPadding = TachiyomiBottomNavigationView.withBottomNavPadding(
-            WindowInsets.navigationBars.asPaddingValues(),
-        ),
+        modifier = Modifier.systemBarsPadding(),
     ) {
         if (isFDroid) {
             item {
@@ -169,7 +162,7 @@ fun MoreScreen(
             TextPreferenceWidget(
                 title = stringResource(R.string.label_help),
                 icon = Icons.Outlined.HelpOutline,
-                onPreferenceClick = { uriHandler.openUri(MoreController.URL_HELP) },
+                onPreferenceClick = { uriHandler.openUri(Constants.URL_HELP) },
             )
         }
     }

+ 144 - 0
app/src/main/java/eu/kanade/presentation/more/NewUpdateScreen.kt

@@ -0,0 +1,144 @@
+package eu.kanade.presentation.more
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.windowInsetsPadding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.OpenInNew
+import androidx.compose.material.icons.outlined.NewReleases
+import androidx.compose.material3.Button
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.NavigationBarDefaults
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.zIndex
+import com.halilibo.richtext.markdown.Markdown
+import com.halilibo.richtext.ui.RichTextStyle
+import com.halilibo.richtext.ui.material3.Material3RichText
+import com.halilibo.richtext.ui.string.RichTextStringStyle
+import eu.kanade.presentation.components.Scaffold
+import eu.kanade.presentation.util.padding
+import eu.kanade.presentation.util.secondaryItemAlpha
+import eu.kanade.tachiyomi.R
+
+@Composable
+fun NewUpdateScreen(
+    versionName: String,
+    changelogInfo: String,
+    onOpenInBrowser: () -> Unit,
+    onRejectUpdate: () -> Unit,
+    onAcceptUpdate: () -> Unit,
+) {
+    Scaffold(
+        bottomBar = {
+            val strokeWidth = Dp.Hairline
+            val borderColor = MaterialTheme.colorScheme.outline
+            Column(
+                modifier = Modifier
+                    .background(MaterialTheme.colorScheme.background)
+                    .drawBehind {
+                        drawLine(
+                            borderColor,
+                            Offset(0f, 0f),
+                            Offset(size.width, 0f),
+                            strokeWidth.value,
+                        )
+                    }
+                    .windowInsetsPadding(NavigationBarDefaults.windowInsets)
+                    .padding(
+                        horizontal = MaterialTheme.padding.medium,
+                        vertical = MaterialTheme.padding.small,
+                    ),
+            ) {
+                Button(
+                    modifier = Modifier.fillMaxWidth(),
+                    onClick = onAcceptUpdate,
+                ) {
+                    Text(text = stringResource(id = R.string.update_check_confirm))
+                }
+                TextButton(
+                    modifier = Modifier.fillMaxWidth(),
+                    onClick = onRejectUpdate,
+                ) {
+                    Text(text = stringResource(R.string.action_not_now))
+                }
+            }
+        },
+    ) { paddingValues ->
+        // Status bar scrim
+        Box(
+            modifier = Modifier
+                .zIndex(2f)
+                .secondaryItemAlpha()
+                .background(MaterialTheme.colorScheme.background)
+                .fillMaxWidth()
+                .height(paddingValues.calculateTopPadding()),
+        )
+
+        Column(
+            modifier = Modifier
+                .verticalScroll(rememberScrollState())
+                .padding(paddingValues)
+                .padding(top = 48.dp)
+                .padding(horizontal = MaterialTheme.padding.medium),
+        ) {
+            Icon(
+                imageVector = Icons.Outlined.NewReleases,
+                contentDescription = null,
+                modifier = Modifier
+                    .padding(bottom = MaterialTheme.padding.small)
+                    .size(48.dp),
+                tint = MaterialTheme.colorScheme.primary,
+            )
+            Text(
+                text = stringResource(R.string.update_check_notification_update_available),
+                style = MaterialTheme.typography.headlineLarge,
+            )
+            Text(
+                text = versionName,
+                modifier = Modifier.secondaryItemAlpha(),
+                style = MaterialTheme.typography.titleSmall,
+            )
+
+            Material3RichText(
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .padding(vertical = MaterialTheme.padding.large),
+                style = RichTextStyle(
+                    stringStyle = RichTextStringStyle(
+                        linkStyle = SpanStyle(color = MaterialTheme.colorScheme.primary),
+                    ),
+                ),
+            ) {
+                Markdown(content = changelogInfo)
+
+                TextButton(
+                    onClick = onOpenInBrowser,
+                    modifier = Modifier.padding(top = MaterialTheme.padding.small),
+                ) {
+                    Text(text = stringResource(R.string.update_check_open))
+                    Spacer(modifier = Modifier.width(MaterialTheme.padding.tiny))
+                    Icon(imageVector = Icons.Default.OpenInNew, contentDescription = null)
+                }
+            }
+        }
+    }
+}

+ 12 - 7
app/src/main/java/eu/kanade/presentation/more/settings/screen/AboutScreen.kt

@@ -19,7 +19,6 @@ import androidx.compose.ui.unit.dp
 import cafe.adriel.voyager.core.screen.Screen
 import cafe.adriel.voyager.navigator.LocalNavigator
 import cafe.adriel.voyager.navigator.currentOrThrow
-import com.bluelinelabs.conductor.Router
 import eu.kanade.domain.ui.UiPreferences
 import eu.kanade.presentation.components.AppBar
 import eu.kanade.presentation.components.LinkIcon
@@ -29,13 +28,12 @@ import eu.kanade.presentation.more.LogoHeader
 import eu.kanade.presentation.more.about.LicensesScreen
 import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
 import eu.kanade.presentation.util.LocalBackPress
-import eu.kanade.presentation.util.LocalRouter
 import eu.kanade.tachiyomi.BuildConfig
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.updater.AppUpdateChecker
 import eu.kanade.tachiyomi.data.updater.AppUpdateResult
 import eu.kanade.tachiyomi.data.updater.RELEASE_URL
-import eu.kanade.tachiyomi.ui.more.NewUpdateDialogController
+import eu.kanade.tachiyomi.ui.more.NewUpdateScreen
 import eu.kanade.tachiyomi.util.CrashLogUtil
 import eu.kanade.tachiyomi.util.lang.toDateTimestampString
 import eu.kanade.tachiyomi.util.lang.withIOContext
@@ -61,7 +59,6 @@ object AboutScreen : Screen {
         val uriHandler = LocalUriHandler.current
         val handleBack = LocalBackPress.current
         val navigator = LocalNavigator.currentOrThrow
-        val router = LocalRouter.currentOrThrow
 
         Scaffold(
             topBar = { scrollBehavior ->
@@ -96,7 +93,15 @@ object AboutScreen : Screen {
                             title = stringResource(R.string.check_for_updates),
                             onPreferenceClick = {
                                 scope.launch {
-                                    checkVersion(context, router)
+                                    checkVersion(context) { result ->
+                                        val updateScreen = NewUpdateScreen(
+                                            versionName = result.release.version,
+                                            changelogInfo = result.release.info,
+                                            releaseLink = result.release.releaseLink,
+                                            downloadLink = result.release.getDownloadLink(),
+                                        )
+                                        navigator.push(updateScreen)
+                                    }
                                 }
                             },
                         )
@@ -178,14 +183,14 @@ object AboutScreen : Screen {
     /**
      * Checks version and shows a user prompt if an update is available.
      */
-    private suspend fun checkVersion(context: Context, router: Router) {
+    private suspend fun checkVersion(context: Context, onAvailableUpdate: (AppUpdateResult.NewUpdate) -> Unit) {
         val updateChecker = AppUpdateChecker()
         withUIContext {
             context.toast(R.string.update_check_look_for_updates)
             try {
                 when (val result = withIOContext { updateChecker.checkForUpdate(context, isUserPrompt = true) }) {
                     is AppUpdateResult.NewUpdate -> {
-                        NewUpdateDialogController(result).showDialog(router)
+                        onAvailableUpdate(result)
                     }
                     is AppUpdateResult.NoNewUpdate -> {
                         context.toast(R.string.update_check_no_new_updates)

+ 0 - 3
app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt

@@ -9,7 +9,6 @@ import androidx.compose.material.icons.outlined.Refresh
 import androidx.compose.material.icons.outlined.SelectAll
 import androidx.compose.material3.Icon
 import androidx.compose.material3.IconButton
-import androidx.compose.material3.ScaffoldDefaults
 import androidx.compose.material3.SnackbarHost
 import androidx.compose.material3.SnackbarHostState
 import androidx.compose.material3.TopAppBarScrollBehavior
@@ -36,7 +35,6 @@ import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.download.model.Download
 import eu.kanade.tachiyomi.ui.updates.UpdatesItem
 import eu.kanade.tachiyomi.ui.updates.UpdatesState
-import eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
 import kotlin.time.Duration.Companion.seconds
@@ -87,7 +85,6 @@ fun UpdateScreen(
             )
         },
         snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
-        contentWindowInsets = TachiyomiBottomNavigationView.withBottomNavInset(ScaffoldDefaults.contentWindowInsets),
     ) { contentPadding ->
         when {
             state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding))

+ 5 - 6
app/src/main/java/eu/kanade/presentation/util/Navigator.kt

@@ -4,12 +4,7 @@ import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.runtime.ProvidableCompositionLocal
 import androidx.compose.runtime.compositionLocalOf
 import androidx.compose.runtime.staticCompositionLocalOf
-import com.bluelinelabs.conductor.Router
-
-/**
- * For interop with Conductor
- */
-val LocalRouter: ProvidableCompositionLocal<Router?> = staticCompositionLocalOf { null }
+import cafe.adriel.voyager.navigator.Navigator
 
 /**
  * For invoking back press to the parent activity
@@ -17,3 +12,7 @@ val LocalRouter: ProvidableCompositionLocal<Router?> = staticCompositionLocalOf
 val LocalBackPress: ProvidableCompositionLocal<(() -> Unit)?> = staticCompositionLocalOf { null }
 
 val LocalNavigatorContentPadding: ProvidableCompositionLocal<PaddingValues> = compositionLocalOf { PaddingValues() }
+
+interface Tab : cafe.adriel.voyager.navigator.tab.Tab {
+    suspend fun onReselect(navigator: Navigator) {}
+}

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt

@@ -23,8 +23,8 @@ import eu.kanade.tachiyomi.data.library.LibraryUpdateService
 import eu.kanade.tachiyomi.data.updater.AppUpdateService
 import eu.kanade.tachiyomi.source.SourceManager
 import eu.kanade.tachiyomi.ui.main.MainActivity
-import eu.kanade.tachiyomi.ui.manga.MangaController
 import eu.kanade.tachiyomi.ui.reader.ReaderActivity
+import eu.kanade.tachiyomi.util.Constants
 import eu.kanade.tachiyomi.util.lang.launchIO
 import eu.kanade.tachiyomi.util.storage.DiskUtil
 import eu.kanade.tachiyomi.util.storage.getUriCompat
@@ -457,7 +457,7 @@ class NotificationReceiver : BroadcastReceiver() {
             val newIntent =
                 Intent(context, MainActivity::class.java).setAction(MainActivity.SHORTCUT_MANGA)
                     .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
-                    .putExtra(MangaController.MANGA_EXTRA, manga.id)
+                    .putExtra(Constants.MANGA_EXTRA, manga.id)
                     .putExtra("notificationId", manga.id.hashCode())
                     .putExtra("groupId", groupId)
             return PendingIntent.getActivity(context, manga.id.hashCode(), newIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/glance/UpdatesGridGlanceWidget.kt

@@ -48,7 +48,7 @@ import eu.kanade.domain.manga.model.MangaCover
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.core.security.SecurityPreferences
 import eu.kanade.tachiyomi.ui.main.MainActivity
-import eu.kanade.tachiyomi.ui.manga.MangaController
+import eu.kanade.tachiyomi.util.Constants
 import eu.kanade.tachiyomi.util.lang.launchIO
 import eu.kanade.tachiyomi.util.system.dpToPx
 import kotlinx.coroutines.MainScope
@@ -136,7 +136,7 @@ class UpdatesGridGlanceWidget : GlanceAppWidget() {
                                 ) {
                                     val intent = Intent(LocalContext.current, MainActivity::class.java).apply {
                                         action = MainActivity.SHORTCUT_MANGA
-                                        putExtra(MangaController.MANGA_EXTRA, mangaId)
+                                        putExtra(Constants.MANGA_EXTRA, mangaId)
                                         addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                                         addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
 

+ 0 - 86
app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/BaseController.kt

@@ -1,86 +0,0 @@
-package eu.kanade.tachiyomi.ui.base.controller
-
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.appcompat.app.AppCompatActivity
-import androidx.viewbinding.ViewBinding
-import com.bluelinelabs.conductor.Controller
-import com.bluelinelabs.conductor.ControllerChangeHandler
-import com.bluelinelabs.conductor.ControllerChangeType
-import eu.kanade.tachiyomi.util.system.logcat
-import eu.kanade.tachiyomi.util.view.hideKeyboard
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.MainScope
-import kotlinx.coroutines.cancel
-
-abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) : Controller(bundle) {
-
-    protected lateinit var binding: VB
-        private set
-
-    lateinit var viewScope: CoroutineScope
-
-    init {
-        retainViewMode = RetainViewMode.RETAIN_DETACH
-
-        addLifecycleListener(
-            object : LifecycleListener() {
-                override fun postCreateView(controller: Controller, view: View) {
-                    onViewCreated(view)
-                }
-
-                override fun preCreateView(controller: Controller) {
-                    viewScope = MainScope()
-                    logcat { "Create view for ${controller.instance()}" }
-                }
-
-                override fun preAttach(controller: Controller, view: View) {
-                    logcat { "Attach view for ${controller.instance()}" }
-                }
-
-                override fun preDetach(controller: Controller, view: View) {
-                    logcat { "Detach view for ${controller.instance()}" }
-                }
-
-                override fun preDestroyView(controller: Controller, view: View) {
-                    viewScope.cancel()
-                    logcat { "Destroy view for ${controller.instance()}" }
-                }
-            },
-        )
-    }
-
-    abstract fun createBinding(inflater: LayoutInflater): VB
-
-    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedViewState: Bundle?): View {
-        binding = createBinding(inflater)
-        return binding.root
-    }
-
-    open fun onViewCreated(view: View) {}
-
-    override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
-        view?.hideKeyboard()
-
-        if (type.isEnter) {
-            setTitle()
-            setHasOptionsMenu(true)
-        }
-
-        super.onChangeStarted(handler, type)
-    }
-
-    open fun getTitle(): String? {
-        return null
-    }
-
-    fun setTitle(title: String? = null) {
-        (activity as? AppCompatActivity)?.supportActionBar?.title = title ?: getTitle()
-    }
-
-    private fun Controller.instance(): String {
-        return "${javaClass.simpleName}@${Integer.toHexString(hashCode())}"
-    }
-}

+ 0 - 49
app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/ComposeController.kt

@@ -1,49 +0,0 @@
-package eu.kanade.tachiyomi.ui.base.controller
-
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import androidx.activity.OnBackPressedDispatcherOwner
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import eu.kanade.presentation.util.LocalRouter
-import eu.kanade.tachiyomi.databinding.ComposeControllerBinding
-import eu.kanade.tachiyomi.util.view.setComposeContent
-
-/**
- * Basic Compose controller without a presenter.
- */
-abstract class BasicFullComposeController(bundle: Bundle? = null) :
-    BaseController<ComposeControllerBinding>(bundle),
-    ComposeContentController {
-
-    override fun createBinding(inflater: LayoutInflater) =
-        ComposeControllerBinding.inflate(inflater)
-
-    override fun onViewCreated(view: View) {
-        super.onViewCreated(view)
-
-        binding.root.apply {
-            setComposeContent {
-                CompositionLocalProvider(LocalRouter provides router) {
-                    ComposeContent()
-                }
-            }
-        }
-    }
-
-    // Let Compose view handle this
-    override fun handleBack(): Boolean {
-        val dispatcher = (activity as? OnBackPressedDispatcherOwner)?.onBackPressedDispatcher ?: return false
-        return if (dispatcher.hasEnabledCallbacks()) {
-            dispatcher.onBackPressed()
-            true
-        } else {
-            false
-        }
-    }
-}
-
-interface ComposeContentController {
-    @Composable fun ComposeContent()
-}

+ 0 - 25
app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/ConductorExtensions.kt

@@ -1,25 +0,0 @@
-package eu.kanade.tachiyomi.ui.base.controller
-
-import androidx.core.net.toUri
-import com.bluelinelabs.conductor.Controller
-import com.bluelinelabs.conductor.Router
-import com.bluelinelabs.conductor.RouterTransaction
-import eu.kanade.tachiyomi.util.system.openInBrowser
-
-fun Router.setRoot(controller: Controller, id: Int) {
-    setRoot(controller.withFadeTransaction().tag(id.toString()))
-}
-
-fun Router.pushController(controller: Controller) {
-    pushController(controller.withFadeTransaction())
-}
-
-fun Controller.withFadeTransaction(): RouterTransaction {
-    return RouterTransaction.with(this)
-        .pushChangeHandler(OneWayFadeChangeHandler())
-        .popChangeHandler(OneWayFadeChangeHandler())
-}
-
-fun Controller.openInBrowser(url: String) {
-    activity?.openInBrowser(url.toUri())
-}

+ 0 - 119
app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/DialogController.kt

@@ -1,119 +0,0 @@
-package eu.kanade.tachiyomi.ui.base.controller
-
-import android.app.Dialog
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import com.bluelinelabs.conductor.Controller
-import com.bluelinelabs.conductor.Router
-import com.bluelinelabs.conductor.RouterTransaction
-import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler
-
-/**
- * A controller that displays a dialog window, floating on top of its activity's window.
- * This is a wrapper over [Dialog] object like [android.app.DialogFragment].
- *
- * Implementations should override this class and implement [.onCreateDialog] to create a custom dialog, such as an [android.app.AlertDialog]
- */
-abstract class DialogController : Controller {
-
-    protected var dialog: Dialog? = null
-        private set
-
-    private var dismissed = false
-
-    /**
-     * Convenience constructor for use when no arguments are needed.
-     */
-    protected constructor() : super(null)
-
-    /**
-     * Constructor that takes arguments that need to be retained across restarts.
-     *
-     * @param args Any arguments that need to be retained.
-     */
-    protected constructor(args: Bundle?) : super(args)
-
-    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedViewState: Bundle?): View {
-        dialog = onCreateDialog(savedViewState)
-        dialog!!.setOwnerActivity(activity!!)
-        dialog!!.setOnDismissListener { dismissDialog() }
-        if (savedViewState != null) {
-            val dialogState = savedViewState.getBundle(SAVED_DIALOG_STATE_TAG)
-            if (dialogState != null) {
-                dialog!!.onRestoreInstanceState(dialogState)
-            }
-        }
-        return View(activity) // stub view
-    }
-
-    override fun onSaveViewState(view: View, outState: Bundle) {
-        super.onSaveViewState(view, outState)
-        val dialogState = dialog!!.onSaveInstanceState()
-        outState.putBundle(SAVED_DIALOG_STATE_TAG, dialogState)
-    }
-
-    override fun onAttach(view: View) {
-        super.onAttach(view)
-        dialog!!.show()
-    }
-
-    override fun onDetach(view: View) {
-        super.onDetach(view)
-        dialog!!.hide()
-    }
-
-    override fun onDestroyView(view: View) {
-        super.onDestroyView(view)
-        dialog!!.setOnDismissListener(null)
-        dialog!!.dismiss()
-        dialog = null
-    }
-
-    /**
-     * Display the dialog, create a transaction and pushing the controller.
-     * @param router The router on which the transaction will be applied
-     */
-    open fun showDialog(router: Router) {
-        showDialog(router, null)
-    }
-
-    /**
-     * Display the dialog, create a transaction and pushing the controller.
-     * @param router The router on which the transaction will be applied
-     * @param tag The tag for this controller
-     */
-    fun showDialog(router: Router, tag: String?) {
-        dismissed = false
-        router.pushController(
-            RouterTransaction.with(this)
-                .pushChangeHandler(SimpleSwapChangeHandler(false))
-                .popChangeHandler(SimpleSwapChangeHandler(false))
-                .tag(tag),
-        )
-    }
-
-    /**
-     * Dismiss the dialog and pop this controller
-     */
-    fun dismissDialog() {
-        if (dismissed) {
-            return
-        }
-        router.popController(this)
-        dismissed = true
-    }
-
-    /**
-     * Build your own custom Dialog container such as an [android.app.AlertDialog]
-     *
-     * @param savedViewState A bundle for the view's state, which would have been created in [.onSaveViewState] or `null` if no saved state exists.
-     * @return Return a new Dialog instance to be displayed by the Controller
-     */
-    protected abstract fun onCreateDialog(savedViewState: Bundle?): Dialog
-
-    companion object {
-        private const val SAVED_DIALOG_STATE_TAG = "android:savedDialogState"
-    }
-}

+ 0 - 46
app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/OneWayFadeChangeHandler.kt

@@ -1,46 +0,0 @@
-package eu.kanade.tachiyomi.ui.base.controller
-
-import android.animation.Animator
-import android.animation.AnimatorSet
-import android.animation.ObjectAnimator
-import android.view.View
-import android.view.ViewGroup
-import com.bluelinelabs.conductor.ControllerChangeHandler
-import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
-
-/**
- * A variation of [FadeChangeHandler] that only fades in.
- */
-class OneWayFadeChangeHandler : FadeChangeHandler {
-    constructor()
-    constructor(removesFromViewOnPush: Boolean) : super(removesFromViewOnPush)
-    constructor(duration: Long) : super(duration)
-    constructor(duration: Long, removesFromViewOnPush: Boolean) : super(
-        duration,
-        removesFromViewOnPush,
-    )
-
-    override fun getAnimator(
-        container: ViewGroup,
-        from: View?,
-        to: View?,
-        isPush: Boolean,
-        toAddedToContainer: Boolean,
-    ): Animator {
-        val animator = AnimatorSet()
-        if (to != null) {
-            val start: Float = if (toAddedToContainer) 0F else to.alpha
-            animator.play(ObjectAnimator.ofFloat(to, View.ALPHA, start, 1f))
-        }
-
-        if (from != null && (!isPush || removesFromViewOnPush())) {
-            from.alpha = 0f
-        }
-
-        return animator
-    }
-
-    override fun copy(): ControllerChangeHandler {
-        return OneWayFadeChangeHandler(animationDuration, removesFromViewOnPush())
-    }
-}

+ 0 - 3
app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/RootController.kt

@@ -1,3 +0,0 @@
-package eu.kanade.tachiyomi.ui.base.controller
-
-interface RootController

+ 0 - 27
app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseController.kt

@@ -1,27 +0,0 @@
-package eu.kanade.tachiyomi.ui.browse
-
-import android.os.Bundle
-import androidx.compose.runtime.Composable
-import androidx.core.os.bundleOf
-import cafe.adriel.voyager.navigator.Navigator
-import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
-import eu.kanade.tachiyomi.ui.base.controller.RootController
-
-class BrowseController : BasicFullComposeController, RootController {
-
-    @Suppress("unused")
-    constructor(bundle: Bundle? = null) : this(bundle?.getBoolean(TO_EXTENSIONS_EXTRA) ?: false)
-
-    constructor(toExtensions: Boolean = false) : super(
-        bundleOf(TO_EXTENSIONS_EXTRA to toExtensions),
-    )
-
-    private val toExtensions = args.getBoolean(TO_EXTENSIONS_EXTRA, false)
-
-    @Composable
-    override fun ComposeContent() {
-        Navigator(screen = BrowseScreen(toExtensions = toExtensions))
-    }
-}
-
-private const val TO_EXTENSIONS_EXTRA = "to_extensions"

+ 22 - 4
app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseScreen.kt → app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt

@@ -1,17 +1,23 @@
 package eu.kanade.tachiyomi.ui.browse
 
+import androidx.compose.animation.graphics.res.animatedVectorResource
+import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
+import androidx.compose.animation.graphics.vector.AnimatedImageVector
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
 import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
 import cafe.adriel.voyager.core.model.ScreenModel
 import cafe.adriel.voyager.core.model.coroutineScope
 import cafe.adriel.voyager.core.model.rememberScreenModel
-import cafe.adriel.voyager.core.screen.Screen
+import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
+import cafe.adriel.voyager.navigator.tab.TabOptions
 import eu.kanade.core.prefs.asState
 import eu.kanade.domain.base.BasePreferences
 import eu.kanade.presentation.components.TabbedScreen
+import eu.kanade.presentation.util.Tab
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreenModel
 import eu.kanade.tachiyomi.ui.browse.extension.extensionsTab
@@ -22,9 +28,21 @@ import eu.kanade.tachiyomi.util.storage.DiskUtil
 import uy.kohesive.injekt.Injekt
 import uy.kohesive.injekt.api.get
 
-data class BrowseScreen(
-    private val toExtensions: Boolean,
-) : Screen {
+data class BrowseTab(
+    private val toExtensions: Boolean = false,
+) : Tab {
+
+    override val options: TabOptions
+        @Composable
+        get() {
+            val isSelected = LocalTabNavigator.current.current.key == key
+            val image = AnimatedImageVector.animatedVectorResource(R.drawable.anim_browse_enter)
+            return TabOptions(
+                index = 3u,
+                title = stringResource(R.string.browse),
+                icon = rememberAnimatedVectorPainter(image, isSelected),
+            )
+        }
 
     @Composable
     override fun Content() {

+ 0 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaScreen.kt

@@ -11,7 +11,6 @@ import cafe.adriel.voyager.navigator.LocalNavigator
 import cafe.adriel.voyager.navigator.currentOrThrow
 import eu.kanade.presentation.browse.MigrateMangaScreen
 import eu.kanade.presentation.components.LoadingScreen
-import eu.kanade.presentation.util.LocalRouter
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateSearchScreen
 import eu.kanade.tachiyomi.ui.manga.MangaScreen
@@ -26,7 +25,6 @@ data class MigrationMangaScreen(
     override fun Content() {
         val context = LocalContext.current
         val navigator = LocalNavigator.currentOrThrow
-        val router = LocalRouter.currentOrThrow
         val screenModel = rememberScreenModel { MigrationMangaScreenModel(sourceId) }
 
         val state by screenModel.state.collectAsState()

+ 2 - 7
app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt

@@ -35,7 +35,6 @@ import eu.kanade.domain.manga.model.hasCustomCover
 import eu.kanade.domain.track.interactor.GetTracks
 import eu.kanade.domain.track.interactor.InsertTrack
 import eu.kanade.presentation.browse.MigrateSearchScreen
-import eu.kanade.presentation.util.LocalRouter
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.core.preference.Preference
 import eu.kanade.tachiyomi.core.preference.PreferenceStore
@@ -45,9 +44,7 @@ import eu.kanade.tachiyomi.data.track.TrackManager
 import eu.kanade.tachiyomi.source.Source
 import eu.kanade.tachiyomi.source.SourceManager
 import eu.kanade.tachiyomi.source.model.SChapter
-import eu.kanade.tachiyomi.ui.base.controller.pushController
 import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags
-import eu.kanade.tachiyomi.ui.manga.MangaController
 import eu.kanade.tachiyomi.ui.manga.MangaScreen
 import eu.kanade.tachiyomi.util.lang.launchIO
 import eu.kanade.tachiyomi.util.lang.launchUI
@@ -60,7 +57,6 @@ class MigrateSearchScreen(private val mangaId: Long) : Screen {
     @Composable
     override fun Content() {
         val navigator = LocalNavigator.currentOrThrow
-        val router = LocalRouter.currentOrThrow
         val screenModel = rememberScreenModel { MigrateSearchScreenModel(mangaId = mangaId) }
         val state by screenModel.state.collectAsState()
 
@@ -76,7 +72,7 @@ class MigrateSearchScreen(private val mangaId: Long) : Screen {
                 if (!screenModel.incognitoMode.get()) {
                     screenModel.lastUsedSourceId.set(it.id)
                 }
-                router.pushController(SourceSearchController(state.manga, it, state.searchQuery))
+                navigator.push(SourceSearchScreen(state.manga!!, it.id, state.searchQuery))
             },
             onClickItem = { screenModel.setDialog(MigrateSearchDialog.Migrate(it)) },
             onLongClickItem = { navigator.push(MangaScreen(it.id, true)) },
@@ -99,8 +95,7 @@ class MigrateSearchScreen(private val mangaId: Long) : Screen {
                             navigator.popUntil { navigator.items.contains(lastItem) }
                             navigator.push(MangaScreen(dialog.manga.id))
                         } else {
-                            navigator.pop()
-                            router.pushController(MangaController(dialog.manga.id))
+                            navigator.replace(MangaScreen(dialog.manga.id))
                         }
                     },
                 )

+ 0 - 34
app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchController.kt

@@ -1,34 +0,0 @@
-package eu.kanade.tachiyomi.ui.browse.migration.search
-
-import android.os.Bundle
-import androidx.compose.runtime.Composable
-import androidx.core.os.bundleOf
-import cafe.adriel.voyager.navigator.Navigator
-import eu.kanade.domain.manga.model.Manga
-import eu.kanade.tachiyomi.source.CatalogueSource
-import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
-import eu.kanade.tachiyomi.util.system.getSerializableCompat
-
-class SourceSearchController(bundle: Bundle) : BasicFullComposeController(bundle) {
-
-    constructor(manga: Manga? = null, source: CatalogueSource, searchQuery: String? = null) : this(
-        bundleOf(
-            SOURCE_ID_KEY to source.id,
-            MANGA_KEY to manga,
-            SEARCH_QUERY_KEY to searchQuery,
-        ),
-    )
-
-    private var oldManga: Manga = args.getSerializableCompat(MANGA_KEY)!!
-    private val sourceId = args.getLong(SOURCE_ID_KEY)
-    private val query = args.getString(SEARCH_QUERY_KEY)
-
-    @Composable
-    override fun ComposeContent() {
-        Navigator(screen = SourceSearchScreen(oldManga, sourceId, query))
-    }
-}
-
-private const val MANGA_KEY = "oldManga"
-private const val SOURCE_ID_KEY = "sourceId"
-private const val SEARCH_QUERY_KEY = "searchQuery"

+ 14 - 20
app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt

@@ -12,6 +12,7 @@ import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.ui.platform.LocalConfiguration
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.LocalUriHandler
@@ -26,17 +27,15 @@ import eu.kanade.presentation.browse.BrowseSourceContent
 import eu.kanade.presentation.components.ExtendedFloatingActionButton
 import eu.kanade.presentation.components.Scaffold
 import eu.kanade.presentation.components.SearchToolbar
-import eu.kanade.presentation.util.LocalRouter
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.source.LocalSource
 import eu.kanade.tachiyomi.source.online.HttpSource
-import eu.kanade.tachiyomi.ui.base.controller.pushController
-import eu.kanade.tachiyomi.ui.base.controller.setRoot
-import eu.kanade.tachiyomi.ui.browse.BrowseController
 import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel
-import eu.kanade.tachiyomi.ui.manga.MangaController
-import eu.kanade.tachiyomi.ui.more.MoreController
+import eu.kanade.tachiyomi.ui.home.HomeScreen
+import eu.kanade.tachiyomi.ui.manga.MangaScreen
 import eu.kanade.tachiyomi.ui.webview.WebViewActivity
+import eu.kanade.tachiyomi.util.Constants
+import kotlinx.coroutines.launch
 
 data class SourceSearchScreen(
     private val oldManga: Manga,
@@ -48,27 +47,20 @@ data class SourceSearchScreen(
     override fun Content() {
         val context = LocalContext.current
         val uriHandler = LocalUriHandler.current
-        val router = LocalRouter.currentOrThrow
         val navigator = LocalNavigator.currentOrThrow
+        val scope = rememberCoroutineScope()
 
         val screenModel = rememberScreenModel { BrowseSourceScreenModel(sourceId = sourceId, searchQuery = query) }
         val state by screenModel.state.collectAsState()
 
         val snackbarHostState = remember { SnackbarHostState() }
 
-        val navigateUp: () -> Unit = {
-            when {
-                navigator.canPop -> navigator.pop()
-                router.backstackSize > 1 -> router.popCurrentController()
-            }
-        }
-
         Scaffold(
             topBar = { scrollBehavior ->
                 SearchToolbar(
                     searchQuery = state.toolbarQuery ?: "",
                     onChangeSearchQuery = screenModel::setToolbarQuery,
-                    onClickCloseSearch = navigateUp,
+                    onClickCloseSearch = navigator::pop,
                     onSearch = { screenModel.search(it) },
                     scrollBehavior = scrollBehavior,
                 )
@@ -102,7 +94,7 @@ data class SourceSearchScreen(
                     val intent = WebViewActivity.newIntent(context, source.baseUrl, source.id, source.name)
                     context.startActivity(intent)
                 },
-                onHelpClick = { uriHandler.openUri(MoreController.URL_HELP) },
+                onHelpClick = { uriHandler.openUri(Constants.URL_HELP) },
                 onLocalSourceHelpClick = { uriHandler.openUri(LocalSource.HELP_URL) },
                 onMangaClick = openMigrateDialog,
                 onMangaLongClick = openMigrateDialog,
@@ -116,11 +108,13 @@ data class SourceSearchScreen(
                     newManga = dialog.newManga,
                     screenModel = rememberScreenModel { MigrateDialogScreenModel() },
                     onDismissRequest = { screenModel.setDialog(null) },
-                    onClickTitle = { router.pushController(MangaController(dialog.newManga.id)) },
+                    onClickTitle = { navigator.push(MangaScreen(dialog.newManga.id)) },
                     onPopScreen = {
-                        // TODO: Push to manga screen and remove this and the previous screen when it moves to Voyager
-                        router.setRoot(BrowseController(toExtensions = false), R.id.nav_browse)
-                        router.pushController(MangaController(dialog.newManga.id))
+                        scope.launch {
+                            navigator.popUntilRoot()
+                            HomeScreen.openTab(HomeScreen.Tab.Browse())
+                            navigator.push(MangaScreen(dialog.newManga.id))
+                        }
                     },
                 )
             }

+ 0 - 17
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesFilterController.kt

@@ -1,17 +0,0 @@
-package eu.kanade.tachiyomi.ui.browse.source
-
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import cafe.adriel.voyager.navigator.Navigator
-import eu.kanade.presentation.util.LocalRouter
-import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
-
-class SourceFilterController : BasicFullComposeController() {
-
-    @Composable
-    override fun ComposeContent() {
-        CompositionLocalProvider(LocalRouter provides router) {
-            Navigator(screen = SourcesFilterScreen())
-        }
-    }
-}

+ 4 - 4
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesFilterScreen.kt

@@ -7,10 +7,10 @@ import androidx.compose.runtime.getValue
 import androidx.compose.ui.platform.LocalContext
 import cafe.adriel.voyager.core.model.rememberScreenModel
 import cafe.adriel.voyager.core.screen.Screen
+import cafe.adriel.voyager.navigator.LocalNavigator
 import cafe.adriel.voyager.navigator.currentOrThrow
 import eu.kanade.presentation.browse.SourcesFilterScreen
 import eu.kanade.presentation.components.LoadingScreen
-import eu.kanade.presentation.util.LocalRouter
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.util.system.toast
 
@@ -18,7 +18,7 @@ class SourcesFilterScreen : Screen {
 
     @Composable
     override fun Content() {
-        val router = LocalRouter.currentOrThrow
+        val navigator = LocalNavigator.currentOrThrow
         val screenModel = rememberScreenModel { SourcesFilterScreenModel() }
         val state by screenModel.state.collectAsState()
 
@@ -31,7 +31,7 @@ class SourcesFilterScreen : Screen {
             val context = LocalContext.current
             LaunchedEffect(Unit) {
                 context.toast(R.string.internal_error)
-                router.popCurrentController()
+                navigator.pop()
             }
             return
         }
@@ -39,7 +39,7 @@ class SourcesFilterScreen : Screen {
         val successState = state as SourcesFilterState.Success
 
         SourcesFilterScreen(
-            navigateUp = router::popCurrentController,
+            navigateUp = navigator::pop,
             state = successState,
             onClickLanguage = screenModel::toggleLanguage,
             onClickSource = screenModel::toggleSource,

+ 7 - 8
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesTab.kt

@@ -10,22 +10,21 @@ import androidx.compose.runtime.getValue
 import androidx.compose.ui.res.stringResource
 import cafe.adriel.voyager.core.model.rememberScreenModel
 import cafe.adriel.voyager.core.screen.Screen
+import cafe.adriel.voyager.navigator.LocalNavigator
 import cafe.adriel.voyager.navigator.currentOrThrow
 import eu.kanade.presentation.browse.SourceOptionsDialog
 import eu.kanade.presentation.browse.SourcesScreen
 import eu.kanade.presentation.components.AppBar
 import eu.kanade.presentation.components.TabContent
-import eu.kanade.presentation.util.LocalRouter
 import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.ui.base.controller.pushController
-import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
-import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
+import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
+import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen
 import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.launch
 
 @Composable
 fun Screen.sourcesTab(): TabContent {
-    val router = LocalRouter.currentOrThrow
+    val navigator = LocalNavigator.currentOrThrow
     val screenModel = rememberScreenModel { SourcesScreenModel() }
     val state by screenModel.state.collectAsState()
 
@@ -35,12 +34,12 @@ fun Screen.sourcesTab(): TabContent {
             AppBar.Action(
                 title = stringResource(R.string.action_global_search),
                 icon = Icons.Outlined.TravelExplore,
-                onClick = { router.pushController(GlobalSearchController()) },
+                onClick = { navigator.push(GlobalSearchScreen()) },
             ),
             AppBar.Action(
                 title = stringResource(R.string.action_filter),
                 icon = Icons.Outlined.FilterList,
-                onClick = { router.pushController(SourceFilterController()) },
+                onClick = { navigator.push(SourcesFilterScreen()) },
             ),
         ),
         content = { contentPadding, snackbarHostState ->
@@ -49,7 +48,7 @@ fun Screen.sourcesTab(): TabContent {
                 contentPadding = contentPadding,
                 onClickItem = { source, query ->
                     screenModel.onOpenSource(source)
-                    router.pushController(BrowseSourceController(source.id, query))
+                    navigator.push(BrowseSourceScreen(source.id, query))
                 },
                 onClickPin = screenModel::togglePin,
                 onLongClickItem = screenModel::showSourceDialog,

+ 0 - 69
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt

@@ -1,69 +0,0 @@
-package eu.kanade.tachiyomi.ui.browse.source.browse
-
-import android.os.Bundle
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.core.os.bundleOf
-import cafe.adriel.voyager.navigator.CurrentScreen
-import cafe.adriel.voyager.navigator.Navigator
-import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.flow.collectLatest
-import kotlinx.coroutines.flow.consumeAsFlow
-import kotlinx.coroutines.launch
-
-class BrowseSourceController(bundle: Bundle) : BasicFullComposeController(bundle) {
-
-    constructor(sourceId: Long, query: String? = null) : this(
-        bundleOf(
-            SOURCE_ID_KEY to sourceId,
-            SEARCH_QUERY_KEY to query,
-        ),
-    )
-
-    private val sourceId = args.getLong(SOURCE_ID_KEY)
-    private val initialQuery = args.getString(SEARCH_QUERY_KEY)
-
-    private val queryEvent = Channel<BrowseSourceScreen.SearchType>()
-
-    @Composable
-    override fun ComposeContent() {
-        Navigator(screen = BrowseSourceScreen(sourceId = sourceId, query = initialQuery)) { navigator ->
-            CurrentScreen()
-
-            LaunchedEffect(Unit) {
-                queryEvent.consumeAsFlow()
-                    .collectLatest {
-                        val screen = (navigator.lastItem as? BrowseSourceScreen)
-                        when (it) {
-                            is BrowseSourceScreen.SearchType.Genre -> screen?.searchGenre(it.txt)
-                            is BrowseSourceScreen.SearchType.Text -> screen?.search(it.txt)
-                        }
-                    }
-            }
-        }
-    }
-
-    /**
-     * Restarts the request with a new query.
-     *
-     * @param newQuery the new query.
-     */
-    fun searchWithQuery(newQuery: String) {
-        viewScope.launch { queryEvent.send(BrowseSourceScreen.SearchType.Text(newQuery)) }
-    }
-
-    /**
-     * Attempts to restart the request with a new genre-filtered query.
-     * If the genre name can't be found the filters,
-     * the standard searchWithQuery search method is used instead.
-     *
-     * @param genreName the name of the genre
-     */
-    fun searchWithGenre(genreName: String) {
-        viewScope.launch { queryEvent.send(BrowseSourceScreen.SearchType.Genre(genreName)) }
-    }
-}
-
-private const val SOURCE_ID_KEY = "sourceId"
-private const val SEARCH_QUERY_KEY = "searchQuery"

+ 8 - 24
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt

@@ -1,6 +1,5 @@
 package eu.kanade.tachiyomi.ui.browse.source.browse
 
-import androidx.activity.compose.BackHandler
 import androidx.compose.foundation.background
 import androidx.compose.foundation.horizontalScroll
 import androidx.compose.foundation.layout.Arrangement
@@ -49,16 +48,13 @@ import eu.kanade.presentation.components.ChangeCategoryDialog
 import eu.kanade.presentation.components.Divider
 import eu.kanade.presentation.components.DuplicateMangaDialog
 import eu.kanade.presentation.components.Scaffold
-import eu.kanade.presentation.util.LocalRouter
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.source.LocalSource
-import eu.kanade.tachiyomi.source.model.FilterList
 import eu.kanade.tachiyomi.source.online.HttpSource
-import eu.kanade.tachiyomi.ui.base.controller.pushController
-import eu.kanade.tachiyomi.ui.category.CategoryController
-import eu.kanade.tachiyomi.ui.manga.MangaController
-import eu.kanade.tachiyomi.ui.more.MoreController
+import eu.kanade.tachiyomi.ui.category.CategoryScreen
+import eu.kanade.tachiyomi.ui.manga.MangaScreen
 import eu.kanade.tachiyomi.ui.webview.WebViewActivity
+import eu.kanade.tachiyomi.util.Constants
 import eu.kanade.tachiyomi.util.lang.launchIO
 import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.flow.collectLatest
@@ -73,7 +69,6 @@ data class BrowseSourceScreen(
 
     @Composable
     override fun Content() {
-        val router = LocalRouter.currentOrThrow
         val navigator = LocalNavigator.currentOrThrow
         val scope = rememberCoroutineScope()
         val context = LocalContext.current
@@ -93,13 +88,6 @@ data class BrowseSourceScreen(
             context.startActivity(intent)
         }
 
-        val navigateUp: () -> Unit = {
-            when {
-                navigator.canPop -> navigator.pop()
-                router.backstackSize > 1 -> router.popCurrentController()
-            }
-        }
-
         Scaffold(
             topBar = {
                 Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) {
@@ -109,7 +97,7 @@ data class BrowseSourceScreen(
                         source = screenModel.source,
                         displayMode = screenModel.displayMode,
                         onDisplayModeChange = { screenModel.displayMode = it },
-                        navigateUp = navigateUp,
+                        navigateUp = navigator::pop,
                         onWebViewClick = onWebViewClick,
                         onHelpClick = onHelpClick,
                         onSearch = { screenModel.search(it) },
@@ -197,9 +185,9 @@ data class BrowseSourceScreen(
                 snackbarHostState = snackbarHostState,
                 contentPadding = paddingValues,
                 onWebViewClick = onWebViewClick,
-                onHelpClick = { uriHandler.openUri(MoreController.URL_HELP) },
+                onHelpClick = { uriHandler.openUri(Constants.URL_HELP) },
                 onLocalSourceHelpClick = onHelpClick,
-                onMangaClick = { router.pushController(MangaController(it.id, true)) },
+                onMangaClick = { navigator.push((MangaScreen(it.id, true))) },
                 onMangaLongClick = { manga ->
                     scope.launchIO {
                         val duplicateManga = screenModel.getDuplicateLibraryManga(manga)
@@ -226,7 +214,7 @@ data class BrowseSourceScreen(
                 DuplicateMangaDialog(
                     onDismissRequest = onDismissRequest,
                     onConfirm = { screenModel.addFavorite(dialog.manga) },
-                    onOpenManga = { router.pushController(MangaController(dialog.duplicate.id)) },
+                    onOpenManga = { navigator.push(MangaScreen(dialog.duplicate.id)) },
                     duplicateFrom = screenModel.getSourceOrStub(dialog.duplicate),
                 )
             }
@@ -243,9 +231,7 @@ data class BrowseSourceScreen(
                 ChangeCategoryDialog(
                     initialSelection = dialog.initialSelection,
                     onDismissRequest = onDismissRequest,
-                    onEditCategories = {
-                        router.pushController(CategoryController())
-                    },
+                    onEditCategories = { navigator.push(CategoryScreen()) },
                     onConfirm = { include, _ ->
                         screenModel.changeMangaFavorite(dialog.manga)
                         screenModel.moveMangaToCategories(dialog.manga, include)
@@ -255,8 +241,6 @@ data class BrowseSourceScreen(
             else -> {}
         }
 
-        BackHandler(onBack = navigateUp)
-
         LaunchedEffect(state.filters) {
             screenModel.initFilterSheet(context)
         }

+ 0 - 25
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchController.kt

@@ -1,25 +0,0 @@
-package eu.kanade.tachiyomi.ui.browse.source.globalsearch
-
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import cafe.adriel.voyager.navigator.Navigator
-import eu.kanade.presentation.util.LocalRouter
-import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
-
-class GlobalSearchController(
-    val searchQuery: String = "",
-    val extensionFilter: String = "",
-) : BasicFullComposeController() {
-
-    @Composable
-    override fun ComposeContent() {
-        CompositionLocalProvider(LocalRouter provides router) {
-            Navigator(
-                screen = GlobalSearchScreen(
-                    searchQuery = searchQuery,
-                    extensionFilter = extensionFilter,
-                ),
-            )
-        }
-    }
-}

+ 8 - 9
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt

@@ -5,12 +5,11 @@ import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
 import cafe.adriel.voyager.core.model.rememberScreenModel
 import cafe.adriel.voyager.core.screen.Screen
+import cafe.adriel.voyager.navigator.LocalNavigator
 import cafe.adriel.voyager.navigator.currentOrThrow
 import eu.kanade.presentation.browse.GlobalSearchScreen
-import eu.kanade.presentation.util.LocalRouter
-import eu.kanade.tachiyomi.ui.base.controller.pushController
-import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
-import eu.kanade.tachiyomi.ui.manga.MangaController
+import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
+import eu.kanade.tachiyomi.ui.manga.MangaScreen
 
 class GlobalSearchScreen(
     val searchQuery: String = "",
@@ -19,7 +18,7 @@ class GlobalSearchScreen(
 
     @Composable
     override fun Content() {
-        val router = LocalRouter.currentOrThrow
+        val navigator = LocalNavigator.currentOrThrow
 
         val screenModel = rememberScreenModel {
             GlobalSearchScreenModel(
@@ -31,7 +30,7 @@ class GlobalSearchScreen(
 
         GlobalSearchScreen(
             state = state,
-            navigateUp = router::popCurrentController,
+            navigateUp = navigator::pop,
             onChangeSearchQuery = screenModel::updateSearchQuery,
             onSearch = screenModel::search,
             getManga = { source, manga ->
@@ -44,10 +43,10 @@ class GlobalSearchScreen(
                 if (!screenModel.incognitoMode.get()) {
                     screenModel.lastUsedSourceId.set(it.id)
                 }
-                router.pushController(BrowseSourceController(it.id, state.searchQuery))
+                navigator.push(BrowseSourceScreen(it.id, state.searchQuery))
             },
-            onClickItem = { router.pushController(MangaController(it.id, true)) },
-            onLongClickItem = { router.pushController(MangaController(it.id, true)) },
+            onClickItem = { navigator.push(MangaScreen(it.id, true)) },
+            onLongClickItem = { navigator.push(MangaScreen(it.id, true)) },
         )
     }
 }

+ 0 - 17
app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryController.kt

@@ -1,17 +0,0 @@
-package eu.kanade.tachiyomi.ui.category
-
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import cafe.adriel.voyager.navigator.Navigator
-import eu.kanade.presentation.util.LocalRouter
-import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
-
-class CategoryController : BasicFullComposeController() {
-
-    @Composable
-    override fun ComposeContent() {
-        CompositionLocalProvider(LocalRouter provides router) {
-            Navigator(screen = CategoryScreen())
-        }
-    }
-}

+ 1 - 8
app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryScreen.kt

@@ -15,7 +15,6 @@ import eu.kanade.presentation.category.components.CategoryCreateDialog
 import eu.kanade.presentation.category.components.CategoryDeleteDialog
 import eu.kanade.presentation.category.components.CategoryRenameDialog
 import eu.kanade.presentation.components.LoadingScreen
-import eu.kanade.presentation.util.LocalRouter
 import eu.kanade.tachiyomi.util.system.toast
 import kotlinx.coroutines.flow.collectLatest
 
@@ -27,7 +26,6 @@ class CategoryScreen : Screen {
     @Composable
     override fun Content() {
         val context = LocalContext.current
-        val router = LocalRouter.currentOrThrow
         val navigator = LocalNavigator.currentOrThrow
         val screenModel = rememberScreenModel { CategoryScreenModel() }
 
@@ -47,12 +45,7 @@ class CategoryScreen : Screen {
             onClickDelete = { screenModel.showDialog(CategoryDialog.Delete(it)) },
             onClickMoveUp = screenModel::moveUp,
             onClickMoveDown = screenModel::moveDown,
-            navigateUp = {
-                when {
-                    navigator.canPop -> navigator.pop()
-                    router.backstackSize > 1 -> router.handleBack()
-                }
-            },
+            navigateUp = navigator::pop,
         )
 
         when (val dialog = successState.dialog) {

+ 0 - 15
app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadController.kt

@@ -1,15 +0,0 @@
-package eu.kanade.tachiyomi.ui.download
-
-import androidx.compose.runtime.Composable
-import cafe.adriel.voyager.navigator.Navigator
-import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
-
-/**
- * Controller that shows the currently active downloads.
- */
-class DownloadController : BasicFullComposeController() {
-    @Composable
-    override fun ComposeContent() {
-        Navigator(screen = DownloadQueueScreen)
-    }
-}

+ 3 - 3
app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadQueueScreen.kt

@@ -47,6 +47,7 @@ import androidx.core.view.updatePadding
 import androidx.recyclerview.widget.LinearLayoutManager
 import cafe.adriel.voyager.core.model.rememberScreenModel
 import cafe.adriel.voyager.core.screen.Screen
+import cafe.adriel.voyager.navigator.LocalNavigator
 import cafe.adriel.voyager.navigator.currentOrThrow
 import eu.kanade.presentation.components.AppBar
 import eu.kanade.presentation.components.EmptyScreen
@@ -54,7 +55,6 @@ import eu.kanade.presentation.components.ExtendedFloatingActionButton
 import eu.kanade.presentation.components.OverflowMenu
 import eu.kanade.presentation.components.Pill
 import eu.kanade.presentation.components.Scaffold
-import eu.kanade.presentation.util.LocalRouter
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.download.DownloadService
 import eu.kanade.tachiyomi.databinding.DownloadListBinding
@@ -66,7 +66,7 @@ object DownloadQueueScreen : Screen {
     @Composable
     override fun Content() {
         val context = LocalContext.current
-        val router = LocalRouter.currentOrThrow
+        val navigator = LocalNavigator.currentOrThrow
         val scope = rememberCoroutineScope()
         val screenModel = rememberScreenModel { DownloadQueueScreenModel() }
         val downloadList by screenModel.state.collectAsState()
@@ -121,7 +121,7 @@ object DownloadQueueScreen : Screen {
                             }
                         }
                     },
-                    navigateUp = router::popCurrentController,
+                    navigateUp = navigator::pop,
                     actions = {
                         if (downloadList.isNotEmpty()) {
                             OverflowMenu { closeMenu ->

+ 0 - 26
app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryController.kt

@@ -1,26 +0,0 @@
-package eu.kanade.tachiyomi.ui.history
-
-import androidx.compose.runtime.Composable
-import cafe.adriel.voyager.navigator.Navigator
-import eu.kanade.domain.history.interactor.GetNextChapters
-import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
-import eu.kanade.tachiyomi.ui.base.controller.RootController
-import eu.kanade.tachiyomi.util.lang.launchIO
-import uy.kohesive.injekt.Injekt
-import uy.kohesive.injekt.api.get
-
-class HistoryController : BasicFullComposeController(), RootController {
-
-    @Composable
-    override fun ComposeContent() {
-        Navigator(screen = HistoryScreen)
-    }
-
-    fun resumeLastChapterRead() {
-        val context = activity ?: return
-        viewScope.launchIO {
-            val chapter = Injekt.get<GetNextChapters>().await(onlyUnread = false).firstOrNull()
-            HistoryScreen.openChapter(context, chapter)
-        }
-    }
-}

+ 5 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryScreenModel.kt

@@ -15,6 +15,7 @@ import eu.kanade.domain.history.model.HistoryWithRelations
 import eu.kanade.presentation.history.HistoryUiModel
 import eu.kanade.tachiyomi.util.lang.launchIO
 import eu.kanade.tachiyomi.util.lang.toDateKey
+import eu.kanade.tachiyomi.util.lang.withIOContext
 import eu.kanade.tachiyomi.util.system.logcat
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.channels.Channel
@@ -76,6 +77,10 @@ class HistoryScreenModel(
             }
     }
 
+    suspend fun getNextChapter(): Chapter? {
+        return withIOContext { getNextChapters.await(onlyUnread = false).firstOrNull() }
+    }
+
     fun getNextChapterForManga(mangaId: Long, chapterId: Long) {
         coroutineScope.launchIO {
             sendNextChapterEvent(getNextChapters.await(mangaId, chapterId, onlyUnread = false))

+ 39 - 7
app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryScreen.kt → app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryTab.kt

@@ -1,34 +1,60 @@
 package eu.kanade.tachiyomi.ui.history
 
 import android.content.Context
+import androidx.compose.animation.graphics.res.animatedVectorResource
+import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
+import androidx.compose.animation.graphics.vector.AnimatedImageVector
 import androidx.compose.material3.SnackbarHostState
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
 import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
 import cafe.adriel.voyager.core.model.rememberScreenModel
-import cafe.adriel.voyager.core.screen.Screen
+import cafe.adriel.voyager.navigator.LocalNavigator
+import cafe.adriel.voyager.navigator.Navigator
 import cafe.adriel.voyager.navigator.currentOrThrow
+import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
+import cafe.adriel.voyager.navigator.tab.TabOptions
 import eu.kanade.domain.chapter.model.Chapter
 import eu.kanade.presentation.history.HistoryScreen
 import eu.kanade.presentation.history.components.HistoryDeleteAllDialog
 import eu.kanade.presentation.history.components.HistoryDeleteDialog
-import eu.kanade.presentation.util.LocalRouter
+import eu.kanade.presentation.util.Tab
 import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.ui.base.controller.pushController
 import eu.kanade.tachiyomi.ui.main.MainActivity
-import eu.kanade.tachiyomi.ui.manga.MangaController
+import eu.kanade.tachiyomi.ui.manga.MangaScreen
 import eu.kanade.tachiyomi.ui.reader.ReaderActivity
+import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.consumeAsFlow
 
-object HistoryScreen : Screen {
+object HistoryTab : Tab {
 
     private val snackbarHostState = SnackbarHostState()
 
+    private val resumeLastChapterReadEvent = Channel<Unit>()
+
+    override val options: TabOptions
+        @Composable
+        get() {
+            val isSelected = LocalTabNavigator.current.current.key == key
+            val image = AnimatedImageVector.animatedVectorResource(R.drawable.anim_history_enter)
+            return TabOptions(
+                index = 2u,
+                title = stringResource(R.string.label_recent_manga),
+                icon = rememberAnimatedVectorPainter(image, isSelected),
+            )
+        }
+
+    override suspend fun onReselect(navigator: Navigator) {
+        resumeLastChapterReadEvent.send(Unit)
+    }
+
     @Composable
     override fun Content() {
-        val router = LocalRouter.currentOrThrow
+        val navigator = LocalNavigator.currentOrThrow
         val context = LocalContext.current
         val screenModel = rememberScreenModel { HistoryScreenModel() }
         val state by screenModel.state.collectAsState()
@@ -39,7 +65,7 @@ object HistoryScreen : Screen {
             incognitoMode = screenModel.isIncognitoMode,
             downloadedOnlyMode = screenModel.isDownloadOnly,
             onSearchQueryChange = screenModel::updateSearchQuery,
-            onClickCover = { router.pushController(MangaController(it)) },
+            onClickCover = { navigator.push(MangaScreen(it)) },
             onClickResume = screenModel::getNextChapterForManga,
             onDialogChange = screenModel::setDialog,
         )
@@ -84,6 +110,12 @@ object HistoryScreen : Screen {
                 }
             }
         }
+
+        LaunchedEffect(Unit) {
+            resumeLastChapterReadEvent.consumeAsFlow().collectLatest {
+                openChapter(context, screenModel.getNextChapter())
+            }
+        }
     }
 
     suspend fun openChapter(context: Context, chapter: Chapter?) {

+ 288 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/home/HomeScreen.kt

@@ -0,0 +1,288 @@
+package eu.kanade.tachiyomi.ui.home
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.expandVertically
+import androidx.compose.animation.shrinkVertically
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.consumedWindowInsets
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Badge
+import androidx.compose.material3.BadgedBox
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.NavigationBarItem
+import androidx.compose.material3.NavigationRailItem
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.produceState
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.pluralStringResource
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.util.fastForEach
+import cafe.adriel.voyager.core.screen.Screen
+import cafe.adriel.voyager.navigator.LocalNavigator
+import cafe.adriel.voyager.navigator.currentOrThrow
+import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
+import cafe.adriel.voyager.navigator.tab.TabNavigator
+import eu.kanade.domain.library.service.LibraryPreferences
+import eu.kanade.domain.source.service.SourcePreferences
+import eu.kanade.presentation.components.NavigationBar
+import eu.kanade.presentation.components.NavigationRail
+import eu.kanade.presentation.components.Scaffold
+import eu.kanade.presentation.util.Tab
+import eu.kanade.presentation.util.Transition
+import eu.kanade.presentation.util.isTabletUi
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.ui.browse.BrowseTab
+import eu.kanade.tachiyomi.ui.history.HistoryTab
+import eu.kanade.tachiyomi.ui.library.LibraryTab
+import eu.kanade.tachiyomi.ui.manga.MangaScreen
+import eu.kanade.tachiyomi.ui.more.MoreTab
+import eu.kanade.tachiyomi.ui.updates.UpdatesTab
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.launch
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+
+object HomeScreen : Screen {
+
+    private val librarySearchEvent = Channel<String>()
+    private val openTabEvent = Channel<Tab>()
+    private val showBottomNavEvent = Channel<Boolean>()
+
+    private val tabs = listOf(
+        LibraryTab,
+        UpdatesTab,
+        HistoryTab,
+        BrowseTab(),
+        MoreTab(),
+    )
+
+    @Composable
+    override fun Content() {
+        val navigator = LocalNavigator.currentOrThrow
+        TabNavigator(
+            tab = LibraryTab,
+        ) { tabNavigator ->
+            // Provide usable navigator to content screen
+            CompositionLocalProvider(LocalNavigator provides navigator) {
+                Row(verticalAlignment = Alignment.CenterVertically) {
+                    if (isTabletUi()) {
+                        NavigationRail {
+                            tabs.fastForEach {
+                                NavigationRailItem(it)
+                            }
+                        }
+                    }
+                    Scaffold(
+                        bottomBar = {
+                            if (!isTabletUi()) {
+                                val bottomNavVisible by produceState(initialValue = true) {
+                                    showBottomNavEvent.receiveAsFlow().collectLatest { value = it }
+                                }
+                                AnimatedVisibility(
+                                    visible = bottomNavVisible,
+                                    enter = expandVertically(),
+                                    exit = shrinkVertically(),
+                                ) {
+                                    NavigationBar {
+                                        tabs.fastForEach {
+                                            NavigationBarItem(it)
+                                        }
+                                    }
+                                }
+                            }
+                        },
+                        contentWindowInsets = WindowInsets(0),
+                    ) { contentPadding ->
+                        Box(
+                            modifier = Modifier
+                                .padding(contentPadding)
+                                .consumedWindowInsets(contentPadding),
+                        ) {
+                            AnimatedContent(
+                                targetState = tabNavigator.current,
+                                transitionSpec = { Transition.OneWayFade },
+                                content = {
+                                    tabNavigator.saveableState(key = "currentTab", it) {
+                                        it.Content()
+                                    }
+                                },
+                            )
+                        }
+                    }
+                }
+            }
+
+            val goToLibraryTab = { tabNavigator.current = LibraryTab }
+            BackHandler(
+                enabled = tabNavigator.current != LibraryTab,
+                onBack = goToLibraryTab,
+            )
+
+            LaunchedEffect(Unit) {
+                launch {
+                    librarySearchEvent.receiveAsFlow().collectLatest {
+                        goToLibraryTab()
+                        LibraryTab.search(it)
+                    }
+                }
+                launch {
+                    openTabEvent.receiveAsFlow().collectLatest {
+                        tabNavigator.current = when (it) {
+                            is Tab.Library -> LibraryTab
+                            Tab.Updates -> UpdatesTab
+                            Tab.History -> HistoryTab
+                            is Tab.Browse -> BrowseTab(it.toExtensions)
+                            is Tab.More -> MoreTab(it.toDownloads)
+                        }
+
+                        if (it is Tab.Library && it.mangaIdToOpen != null) {
+                            navigator.push(MangaScreen(it.mangaIdToOpen))
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    @Composable
+    private fun RowScope.NavigationBarItem(tab: eu.kanade.presentation.util.Tab) {
+        val tabNavigator = LocalTabNavigator.current
+        val navigator = LocalNavigator.currentOrThrow
+        val scope = rememberCoroutineScope()
+        val selected = tabNavigator.current::class == tab::class
+        NavigationBarItem(
+            selected = selected,
+            onClick = {
+                if (!selected) {
+                    tabNavigator.current = tab
+                } else {
+                    scope.launch { tab.onReselect(navigator) }
+                }
+            },
+            icon = { NavigationIconItem(tab) },
+            label = {
+                Text(
+                    text = tab.options.title,
+                    style = MaterialTheme.typography.labelLarge,
+                )
+            },
+            alwaysShowLabel = true,
+        )
+    }
+
+    @Composable
+    fun NavigationRailItem(tab: eu.kanade.presentation.util.Tab) {
+        val tabNavigator = LocalTabNavigator.current
+        val navigator = LocalNavigator.currentOrThrow
+        val scope = rememberCoroutineScope()
+        val selected = tabNavigator.current::class == tab::class
+        NavigationRailItem(
+            selected = selected,
+            onClick = {
+                if (!selected) {
+                    tabNavigator.current = tab
+                } else {
+                    scope.launch { tab.onReselect(navigator) }
+                }
+            },
+            icon = { NavigationIconItem(tab) },
+            label = {
+                Text(
+                    text = tab.options.title,
+                    style = MaterialTheme.typography.labelLarge,
+                )
+            },
+            alwaysShowLabel = true,
+        )
+    }
+
+    @Composable
+    private fun NavigationIconItem(tab: eu.kanade.presentation.util.Tab) {
+        BadgedBox(
+            badge = {
+                when {
+                    tab is UpdatesTab -> {
+                        val count by produceState(initialValue = 0) {
+                            val pref = Injekt.get<LibraryPreferences>()
+                            combine(
+                                pref.showUpdatesNavBadge().changes(),
+                                pref.unreadUpdatesCount().changes(),
+                            ) { show, count -> if (show) count else 0 }
+                                .collectLatest { value = it }
+                        }
+                        if (count > 0) {
+                            Badge {
+                                val desc = pluralStringResource(
+                                    id = R.plurals.notification_chapters_generic,
+                                    count = count,
+                                    count,
+                                )
+                                Text(
+                                    text = count.toString(),
+                                    modifier = Modifier.semantics { contentDescription = desc },
+                                )
+                            }
+                        }
+                    }
+                    BrowseTab::class.isInstance(tab) -> {
+                        val count by produceState(initialValue = 0) {
+                            Injekt.get<SourcePreferences>().extensionUpdatesCount().changes()
+                                .collectLatest { value = it }
+                        }
+                        if (count > 0) {
+                            Badge {
+                                val desc = pluralStringResource(
+                                    id = R.plurals.update_check_notification_ext_updates,
+                                    count = count,
+                                    count,
+                                )
+                                Text(
+                                    text = count.toString(),
+                                    modifier = Modifier.semantics { contentDescription = desc },
+                                )
+                            }
+                        }
+                    }
+                }
+            },
+        ) {
+            Icon(painter = tab.options.icon!!, contentDescription = tab.options.title)
+        }
+    }
+
+    suspend fun search(query: String) {
+        librarySearchEvent.send(query)
+    }
+
+    suspend fun openTab(tab: Tab) {
+        openTabEvent.send(tab)
+    }
+
+    suspend fun showBottomNav(show: Boolean) {
+        showBottomNavEvent.send(show)
+    }
+
+    sealed class Tab {
+        data class Library(val mangaIdToOpen: Long? = null) : Tab()
+        object Updates : Tab()
+        object History : Tab()
+        data class Browse(val toExtensions: Boolean = false) : Tab()
+        data class More(val toDownloads: Boolean) : Tab()
+    }
+}

+ 0 - 53
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt

@@ -1,53 +0,0 @@
-package eu.kanade.tachiyomi.ui.library
-
-import android.os.Bundle
-import android.view.View
-import androidx.compose.runtime.Composable
-import cafe.adriel.voyager.navigator.Navigator
-import eu.kanade.domain.category.model.Category
-import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
-import eu.kanade.tachiyomi.ui.base.controller.RootController
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.flow.collectLatest
-import kotlinx.coroutines.launch
-
-class LibraryController(
-    bundle: Bundle? = null,
-) : BasicFullComposeController(bundle), RootController {
-
-    /**
-     * Sheet containing filter/sort/display items.
-     */
-    private var settingsSheet: LibrarySettingsSheet? = null
-
-    @Composable
-    override fun ComposeContent() {
-        Navigator(screen = LibraryScreen)
-    }
-
-    override fun onViewCreated(view: View) {
-        super.onViewCreated(view)
-
-        settingsSheet = LibrarySettingsSheet(router)
-        viewScope.launch {
-            LibraryScreen.openSettingsSheetEvent
-                .collectLatest(::showSettingsSheet)
-        }
-    }
-
-    override fun onDestroyView(view: View) {
-        settingsSheet?.sheetScope?.cancel()
-        settingsSheet = null
-        super.onDestroyView(view)
-    }
-
-    fun showSettingsSheet(category: Category? = null) {
-        if (category != null) {
-            settingsSheet?.show(category)
-        } else {
-            viewScope.launch { LibraryScreen.requestOpenSettingsSheet() }
-        }
-    }
-
-    fun search(query: String) = LibraryScreen.search(query)
-}

+ 6 - 6
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt

@@ -1,9 +1,9 @@
 package eu.kanade.tachiyomi.ui.library
 
+import android.app.Activity
 import android.content.Context
 import android.util.AttributeSet
 import android.view.View
-import com.bluelinelabs.conductor.Router
 import eu.kanade.domain.base.BasePreferences
 import eu.kanade.domain.category.interactor.SetDisplayModeForCategory
 import eu.kanade.domain.category.interactor.SetSortModeForCategory
@@ -28,11 +28,11 @@ import uy.kohesive.injekt.api.get
 import uy.kohesive.injekt.injectLazy
 
 class LibrarySettingsSheet(
-    router: Router,
+    activity: Activity,
     private val trackManager: TrackManager = Injekt.get(),
     private val setDisplayModeForCategory: SetDisplayModeForCategory = Injekt.get(),
     private val setSortModeForCategory: SetSortModeForCategory = Injekt.get(),
-) : TabbedBottomSheetDialog(router.activity!!) {
+) : TabbedBottomSheetDialog(activity) {
 
     val filters: Filter
     private val sort: Sort
@@ -41,9 +41,9 @@ class LibrarySettingsSheet(
     val sheetScope = CoroutineScope(Job() + Dispatchers.IO)
 
     init {
-        filters = Filter(router.activity!!)
-        sort = Sort(router.activity!!)
-        display = Display(router.activity!!)
+        filters = Filter(activity)
+        sort = Sort(activity)
+        display = Display(activity)
     }
 
     /**

+ 99 - 89
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreen.kt → app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt

@@ -1,10 +1,12 @@
 package eu.kanade.tachiyomi.ui.library
 
 import androidx.activity.compose.BackHandler
+import androidx.compose.animation.graphics.res.animatedVectorResource
+import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
+import androidx.compose.animation.graphics.vector.AnimatedImageVector
 import androidx.compose.foundation.layout.padding
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.outlined.HelpOutline
-import androidx.compose.material3.ScaffoldDefaults
 import androidx.compose.material3.SnackbarHost
 import androidx.compose.material3.SnackbarHostState
 import androidx.compose.runtime.Composable
@@ -21,9 +23,11 @@ import androidx.compose.ui.platform.LocalUriHandler
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.util.fastAll
 import cafe.adriel.voyager.core.model.rememberScreenModel
-import cafe.adriel.voyager.core.screen.Screen
+import cafe.adriel.voyager.navigator.LocalNavigator
+import cafe.adriel.voyager.navigator.Navigator
 import cafe.adriel.voyager.navigator.currentOrThrow
-import com.bluelinelabs.conductor.Router
+import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
+import cafe.adriel.voyager.navigator.tab.TabOptions
 import eu.kanade.domain.category.model.Category
 import eu.kanade.domain.library.model.LibraryManga
 import eu.kanade.domain.library.model.display
@@ -39,27 +43,42 @@ import eu.kanade.presentation.components.Scaffold
 import eu.kanade.presentation.library.components.LibraryContent
 import eu.kanade.presentation.library.components.LibraryToolbar
 import eu.kanade.presentation.manga.components.DownloadCustomAmountDialog
-import eu.kanade.presentation.util.LocalRouter
+import eu.kanade.presentation.util.Tab
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.library.LibraryUpdateService
-import eu.kanade.tachiyomi.ui.base.controller.pushController
-import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
-import eu.kanade.tachiyomi.ui.category.CategoryController
+import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen
+import eu.kanade.tachiyomi.ui.category.CategoryScreen
+import eu.kanade.tachiyomi.ui.home.HomeScreen
 import eu.kanade.tachiyomi.ui.main.MainActivity
-import eu.kanade.tachiyomi.ui.manga.MangaController
+import eu.kanade.tachiyomi.ui.manga.MangaScreen
 import eu.kanade.tachiyomi.ui.reader.ReaderActivity
 import eu.kanade.tachiyomi.util.lang.launchIO
-import eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.receiveAsFlow
 import kotlinx.coroutines.launch
 
-object LibraryScreen : Screen {
+object LibraryTab : Tab {
+
+    override val options: TabOptions
+        @Composable
+        get() {
+            val isSelected = LocalTabNavigator.current.current.key == key
+            val image = AnimatedImageVector.animatedVectorResource(R.drawable.anim_library_enter)
+            return TabOptions(
+                index = 0u,
+                title = stringResource(R.string.label_library),
+                icon = rememberAnimatedVectorPainter(image, isSelected),
+            )
+        }
+
+    override suspend fun onReselect(navigator: Navigator) {
+        requestOpenSettingsSheet()
+    }
 
     @Composable
     override fun Content() {
-        val router = LocalRouter.currentOrThrow
+        val navigator = LocalNavigator.currentOrThrow
         val context = LocalContext.current
         val scope = rememberCoroutineScope()
         val haptic = LocalHapticFeedback.current
@@ -104,7 +123,7 @@ object LibraryScreen : Screen {
                         scope.launch {
                             val randomItem = screenModel.getRandomLibraryItemForCurrentCategory()
                             if (randomItem != null) {
-                                router.openManga(randomItem.libraryManga.manga.id)
+                                navigator.push(MangaScreen(randomItem.libraryManga.manga.id))
                             } else {
                                 snackbarHostState.showSnackbar(context.getString(R.string.information_no_entries_found))
                             }
@@ -127,66 +146,63 @@ object LibraryScreen : Screen {
                 )
             },
             snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
-            contentWindowInsets = TachiyomiBottomNavigationView.withBottomNavInset(ScaffoldDefaults.contentWindowInsets),
         ) { contentPadding ->
-            if (state.isLoading) {
-                LoadingScreen(modifier = Modifier.padding(contentPadding))
-                return@Scaffold
-            }
-
-            if (state.searchQuery.isNullOrEmpty() && state.libraryCount == 0) {
-                val handler = LocalUriHandler.current
-                EmptyScreen(
-                    textResource = R.string.information_empty_library,
-                    modifier = Modifier.padding(contentPadding),
-                    actions = listOf(
-                        EmptyScreenAction(
-                            stringResId = R.string.getting_started_guide,
-                            icon = Icons.Outlined.HelpOutline,
-                            onClick = { handler.openUri("https://tachiyomi.org/help/guides/getting-started") },
+            when {
+                state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding))
+                state.searchQuery.isNullOrEmpty() && state.libraryCount == 0 -> {
+                    val handler = LocalUriHandler.current
+                    EmptyScreen(
+                        textResource = R.string.information_empty_library,
+                        modifier = Modifier.padding(contentPadding),
+                        actions = listOf(
+                            EmptyScreenAction(
+                                stringResId = R.string.getting_started_guide,
+                                icon = Icons.Outlined.HelpOutline,
+                                onClick = { handler.openUri("https://tachiyomi.org/help/guides/getting-started") },
+                            ),
                         ),
-                    ),
-                )
-                return@Scaffold
+                    )
+                }
+                else -> {
+                    LibraryContent(
+                        categories = state.categories,
+                        searchQuery = state.searchQuery,
+                        selection = state.selection,
+                        contentPadding = contentPadding,
+                        currentPage = { screenModel.activeCategory },
+                        isLibraryEmpty = state.libraryCount == 0,
+                        showPageTabs = state.showCategoryTabs,
+                        onChangeCurrentPage = { screenModel.activeCategory = it },
+                        onMangaClicked = { navigator.push(MangaScreen(it)) },
+                        onContinueReadingClicked = { it: LibraryManga ->
+                            scope.launchIO {
+                                val chapter = screenModel.getNextUnreadChapter(it.manga)
+                                if (chapter != null) {
+                                    context.startActivity(ReaderActivity.newIntent(context, chapter.mangaId, chapter.id))
+                                } else {
+                                    snackbarHostState.showSnackbar(context.getString(R.string.no_next_chapter))
+                                }
+                            }
+                            Unit
+                        }.takeIf { state.showMangaContinueButton },
+                        onToggleSelection = { screenModel.toggleSelection(it) },
+                        onToggleRangeSelection = {
+                            screenModel.toggleRangeSelection(it)
+                            haptic.performHapticFeedback(HapticFeedbackType.LongPress)
+                        },
+                        onRefresh = onClickRefresh,
+                        onGlobalSearchClicked = {
+                            navigator.push(GlobalSearchScreen(screenModel.state.value.searchQuery ?: ""))
+                        },
+                        getNumberOfMangaForCategory = { state.getMangaCountForCategory(it) },
+                        getDisplayModeForPage = { state.categories[it].display },
+                        getColumnsForOrientation = { screenModel.getColumnsPreferenceForCurrentOrientation(it) },
+                        getLibraryForPage = { state.getLibraryItemsByPage(it) },
+                        isDownloadOnly = screenModel.isDownloadOnly,
+                        isIncognitoMode = screenModel.isIncognitoMode,
+                    )
+                }
             }
-
-            LibraryContent(
-                categories = state.categories,
-                searchQuery = state.searchQuery,
-                selection = state.selection,
-                contentPadding = contentPadding,
-                currentPage = { screenModel.activeCategory },
-                isLibraryEmpty = state.libraryCount == 0,
-                showPageTabs = state.showCategoryTabs,
-                onChangeCurrentPage = { screenModel.activeCategory = it },
-                onMangaClicked = { router.openManga(it) },
-                onContinueReadingClicked = { it: LibraryManga ->
-                    scope.launchIO {
-                        val chapter = screenModel.getNextUnreadChapter(it.manga)
-                        if (chapter != null) {
-                            context.startActivity(ReaderActivity.newIntent(context, chapter.mangaId, chapter.id))
-                        } else {
-                            snackbarHostState.showSnackbar(context.getString(R.string.no_next_chapter))
-                        }
-                    }
-                    Unit
-                }.takeIf { state.showMangaContinueButton },
-                onToggleSelection = { screenModel.toggleSelection(it) },
-                onToggleRangeSelection = {
-                    screenModel.toggleRangeSelection(it)
-                    haptic.performHapticFeedback(HapticFeedbackType.LongPress)
-                },
-                onRefresh = onClickRefresh,
-                onGlobalSearchClicked = {
-                    router.pushController(GlobalSearchController(screenModel.state.value.searchQuery ?: ""))
-                },
-                getNumberOfMangaForCategory = { state.getMangaCountForCategory(it) },
-                getDisplayModeForPage = { state.categories[it].display },
-                getColumnsForOrientation = { screenModel.getColumnsPreferenceForCurrentOrientation(it) },
-                getLibraryForPage = { state.getLibraryItemsByPage(it) },
-                isDownloadOnly = screenModel.isDownloadOnly,
-                isIncognitoMode = screenModel.isIncognitoMode,
-            )
         }
 
         val onDismissRequest = screenModel::closeDialog
@@ -197,7 +213,7 @@ object LibraryScreen : Screen {
                     onDismissRequest = onDismissRequest,
                     onEditCategories = {
                         screenModel.clearSelection()
-                        router.pushController(CategoryController())
+                        navigator.push(CategoryScreen())
                     },
                     onConfirm = { include, exclude ->
                         screenModel.clearSelection()
@@ -236,11 +252,9 @@ object LibraryScreen : Screen {
         }
 
         LaunchedEffect(state.selectionMode) {
-            // Could perhaps be removed when navigation is in a Compose world
-            if (router.backstackSize == 1) {
-                (context as? MainActivity)?.showBottomNav(!state.selectionMode)
-            }
+            HomeScreen.showBottomNav(!state.selectionMode)
         }
+
         LaunchedEffect(state.isLoading) {
             if (!state.isLoading) {
                 (context as? MainActivity)?.ready = true
@@ -248,23 +262,19 @@ object LibraryScreen : Screen {
         }
 
         LaunchedEffect(Unit) {
-            launch { queryEvent.collectLatest(screenModel::search) }
-            launch { requestSettingsSheetEvent.collectLatest { onClickFilter() } }
+            launch { queryEvent.receiveAsFlow().collect(screenModel::search) }
+            launch { requestSettingsSheetEvent.receiveAsFlow().collectLatest { onClickFilter() } }
         }
     }
 
-    private fun Router.openManga(mangaId: Long) {
-        pushController(MangaController(mangaId))
-    }
-
     // For invoking search from other screen
-    private val queryEvent = MutableSharedFlow<String>(replay = 1)
-    fun search(query: String) = queryEvent.tryEmit(query)
+    private val queryEvent = Channel<String>()
+    suspend fun search(query: String) = queryEvent.send(query)
 
     // For opening settings sheet in LibraryController
-    private val requestSettingsSheetEvent = MutableSharedFlow<Unit>()
-    private val openSettingsSheetEvent_ = MutableSharedFlow<Category>()
-    val openSettingsSheetEvent = openSettingsSheetEvent_.asSharedFlow()
-    private suspend fun sendSettingsSheetIntent(category: Category) = openSettingsSheetEvent_.emit(category)
-    suspend fun requestOpenSettingsSheet() = requestSettingsSheetEvent.emit(Unit)
+    private val requestSettingsSheetEvent = Channel<Unit>()
+    private val openSettingsSheetEvent_ = Channel<Category>()
+    val openSettingsSheetEvent = openSettingsSheetEvent_.receiveAsFlow()
+    private suspend fun sendSettingsSheetIntent(category: Category) = openSettingsSheetEvent_.send(category)
+    suspend fun requestOpenSettingsSheet() = requestSettingsSheetEvent.send(Unit)
 }

+ 173 - 410
app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt

@@ -6,33 +6,44 @@ import android.content.Intent
 import android.graphics.Color
 import android.os.Build
 import android.os.Bundle
-import android.view.ViewGroup
+import android.view.View
 import android.view.Window
 import android.widget.Toast
-import androidx.appcompat.view.ActionMode
+import androidx.activity.compose.BackHandler
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
 import androidx.core.animation.doOnEnd
-import androidx.core.graphics.ColorUtils
 import androidx.core.splashscreen.SplashScreen
 import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
 import androidx.core.view.ViewCompat
 import androidx.core.view.WindowCompat
 import androidx.core.view.WindowInsetsCompat
-import androidx.core.view.isVisible
 import androidx.interpolator.view.animation.FastOutSlowInInterpolator
 import androidx.interpolator.view.animation.LinearOutSlowInInterpolator
 import androidx.lifecycle.lifecycleScope
-import com.bluelinelabs.conductor.Conductor
-import com.bluelinelabs.conductor.Controller
-import com.bluelinelabs.conductor.ControllerChangeHandler
-import com.bluelinelabs.conductor.Router
-import com.bluelinelabs.conductor.RouterTransaction
-import com.google.android.material.navigation.NavigationBarView
+import cafe.adriel.voyager.navigator.LocalNavigator
+import cafe.adriel.voyager.navigator.Navigator
+import cafe.adriel.voyager.navigator.NavigatorDisposeBehavior
+import cafe.adriel.voyager.navigator.currentOrThrow
+import cafe.adriel.voyager.transitions.ScreenTransition
 import com.google.android.material.transition.platform.MaterialContainerTransformSharedElementCallback
-import dev.chrisbanes.insetter.applyInsetter
 import eu.kanade.domain.base.BasePreferences
+import eu.kanade.domain.category.model.Category
 import eu.kanade.domain.library.service.LibraryPreferences
 import eu.kanade.domain.source.service.SourcePreferences
 import eu.kanade.domain.ui.UiPreferences
+import eu.kanade.presentation.util.Transition
+import eu.kanade.presentation.util.collectAsState
 import eu.kanade.tachiyomi.BuildConfig
 import eu.kanade.tachiyomi.Migrations
 import eu.kanade.tachiyomi.R
@@ -40,39 +51,29 @@ import eu.kanade.tachiyomi.data.cache.ChapterCache
 import eu.kanade.tachiyomi.data.notification.NotificationReceiver
 import eu.kanade.tachiyomi.data.updater.AppUpdateChecker
 import eu.kanade.tachiyomi.data.updater.AppUpdateResult
-import eu.kanade.tachiyomi.databinding.MainActivityBinding
-import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
+import eu.kanade.tachiyomi.data.updater.RELEASE_URL
 import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
-import eu.kanade.tachiyomi.ui.base.controller.ComposeContentController
-import eu.kanade.tachiyomi.ui.base.controller.DialogController
-import eu.kanade.tachiyomi.ui.base.controller.RootController
-import eu.kanade.tachiyomi.ui.base.controller.pushController
-import eu.kanade.tachiyomi.ui.base.controller.setRoot
-import eu.kanade.tachiyomi.ui.browse.BrowseController
-import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
-import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
-import eu.kanade.tachiyomi.ui.download.DownloadController
-import eu.kanade.tachiyomi.ui.history.HistoryController
-import eu.kanade.tachiyomi.ui.library.LibraryController
-import eu.kanade.tachiyomi.ui.manga.MangaController
-import eu.kanade.tachiyomi.ui.more.MoreController
-import eu.kanade.tachiyomi.ui.more.NewUpdateDialogController
-import eu.kanade.tachiyomi.ui.setting.SettingsMainController
-import eu.kanade.tachiyomi.ui.updates.UpdatesController
-import eu.kanade.tachiyomi.util.lang.launchIO
-import eu.kanade.tachiyomi.util.lang.launchUI
-import eu.kanade.tachiyomi.util.preference.asHotFlow
+import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
+import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen
+import eu.kanade.tachiyomi.ui.home.HomeScreen
+import eu.kanade.tachiyomi.ui.library.LibrarySettingsSheet
+import eu.kanade.tachiyomi.ui.library.LibraryTab
+import eu.kanade.tachiyomi.ui.manga.MangaScreen
+import eu.kanade.tachiyomi.ui.more.NewUpdateScreen
+import eu.kanade.tachiyomi.util.Constants
 import eu.kanade.tachiyomi.util.system.dpToPx
-import eu.kanade.tachiyomi.util.system.getThemeColor
-import eu.kanade.tachiyomi.util.system.isTabletUi
 import eu.kanade.tachiyomi.util.system.logcat
+import eu.kanade.tachiyomi.util.system.openInBrowser
 import eu.kanade.tachiyomi.util.system.toast
+import eu.kanade.tachiyomi.util.view.setComposeContent
 import eu.kanade.tachiyomi.util.view.setNavigationBarTransparentCompat
+import kotlinx.coroutines.cancel
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.drop
 import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.merge
 import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
 import logcat.LogPriority
 import uy.kohesive.injekt.Injekt
 import uy.kohesive.injekt.api.get
@@ -86,24 +87,20 @@ class MainActivity : BaseActivity() {
     private val uiPreferences: UiPreferences by injectLazy()
     private val preferences: BasePreferences by injectLazy()
 
-    lateinit var binding: MainActivityBinding
-
-    private lateinit var router: Router
-
-    private val startScreenId = R.id.nav_library
-    private var isConfirmingExit: Boolean = false
     private var isHandlingShortcut: Boolean = false
 
-    /**
-     * App bar lift state for backstack
-     */
-    private val backstackLiftState = mutableMapOf<String, Boolean>()
-
     private val chapterCache: ChapterCache by injectLazy()
 
     // To be checked by splash screen. If true then splash screen will be removed.
     var ready = false
 
+    /**
+     * Sheet containing filter/sort/display items.
+     */
+    private var settingsSheet: LibrarySettingsSheet? = null
+
+    private lateinit var navigator: Navigator
+
     override fun onCreate(savedInstanceState: Bundle?) {
         // Prevent splash screen showing up on configuration changes
         val splashScreen = if (savedInstanceState == null) installSplashScreen() else null
@@ -132,154 +129,138 @@ class MainActivity : BaseActivity() {
             false
         }
 
-        binding = MainActivityBinding.inflate(layoutInflater)
-
         // Do not let the launcher create a new activity http://stackoverflow.com/questions/16283079
         if (!isTaskRoot) {
             finish()
             return
         }
 
-        setContentView(binding.root)
-        setSupportActionBar(binding.toolbar)
-
         // Draw edge-to-edge
         WindowCompat.setDecorFitsSystemWindows(window, false)
-        binding.bottomNav?.applyInsetter {
-            type(navigationBars = true) {
-                padding()
-            }
-        }
 
-        val startTime = System.currentTimeMillis()
-        splashScreen?.setKeepVisibleCondition {
-            val elapsed = System.currentTimeMillis() - startTime
-            elapsed <= SPLASH_MIN_DURATION || (!ready && elapsed <= SPLASH_MAX_DURATION)
-        }
-        setSplashScreenExitAnimation(splashScreen)
+        settingsSheet = LibrarySettingsSheet(this)
+        LibraryTab.openSettingsSheetEvent
+            .onEach(::showSettingsSheet)
+            .launchIn(lifecycleScope)
 
-        nav.setOnItemSelectedListener { item ->
-            val id = item.itemId
-
-            val currentRoot = router.backstack.firstOrNull()
-            if (currentRoot?.tag()?.toIntOrNull() != id) {
-                when (id) {
-                    R.id.nav_library -> router.setRoot(LibraryController(), id)
-                    R.id.nav_updates -> router.setRoot(UpdatesController(), id)
-                    R.id.nav_history -> router.setRoot(HistoryController(), id)
-                    R.id.nav_browse -> router.setRoot(BrowseController(toExtensions = false), id)
-                    R.id.nav_more -> router.setRoot(MoreController(), id)
+        setComposeContent {
+            Navigator(
+                screen = HomeScreen,
+                disposeBehavior = NavigatorDisposeBehavior(disposeNestedNavigators = false, disposeSteps = true),
+            ) { navigator ->
+                if (navigator.size == 1) {
+                    ConfirmExit()
                 }
-            } else if (!isHandlingShortcut) {
-                when (id) {
-                    R.id.nav_library -> {
-                        val controller = router.getControllerWithTag(id.toString()) as? LibraryController
-                        controller?.showSettingsSheet()
-                    }
-                    R.id.nav_updates -> {
-                        if (router.backstackSize == 1) {
-                            router.pushController(DownloadController())
-                        }
-                    }
-                    R.id.nav_history -> {
-                        if (router.backstackSize == 1) {
-                            try {
-                                val historyController = router.backstack[0].controller as HistoryController
-                                historyController.resumeLastChapterRead()
-                            } catch (e: Exception) {
-                                toast(R.string.cant_open_last_read_chapter)
+
+                // Shows current screen
+                ScreenTransition(navigator = navigator, transition = { Transition.OneWayFade })
+
+                // Pop source-related screens when incognito mode is turned off
+                LaunchedEffect(Unit) {
+                    preferences.incognitoMode().changes()
+                        .drop(1)
+                        .onEach {
+                            if (!it) {
+                                val currentScreen = navigator.lastItem
+                                if (currentScreen is BrowseSourceScreen ||
+                                    (currentScreen is MangaScreen && currentScreen.fromSource)
+                                ) {
+                                    navigator.popUntilRoot()
+                                }
                             }
                         }
-                    }
-                    R.id.nav_more -> {
-                        if (router.backstackSize == 1) {
-                            router.pushController(SettingsMainController())
-                        }
-                    }
+                        .launchIn(this)
                 }
-            }
-            true
-        }
 
-        val container: ViewGroup = binding.controllerContainer
-        router = Conductor.attachRouter(this, container, savedInstanceState)
-            .setPopRootControllerMode(Router.PopRootControllerMode.NEVER)
-        router.addChangeListener(
-            object : ControllerChangeHandler.ControllerChangeListener {
-                override fun onChangeStarted(
-                    to: Controller?,
-                    from: Controller?,
-                    isPush: Boolean,
-                    container: ViewGroup,
-                    handler: ControllerChangeHandler,
-                ) {
-                    syncActivityViewWithController(to, from, isPush)
+                LaunchedEffect(navigator) {
+                    [email protected] = navigator
                 }
 
-                override fun onChangeCompleted(
-                    to: Controller?,
-                    from: Controller?,
-                    isPush: Boolean,
-                    container: ViewGroup,
-                    handler: ControllerChangeHandler,
-                ) {
-                }
-            },
-        )
-        if (!router.hasRootController()) {
-            // Set start screen
-            if (!handleIntentAction(intent)) {
-                moveToStartScreen()
+                CheckForUpdate()
+            }
+
+            var showChangelog by remember { mutableStateOf(didMigration && !BuildConfig.DEBUG) }
+            if (showChangelog) {
+                AlertDialog(
+                    onDismissRequest = { showChangelog = false },
+                    title = { Text(text = stringResource(R.string.updated_version, BuildConfig.VERSION_NAME)) },
+                    dismissButton = {
+                        TextButton(onClick = { openInBrowser(RELEASE_URL) }) {
+                            Text(text = stringResource(R.string.whats_new))
+                        }
+                    },
+                    confirmButton = {
+                        TextButton(onClick = { showChangelog = false }) {
+                            Text(text = stringResource(android.R.string.ok))
+                        }
+                    },
+                )
             }
         }
-        syncActivityViewWithController()
 
-        binding.toolbar.setNavigationOnClickListener {
-            onBackPressed()
+        val startTime = System.currentTimeMillis()
+        splashScreen?.setKeepVisibleCondition {
+            val elapsed = System.currentTimeMillis() - startTime
+            elapsed <= SPLASH_MIN_DURATION || (!ready && elapsed <= SPLASH_MAX_DURATION)
         }
+        setSplashScreenExitAnimation(splashScreen)
 
         if (savedInstanceState == null) {
+            // Set start screen
+            lifecycleScope.launch { handleIntentAction(intent) }
+
             // Reset Incognito Mode on relaunch
             preferences.incognitoMode().set(false)
+        }
+    }
 
-            // Show changelog prompt on update
-            if (didMigration && !BuildConfig.DEBUG) {
-                WhatsNewDialogController().showDialog(router)
-            }
+    private fun showSettingsSheet(category: Category? = null) {
+        if (category != null) {
+            settingsSheet?.show(category)
         } else {
-            // Restore selected nav item
-            router.backstack.firstOrNull()?.tag()?.toIntOrNull()?.let {
-                nav.menu.findItem(it).isChecked = true
-            }
+            lifecycleScope.launch { LibraryTab.requestOpenSettingsSheet() }
         }
+    }
 
-        merge(libraryPreferences.showUpdatesNavBadge().changes(), libraryPreferences.unreadUpdatesCount().changes())
-            .onEach { setUnreadUpdatesBadge() }
-            .launchIn(lifecycleScope)
-
-        sourcePreferences.extensionUpdatesCount()
-            .asHotFlow { setExtensionsBadge() }
-            .launchIn(lifecycleScope)
-
-        preferences.downloadedOnly()
-            .asHotFlow { binding.downloadedOnly.isVisible = it }
-            .launchIn(lifecycleScope)
+    @Composable
+    private fun ConfirmExit() {
+        val scope = rememberCoroutineScope()
+        val confirmExit by preferences.confirmExit().collectAsState()
+        var waitingConfirmation by remember { mutableStateOf(false) }
+        BackHandler(enabled = !waitingConfirmation && confirmExit) {
+            scope.launch {
+                waitingConfirmation = true
+                val toast = toast(R.string.confirm_exit, Toast.LENGTH_LONG)
+                delay(2.seconds)
+                toast.cancel()
+                waitingConfirmation = false
+            }
+        }
+    }
 
-        binding.incognitoMode.isVisible = preferences.incognitoMode().get()
-        preferences.incognitoMode().changes()
-            .drop(1)
-            .onEach {
-                binding.incognitoMode.isVisible = it
-
-                // Close BrowseSourceController and its MangaController child when incognito mode is disabled
-                if (!it) {
-                    val fg = router.backstack.lastOrNull()?.controller
-                    if (fg is BrowseSourceController || fg is MangaController && fg.fromSource) {
-                        router.popToRoot()
+    @Composable
+    private fun CheckForUpdate() {
+        val context = LocalContext.current
+        val navigator = LocalNavigator.currentOrThrow
+        LaunchedEffect(Unit) {
+            // App updates
+            if (BuildConfig.INCLUDE_UPDATER) {
+                try {
+                    val result = AppUpdateChecker().checkForUpdate(context)
+                    if (result is AppUpdateResult.NewUpdate) {
+                        val updateScreen = NewUpdateScreen(
+                            versionName = result.release.version,
+                            changelogInfo = result.release.info,
+                            releaseLink = result.release.releaseLink,
+                            downloadLink = result.release.getDownloadLink(),
+                        )
+                        navigator.push(updateScreen)
                     }
+                } catch (e: Exception) {
+                    logcat(LogPriority.ERROR, e)
                 }
             }
-            .launchIn(lifecycleScope)
+        }
     }
 
     /**
@@ -289,16 +270,16 @@ class MainActivity : BaseActivity() {
      * after the animation is finished.
      */
     private fun setSplashScreenExitAnimation(splashScreen: SplashScreen?) {
+        val root = findViewById<View>(android.R.id.content)
         val setNavbarScrim = {
             // Make sure navigation bar is on bottom before we modify it
-            ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, insets ->
+            ViewCompat.setOnApplyWindowInsetsListener(root) { _, insets ->
                 if (insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom > 0) {
-                    val elevation = binding.bottomNav?.elevation ?: 0F
-                    window.setNavigationBarTransparentCompat(this@MainActivity, elevation)
+                    window.setNavigationBarTransparentCompat(this@MainActivity, 3.dpToPx.toFloat())
                 }
                 insets
             }
-            ViewCompat.requestApplyInsets(binding.root)
+            ViewCompat.requestApplyInsets(root)
         }
 
         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S && splashScreen != null) {
@@ -316,7 +297,7 @@ class MainActivity : BaseActivity() {
                     duration = SPLASH_EXIT_ANIM_DURATION
                     addUpdateListener { va ->
                         val value = va.animatedValue as Float
-                        binding.root.translationY = value * 16.dpToPx
+                        root.translationY = value * 16.dpToPx
                     }
                 }
 
@@ -344,69 +325,13 @@ class MainActivity : BaseActivity() {
     }
 
     override fun onNewIntent(intent: Intent) {
-        if (!handleIntentAction(intent)) {
+        val handle = runBlocking { handleIntentAction(intent) }
+        if (!handle) {
             super.onNewIntent(intent)
         }
     }
 
-    override fun onResume() {
-        super.onResume()
-        checkForUpdates()
-    }
-
-    private fun checkForUpdates() {
-        lifecycleScope.launchIO {
-            // App updates
-            if (BuildConfig.INCLUDE_UPDATER) {
-                try {
-                    val result = AppUpdateChecker().checkForUpdate(this@MainActivity)
-                    if (result is AppUpdateResult.NewUpdate) {
-                        NewUpdateDialogController(result).showDialog(router)
-                    }
-                } catch (e: Exception) {
-                    logcat(LogPriority.ERROR, e)
-                }
-            }
-
-            // Extension updates
-            try {
-                ExtensionGithubApi().checkForUpdates(
-                    this@MainActivity,
-                    fromAvailableExtensionList = true,
-                )?.let { pendingUpdates ->
-                    sourcePreferences.extensionUpdatesCount().set(pendingUpdates.size)
-                }
-            } catch (e: Exception) {
-                logcat(LogPriority.ERROR, e)
-            }
-        }
-    }
-
-    private fun setUnreadUpdatesBadge() {
-        val updates = if (libraryPreferences.showUpdatesNavBadge().get()) libraryPreferences.unreadUpdatesCount().get() else 0
-        if (updates > 0) {
-            nav.getOrCreateBadge(R.id.nav_updates).apply {
-                number = updates
-                setContentDescriptionQuantityStringsResource(R.plurals.notification_chapters_generic)
-            }
-        } else {
-            nav.removeBadge(R.id.nav_updates)
-        }
-    }
-
-    private fun setExtensionsBadge() {
-        val updates = sourcePreferences.extensionUpdatesCount().get()
-        if (updates > 0) {
-            nav.getOrCreateBadge(R.id.nav_browse).apply {
-                number = updates
-                setContentDescriptionQuantityStringsResource(R.plurals.update_check_notification_ext_updates)
-            }
-        } else {
-            nav.removeBadge(R.id.nav_browse)
-        }
-    }
-
-    private fun handleIntentAction(intent: Intent): Boolean {
+    private suspend fun handleIntentAction(intent: Intent): Boolean {
         val notificationId = intent.getIntExtra("notificationId", -1)
         if (notificationId > -1) {
             NotificationReceiver.dismissNotification(applicationContext, notificationId, intent.getIntExtra("groupId", 0))
@@ -415,32 +340,19 @@ class MainActivity : BaseActivity() {
         isHandlingShortcut = true
 
         when (intent.action) {
-            SHORTCUT_LIBRARY -> setSelectedNavItem(R.id.nav_library)
-            SHORTCUT_RECENTLY_UPDATED -> setSelectedNavItem(R.id.nav_updates)
-            SHORTCUT_RECENTLY_READ -> setSelectedNavItem(R.id.nav_history)
-            SHORTCUT_CATALOGUES -> setSelectedNavItem(R.id.nav_browse)
-            SHORTCUT_EXTENSIONS -> {
-                if (router.backstackSize > 1) {
-                    router.popToRoot()
-                }
-                setSelectedNavItem(R.id.nav_browse)
-                router.pushController(BrowseController(toExtensions = true))
-            }
+            SHORTCUT_LIBRARY -> HomeScreen.openTab(HomeScreen.Tab.Library())
+            SHORTCUT_RECENTLY_UPDATED -> HomeScreen.openTab(HomeScreen.Tab.Updates)
+            SHORTCUT_RECENTLY_READ -> HomeScreen.openTab(HomeScreen.Tab.History)
+            SHORTCUT_CATALOGUES -> HomeScreen.openTab(HomeScreen.Tab.Browse(false))
+            SHORTCUT_EXTENSIONS -> HomeScreen.openTab(HomeScreen.Tab.Browse(true))
             SHORTCUT_MANGA -> {
-                val extras = intent.extras ?: return false
-                val fgController = router.backstack.lastOrNull()?.controller as? MangaController
-                if (fgController?.mangaId != extras.getLong(MangaController.MANGA_EXTRA)) {
-                    router.popToRoot()
-                    setSelectedNavItem(R.id.nav_library)
-                    router.pushController(RouterTransaction.with(MangaController(extras)))
-                }
+                val idToOpen = intent.extras?.getLong(Constants.MANGA_EXTRA) ?: return false
+                navigator.popUntilRoot()
+                HomeScreen.openTab(HomeScreen.Tab.Library(idToOpen))
             }
             SHORTCUT_DOWNLOADS -> {
-                if (router.backstackSize > 1) {
-                    router.popToRoot()
-                }
-                setSelectedNavItem(R.id.nav_more)
-                router.pushController(DownloadController())
+                navigator.popUntilRoot()
+                HomeScreen.openTab(HomeScreen.Tab.More(toDownloads = true))
             }
             Intent.ACTION_SEARCH, Intent.ACTION_SEND, "com.google.android.gms.actions.SEARCH_ACTION" -> {
                 // If the intent match the "standard" Android search intent
@@ -449,20 +361,16 @@ class MainActivity : BaseActivity() {
                 // Get the search query provided in extras, and if not null, perform a global search with it.
                 val query = intent.getStringExtra(SearchManager.QUERY) ?: intent.getStringExtra(Intent.EXTRA_TEXT)
                 if (query != null && query.isNotEmpty()) {
-                    if (router.backstackSize > 1) {
-                        router.popToRoot()
-                    }
-                    router.pushController(GlobalSearchController(query))
+                    navigator.popUntilRoot()
+                    navigator.push(GlobalSearchScreen(query))
                 }
             }
             INTENT_SEARCH -> {
                 val query = intent.getStringExtra(INTENT_SEARCH_QUERY)
                 if (query != null && query.isNotEmpty()) {
-                    val filter = intent.getStringExtra(INTENT_SEARCH_FILTER)
-                    if (router.backstackSize > 1) {
-                        router.popToRoot()
-                    }
-                    router.pushController(GlobalSearchController(query, filter ?: ""))
+                    val filter = intent.getStringExtra(INTENT_SEARCH_FILTER) ?: ""
+                    navigator.popUntilRoot()
+                    navigator.push(GlobalSearchScreen(query, filter))
                 }
             }
             else -> {
@@ -476,167 +384,22 @@ class MainActivity : BaseActivity() {
         return true
     }
 
-    @Suppress("UNNECESSARY_SAFE_CALL")
     override fun onDestroy() {
+        settingsSheet?.sheetScope?.cancel()
+        settingsSheet = null
         super.onDestroy()
-
-        // Binding sometimes isn't actually instantiated yet somehow
-        nav?.setOnItemSelectedListener(null)
-        binding?.toolbar?.setNavigationOnClickListener(null)
     }
 
     override fun onBackPressed() {
-        if (router.handleBack()) {
-            // A Router is consuming back press
-            return
-        }
-        val backstackSize = router.backstackSize
-        val startScreen = router.getControllerWithTag("$startScreenId")
-        if (backstackSize == 1 && startScreen == null) {
-            // Return to start screen
-            moveToStartScreen()
-        } else if (shouldHandleExitConfirmation()) {
-            // Exit confirmation (resets after 2 seconds)
-            lifecycleScope.launchUI { resetExitConfirmation() }
-        } else if (backstackSize == 1) {
-            // Regular back (i.e. closing the app)
-            if (libraryPreferences.autoClearChapterCache().get()) {
-                chapterCache.clear()
-            }
-            super.onBackPressed()
+        if (navigator.size == 1 &&
+            !onBackPressedDispatcher.hasEnabledCallbacks() &&
+            libraryPreferences.autoClearChapterCache().get()
+        ) {
+            chapterCache.clear()
         }
+        super.onBackPressed()
     }
 
-    fun moveToStartScreen() {
-        setSelectedNavItem(startScreenId)
-    }
-
-    override fun onSupportActionModeStarted(mode: ActionMode) {
-        binding.appbar.apply {
-            tag = isTransparentWhenNotLifted
-            isTransparentWhenNotLifted = false
-        }
-        // Color taken from m3_appbar_background
-        window.statusBarColor = ColorUtils.compositeColors(
-            getColor(R.color.m3_appbar_overlay_color),
-            getThemeColor(R.attr.colorSurface),
-        )
-        super.onSupportActionModeStarted(mode)
-    }
-
-    override fun onSupportActionModeFinished(mode: ActionMode) {
-        binding.appbar.apply {
-            isTransparentWhenNotLifted = (tag as? Boolean) ?: false
-            tag = null
-        }
-        window.statusBarColor = getThemeColor(android.R.attr.statusBarColor)
-        super.onSupportActionModeFinished(mode)
-    }
-
-    private suspend fun resetExitConfirmation() {
-        isConfirmingExit = true
-        val toast = toast(R.string.confirm_exit, Toast.LENGTH_LONG)
-        delay(2.seconds)
-        toast.cancel()
-        isConfirmingExit = false
-    }
-
-    private fun shouldHandleExitConfirmation(): Boolean {
-        return router.backstackSize == 1 &&
-            router.getControllerWithTag("$startScreenId") != null &&
-            preferences.confirmExit().get() &&
-            !isConfirmingExit
-    }
-
-    fun setSelectedNavItem(itemId: Int) {
-        if (!isFinishing) {
-            nav.selectedItemId = itemId
-        }
-    }
-
-    private fun syncActivityViewWithController(
-        to: Controller? = null,
-        from: Controller? = null,
-        isPush: Boolean = true,
-    ) {
-        var internalTo = to
-
-        if (internalTo == null) {
-            // Should go here when the activity is recreated and dialog controller is on top of the backstack
-            // Then we'll assume the top controller is the parent controller of this dialog
-            val backstack = router.backstack
-            internalTo = backstack.lastOrNull()?.controller
-            if (internalTo is DialogController) {
-                internalTo = backstack.getOrNull(backstack.size - 2)?.controller ?: return
-            }
-        } else {
-            // Ignore changes for normal transactions
-            if (from is DialogController || internalTo is DialogController) {
-                return
-            }
-        }
-
-        supportActionBar?.setDisplayHomeAsUpEnabled(router.backstackSize != 1)
-
-        // Always show appbar again when changing controllers
-        binding.appbar.setExpanded(true)
-
-        if ((from == null || from is RootController) && internalTo !is RootController) {
-            showNav(false)
-        }
-        if (internalTo is RootController) {
-            // Always show bottom nav again when returning to a RootController
-            showNav(true)
-        }
-
-        val isComposeController = internalTo is ComposeContentController
-        binding.appbar.isVisible = !isComposeController
-        binding.controllerContainer.enableScrollingBehavior(!isComposeController)
-
-        if (!isTabletUi()) {
-            // Save lift state
-            if (isPush) {
-                if (router.backstackSize > 1) {
-                    // Save lift state
-                    from?.let {
-                        backstackLiftState[it.instanceId] = binding.appbar.isLifted
-                    }
-                } else {
-                    backstackLiftState.clear()
-                }
-                binding.appbar.isLifted = false
-            } else {
-                internalTo?.let {
-                    binding.appbar.isLifted = backstackLiftState.getOrElse(it.instanceId) { false }
-                }
-                from?.let {
-                    backstackLiftState.remove(it.instanceId)
-                }
-            }
-        }
-    }
-
-    private fun showNav(visible: Boolean) {
-        showBottomNav(visible)
-        showSideNav(visible)
-    }
-
-    // Also used from some controllers to swap bottom nav with action toolbar
-    fun showBottomNav(visible: Boolean) {
-        if (visible) {
-            binding.bottomNav?.slideUp()
-        } else {
-            binding.bottomNav?.slideDown()
-        }
-    }
-
-    private fun showSideNav(visible: Boolean) {
-        binding.sideNav?.isVisible = visible
-    }
-
-    private val nav: NavigationBarView
-        get() = binding.bottomNav ?: binding.sideNav!!
-
     init {
         registerSecureActivity(this)
     }

+ 0 - 24
app/src/main/java/eu/kanade/tachiyomi/ui/main/WhatsNewDialogController.kt

@@ -1,24 +0,0 @@
-package eu.kanade.tachiyomi.ui.main
-
-import android.app.Dialog
-import android.os.Bundle
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import eu.kanade.tachiyomi.BuildConfig
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.updater.RELEASE_URL
-import eu.kanade.tachiyomi.ui.base.controller.DialogController
-import eu.kanade.tachiyomi.ui.base.controller.openInBrowser
-
-class WhatsNewDialogController(bundle: Bundle? = null) : DialogController(bundle) {
-
-    @Suppress("DEPRECATION")
-    override fun onCreateDialog(savedViewState: Bundle?): Dialog {
-        return MaterialAlertDialogBuilder(activity!!)
-            .setTitle(activity!!.getString(R.string.updated_version, BuildConfig.VERSION_NAME))
-            .setPositiveButton(android.R.string.ok, null)
-            .setNeutralButton(R.string.whats_new) { _, _ ->
-                openInBrowser(RELEASE_URL)
-            }
-            .create()
-    }
-}

+ 0 - 34
app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt

@@ -1,34 +0,0 @@
-package eu.kanade.tachiyomi.ui.manga
-
-import android.os.Bundle
-import androidx.compose.runtime.Composable
-import androidx.core.os.bundleOf
-import cafe.adriel.voyager.navigator.Navigator
-import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
-
-class MangaController : BasicFullComposeController {
-
-    @Suppress("unused")
-    constructor(bundle: Bundle) : this(bundle.getLong(MANGA_EXTRA))
-
-    constructor(
-        mangaId: Long,
-        fromSource: Boolean = false,
-    ) : super(bundleOf(MANGA_EXTRA to mangaId, FROM_SOURCE_EXTRA to fromSource))
-
-    val mangaId: Long
-        get() = args.getLong(MANGA_EXTRA)
-
-    val fromSource: Boolean
-        get() = args.getBoolean(FROM_SOURCE_EXTRA)
-
-    @Composable
-    override fun ComposeContent() {
-        Navigator(screen = MangaScreen(mangaId, fromSource))
-    }
-
-    companion object {
-        const val FROM_SOURCE_EXTRA = "from_source"
-        const val MANGA_EXTRA = "manga"
-    }
-}

+ 26 - 42
app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt

@@ -16,6 +16,7 @@ import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.hapticfeedback.HapticFeedbackType
@@ -28,7 +29,6 @@ import cafe.adriel.voyager.navigator.LocalNavigator
 import cafe.adriel.voyager.navigator.Navigator
 import cafe.adriel.voyager.navigator.currentOrThrow
 import cafe.adriel.voyager.transitions.ScreenTransition
-import com.bluelinelabs.conductor.Router
 import eu.kanade.domain.chapter.model.Chapter
 import eu.kanade.domain.manga.model.Manga
 import eu.kanade.domain.manga.model.hasCustomCover
@@ -43,31 +43,27 @@ import eu.kanade.presentation.manga.components.DeleteChaptersDialog
 import eu.kanade.presentation.manga.components.DownloadCustomAmountDialog
 import eu.kanade.presentation.manga.components.MangaCoverDialog
 import eu.kanade.presentation.util.LocalNavigatorContentPadding
-import eu.kanade.presentation.util.LocalRouter
 import eu.kanade.presentation.util.isTabletUi
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.source.Source
 import eu.kanade.tachiyomi.source.isLocalOrStub
 import eu.kanade.tachiyomi.source.online.HttpSource
-import eu.kanade.tachiyomi.ui.base.controller.pushController
 import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateSearchScreen
-import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
-import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
+import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
+import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen
 import eu.kanade.tachiyomi.ui.category.CategoryScreen
-import eu.kanade.tachiyomi.ui.history.HistoryController
-import eu.kanade.tachiyomi.ui.library.LibraryController
-import eu.kanade.tachiyomi.ui.main.MainActivity
+import eu.kanade.tachiyomi.ui.home.HomeScreen
 import eu.kanade.tachiyomi.ui.manga.track.TrackInfoDialogHomeScreen
 import eu.kanade.tachiyomi.ui.reader.ReaderActivity
-import eu.kanade.tachiyomi.ui.updates.UpdatesController
 import eu.kanade.tachiyomi.ui.webview.WebViewActivity
 import eu.kanade.tachiyomi.util.system.copyToClipboard
 import eu.kanade.tachiyomi.util.system.toShareIntent
 import eu.kanade.tachiyomi.util.system.toast
+import kotlinx.coroutines.launch
 
 class MangaScreen(
     private val mangaId: Long,
-    private val fromSource: Boolean = false,
+    val fromSource: Boolean = false,
 ) : Screen {
 
     override val key = uniqueScreenKey
@@ -75,9 +71,9 @@ class MangaScreen(
     @Composable
     override fun Content() {
         val navigator = LocalNavigator.currentOrThrow
-        val router = LocalRouter.currentOrThrow
         val context = LocalContext.current
         val haptic = LocalHapticFeedback.current
+        val scope = rememberCoroutineScope()
         val screenModel = rememberScreenModel { MangaInfoScreenModel(context, mangaId, fromSource) }
 
         val state by screenModel.state.collectAsState()
@@ -94,7 +90,7 @@ class MangaScreen(
             state = successState,
             snackbarHostState = screenModel.snackbarHostState,
             isTabletUi = isTabletUi(),
-            onBackClicked = router::popCurrentController,
+            onBackClicked = navigator::pop,
             onChapterClicked = { openChapter(context, it) },
             onDownloadChapter = screenModel::runChapterDownloadActions.takeIf { !successState.source.isLocalOrStub() },
             onAddToLibraryClicked = {
@@ -104,11 +100,11 @@ class MangaScreen(
             onWebViewClicked = { openMangaInWebView(context, screenModel.manga, screenModel.source) }.takeIf { isHttpSource },
             onWebViewLongClicked = { copyMangaUrl(context, screenModel.manga, screenModel.source) }.takeIf { isHttpSource },
             onTrackingClicked = screenModel::showTrackDialog.takeIf { successState.trackingAvailable },
-            onTagClicked = { performGenreSearch(router, it, screenModel.source!!) },
+            onTagClicked = { scope.launch { performGenreSearch(navigator, it, screenModel.source!!) } },
             onFilterButtonClicked = screenModel::showSettingsDialog,
             onRefresh = screenModel::fetchAllFromSource,
             onContinueReading = { continueReading(context, screenModel.getNextUnreadChapter()) },
-            onSearch = { query, global -> performSearch(router, query, global) },
+            onSearch = { query, global -> scope.launch { performSearch(navigator, query, global) } },
             onCoverClicked = screenModel::showCoverDialog,
             onShareClicked = { shareManga(context, screenModel.manga, screenModel.source) }.takeIf { isHttpSource },
             onDownloadActionClicked = screenModel::runDownloadAction.takeIf { !successState.source.isLocalOrStub() },
@@ -268,33 +264,24 @@ class MangaScreen(
      *
      * @param query the search query to the parent controller
      */
-    private fun performSearch(router: Router, query: String, global: Boolean) {
+    private suspend fun performSearch(navigator: Navigator, query: String, global: Boolean) {
         if (global) {
-            router.pushController(GlobalSearchController(query))
+            navigator.push(GlobalSearchScreen(query))
             return
         }
 
-        if (router.backstackSize < 2) {
+        if (navigator.size < 2) {
             return
         }
 
-        when (val previousController = router.backstack[router.backstackSize - 2].controller) {
-            is LibraryController -> {
-                router.handleBack()
+        when (val previousController = navigator.items[navigator.size - 2]) {
+            is HomeScreen -> {
+                navigator.pop()
                 previousController.search(query)
             }
-            is UpdatesController,
-            is HistoryController,
-            -> {
-                // Manually navigate to LibraryController
-                router.handleBack()
-                (router.activity as MainActivity).setSelectedNavItem(R.id.nav_library)
-                val controller = router.getControllerWithTag(R.id.nav_library.toString()) as LibraryController
-                controller.search(query)
-            }
-            is BrowseSourceController -> {
-                router.handleBack()
-                previousController.searchWithQuery(query)
+            is BrowseSourceScreen -> {
+                navigator.pop()
+                previousController.search(query)
             }
         }
     }
@@ -304,20 +291,17 @@ class MangaScreen(
      *
      * @param genreName the search genre to the parent controller
      */
-    private fun performGenreSearch(router: Router, genreName: String, source: Source) {
-        if (router.backstackSize < 2) {
+    private suspend fun performGenreSearch(navigator: Navigator, genreName: String, source: Source) {
+        if (navigator.size < 2) {
             return
         }
 
-        val previousController = router.backstack[router.backstackSize - 2].controller
-
-        if (previousController is BrowseSourceController &&
-            source is HttpSource
-        ) {
-            router.handleBack()
-            previousController.searchWithGenre(genreName)
+        val previousController = navigator.items[navigator.size - 2]
+        if (previousController is BrowseSourceScreen && source is HttpSource) {
+            navigator.pop()
+            previousController.searchGenre(genreName)
         } else {
-            performSearch(router, genreName, global = false)
+            performSearch(navigator, genreName, global = false)
         }
     }
 

+ 0 - 18
app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreController.kt

@@ -1,18 +0,0 @@
-package eu.kanade.tachiyomi.ui.more
-
-import androidx.compose.runtime.Composable
-import cafe.adriel.voyager.navigator.Navigator
-import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
-import eu.kanade.tachiyomi.ui.base.controller.RootController
-
-class MoreController : BasicFullComposeController(), RootController {
-
-    @Composable
-    override fun ComposeContent() {
-        Navigator(screen = MoreScreen)
-    }
-
-    companion object {
-        const val URL_HELP = "https://tachiyomi.org/help/"
-    }
-}

+ 39 - 15
app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreScreen.kt → app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreTab.kt

@@ -1,26 +1,33 @@
 package eu.kanade.tachiyomi.ui.more
 
+import androidx.compose.animation.graphics.res.animatedVectorResource
+import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
+import androidx.compose.animation.graphics.vector.AnimatedImageVector
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
 import cafe.adriel.voyager.core.model.ScreenModel
 import cafe.adriel.voyager.core.model.coroutineScope
 import cafe.adriel.voyager.core.model.rememberScreenModel
-import cafe.adriel.voyager.core.screen.Screen
+import cafe.adriel.voyager.navigator.LocalNavigator
+import cafe.adriel.voyager.navigator.Navigator
 import cafe.adriel.voyager.navigator.currentOrThrow
+import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
+import cafe.adriel.voyager.navigator.tab.TabOptions
 import eu.kanade.core.prefs.asState
 import eu.kanade.domain.base.BasePreferences
 import eu.kanade.presentation.more.MoreScreen
-import eu.kanade.presentation.util.LocalRouter
+import eu.kanade.presentation.util.Tab
+import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.download.DownloadManager
 import eu.kanade.tachiyomi.data.download.DownloadService
-import eu.kanade.tachiyomi.ui.base.controller.pushController
-import eu.kanade.tachiyomi.ui.category.CategoryController
-import eu.kanade.tachiyomi.ui.download.DownloadController
-import eu.kanade.tachiyomi.ui.setting.SettingsMainController
-import eu.kanade.tachiyomi.ui.stats.StatsController
+import eu.kanade.tachiyomi.ui.category.CategoryScreen
+import eu.kanade.tachiyomi.ui.download.DownloadQueueScreen
+import eu.kanade.tachiyomi.ui.setting.SettingsScreen
+import eu.kanade.tachiyomi.ui.stats.StatsScreen
 import eu.kanade.tachiyomi.util.lang.launchIO
 import eu.kanade.tachiyomi.util.system.isInstalledFromFDroid
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -31,11 +38,28 @@ import kotlinx.coroutines.flow.combine
 import uy.kohesive.injekt.Injekt
 import uy.kohesive.injekt.api.get
 
-object MoreScreen : Screen {
+data class MoreTab(private val toDownloads: Boolean = false) : Tab {
+
+    override val options: TabOptions
+        @Composable
+        get() {
+            val isSelected = LocalTabNavigator.current.current.key == key
+            val image = AnimatedImageVector.animatedVectorResource(R.drawable.anim_more_enter)
+            return TabOptions(
+                index = 4u,
+                title = stringResource(R.string.label_more),
+                icon = rememberAnimatedVectorPainter(image, isSelected),
+            )
+        }
+
+    override suspend fun onReselect(navigator: Navigator) {
+        navigator.push(SettingsScreen.toMainScreen())
+    }
+
     @Composable
     override fun Content() {
-        val router = LocalRouter.currentOrThrow
         val context = LocalContext.current
+        val navigator = LocalNavigator.currentOrThrow
         val screenModel = rememberScreenModel { MoreScreenModel() }
         val downloadQueueState by screenModel.downloadQueueState.collectAsState()
         MoreScreen(
@@ -45,12 +69,12 @@ object MoreScreen : Screen {
             incognitoMode = screenModel.incognitoMode,
             onIncognitoModeChange = { screenModel.incognitoMode = it },
             isFDroid = context.isInstalledFromFDroid(),
-            onClickDownloadQueue = { router.pushController(DownloadController()) },
-            onClickCategories = { router.pushController(CategoryController()) },
-            onClickStats = { router.pushController(StatsController()) },
-            onClickBackupAndRestore = { router.pushController(SettingsMainController.toBackupScreen()) },
-            onClickSettings = { router.pushController(SettingsMainController()) },
-            onClickAbout = { router.pushController(SettingsMainController.toAboutScreen()) },
+            onClickDownloadQueue = { navigator.push(DownloadQueueScreen) },
+            onClickCategories = { navigator.push(CategoryScreen()) },
+            onClickStats = { navigator.push(StatsScreen()) },
+            onClickBackupAndRestore = { navigator.push(SettingsScreen.toBackupScreen()) },
+            onClickSettings = { navigator.push(SettingsScreen.toMainScreen()) },
+            onClickAbout = { navigator.push(SettingsScreen.toAboutScreen()) },
         )
     }
 }

+ 0 - 62
app/src/main/java/eu/kanade/tachiyomi/ui/more/NewUpdateDialogController.kt

@@ -1,62 +0,0 @@
-package eu.kanade.tachiyomi.ui.more
-
-import android.app.Dialog
-import android.os.Bundle
-import android.text.method.LinkMovementMethod
-import android.view.View
-import android.widget.TextView
-import androidx.core.os.bundleOf
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.updater.AppUpdateResult
-import eu.kanade.tachiyomi.data.updater.AppUpdateService
-import eu.kanade.tachiyomi.ui.base.controller.DialogController
-import eu.kanade.tachiyomi.ui.base.controller.openInBrowser
-import io.noties.markwon.Markwon
-
-class NewUpdateDialogController(bundle: Bundle? = null) : DialogController(bundle) {
-
-    constructor(update: AppUpdateResult.NewUpdate) : this(
-        bundleOf(
-            BODY_KEY to update.release.info,
-            VERSION_KEY to update.release.version,
-            RELEASE_URL_KEY to update.release.releaseLink,
-            DOWNLOAD_URL_KEY to update.release.getDownloadLink(),
-        ),
-    )
-
-    override fun onCreateDialog(savedViewState: Bundle?): Dialog {
-        val releaseBody = args.getString(BODY_KEY)!!
-            .replace("""---(\R|.)*Checksums(\R|.)*""".toRegex(), "")
-        val info = Markwon.create(activity!!).toMarkdown(releaseBody)
-
-        return MaterialAlertDialogBuilder(activity!!)
-            .setTitle(R.string.update_check_notification_update_available)
-            .setMessage(info)
-            .setPositiveButton(R.string.update_check_confirm) { _, _ ->
-                applicationContext?.let { context ->
-                    // Start download
-                    val url = args.getString(DOWNLOAD_URL_KEY)!!
-                    val version = args.getString(VERSION_KEY)
-                    AppUpdateService.start(context, url, version)
-                }
-            }
-            .setNeutralButton(R.string.update_check_open) { _, _ ->
-                openInBrowser(args.getString(RELEASE_URL_KEY)!!)
-            }
-            .create()
-    }
-
-    override fun onAttach(view: View) {
-        super.onAttach(view)
-
-        // Make links in Markdown text clickable
-        (dialog?.findViewById(android.R.id.message) as? TextView)?.movementMethod =
-            LinkMovementMethod.getInstance()
-    }
-}
-
-private const val BODY_KEY = "NewUpdateDialogController.body"
-private const val VERSION_KEY = "NewUpdateDialogController.version"
-private const val RELEASE_URL_KEY = "NewUpdateDialogController.release_url"
-private const val DOWNLOAD_URL_KEY = "NewUpdateDialogController.download_url"

+ 41 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/more/NewUpdateScreen.kt

@@ -0,0 +1,41 @@
+package eu.kanade.tachiyomi.ui.more
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalContext
+import cafe.adriel.voyager.core.screen.Screen
+import cafe.adriel.voyager.navigator.LocalNavigator
+import cafe.adriel.voyager.navigator.currentOrThrow
+import eu.kanade.presentation.more.NewUpdateScreen
+import eu.kanade.tachiyomi.data.updater.AppUpdateService
+import eu.kanade.tachiyomi.util.system.openInBrowser
+
+class NewUpdateScreen(
+    private val versionName: String,
+    private val changelogInfo: String,
+    private val releaseLink: String,
+    private val downloadLink: String,
+) : Screen {
+    @Composable
+    override fun Content() {
+        val navigator = LocalNavigator.currentOrThrow
+        val context = LocalContext.current
+        val changelogInfoNoChecksum = remember {
+            changelogInfo.replace("""---(\R|.)*Checksums(\R|.)*""".toRegex(), "")
+        }
+        NewUpdateScreen(
+            versionName = versionName,
+            changelogInfo = changelogInfoNoChecksum,
+            onOpenInBrowser = { context.openInBrowser(releaseLink) },
+            onRejectUpdate = navigator::pop,
+            onAcceptUpdate = {
+                AppUpdateService.start(
+                    context = context,
+                    url = downloadLink,
+                    title = versionName,
+                )
+                navigator.pop()
+            },
+        )
+    }
+}

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt

@@ -56,7 +56,6 @@ import eu.kanade.tachiyomi.ui.base.delegate.SecureActivityDelegateImpl
 import eu.kanade.tachiyomi.ui.base.delegate.ThemingDelegate
 import eu.kanade.tachiyomi.ui.base.delegate.ThemingDelegateImpl
 import eu.kanade.tachiyomi.ui.main.MainActivity
-import eu.kanade.tachiyomi.ui.manga.MangaController
 import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.AddToLibraryFirst
 import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.Error
 import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.Success
@@ -71,6 +70,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer
 import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
 import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
 import eu.kanade.tachiyomi.ui.webview.WebViewActivity
+import eu.kanade.tachiyomi.util.Constants
 import eu.kanade.tachiyomi.util.preference.toggle
 import eu.kanade.tachiyomi.util.system.applySystemAnimatorScale
 import eu.kanade.tachiyomi.util.system.createReaderThemeContext
@@ -375,7 +375,7 @@ class ReaderActivity :
                 startActivity(
                     Intent(this, MainActivity::class.java).apply {
                         action = MainActivity.SHORTCUT_MANGA
-                        putExtra(MangaController.MANGA_EXTRA, id)
+                        putExtra(Constants.MANGA_EXTRA, id)
                         addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
                     },
                 )

+ 0 - 37
app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt

@@ -1,37 +0,0 @@
-package eu.kanade.tachiyomi.ui.setting
-
-import android.os.Bundle
-import androidx.compose.runtime.Composable
-import androidx.core.os.bundleOf
-import cafe.adriel.voyager.navigator.Navigator
-import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
-
-class SettingsMainController(bundle: Bundle = bundleOf()) : BasicFullComposeController(bundle) {
-
-    private val toBackupScreen = args.getBoolean(TO_BACKUP_SCREEN)
-    private val toAboutScreen = args.getBoolean(TO_ABOUT_SCREEN)
-
-    @Composable
-    override fun ComposeContent() {
-        Navigator(
-            screen = when {
-                toBackupScreen -> SettingsScreen.toBackupScreen()
-                toAboutScreen -> SettingsScreen.toAboutScreen()
-                else -> SettingsScreen.toMainScreen()
-            },
-        )
-    }
-
-    companion object {
-        fun toBackupScreen(): SettingsMainController {
-            return SettingsMainController(bundleOf(TO_BACKUP_SCREEN to true))
-        }
-
-        fun toAboutScreen(): SettingsMainController {
-            return SettingsMainController(bundleOf(TO_ABOUT_SCREEN to true))
-        }
-    }
-}
-
-private const val TO_BACKUP_SCREEN = "to_backup_screen"
-private const val TO_ABOUT_SCREEN = "to_about_screen"

+ 2 - 10
app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsScreen.kt

@@ -13,7 +13,6 @@ import eu.kanade.presentation.more.settings.screen.SettingsBackupScreen
 import eu.kanade.presentation.more.settings.screen.SettingsGeneralScreen
 import eu.kanade.presentation.more.settings.screen.SettingsMainScreen
 import eu.kanade.presentation.util.LocalBackPress
-import eu.kanade.presentation.util.LocalRouter
 import eu.kanade.presentation.util.Transition
 import eu.kanade.presentation.util.isTabletUi
 
@@ -24,15 +23,8 @@ class SettingsScreen private constructor(
 
     @Composable
     override fun Content() {
-        val router = LocalRouter.currentOrThrow
         val navigator = LocalNavigator.currentOrThrow
         if (!isTabletUi()) {
-            val back: () -> Unit = {
-                when {
-                    navigator.canPop -> navigator.pop()
-                    router.backstackSize > 1 -> router.handleBack()
-                }
-            }
             Navigator(
                 screen = if (toBackup) {
                     SettingsBackupScreen
@@ -42,7 +34,7 @@ class SettingsScreen private constructor(
                     SettingsMainScreen
                 },
                 content = {
-                    CompositionLocalProvider(LocalBackPress provides back) {
+                    CompositionLocalProvider(LocalBackPress provides navigator::pop) {
                         ScreenTransition(
                             navigator = it,
                             transition = { Transition.OneWayFade },
@@ -62,7 +54,7 @@ class SettingsScreen private constructor(
             ) {
                 TwoPanelBox(
                     startContent = {
-                        CompositionLocalProvider(LocalBackPress provides router::popCurrentController) {
+                        CompositionLocalProvider(LocalBackPress provides navigator::pop) {
                             SettingsMainScreen.Content(twoPane = true)
                         }
                     },

+ 0 - 13
app/src/main/java/eu/kanade/tachiyomi/ui/stats/StatsController.kt

@@ -1,13 +0,0 @@
-package eu.kanade.tachiyomi.ui.stats
-
-import androidx.compose.runtime.Composable
-import cafe.adriel.voyager.navigator.Navigator
-import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
-
-class StatsController : BasicFullComposeController() {
-
-    @Composable
-    override fun ComposeContent() {
-        Navigator(screen = StatsScreen())
-    }
-}

+ 3 - 5
app/src/main/java/eu/kanade/tachiyomi/ui/stats/StatsScreen.kt

@@ -3,18 +3,17 @@ package eu.kanade.tachiyomi.ui.stats
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
-import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.stringResource
 import cafe.adriel.voyager.core.model.rememberScreenModel
 import cafe.adriel.voyager.core.screen.Screen
 import cafe.adriel.voyager.core.screen.uniqueScreenKey
+import cafe.adriel.voyager.navigator.LocalNavigator
 import cafe.adriel.voyager.navigator.currentOrThrow
 import eu.kanade.presentation.components.AppBar
 import eu.kanade.presentation.components.LoadingScreen
 import eu.kanade.presentation.components.Scaffold
 import eu.kanade.presentation.more.stats.StatsScreenContent
 import eu.kanade.presentation.more.stats.StatsScreenState
-import eu.kanade.presentation.util.LocalRouter
 import eu.kanade.tachiyomi.R
 
 class StatsScreen : Screen {
@@ -23,8 +22,7 @@ class StatsScreen : Screen {
 
     @Composable
     override fun Content() {
-        val router = LocalRouter.currentOrThrow
-        val context = LocalContext.current
+        val navigator = LocalNavigator.currentOrThrow
 
         val screenModel = rememberScreenModel { StatsScreenModel() }
         val state by screenModel.state.collectAsState()
@@ -38,7 +36,7 @@ class StatsScreen : Screen {
             topBar = { scrollBehavior ->
                 AppBar(
                     title = stringResource(R.string.label_stats),
-                    navigateUp = router::popCurrentController,
+                    navigateUp = navigator::pop,
                     scrollBehavior = scrollBehavior,
                 )
             },

+ 0 - 13
app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesController.kt

@@ -1,13 +0,0 @@
-package eu.kanade.tachiyomi.ui.updates
-
-import androidx.compose.runtime.Composable
-import cafe.adriel.voyager.navigator.Navigator
-import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
-import eu.kanade.tachiyomi.ui.base.controller.RootController
-
-class UpdatesController : BasicFullComposeController(), RootController {
-    @Composable
-    override fun ComposeContent() {
-        Navigator(screen = UpdatesScreen)
-    }
-}

+ 34 - 8
app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreen.kt → app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt

@@ -1,29 +1,54 @@
 package eu.kanade.tachiyomi.ui.updates
 
+import androidx.compose.animation.graphics.res.animatedVectorResource
+import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
+import androidx.compose.animation.graphics.vector.AnimatedImageVector
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
 import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
 import cafe.adriel.voyager.core.model.rememberScreenModel
-import cafe.adriel.voyager.core.screen.Screen
+import cafe.adriel.voyager.navigator.LocalNavigator
+import cafe.adriel.voyager.navigator.Navigator
 import cafe.adriel.voyager.navigator.currentOrThrow
+import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
+import cafe.adriel.voyager.navigator.tab.TabOptions
 import eu.kanade.presentation.updates.UpdateScreen
 import eu.kanade.presentation.updates.UpdatesDeleteConfirmationDialog
-import eu.kanade.presentation.util.LocalRouter
+import eu.kanade.presentation.util.Tab
 import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.ui.base.controller.pushController
+import eu.kanade.tachiyomi.ui.download.DownloadQueueScreen
+import eu.kanade.tachiyomi.ui.home.HomeScreen
 import eu.kanade.tachiyomi.ui.main.MainActivity
-import eu.kanade.tachiyomi.ui.manga.MangaController
+import eu.kanade.tachiyomi.ui.manga.MangaScreen
 import eu.kanade.tachiyomi.ui.reader.ReaderActivity
 import eu.kanade.tachiyomi.ui.updates.UpdatesScreenModel.Event
 import kotlinx.coroutines.flow.collectLatest
 
-object UpdatesScreen : Screen {
+object UpdatesTab : Tab {
+
+    override val options: TabOptions
+        @Composable
+        get() {
+            val isSelected = LocalTabNavigator.current.current.key == key
+            val image = AnimatedImageVector.animatedVectorResource(R.drawable.anim_updates_enter)
+            return TabOptions(
+                index = 1u,
+                title = stringResource(R.string.label_recent_updates),
+                icon = rememberAnimatedVectorPainter(image, isSelected),
+            )
+        }
+
+    override suspend fun onReselect(navigator: Navigator) {
+        navigator.push(DownloadQueueScreen)
+    }
+
     @Composable
     override fun Content() {
         val context = LocalContext.current
-        val router = LocalRouter.currentOrThrow
+        val navigator = LocalNavigator.currentOrThrow
         val screenModel = rememberScreenModel { UpdatesScreenModel() }
         val state by screenModel.state.collectAsState()
 
@@ -34,7 +59,7 @@ object UpdatesScreen : Screen {
             downloadedOnlyMode = screenModel.isDownloadOnly,
             lastUpdated = screenModel.lastUpdated,
             relativeTime = screenModel.relativeTime,
-            onClickCover = { item -> router.pushController(MangaController(item.update.mangaId)) },
+            onClickCover = { item -> navigator.push(MangaScreen(item.update.mangaId)) },
             onSelectAll = screenModel::toggleAllSelection,
             onInvertSelection = screenModel::invertSelection,
             onUpdateLibrary = screenModel::updateLibrary,
@@ -77,8 +102,9 @@ object UpdatesScreen : Screen {
         }
 
         LaunchedEffect(state.selectionMode) {
-            (context as? MainActivity)?.showBottomNav(!state.selectionMode)
+            HomeScreen.showBottomNav(!state.selectionMode)
         }
+
         LaunchedEffect(state.isLoading) {
             if (!state.isLoading) {
                 (context as? MainActivity)?.ready = true

+ 7 - 0
app/src/main/java/eu/kanade/tachiyomi/util/Constants.kt

@@ -0,0 +1,7 @@
+package eu.kanade.tachiyomi.util
+
+object Constants {
+    const val URL_HELP = "https://tachiyomi.org/help/"
+
+    const val MANGA_EXTRA = "manga"
+}

+ 0 - 196
app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiBottomNavigationView.kt

@@ -1,196 +0,0 @@
-package eu.kanade.tachiyomi.widget
-
-import android.animation.Animator
-import android.animation.AnimatorListenerAdapter
-import android.animation.TimeInterpolator
-import android.content.Context
-import android.os.Parcel
-import android.os.Parcelable
-import android.util.AttributeSet
-import android.view.ViewPropertyAnimator
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.WindowInsets
-import androidx.compose.foundation.layout.calculateEndPadding
-import androidx.compose.foundation.layout.calculateStartPadding
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.ReadOnlyComposable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.max
-import androidx.customview.view.AbsSavedState
-import androidx.interpolator.view.animation.FastOutLinearInInterpolator
-import androidx.interpolator.view.animation.LinearOutSlowInInterpolator
-import com.google.android.material.bottomnavigation.BottomNavigationView
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.util.system.applySystemAnimatorScale
-import eu.kanade.tachiyomi.util.system.pxToDp
-import kotlin.math.max
-
-class TachiyomiBottomNavigationView @JvmOverloads constructor(
-    context: Context,
-    attrs: AttributeSet? = null,
-    defStyleAttr: Int = R.attr.bottomNavigationStyle,
-    defStyleRes: Int = R.style.Widget_Design_BottomNavigationView,
-) : BottomNavigationView(context, attrs, defStyleAttr, defStyleRes) {
-
-    private var currentAnimator: ViewPropertyAnimator? = null
-
-    private var currentState = STATE_UP
-
-    override fun onSaveInstanceState(): Parcelable {
-        val superState = super.onSaveInstanceState()
-        return SavedState(superState).also {
-            it.currentState = currentState
-            it.translationY = translationY
-        }
-    }
-
-    override fun onRestoreInstanceState(state: Parcelable?) {
-        if (state is SavedState) {
-            super.onRestoreInstanceState(state.superState)
-            super.setTranslationY(state.translationY)
-            currentState = state.currentState
-        } else {
-            super.onRestoreInstanceState(state)
-        }
-    }
-
-    override fun setTranslationY(translationY: Float) {
-        // Disallow translation change when state down
-        if (currentState == STATE_DOWN) return
-        super.setTranslationY(translationY)
-    }
-
-    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
-        super.onSizeChanged(w, h, oldw, oldh)
-        bottomNavPadding = h.pxToDp.dp
-    }
-
-    /**
-     * Shows this view up.
-     */
-    fun slideUp() = post {
-        currentAnimator?.cancel()
-        clearAnimation()
-
-        currentState = STATE_UP
-        animateTranslation(
-            0F,
-            SLIDE_UP_ANIMATION_DURATION,
-            LinearOutSlowInInterpolator(),
-        )
-        bottomNavPadding = height.pxToDp.dp
-    }
-
-    /**
-     * Hides this view down. [setTranslationY] won't work until [slideUp] is called.
-     */
-    fun slideDown() = post {
-        currentAnimator?.cancel()
-        clearAnimation()
-
-        currentState = STATE_DOWN
-        animateTranslation(
-            height.toFloat(),
-            SLIDE_DOWN_ANIMATION_DURATION,
-            FastOutLinearInInterpolator(),
-        )
-        bottomNavPadding = 0.dp
-    }
-
-    private fun animateTranslation(targetY: Float, duration: Long, interpolator: TimeInterpolator) {
-        currentAnimator = animate()
-            .translationY(targetY)
-            .setInterpolator(interpolator)
-            .setDuration(duration)
-            .applySystemAnimatorScale(context)
-            .setListener(
-                object : AnimatorListenerAdapter() {
-                    override fun onAnimationEnd(animation: Animator) {
-                        currentAnimator = null
-                        postInvalidate()
-                    }
-                },
-            )
-    }
-
-    internal class SavedState : AbsSavedState {
-        var currentState = STATE_UP
-        var translationY = 0F
-
-        constructor(superState: Parcelable) : super(superState)
-
-        constructor(source: Parcel, loader: ClassLoader?) : super(source, loader) {
-            currentState = source.readInt()
-            translationY = source.readFloat()
-        }
-
-        override fun writeToParcel(out: Parcel, flags: Int) {
-            super.writeToParcel(out, flags)
-            out.writeInt(currentState)
-            out.writeFloat(translationY)
-        }
-
-        companion object {
-            @JvmField
-            val CREATOR: Parcelable.ClassLoaderCreator<SavedState> = object : Parcelable.ClassLoaderCreator<SavedState> {
-                override fun createFromParcel(source: Parcel, loader: ClassLoader): SavedState {
-                    return SavedState(source, loader)
-                }
-
-                override fun createFromParcel(source: Parcel): SavedState {
-                    return SavedState(source, null)
-                }
-
-                override fun newArray(size: Int): Array<SavedState> {
-                    return newArray(size)
-                }
-            }
-        }
-    }
-
-    companion object {
-        private const val STATE_DOWN = 1
-        private const val STATE_UP = 2
-
-        private const val SLIDE_UP_ANIMATION_DURATION = 225L
-        private const val SLIDE_DOWN_ANIMATION_DURATION = 175L
-
-        private var bottomNavPadding by mutableStateOf(0.dp)
-
-        /**
-         * Merges [bottomNavPadding] to the origin's [PaddingValues] bottom side.
-         */
-        @ReadOnlyComposable
-        @Composable
-        fun withBottomNavPadding(origin: PaddingValues = PaddingValues()): PaddingValues {
-            val layoutDirection = LocalLayoutDirection.current
-            return PaddingValues(
-                start = origin.calculateStartPadding(layoutDirection),
-                top = origin.calculateTopPadding(),
-                end = origin.calculateEndPadding(layoutDirection),
-                bottom = max(origin.calculateBottomPadding(), bottomNavPadding),
-            )
-        }
-
-        /**
-         * @see withBottomNavPadding
-         */
-        @ReadOnlyComposable
-        @Composable
-        fun withBottomNavInset(origin: WindowInsets): WindowInsets {
-            val density = LocalDensity.current
-            val layoutDirection = LocalLayoutDirection.current
-            return WindowInsets(
-                left = origin.getLeft(density, layoutDirection),
-                top = origin.getTop(density),
-                right = origin.getRight(density, layoutDirection),
-                bottom = max(origin.getBottom(density), with(density) { bottomNavPadding.roundToPx() }),
-            )
-        }
-    }
-}

+ 0 - 53
app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiChangeHandlerFrameLayout.kt

@@ -1,53 +0,0 @@
-package eu.kanade.tachiyomi.widget
-
-import android.content.Context
-import android.util.AttributeSet
-import androidx.coordinatorlayout.widget.CoordinatorLayout
-import com.bluelinelabs.conductor.ChangeHandlerFrameLayout
-
-/**
- * [ChangeHandlerFrameLayout] with the ability to draw behind the header sibling in [CoordinatorLayout].
- * The layout behavior of this view is set to [TachiyomiScrollingViewBehavior] and should not be changed.
- */
-class TachiyomiChangeHandlerFrameLayout(
-    context: Context,
-    attrs: AttributeSet,
-) : ChangeHandlerFrameLayout(context, attrs), CoordinatorLayout.AttachedBehavior {
-
-    /**
-     * If true, this view will draw behind the header sibling.
-     *
-     * @see TachiyomiScrollingViewBehavior.shouldHeaderOverlap
-     */
-    var overlapHeader = false
-        set(value) {
-            if (field != value) {
-                field = value
-                (layoutParams as? CoordinatorLayout.LayoutParams)?.behavior = behavior.apply {
-                    shouldHeaderOverlap = value
-                }
-                if (!value) {
-                    // The behavior doesn't reset translationY when shouldHeaderOverlap is false
-                    translationY = 0F
-                }
-                forceLayout()
-            }
-        }
-
-    fun enableScrollingBehavior(enable: Boolean) {
-        (layoutParams as? CoordinatorLayout.LayoutParams)?.behavior = if (enable) {
-            behavior.apply {
-                shouldHeaderOverlap = overlapHeader
-            }
-        } else {
-            null
-        }
-        if (!enable) {
-            // The behavior doesn't reset translationY when shouldHeaderOverlap is false
-            translationY = 0F
-        }
-        forceLayout()
-    }
-
-    override fun getBehavior() = TachiyomiScrollingViewBehavior()
-}

+ 2 - 2
app/src/main/res/drawable/anim_more_enter.xml

@@ -2,8 +2,8 @@
     xmlns:aapt="http://schemas.android.com/aapt">
     <aapt:attr name="android:drawable">
         <vector
-            android:width="1080dp"
-            android:height="1080dp"
+            android:width="24dp"
+            android:height="24dp"
             android:viewportWidth="1080"
             android:viewportHeight="1080">
             <group android:name="_R_G">

+ 0 - 80
app/src/main/res/layout-sw720dp/main_activity.xml

@@ -1,80 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<eu.kanade.tachiyomi.widget.TachiyomiCoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:id="@+id/root_coordinator"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:orientation="vertical">
-
-    <androidx.constraintlayout.widget.ConstraintLayout
-        android:layout_width="match_parent"
-        android:layout_height="match_parent">
-
-        <com.google.android.material.appbar.TachiyomiAppBarLayout
-            android:id="@+id/appbar"
-            android:layout_width="0dp"
-            android:layout_height="wrap_content"
-            android:fitsSystemWindows="true"
-            app:layout_constraintEnd_toEndOf="parent"
-            app:layout_constraintStart_toEndOf="@id/side_nav"
-            app:layout_constraintTop_toTopOf="parent">
-
-            <com.google.android.material.appbar.MaterialToolbar
-                android:id="@+id/toolbar"
-                android:layout_width="match_parent"
-                android:layout_height="?attr/actionBarSize"
-                android:theme="?attr/actionBarTheme" />
-
-            <TextView
-                android:id="@+id/downloaded_only"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:background="?attr/colorTertiary"
-                android:gravity="center"
-                android:padding="4dp"
-                android:text="@string/label_downloaded_only"
-                android:textAlignment="center"
-                android:textAppearance="?attr/textAppearanceLabelMedium"
-                android:textColor="?attr/colorOnTertiary"
-                android:visibility="gone"
-                tools:visibility="visible" />
-
-            <TextView
-                android:id="@+id/incognito_mode"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:background="?attr/colorPrimary"
-                android:gravity="center"
-                android:padding="4dp"
-                android:text="@string/pref_incognito_mode"
-                android:textAlignment="center"
-                android:textAppearance="?attr/textAppearanceLabelMedium"
-                android:textColor="?attr/colorOnPrimary"
-                android:visibility="gone"
-                tools:visibility="visible" />
-
-        </com.google.android.material.appbar.TachiyomiAppBarLayout>
-
-        <com.google.android.material.navigationrail.NavigationRailView
-            android:id="@+id/side_nav"
-            android:layout_width="wrap_content"
-            android:layout_height="match_parent"
-            android:paddingTop="?attr/actionBarSize"
-            app:elevation="1dp"
-            app:layout_constraintStart_toStartOf="parent"
-            app:menu="@menu/main_nav"
-            app:menuGravity="center" />
-
-        <eu.kanade.tachiyomi.widget.TachiyomiChangeHandlerFrameLayout
-            android:id="@+id/controller_container"
-            android:layout_width="0dp"
-            android:layout_height="0dp"
-            app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintEnd_toEndOf="parent"
-            app:layout_constraintStart_toEndOf="@+id/side_nav"
-            app:layout_constraintTop_toBottomOf="@+id/appbar" />
-
-    </androidx.constraintlayout.widget.ConstraintLayout>
-
-</eu.kanade.tachiyomi.widget.TachiyomiCoordinatorLayout>

+ 0 - 68
app/src/main/res/layout/main_activity.xml

@@ -1,68 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<eu.kanade.tachiyomi.widget.TachiyomiCoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:id="@+id/root_coordinator"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:orientation="vertical">
-
-    <eu.kanade.tachiyomi.widget.TachiyomiChangeHandlerFrameLayout
-        android:id="@+id/controller_container"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent" />
-
-    <com.google.android.material.appbar.TachiyomiAppBarLayout
-        android:id="@+id/appbar"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:fitsSystemWindows="true"
-        app:elevation="0dp">
-
-        <com.google.android.material.appbar.MaterialToolbar
-            android:id="@+id/toolbar"
-            android:layout_width="match_parent"
-            android:layout_height="?attr/actionBarSize"
-            android:theme="?attr/actionBarTheme" />
-
-        <TextView
-            android:id="@+id/downloaded_only"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:background="?attr/colorTertiary"
-            android:gravity="center"
-            android:padding="4dp"
-            android:text="@string/label_downloaded_only"
-            android:textAlignment="center"
-            android:textAppearance="?attr/textAppearanceLabelMedium"
-            android:textColor="?attr/colorOnTertiary"
-            android:visibility="gone"
-            tools:visibility="visible" />
-
-        <TextView
-            android:id="@+id/incognito_mode"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:background="?attr/colorPrimary"
-            android:gravity="center"
-            android:padding="4dp"
-            android:text="@string/pref_incognito_mode"
-            android:textAlignment="center"
-            android:textAppearance="?attr/textAppearanceLabelMedium"
-            android:textColor="?attr/colorOnPrimary"
-            android:visibility="gone"
-            tools:visibility="visible" />
-
-    </com.google.android.material.appbar.TachiyomiAppBarLayout>
-
-    <eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView
-        android:id="@+id/bottom_nav"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_gravity="bottom"
-        android:clickable="true"
-        app:layout_insetEdge="bottom"
-        app:menu="@menu/main_nav"
-        tools:ignore="KeyboardInaccessibleWidget" />
-
-</eu.kanade.tachiyomi.widget.TachiyomiCoordinatorLayout>

+ 7 - 5
gradle/libs.versions.toml

@@ -6,7 +6,8 @@ coil_version = "2.2.2"
 shizuku_version = "12.2.0"
 sqldelight = "1.5.4"
 leakcanary = "2.10"
-voyager = "1.0.0-rc06"
+voyager = "1.0.0-rc07"
+richtext = "0.15.0"
 
 [libraries]
 desugar = "com.android.tools:desugar_jdk_libs:1.2.2"
@@ -52,7 +53,8 @@ image-decoder = "com.github.tachiyomiorg:image-decoder:7879b45"
 
 natural-comparator = "com.github.gpanther:java-nat-sort:natural-comparator-1.1"
 
-markwon = "io.noties.markwon:core:4.6.2"
+richtext-commonmark = { module = "com.halilibo.compose-richtext:richtext-commonmark", version.ref = "richtext" }
+richtext-m3 = { module = "com.halilibo.compose-richtext:richtext-ui-material3", version.ref = "richtext" }
 
 material = "com.google.android.material:material:1.7.0"
 flexible-adapter-core = "com.github.arkon.FlexibleAdapter:flexible-adapter:c8013533"
@@ -63,8 +65,6 @@ insetter = "dev.chrisbanes.insetter:insetter:0.6.1"
 cascade = "me.saket.cascade:cascade-compose:2.0.0-beta1"
 wheelpicker = "com.github.commandiron:WheelPickerCompose:1.0.11"
 
-conductor = "com.bluelinelabs:conductor:3.1.8"
-
 flowbinding-android = "io.github.reactivecircus.flowbinding:flowbinding-android:1.2.0"
 
 logcat = "com.squareup.logcat:logcat:0.1"
@@ -89,6 +89,7 @@ sqldelight-gradle = { module = "com.squareup.sqldelight:gradle-plugin", version.
 junit = "org.junit.jupiter:junit-jupiter:5.9.1"
 
 voyager-navigator = { module = "ca.gosyer:voyager-navigator", version.ref = "voyager" }
+voyager-tab-navigator = { module = "ca.gosyer:voyager-tab-navigator", version.ref = "voyager" }
 voyager-transitions = { module = "ca.gosyer:voyager-transitions", version.ref = "voyager" }
 
 [bundles]
@@ -99,7 +100,8 @@ sqlite = ["sqlitektx", "sqlite-android"]
 nucleus = ["nucleus-core", "nucleus-supportv7"]
 coil = ["coil-core", "coil-gif", "coil-compose"]
 shizuku = ["shizuku-api", "shizuku-provider"]
-voyager = ["voyager-navigator", "voyager-transitions"]
+voyager = ["voyager-navigator", "voyager-tab-navigator", "voyager-transitions"]
+richtext = ["richtext-commonmark", "richtext-m3"]
 
 [plugins]
 kotlinter = { id = "org.jmailen.kotlinter", version = "3.12.0" }

+ 1 - 0
i18n/src/main/res/values/strings.xml

@@ -145,6 +145,7 @@
     <string name="action_webview_refresh">Refresh</string>
     <string name="action_start_downloading_now">Start downloading now</string>
     <string name="action_faq_and_guides">FAQ and Guides</string>
+    <string name="action_not_now">Not now</string>
 
     <!-- Operations -->
     <string name="loading">Loading…</string>