From 3d66eaea8373b6ab5d8e8423be227e0452cb0743 Mon Sep 17 00:00:00 2001 From: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com> Date: Sat, 3 Dec 2022 10:35:30 +0700 Subject: [PATCH] Merge Voyager screens (#8656) * Merge Voyager screens * cleanups --- app/build.gradle.kts | 6 +- .../components/MangaBottomActionMenu.kt | 90 +-- .../presentation/components/NavigationBar.kt | 48 ++ .../presentation/components/NavigationRail.kt | 59 ++ .../presentation/components/Scaffold.kt | 10 +- .../presentation/components/TabbedScreen.kt | 5 +- .../presentation/history/HistoryScreen.kt | 3 - .../eu/kanade/presentation/more/MoreScreen.kt | 15 +- .../presentation/more/NewUpdateScreen.kt | 144 +++++ .../more/settings/screen/AboutScreen.kt | 19 +- .../presentation/updates/UpdatesScreen.kt | 3 - .../eu/kanade/presentation/util/Navigator.kt | 11 +- .../data/notification/NotificationReceiver.kt | 4 +- .../glance/UpdatesGridGlanceWidget.kt | 4 +- .../ui/base/controller/BaseController.kt | 86 --- .../ui/base/controller/ComposeController.kt | 49 -- .../ui/base/controller/ConductorExtensions.kt | 25 - .../ui/base/controller/DialogController.kt | 119 ---- .../controller/OneWayFadeChangeHandler.kt | 46 -- .../ui/base/controller/RootController.kt | 3 - .../tachiyomi/ui/browse/BrowseController.kt | 27 - .../browse/{BrowseScreen.kt => BrowseTab.kt} | 26 +- .../migration/manga/MigrationMangaScreen.kt | 2 - .../migration/search/MigrateSearchScreen.kt | 9 +- .../search/SourceSearchController.kt | 34 - .../migration/search/SourceSearchScreen.kt | 34 +- .../browse/source/SourcesFilterController.kt | 17 - .../ui/browse/source/SourcesFilterScreen.kt | 8 +- .../tachiyomi/ui/browse/source/SourcesTab.kt | 15 +- .../source/browse/BrowseSourceController.kt | 69 -- .../source/browse/BrowseSourceScreen.kt | 32 +- .../globalsearch/GlobalSearchController.kt | 25 - .../source/globalsearch/GlobalSearchScreen.kt | 17 +- .../ui/category/CategoryController.kt | 17 - .../tachiyomi/ui/category/CategoryScreen.kt | 9 +- .../ui/download/DownloadController.kt | 15 - .../ui/download/DownloadQueueScreen.kt | 6 +- .../tachiyomi/ui/history/HistoryController.kt | 26 - .../ui/history/HistoryScreenModel.kt | 5 + .../{HistoryScreen.kt => HistoryTab.kt} | 46 +- .../eu/kanade/tachiyomi/ui/home/HomeScreen.kt | 288 +++++++++ .../tachiyomi/ui/library/LibraryController.kt | 53 -- .../ui/library/LibrarySettingsSheet.kt | 12 +- .../{LibraryScreen.kt => LibraryTab.kt} | 188 +++--- .../kanade/tachiyomi/ui/main/MainActivity.kt | 591 ++++++------------ .../ui/main/WhatsNewDialogController.kt | 24 - .../tachiyomi/ui/manga/MangaController.kt | 34 - .../kanade/tachiyomi/ui/manga/MangaScreen.kt | 68 +- .../tachiyomi/ui/more/MoreController.kt | 18 - .../ui/more/{MoreScreen.kt => MoreTab.kt} | 54 +- .../ui/more/NewUpdateDialogController.kt | 62 -- .../tachiyomi/ui/more/NewUpdateScreen.kt | 41 ++ .../tachiyomi/ui/reader/ReaderActivity.kt | 4 +- .../ui/setting/SettingsMainController.kt | 37 -- .../tachiyomi/ui/setting/SettingsScreen.kt | 12 +- .../tachiyomi/ui/stats/StatsController.kt | 13 - .../kanade/tachiyomi/ui/stats/StatsScreen.kt | 8 +- .../tachiyomi/ui/updates/UpdatesController.kt | 13 - .../{UpdatesScreen.kt => UpdatesTab.kt} | 42 +- .../eu/kanade/tachiyomi/util/Constants.kt | 7 + .../widget/TachiyomiBottomNavigationView.kt | 196 ------ .../TachiyomiChangeHandlerFrameLayout.kt | 53 -- app/src/main/res/drawable/anim_more_enter.xml | 4 +- .../main/res/layout-sw720dp/main_activity.xml | 80 --- app/src/main/res/layout/main_activity.xml | 68 -- gradle/libs.versions.toml | 12 +- i18n/src/main/res/values/strings.xml | 1 + 67 files changed, 1180 insertions(+), 1991 deletions(-) create mode 100644 app/src/main/java/eu/kanade/presentation/components/NavigationBar.kt create mode 100644 app/src/main/java/eu/kanade/presentation/components/NavigationRail.kt create mode 100644 app/src/main/java/eu/kanade/presentation/more/NewUpdateScreen.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/BaseController.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/ComposeController.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/ConductorExtensions.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/DialogController.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/OneWayFadeChangeHandler.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/RootController.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseController.kt rename app/src/main/java/eu/kanade/tachiyomi/ui/browse/{BrowseScreen.kt => BrowseTab.kt} (71%) delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchController.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesFilterController.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchController.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryController.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadController.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryController.kt rename app/src/main/java/eu/kanade/tachiyomi/ui/history/{HistoryScreen.kt => HistoryTab.kt} (68%) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/home/HomeScreen.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt rename app/src/main/java/eu/kanade/tachiyomi/ui/library/{LibraryScreen.kt => LibraryTab.kt} (60%) delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/main/WhatsNewDialogController.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreController.kt rename app/src/main/java/eu/kanade/tachiyomi/ui/more/{MoreScreen.kt => MoreTab.kt} (63%) delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/more/NewUpdateDialogController.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/more/NewUpdateScreen.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/stats/StatsController.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesController.kt rename app/src/main/java/eu/kanade/tachiyomi/ui/updates/{UpdatesScreen.kt => UpdatesTab.kt} (69%) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/util/Constants.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiBottomNavigationView.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiChangeHandlerFrameLayout.kt delete mode 100644 app/src/main/res/layout-sw720dp/main_activity.xml delete mode 100644 app/src/main/res/layout/main_activity.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index eec5e93cfe..c507e5e6db 100644 --- a/app/build.gradle.kts +++ b/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", diff --git a/app/src/main/java/eu/kanade/presentation/components/MangaBottomActionMenu.kt b/app/src/main/java/eu/kanade/presentation/components/MangaBottomActionMenu.kt index ac9f34fb17..fe10650a8a 100644 --- a/app/src/main/java/eu/kanade/presentation/components/MangaBottomActionMenu.kt +++ b/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, + ) } } } diff --git a/app/src/main/java/eu/kanade/presentation/components/NavigationBar.kt b/app/src/main/java/eu/kanade/presentation/components/NavigationBar.kt new file mode 100644 index 0000000000..9c4143f800 --- /dev/null +++ b/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, + ) + } +} diff --git a/app/src/main/java/eu/kanade/presentation/components/NavigationRail.kt b/app/src/main/java/eu/kanade/presentation/components/NavigationRail.kt new file mode 100644 index 0000000000..4a0778b079 --- /dev/null +++ b/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() + } + } +} diff --git a/app/src/main/java/eu/kanade/presentation/components/Scaffold.kt b/app/src/main/java/eu/kanade/presentation/components/Scaffold.kt index e4e05d0ae9..de708bcd5d 100644 --- a/app/src/main/java/eu/kanade/presentation/components/Scaffold.kt +++ b/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, ) } diff --git a/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt b/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt index 3eb46c15a9..fe03d10a58 100644 --- a/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt +++ b/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, ) } diff --git a/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt b/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt index 5157e6f080..17ac024c98 100644 --- a/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt +++ b/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) { diff --git a/app/src/main/java/eu/kanade/presentation/more/MoreScreen.kt b/app/src/main/java/eu/kanade/presentation/more/MoreScreen.kt index 329e821ff6..81961e2340 100644 --- a/app/src/main/java/eu/kanade/presentation/more/MoreScreen.kt +++ b/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) }, ) } } diff --git a/app/src/main/java/eu/kanade/presentation/more/NewUpdateScreen.kt b/app/src/main/java/eu/kanade/presentation/more/NewUpdateScreen.kt new file mode 100644 index 0000000000..0e881a161a --- /dev/null +++ b/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) + } + } + } + } +} diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/AboutScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/AboutScreen.kt index ebd386a96c..8fcf2290f8 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/AboutScreen.kt +++ b/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) diff --git a/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt b/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt index b10dadd6d7..f6aa59c003 100644 --- a/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt +++ b/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)) diff --git a/app/src/main/java/eu/kanade/presentation/util/Navigator.kt b/app/src/main/java/eu/kanade/presentation/util/Navigator.kt index b69dd73c48..39cfa48a09 100644 --- a/app/src/main/java/eu/kanade/presentation/util/Navigator.kt +++ b/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 = staticCompositionLocalOf { null } +import cafe.adriel.voyager.navigator.Navigator /** * For invoking back press to the parent activity @@ -17,3 +12,7 @@ val LocalRouter: ProvidableCompositionLocal = staticCompositionLocalOf val LocalBackPress: ProvidableCompositionLocal<(() -> Unit)?> = staticCompositionLocalOf { null } val LocalNavigatorContentPadding: ProvidableCompositionLocal = compositionLocalOf { PaddingValues() } + +interface Tab : cafe.adriel.voyager.navigator.tab.Tab { + suspend fun onReselect(navigator: Navigator) {} +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt index 46ddfb203e..70873a0f14 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt +++ b/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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/glance/UpdatesGridGlanceWidget.kt b/app/src/main/java/eu/kanade/tachiyomi/glance/UpdatesGridGlanceWidget.kt index 521f037dd2..0958341517 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/glance/UpdatesGridGlanceWidget.kt +++ b/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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/BaseController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/BaseController.kt deleted file mode 100644 index 207f7fed8b..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/BaseController.kt +++ /dev/null @@ -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(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())}" - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/ComposeController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/ComposeController.kt deleted file mode 100644 index de77e41f33..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/ComposeController.kt +++ /dev/null @@ -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(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() -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/ConductorExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/ConductorExtensions.kt deleted file mode 100644 index c7990cd9c1..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/ConductorExtensions.kt +++ /dev/null @@ -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()) -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/DialogController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/DialogController.kt deleted file mode 100644 index 63f63e1d00..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/DialogController.kt +++ /dev/null @@ -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" - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/OneWayFadeChangeHandler.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/OneWayFadeChangeHandler.kt deleted file mode 100644 index a0ea5a8342..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/OneWayFadeChangeHandler.kt +++ /dev/null @@ -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()) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/RootController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/RootController.kt deleted file mode 100644 index 17b27f911b..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/RootController.kt +++ /dev/null @@ -1,3 +0,0 @@ -package eu.kanade.tachiyomi.ui.base.controller - -interface RootController diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseController.kt deleted file mode 100644 index 18f6c44c05..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseController.kt +++ /dev/null @@ -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" diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt similarity index 71% rename from app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseScreen.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt index 1246f8144e..5c73f6d687 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseScreen.kt +++ b/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() { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaScreen.kt index 948987e210..6cc6bc4aa9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaScreen.kt +++ b/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() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt index 108b969f5b..150957f32e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt +++ b/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)) } }, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchController.kt deleted file mode 100644 index bb1d3c3597..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchController.kt +++ /dev/null @@ -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" diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt index fcf9317104..04c71800e9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt +++ b/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)) + } }, ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesFilterController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesFilterController.kt deleted file mode 100644 index 227a257d7b..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesFilterController.kt +++ /dev/null @@ -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()) - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesFilterScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesFilterScreen.kt index 846bf9ca4e..9c5b82980f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesFilterScreen.kt +++ b/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, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesTab.kt index 50c604631d..92ea22e589 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesTab.kt +++ b/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, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt deleted file mode 100644 index bbd9f736ec..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt +++ /dev/null @@ -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() - - @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" diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt index 4ce7fc1473..9d4f821bd4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt +++ b/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) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchController.kt deleted file mode 100644 index 94c534112b..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchController.kt +++ /dev/null @@ -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, - ), - ) - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt index f8a02fcc46..0e9f4eff47 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt +++ b/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)) }, ) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryController.kt deleted file mode 100644 index 9a5357931a..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryController.kt +++ /dev/null @@ -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()) - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryScreen.kt index 02b5e6f3f9..0d10363b62 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryScreen.kt +++ b/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) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadController.kt deleted file mode 100644 index cd2058e2ed..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadController.kt +++ /dev/null @@ -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) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadQueueScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadQueueScreen.kt index b8010fe50e..0f68c1fe3c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadQueueScreen.kt +++ b/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 -> diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryController.kt deleted file mode 100644 index c0410450e7..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryController.kt +++ /dev/null @@ -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().await(onlyUnread = false).firstOrNull() - HistoryScreen.openChapter(context, chapter) - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryScreenModel.kt index 4991c89c94..e549eed144 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryScreenModel.kt +++ b/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)) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryTab.kt similarity index 68% rename from app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryScreen.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryTab.kt index 0e3b943dd0..2685c60a38 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryScreen.kt +++ b/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() + + 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?) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/home/HomeScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/home/HomeScreen.kt new file mode 100644 index 0000000000..7570265146 --- /dev/null +++ b/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() + private val openTabEvent = Channel() + private val showBottomNavEvent = Channel() + + 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() + 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().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() + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt deleted file mode 100644 index 6209a1a0b2..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt +++ /dev/null @@ -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) -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt index ed0deee46c..1a128ae615 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt +++ b/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) } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt similarity index 60% rename from app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreen.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt index 1b0797e8bf..03c1caa8e0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreen.kt +++ b/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(replay = 1) - fun search(query: String) = queryEvent.tryEmit(query) + private val queryEvent = Channel() + suspend fun search(query: String) = queryEvent.send(query) // For opening settings sheet in LibraryController - private val requestSettingsSheetEvent = MutableSharedFlow() - private val openSettingsSheetEvent_ = MutableSharedFlow() - val openSettingsSheetEvent = openSettingsSheetEvent_.asSharedFlow() - private suspend fun sendSettingsSheetIntent(category: Category) = openSettingsSheetEvent_.emit(category) - suspend fun requestOpenSettingsSheet() = requestSettingsSheetEvent.emit(Unit) + private val requestSettingsSheetEvent = Channel() + private val openSettingsSheetEvent_ = Channel() + val openSettingsSheetEvent = openSettingsSheetEvent_.receiveAsFlow() + private suspend fun sendSettingsSheetIntent(category: Category) = openSettingsSheetEvent_.send(category) + suspend fun requestOpenSettingsSheet() = requestSettingsSheetEvent.send(Unit) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index b323207d76..4c05967cfc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/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() - 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,22 +129,72 @@ 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() + + settingsSheet = LibrarySettingsSheet(this) + LibraryTab.openSettingsSheetEvent + .onEach(::showSettingsSheet) + .launchIn(lifecycleScope) + + setComposeContent { + Navigator( + screen = HomeScreen, + disposeBehavior = NavigatorDisposeBehavior(disposeNestedNavigators = false, disposeSteps = true), + ) { navigator -> + if (navigator.size == 1) { + ConfirmExit() + } + + // 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() + } + } + } + .launchIn(this) + } + + LaunchedEffect(navigator) { + this@MainActivity.navigator = navigator + } + + 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)) + } + }, + ) } } @@ -158,128 +205,62 @@ class MainActivity : BaseActivity() { } setSplashScreenExitAnimation(splashScreen) - 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) - } - } 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) - } - } - } - R.id.nav_more -> { - if (router.backstackSize == 1) { - router.pushController(SettingsMainController()) - } - } - } - } - 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) - } - - override fun onChangeCompleted( - to: Controller?, - from: Controller?, - isPush: Boolean, - container: ViewGroup, - handler: ControllerChangeHandler, - ) { - } - }, - ) - if (!router.hasRootController()) { - // Set start screen - if (!handleIntentAction(intent)) { - moveToStartScreen() - } - } - syncActivityViewWithController() - - binding.toolbar.setNavigationOnClickListener { - onBackPressed() - } - 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() } + } + } + + @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 } } + } - 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) - - 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(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) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/WhatsNewDialogController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/WhatsNewDialogController.kt deleted file mode 100644 index f69eb34c9e..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/WhatsNewDialogController.kt +++ /dev/null @@ -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() - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt deleted file mode 100644 index ac6376a9d1..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt +++ /dev/null @@ -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" - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt index 7c74624aa3..2bc912498e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt +++ b/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) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreController.kt deleted file mode 100644 index f3c4d5bed5..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreController.kt +++ /dev/null @@ -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/" - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreTab.kt similarity index 63% rename from app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreScreen.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreTab.kt index 6b3ed27a5e..8fb94f2c21 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreScreen.kt +++ b/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()) }, ) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/more/NewUpdateDialogController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/more/NewUpdateDialogController.kt deleted file mode 100644 index b7bca75a79..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/more/NewUpdateDialogController.kt +++ /dev/null @@ -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" diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/more/NewUpdateScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/more/NewUpdateScreen.kt new file mode 100644 index 0000000000..7951250acc --- /dev/null +++ b/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() + }, + ) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt index 2706f202a3..561affa1d1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt +++ b/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) }, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt deleted file mode 100644 index 35961763a1..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt +++ /dev/null @@ -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" diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsScreen.kt index efb4b5c94b..1c6053352b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsScreen.kt +++ b/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) } }, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/stats/StatsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/stats/StatsController.kt deleted file mode 100644 index 115b595c59..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/stats/StatsController.kt +++ /dev/null @@ -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()) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/stats/StatsScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/stats/StatsScreen.kt index 5a5780aa25..b4e0f384e9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/stats/StatsScreen.kt +++ b/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, ) }, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesController.kt deleted file mode 100644 index e26ed191c7..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesController.kt +++ /dev/null @@ -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) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt similarity index 69% rename from app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreen.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt index 304d3df663..ba3af64f1d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreen.kt +++ b/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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/Constants.kt b/app/src/main/java/eu/kanade/tachiyomi/util/Constants.kt new file mode 100644 index 0000000000..dadc48afb9 --- /dev/null +++ b/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" +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiBottomNavigationView.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiBottomNavigationView.kt deleted file mode 100644 index 0bea9f8a0b..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiBottomNavigationView.kt +++ /dev/null @@ -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 = object : Parcelable.ClassLoaderCreator { - 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 { - 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() }), - ) - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiChangeHandlerFrameLayout.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiChangeHandlerFrameLayout.kt deleted file mode 100644 index 2c63ffabfd..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiChangeHandlerFrameLayout.kt +++ /dev/null @@ -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() -} diff --git a/app/src/main/res/drawable/anim_more_enter.xml b/app/src/main/res/drawable/anim_more_enter.xml index 08f22d3b32..37e1b67fc0 100644 --- a/app/src/main/res/drawable/anim_more_enter.xml +++ b/app/src/main/res/drawable/anim_more_enter.xml @@ -2,8 +2,8 @@ xmlns:aapt="http://schemas.android.com/aapt"> diff --git a/app/src/main/res/layout-sw720dp/main_activity.xml b/app/src/main/res/layout-sw720dp/main_activity.xml deleted file mode 100644 index 8d5efb178e..0000000000 --- a/app/src/main/res/layout-sw720dp/main_activity.xml +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/main_activity.xml b/app/src/main/res/layout/main_activity.xml deleted file mode 100644 index 5e08fb8ef6..0000000000 --- a/app/src/main/res/layout/main_activity.xml +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 19acb0f842..17327c299e 100644 --- a/gradle/libs.versions.toml +++ b/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" } \ No newline at end of file diff --git a/i18n/src/main/res/values/strings.xml b/i18n/src/main/res/values/strings.xml index 3d7430272d..aaa6df186a 100644 --- a/i18n/src/main/res/values/strings.xml +++ b/i18n/src/main/res/values/strings.xml @@ -145,6 +145,7 @@ Refresh Start downloading now FAQ and Guides + Not now Loading…