Merge Voyager screens (#8656)

* Merge Voyager screens

* cleanups

(cherry picked from commit 3d66eaea8373b6ab5d8e8423be227e0452cb0743)

# Conflicts:
#	app/src/main/java/eu/kanade/presentation/components/MangaBottomActionMenu.kt
#	app/src/main/java/eu/kanade/presentation/more/settings/screen/AboutScreen.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/base/changehandler/OneWayFadeChangeHandler.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/ConductorExtensions.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesFilterController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesTab.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryScreen.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/main/WhatsNewDialogController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreTab.kt
#	app/src/main/res/layout/main_activity.xml
This commit is contained in:
Ivan Iskandar 2022-12-03 10:35:30 +07:00 committed by Jobobby04
parent 07c7ec972d
commit 726626f2c5
120 changed files with 1685 additions and 3009 deletions

View File

@ -264,15 +264,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)
@ -294,9 +291,6 @@ dependencies {
implementation(libs.leakcanary.plumber)
// SY -->
// Changelog
implementation(sylibs.changelog)
// Text distance (EH)
implementation (sylibs.simularity)
@ -350,6 +344,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",

View File

@ -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
@ -100,7 +101,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) {
@ -218,11 +223,11 @@ 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,
// SY -->
onClickCleanTitles: (() -> Unit)?,
onClickMigrate: (() -> Unit)?,
@ -231,8 +236,8 @@ fun LibraryBottomActionMenu(
) {
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(
@ -260,18 +265,19 @@ fun LibraryBottomActionMenu(
// SY <--
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,
)
}
Button(
title = stringResource(R.string.action_move_category),
icon = Icons.Outlined.Label,
toConfirm = confirm[0],
onLongClick = { onLongClickItem(0) },
onClick = onChangeCategoryClicked,
)
if (onDownloadClicked != null) {
var downloadExpanded by remember { mutableStateOf(false) }
Button(
@ -290,15 +296,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,
)
// SY -->
if (onMarkAsReadClicked != null) {
Button(

View File

@ -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,
)
}
}

View File

@ -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()
}
}
}

View File

@ -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,
)
}

View File

@ -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,
)
}

View File

@ -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) {

View File

@ -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
@ -32,8 +29,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(
@ -60,10 +56,7 @@ fun MoreScreen(
val uriHandler = LocalUriHandler.current
ScrollbarLazyColumn(
modifier = Modifier.statusBarsPadding(),
contentPadding = TachiyomiBottomNavigationView.withBottomNavPadding(
WindowInsets.navigationBars.asPaddingValues(),
),
modifier = Modifier.systemBarsPadding(),
) {
if (isFDroid) {
item {
@ -209,7 +202,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) },
)
}
}

View File

@ -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)
}
}
}
}
}

View File

@ -8,7 +8,11 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Public
import androidx.compose.runtime.Composable
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.graphics.vector.rememberVectorPainter
import androidx.compose.ui.platform.LocalContext
@ -19,7 +23,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 +32,11 @@ 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.ui.main.WhatsNewDialogController
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
@ -63,7 +64,10 @@ object AboutScreen : Screen {
val uriHandler = LocalUriHandler.current
val handleBack = LocalBackPress.current
val navigator = LocalNavigator.currentOrThrow
val router = LocalRouter.currentOrThrow
// SY -->
var showWhatsNewDialog by remember { mutableStateOf(false) }
// SY <--
Scaffold(
topBar = { scrollBehavior ->
@ -98,7 +102,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)
}
}
},
)
@ -109,7 +121,7 @@ object AboutScreen : Screen {
TextPreferenceWidget(
title = stringResource(R.string.whats_new),
// SY -->
onPreferenceClick = { WhatsNewDialogController().showDialog(router) },
onPreferenceClick = { showWhatsNewDialog = true },
// SY <--
)
}
@ -179,19 +191,25 @@ object AboutScreen : Screen {
}
}
}
// SY -->
if (showWhatsNewDialog) {
WhatsNewDialog(onDismissRequest = { showWhatsNewDialog = false })
}
// SY <--
}
/**
* 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)

View File

@ -0,0 +1,121 @@
package eu.kanade.presentation.more.settings.screen
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.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.window.DialogProperties
import eu.kanade.domain.UnsortedPreferences
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.system.toast
import exh.log.xLogE
import exh.uconfig.EHConfigurator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@Composable
fun ConfigureExhDialog(run: Boolean, onRunning: () -> Unit) {
val unsortedPreferences = remember {
Injekt.get<UnsortedPreferences>()
}
var warnDialogOpen by remember { mutableStateOf(false) }
var configureDialogOpen by remember { mutableStateOf(false) }
var configureFailedDialogOpen by remember { mutableStateOf<Exception?>(null) }
LaunchedEffect(run) {
if (run) {
if (unsortedPreferences.exhShowSettingsUploadWarning().get()) {
warnDialogOpen = true
} else {
configureDialogOpen = true
}
onRunning()
}
}
if (warnDialogOpen) {
AlertDialog(
onDismissRequest = { warnDialogOpen = false },
properties = DialogProperties(
dismissOnBackPress = false,
dismissOnClickOutside = false,
),
confirmButton = {
TextButton(
onClick = {
unsortedPreferences.exhShowSettingsUploadWarning().set(false)
configureDialogOpen = true
warnDialogOpen = false
},
) {
Text(text = stringResource(android.R.string.ok))
}
},
title = {
Text(text = stringResource(R.string.settings_profile_note))
},
text = {
Text(text = stringResource(R.string.settings_profile_note_message))
},
)
}
if (configureDialogOpen) {
val context = LocalContext.current
LaunchedEffect(Unit) {
withContext(Dispatchers.IO + NonCancellable) {
try {
EHConfigurator(context).configureAll()
launchUI {
context.toast(R.string.eh_settings_successfully_uploaded)
}
} catch (e: Exception) {
configureFailedDialogOpen = e
xLogE("Configuration error!", e)
} finally {
configureDialogOpen = false
}
}
}
AlertDialog(
onDismissRequest = {},
properties = DialogProperties(
dismissOnBackPress = false,
dismissOnClickOutside = false,
),
confirmButton = {},
title = {
Text(text = stringResource(R.string.eh_settings_uploading_to_server))
},
text = {
Text(text = stringResource(R.string.eh_settings_uploading_to_server_message))
},
)
}
if (configureFailedDialogOpen != null) {
AlertDialog(
onDismissRequest = { configureFailedDialogOpen = null },
confirmButton = {
TextButton(onClick = { configureFailedDialogOpen = null }) {
Text(text = stringResource(android.R.string.ok))
}
},
title = {
Text(text = stringResource(R.string.eh_settings_configuration_failed))
},
text = {
Text(text = stringResource(R.string.eh_settings_configuration_failed_message, configureFailedDialogOpen?.message.orEmpty()))
},
)
}
}

View File

@ -46,7 +46,6 @@ import eu.kanade.domain.manga.interactor.GetAllManga
import eu.kanade.domain.manga.repository.MangaRepository
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.util.LocalRouter
import eu.kanade.presentation.util.collectAsState
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.ChapterCache
@ -71,7 +70,6 @@ import eu.kanade.tachiyomi.network.PREF_DOH_QUAD101
import eu.kanade.tachiyomi.network.PREF_DOH_QUAD9
import eu.kanade.tachiyomi.network.PREF_DOH_SHECAN
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.util.CrashLogUtil
import eu.kanade.tachiyomi.util.lang.launchNonCancellable
import eu.kanade.tachiyomi.util.lang.withUIContext
@ -82,7 +80,7 @@ import eu.kanade.tachiyomi.util.system.logcat
import eu.kanade.tachiyomi.util.system.powerManager
import eu.kanade.tachiyomi.util.system.setDefaultSettings
import eu.kanade.tachiyomi.util.system.toast
import exh.debug.SettingsDebugController
import exh.debug.SettingsDebugScreen
import exh.log.EHLogLevel
import exh.pref.DelegateSourcePreferences
import exh.source.BlacklistedSources
@ -673,7 +671,7 @@ object SettingsAdvancedScreen : SearchableSettings {
@Composable
private fun getDeveloperToolsGroup(): Preference.PreferenceGroup {
val context = LocalContext.current
val router = LocalRouter.currentOrThrow
val navigator = LocalNavigator.currentOrThrow
val sourcePreferences = remember { Injekt.get<SourcePreferences>() }
val unsortedPreferences = remember { Injekt.get<UnsortedPreferences>() }
val delegateSourcePreferences = remember { Injekt.get<DelegateSourcePreferences>() }
@ -729,7 +727,7 @@ object SettingsAdvancedScreen : SearchableSettings {
HtmlCompat.fromHtml(context.getString(R.string.open_debug_menu_summary), HtmlCompat.FROM_HTML_MODE_COMPACT)
.toAnnotatedString()
},
onClick = { router.pushController(SettingsDebugController()) },
onClick = { navigator.push(SettingsDebugScreen()) },
),
),
)

View File

@ -9,19 +9,18 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.fragment.app.FragmentActivity
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.domain.UnsortedPreferences
import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.util.LocalRouter
import eu.kanade.presentation.util.collectAsState
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.category.repos.RepoController
import eu.kanade.tachiyomi.ui.category.sources.SourceCategoryController
import eu.kanade.tachiyomi.ui.category.repos.RepoScreen
import eu.kanade.tachiyomi.ui.category.sources.SourceCategoryScreen
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -48,13 +47,13 @@ object SettingsBrowseScreen : SearchableSettings {
title = stringResource(R.string.label_sources),
preferenceItems = listOf(
kotlin.run {
val router = LocalRouter.currentOrThrow
val navigator = LocalNavigator.currentOrThrow
val count by sourcePreferences.sourcesTabCategories().collectAsState()
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.action_edit_categories),
subtitle = pluralStringResource(R.plurals.num_categories, count.size, count.size),
onClick = {
router.pushController(SourceCategoryController())
navigator.push(SourceCategoryScreen())
},
)
},
@ -99,13 +98,13 @@ object SettingsBrowseScreen : SearchableSettings {
),
// SY -->
kotlin.run {
val router = LocalRouter.currentOrThrow
val navigator = LocalNavigator.currentOrThrow
val count by unsortedPreferences.extensionRepos().collectAsState()
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.action_edit_repos),
subtitle = pluralStringResource(R.plurals.num_repos, count.size, count.size),
onClick = {
router.pushController(RepoController())
navigator.push(RepoScreen())
},
)
},

View File

@ -46,13 +46,11 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.core.content.ContextCompat.startActivity
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.domain.UnsortedPreferences
import eu.kanade.domain.manga.interactor.DeleteFavoriteEntries
import eu.kanade.domain.manga.interactor.GetExhFavoriteMangaWithMetadata
import eu.kanade.domain.manga.interactor.GetFlatMetadataById
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.util.LocalRouter
import eu.kanade.presentation.util.collectAsState
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.DEVICE_CHARGING
@ -68,7 +66,6 @@ import exh.eh.EHentaiUpdateWorkerConstants
import exh.eh.EHentaiUpdaterStats
import exh.favorites.FavoritesIntroDialog
import exh.metadata.metadata.EHentaiSearchMetadata
import exh.uconfig.WarnConfigureDialogController
import exh.ui.login.EhLoginActivity
import exh.util.nullIfBlank
import kotlinx.serialization.decodeFromString
@ -126,18 +123,18 @@ object SettingsEhScreen : SearchableSettings {
@Composable
override fun getPreferences(): List<Preference> {
val router = LocalRouter.currentOrThrow
val openWarnConfigureDialogController = {
WarnConfigureDialogController.uploadSettings(router)
}
val unsortedPreferences: UnsortedPreferences = remember { Injekt.get() }
val getFlatMetadataById: GetFlatMetadataById = remember { Injekt.get() }
val deleteFavoriteEntries: DeleteFavoriteEntries = remember { Injekt.get() }
val getExhFavoriteMangaWithMetadata: GetExhFavoriteMangaWithMetadata = remember { Injekt.get() }
val exhentaiEnabled by unsortedPreferences.enableExhentai().collectAsState()
var runConfigureDialog by remember { mutableStateOf(false) }
val openWarnConfigureDialogController = { runConfigureDialog = true }
Reconfigure(unsortedPreferences, openWarnConfigureDialogController)
ConfigureExhDialog(run = runConfigureDialog, onRunning = { runConfigureDialog = false })
return listOf(
Preference.PreferenceGroup(
stringResource(R.string.ehentai_prefs_account_settings),

View File

@ -26,15 +26,14 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import androidx.fragment.app.FragmentActivity
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.util.LocalRouter
import eu.kanade.presentation.util.collectAsState
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.core.security.SecurityPreferences
import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.base.delegate.SecureActivityDelegate
import eu.kanade.tachiyomi.ui.category.biometric.BiometricTimesController
import eu.kanade.tachiyomi.ui.category.biometric.BiometricTimesScreen
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.isAuthenticationSupported
import uy.kohesive.injekt.Injekt
@ -98,7 +97,7 @@ object SettingsSecurityScreen : SearchableSettings {
),
// SY -->
kotlin.run {
val router = LocalRouter.currentOrThrow
val navigator = LocalNavigator.currentOrThrow
val count by securityPreferences.authenticatorTimeRanges().collectAsState()
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.action_edit_biometric_lock_times),
@ -108,7 +107,7 @@ object SettingsSecurityScreen : SearchableSettings {
count.size,
),
onClick = {
router.pushController(BiometricTimesController())
navigator.push(BiometricTimesScreen())
},
enabled = useAuth,
)

View File

@ -0,0 +1,156 @@
package eu.kanade.presentation.more.settings.screen
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.components.Divider
import eu.kanade.presentation.components.LazyColumn
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
import kotlinx.serialization.Serializable
import nl.adaptivity.xmlutil.AndroidXmlReader
import nl.adaptivity.xmlutil.serialization.XML
import nl.adaptivity.xmlutil.serialization.XmlSerialName
import nl.adaptivity.xmlutil.serialization.XmlValue
@Composable
fun WhatsNewDialog(onDismissRequest: () -> Unit) {
AlertDialog(
onDismissRequest = onDismissRequest,
confirmButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(android.R.string.cancel))
}
},
title = {
Text(text = stringResource(R.string.whats_new))
},
text = {
Column {
val context = LocalContext.current
val changelog by produceState<List<DisplayChangelog>?>(initialValue = null) {
value = withIOContext {
XML.decodeFromReader<Changelog>(
AndroidXmlReader(
context.resources.openRawResource(
if (isPreviewBuildType) R.raw.changelog_debug else R.raw.changelog_release,
).bufferedReader(),
),
).toDisplayChangelog()
}
}
if (changelog != null) {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.fillMaxSize(),
) {
items(changelog.orEmpty()) { changelog ->
Column(Modifier.fillMaxWidth()) {
Text(
text = stringResource(R.string.changelog_version, changelog.version),
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.titleSmall,
)
Divider(Modifier.padding(vertical = 8.dp))
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
changelog.changelog.forEach {
Text(text = it, style = MaterialTheme.typography.bodySmall)
}
}
}
}
}
}
}
},
modifier = Modifier.fillMaxSize(),
)
}
data class DisplayChangelog(
val version: String,
val changelog: List<AnnotatedString>,
)
@Serializable
@XmlSerialName("changelog", "", "")
data class Changelog(
val bulletedList: Boolean,
val changelogs: List<ChangelogVersion>,
)
@Serializable
@XmlSerialName("changelogversion", "", "")
data class ChangelogVersion(
val versionName: String,
val changeDate: String,
val text: List<ChangelogText>,
)
@Serializable
@XmlSerialName("changelogtext", "", "")
data class ChangelogText(
@XmlValue(true) val value: String,
)
private const val bullet = "\u2022"
fun Changelog.toDisplayChangelog(): List<DisplayChangelog> {
val prefix = if (bulletedList) bullet + "\t\t" else ""
return changelogs.map { version ->
DisplayChangelog(
version = version.versionName,
changelog = version.text.mapIndexed { index, changelogText ->
buildAnnotatedString {
append(prefix)
var inBBCode = false
var isEscape = false
changelogText.value.forEachIndexed { charIndex, c ->
if (!inBBCode && c == '[') {
inBBCode = true
} else if (inBBCode && c == ']') {
inBBCode = false
isEscape = false
} else if (inBBCode && c == '/') {
isEscape = true
} else if (inBBCode && c == 'b') {
if (isEscape) {
try {
pop()
} catch (e: IllegalStateException) {
throw Exception("Exception on ${version.versionName}:$index:$charIndex", e)
}
} else {
pushStyle(SpanStyle(fontWeight = FontWeight.Bold))
}
} else {
append(c)
}
}
}
},
)
}
}

View File

@ -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))

View File

@ -1,15 +1,11 @@
package eu.kanade.presentation.util
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.staticCompositionLocalOf
import com.bluelinelabs.conductor.Router
/**
* For interop with Conductor
*/
val LocalRouter: ProvidableCompositionLocal<Router?> = staticCompositionLocalOf { null }
import cafe.adriel.voyager.navigator.Navigator
/**
* For invoking back press to the parent activity
@ -17,3 +13,12 @@ val LocalRouter: ProvidableCompositionLocal<Router?> = staticCompositionLocalOf
val LocalBackPress: ProvidableCompositionLocal<(() -> Unit)?> = staticCompositionLocalOf { null }
val LocalNavigatorContentPadding: ProvidableCompositionLocal<PaddingValues> = compositionLocalOf { PaddingValues() }
interface Tab : cafe.adriel.voyager.navigator.tab.Tab {
suspend fun onReselect(navigator: Navigator) {}
// SY -->
@Composable
fun isEnabled(): Boolean = true
// SY <--
}

View File

@ -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)

View File

@ -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)

View File

@ -1,41 +0,0 @@
package eu.kanade.tachiyomi.ui.base.changehandler
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.AnimatorChangeHandler
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
/**
* An [AnimatorChangeHandler] that will remove the from view and fade in the to view
*/
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())
}
}

View File

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

View File

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

View File

@ -1,26 +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.ui.base.changehandler.OneWayFadeChangeHandler
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())
}

View File

@ -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"
}
}

View File

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

View File

@ -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"

View File

@ -1,18 +1,24 @@
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.domain.ui.UiPreferences
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
@ -24,9 +30,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() {

View File

@ -9,6 +9,7 @@ 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.domain.source.interactor.GetRemoteManga
import eu.kanade.presentation.browse.FeedAddDialog
@ -17,17 +18,15 @@ import eu.kanade.presentation.browse.FeedDeleteConfirmDialog
import eu.kanade.presentation.browse.FeedScreen
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.manga.MangaController
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
import eu.kanade.tachiyomi.ui.manga.MangaScreen
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
@Composable
fun Screen.feedTab(): TabContent {
val router = LocalRouter.currentOrThrow
val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { FeedScreenModel() }
val state by screenModel.state.collectAsState()
@ -48,25 +47,25 @@ fun Screen.feedTab(): TabContent {
contentPadding = contentPadding,
onClickSavedSearch = { savedSearch, source ->
screenModel.sourcePreferences.lastUsedSource().set(savedSearch.source)
router.pushController(
BrowseSourceController(
source,
navigator.push(
BrowseSourceScreen(
source.id,
savedSearch = savedSearch.id,
),
)
},
onClickSource = { source ->
screenModel.sourcePreferences.lastUsedSource().set(source.id)
router.pushController(
BrowseSourceController(
source,
navigator.push(
BrowseSourceScreen(
source.id,
GetRemoteManga.QUERY_LATEST,
),
)
},
onClickDelete = screenModel::openDeleteDialog,
onClickManga = { manga ->
router.pushController(MangaController(manga.id, true))
navigator.push(MangaScreen(manga.id, true))
},
getMangaState = { manga, source -> screenModel.getManga(initialManga = manga, source = source) },
)

View File

@ -1,43 +0,0 @@
package eu.kanade.tachiyomi.ui.browse.migration.advanced.design
import android.os.Bundle
import androidx.compose.runtime.Composable
import androidx.core.os.bundleOf
import cafe.adriel.voyager.navigator.Navigator
import com.bluelinelabs.conductor.Router
import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationListController
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationProcedureConfig
class PreMigrationController(bundle: Bundle? = null) : BasicFullComposeController(bundle) {
constructor(mangaIds: List<Long>) : this(
bundleOf(
MANGA_IDS_EXTRA to mangaIds.toLongArray(),
),
)
private val config: LongArray = args.getLongArray(MANGA_IDS_EXTRA) ?: LongArray(0)
@Composable
override fun ComposeContent() {
Navigator(screen = PreMigrationScreen(config.toList()))
}
companion object {
private const val MANGA_IDS_EXTRA = "manga_ids"
fun navigateToMigration(skipPre: Boolean, router: Router, mangaIds: List<Long>) {
router.pushController(
if (skipPre) {
MigrationListController(
MigrationProcedureConfig(mangaIds, null),
)
} else {
PreMigrationController(mangaIds)
}.withFadeTransaction().tag(MigrationListController.TAG),
)
}
}
}

View File

@ -45,7 +45,6 @@ import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.ExtendedFloatingActionButton
import eu.kanade.presentation.components.OverflowMenu
import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.util.LocalRouter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.PreMigrationListBinding
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationListScreen
@ -58,7 +57,6 @@ class PreMigrationScreen(val mangaIds: List<Long>) : Screen {
override fun Content() {
val screenModel = rememberScreenModel { PreMigrationScreenModel() }
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val router = LocalRouter.currentOrThrow
val navigator = LocalNavigator.currentOrThrow
var fabExpanded by remember { mutableStateOf(true) }
val items by screenModel.state.collectAsState()
@ -99,12 +97,7 @@ class PreMigrationScreen(val mangaIds: List<Long>) : Screen {
topBar = {
AppBar(
title = stringResource(R.string.select_sources),
navigateUp = {
when {
navigator.canPop -> navigator.pop()
else -> router.popCurrentController()
}
},
navigateUp = navigator::pop,
scrollBehavior = scrollBehavior,
actions = {
IconButton(onClick = { screenModel.massSelect(false) }) {

View File

@ -1,30 +0,0 @@
package eu.kanade.tachiyomi.ui.browse.migration.advanced.process
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.util.system.getSerializableCompat
class MigrationListController(bundle: Bundle? = null) :
BasicFullComposeController(bundle) {
constructor(config: MigrationProcedureConfig) : this(
bundleOf(
CONFIG_EXTRA to config,
),
)
val config = args.getSerializableCompat<MigrationProcedureConfig>(CONFIG_EXTRA)!!
@Composable
override fun ComposeContent() {
Navigator(screen = MigrationListScreen(config))
}
companion object {
const val CONFIG_EXTRA = "config_extra"
const val TAG = "migration_list"
}
}

View File

@ -15,14 +15,9 @@ import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.browse.MigrationListScreen
import eu.kanade.presentation.browse.components.MigrationExitDialog
import eu.kanade.presentation.browse.components.MigrationMangaDialog
import eu.kanade.presentation.util.LocalRouter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.changehandler.OneWayFadeChangeHandler
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationController
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationScreen
import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateSearchScreen
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.ui.manga.MangaScreen
import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.system.toast
@ -39,7 +34,6 @@ class MigrationListScreen(private val config: MigrationProcedureConfig) : Screen
val unfinishedCount by screenModel.unfinishedCount.collectAsState()
val dialog by screenModel.dialog.collectAsState()
val navigator = LocalNavigator.currentOrThrow
val router = LocalRouter.currentOrThrow
val context = LocalContext.current
LaunchedEffect(items) {
if (items.isEmpty()) {
@ -52,11 +46,7 @@ class MigrationListScreen(private val config: MigrationProcedureConfig) : Screen
),
)
if (!screenModel.hideNotFound) {
if (navigator.canPop) {
navigator.pop()
} else {
router.popCurrentController()
}
navigator.pop()
}
}
}
@ -71,56 +61,29 @@ class MigrationListScreen(private val config: MigrationProcedureConfig) : Screen
LaunchedEffect(screenModel) {
screenModel.navigateOut.collect {
if (navigator.canPop) {
if (items.size == 1) {
val hasDetails = navigator.items.any { it is MangaScreen }
if (hasDetails) {
val manga = (items.firstOrNull()?.searchResult?.value as? MigratingManga.SearchResult.Result)?.let {
screenModel.getManga(it.id)
}
withUIContext {
if (manga != null) {
val newStack = navigator.items.filter {
it !is MangaScreen &&
it !is MigrationListScreen &&
it !is PreMigrationScreen
} + MangaScreen(manga.id)
navigator replaceAll newStack.first()
navigator.push(newStack.drop(1))
} else {
navigator.pop()
}
}
if (items.size == 1) {
val hasDetails = navigator.items.any { it is MangaScreen }
if (hasDetails) {
val manga = (items.firstOrNull()?.searchResult?.value as? MigratingManga.SearchResult.Result)?.let {
screenModel.getManga(it.id)
}
} else {
withUIContext {
navigator.pop()
if (manga != null) {
val newStack = navigator.items.filter {
it !is MangaScreen &&
it !is MigrationListScreen &&
it !is PreMigrationScreen
} + MangaScreen(manga.id)
navigator replaceAll newStack.first()
navigator.push(newStack.drop(1))
} else {
navigator.pop()
}
}
}
} else {
if (items.size == 1) {
val hasDetails = router.backstack.any { it.controller is MangaController }
if (hasDetails) {
val manga = (items.firstOrNull()?.searchResult?.value as? MigratingManga.SearchResult.Result)?.let {
screenModel.getManga(it.id)
}
withUIContext {
if (manga != null) {
val newStack = router.backstack.filter {
it.controller !is MangaController &&
it.controller !is MigrationListController &&
it.controller !is PreMigrationController
} + MangaController(manga.id).withFadeTransaction()
router.setBackstack(newStack, OneWayFadeChangeHandler())
} else {
router.popCurrentController()
}
}
}
} else {
withUIContext {
router.popCurrentController()
}
withUIContext {
navigator.pop()
}
}
}
@ -166,13 +129,7 @@ class MigrationListScreen(private val config: MigrationProcedureConfig) : Screen
MigrationListScreenModel.Dialog.MigrationExitDialog -> {
MigrationExitDialog(
onDismissRequest = onDismissRequest,
exitMigration = {
if (navigator.canPop) {
navigator.pop()
} else {
router.popCurrentController()
}
},
exitMigration = navigator::pop,
)
}
null -> Unit

View File

@ -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, 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"

View File

@ -20,13 +20,12 @@ import eu.kanade.presentation.browse.BrowseSourceContent
import eu.kanade.presentation.browse.components.BrowseSourceFloatingActionButton
import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.components.SearchToolbar
import eu.kanade.presentation.util.LocalRouter
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationListScreen
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel
import eu.kanade.tachiyomi.ui.more.MoreController
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import eu.kanade.tachiyomi.util.Constants
data class SourceSearchScreen(
private val oldManga: Manga,
@ -38,7 +37,6 @@ data class SourceSearchScreen(
override fun Content() {
val context = LocalContext.current
val uriHandler = LocalUriHandler.current
val router = LocalRouter.currentOrThrow
val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { BrowseSourceScreenModel(sourceId = sourceId, searchQuery = query) }
@ -46,19 +44,12 @@ data class SourceSearchScreen(
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,
)
@ -100,7 +91,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,
@ -108,7 +99,7 @@ data class SourceSearchScreen(
}
LaunchedEffect(state.filters) {
screenModel.initFilterSheet(context, router)
screenModel.initFilterSheet(context, navigator)
}
}
}

View File

@ -1,15 +0,0 @@
package eu.kanade.tachiyomi.ui.browse.migration.sources
import androidx.compose.runtime.Composable
import cafe.adriel.voyager.navigator.Navigator
import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
class MigrationSourcesController : BasicFullComposeController() {
@Composable
override fun ComposeContent() {
Navigator(screen = MigrationSourcesScreen())
}
}
private const val HELP_URL = "https://tachiyomi.org/help/guides/source-migration/"

View File

@ -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())
}
}
}

View File

@ -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,

View File

@ -10,6 +10,7 @@ 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.domain.source.interactor.GetRemoteManga.Companion.QUERY_POPULAR
import eu.kanade.presentation.browse.SourceCategoriesDialog
@ -17,14 +18,12 @@ 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.SourcesScreen.SmartSearchConfig
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
import eu.kanade.tachiyomi.ui.browse.source.feed.SourceFeedController
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
import exh.ui.smartsearch.SmartSearchController
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
import eu.kanade.tachiyomi.ui.browse.source.feed.SourceFeedScreen
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen
import exh.ui.smartsearch.SmartSearchScreen
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
@ -32,7 +31,7 @@ import kotlinx.coroutines.launch
fun Screen.sourcesTab(
smartSearchConfig: SmartSearchConfig? = null,
): TabContent {
val router = LocalRouter.currentOrThrow
val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { SourcesScreenModel(smartSearchConfig = smartSearchConfig) }
val state by screenModel.state.collectAsState()
@ -47,12 +46,12 @@ fun Screen.sourcesTab(
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()) },
),
)
} else {
@ -65,13 +64,13 @@ fun Screen.sourcesTab(
contentPadding = contentPadding,
onClickItem = { source, query ->
// SY -->
val controller = when {
smartSearchConfig != null -> SmartSearchController(source.id, smartSearchConfig)
(query.isBlank() || query == QUERY_POPULAR) && screenModel.useNewSourceNavigation -> SourceFeedController(source.id)
else -> BrowseSourceController(source.id, query)
val screen = when {
smartSearchConfig != null -> SmartSearchScreen(source.id, smartSearchConfig)
(query.isBlank() || query == QUERY_POPULAR) && screenModel.useNewSourceNavigation -> SourceFeedScreen(source.id)
else -> BrowseSourceScreen(source.id, query)
}
screenModel.onOpenSource(source)
router.pushController(controller)
navigator.push(screen)
// SY <--
},
onClickPin = screenModel::togglePin,

View File

@ -1,147 +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 cafe.adriel.voyager.navigator.CurrentScreen
import cafe.adriel.voyager.navigator.Navigator
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
import eu.kanade.tachiyomi.ui.browse.source.SourcesScreen
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,
// SY -->
smartSearchConfig: SourcesScreen.SmartSearchConfig? = null,
savedSearch: Long? = null,
filterList: String? = null,
// SY <--
) : this(
Bundle().apply {
putLong(SOURCE_ID_KEY, sourceId)
if (query != null) {
putString(SEARCH_QUERY_KEY, query)
}
// SY -->
if (smartSearchConfig != null) {
putSerializable(SMART_SEARCH_CONFIG_KEY, smartSearchConfig)
}
if (savedSearch != null) {
putLong(SAVED_SEARCH_CONFIG_KEY, savedSearch)
}
if (filterList != null) {
putString(FILTERS_CONFIG_KEY, filterList)
}
// SY <--
},
)
constructor(
source: CatalogueSource,
query: String? = null,
// SY -->
smartSearchConfig: SourcesScreen.SmartSearchConfig? = null,
savedSearch: Long? = null,
filterList: String? = null,
// SY <--
) : this(
source.id,
query,
smartSearchConfig,
savedSearch,
filterList,
)
constructor(
source: Source,
query: String? = null,
// SY -->
smartSearchConfig: SourcesScreen.SmartSearchConfig? = null,
savedSearch: Long? = null,
filterList: String? = null,
// SY <--
) : this(
source.id,
query,
smartSearchConfig,
savedSearch,
filterList,
)
private val sourceId = args.getLong(SOURCE_ID_KEY)
private val initialQuery = args.getString(SEARCH_QUERY_KEY)
// SY -->
private val filtersJson = args.getString(FILTERS_CONFIG_KEY)
private val savedSearch = args.getLong(SAVED_SEARCH_CONFIG_KEY, 0).takeUnless { it == 0L }
// SY <--
private val queryEvent = Channel<BrowseSourceScreen.SearchType>()
@Composable
override fun ComposeContent() {
Navigator(
screen = BrowseSourceScreen(
sourceId = sourceId,
query = initialQuery,
// SY -->
filtersJson = filtersJson,
savedSearch = savedSearch,
// SY <--
),
) { 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"
// SY -->
private const val SMART_SEARCH_CONFIG_KEY = "smartSearchConfig"
private const val SAVED_SEARCH_CONFIG_KEY = "savedSearch"
private const val FILTERS_CONFIG_KEY = "filters"
// SY <--

View File

@ -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
@ -52,16 +51,15 @@ 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.online.HttpSource
import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.browse.extension.details.SourcePreferencesScreen
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.browse.source.SourcesScreen
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,6 +71,7 @@ data class BrowseSourceScreen(
// SY -->
private val filtersJson: String? = null,
private val savedSearch: Long? = null,
private val smartSearchConfig: SourcesScreen.SmartSearchConfig? = null,
// SY <--
) : Screen {
@ -80,7 +79,6 @@ data class BrowseSourceScreen(
@Composable
override fun Content() {
val router = LocalRouter.currentOrThrow
val navigator = LocalNavigator.currentOrThrow
val scope = rememberCoroutineScope()
val context = LocalContext.current
@ -109,13 +107,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)) {
@ -125,7 +116,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) },
@ -227,9 +218,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, smartSearchConfig)) },
onMangaLongClick = { manga ->
scope.launchIO {
val duplicateManga = screenModel.getDuplicateLibraryManga(manga)
@ -256,7 +247,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),
)
}
@ -273,9 +264,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)
@ -298,10 +287,8 @@ data class BrowseSourceScreen(
else -> {}
}
BackHandler(onBack = navigateUp)
LaunchedEffect(state.filters) {
screenModel.initFilterSheet(context, router)
screenModel.initFilterSheet(context, navigator)
}
LaunchedEffect(Unit) {

View File

@ -14,7 +14,7 @@ import androidx.paging.cachedIn
import androidx.paging.map
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.coroutineScope
import com.bluelinelabs.conductor.Router
import cafe.adriel.voyager.navigator.Navigator
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.core.prefs.CheckboxState
import eu.kanade.core.prefs.asState
@ -454,7 +454,7 @@ open class BrowseSourceScreenModel(
mutableState.update { it.copy(toolbarQuery = query) }
}
open fun initFilterSheet(context: Context, router: Router) {
open fun initFilterSheet(context: Context, navigator: Navigator) {
val state = state.value
/*if (state.filters.isEmpty()) {
return
@ -463,7 +463,7 @@ open class BrowseSourceScreenModel(
filterSheet = SourceFilterSheet(
context = context,
// SY -->
router = router,
navigator = navigator,
source = source,
searches = emptyList(),
// SY <--

View File

@ -7,7 +7,7 @@ import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.ConcatAdapter
import com.bluelinelabs.conductor.Router
import cafe.adriel.voyager.navigator.Navigator
import com.google.android.material.chip.Chip
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
@ -23,7 +23,7 @@ import exh.source.getMainSource
class SourceFilterSheet(
context: Context,
// SY -->
router: Router,
navigator: Navigator,
source: CatalogueSource,
searches: List<EXHSavedSearch> = emptyList(),
// SY <--
@ -41,7 +41,7 @@ class SourceFilterSheet(
// SY -->
searches = searches,
source = source,
router = router,
navigator = navigator,
dismissSheet = ::dismiss,
// SY <--
)
@ -84,7 +84,7 @@ class SourceFilterSheet(
// SY -->
searches: List<EXHSavedSearch> = emptyList(),
source: CatalogueSource? = null,
router: Router? = null,
navigator: Navigator? = null,
dismissSheet: (() -> Unit)? = null,
// SY <--
) :
@ -116,10 +116,10 @@ class SourceFilterSheet(
// SY -->
recycler.adapter = ConcatAdapter(
listOfNotNull(
router?.let {
navigator?.let {
source?.getMainSource<MangaDex>()
?.let {
MangaDexFabHeaderAdapter(router, it) {
MangaDexFabHeaderAdapter(navigator, it) {
dismissSheet?.invoke()
}
}

View File

@ -1,37 +0,0 @@
package eu.kanade.tachiyomi.ui.browse.source.feed
import android.os.Bundle
import androidx.compose.runtime.Composable
import androidx.core.os.bundleOf
import cafe.adriel.voyager.navigator.Navigator
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
class SourceFeedController : BasicFullComposeController {
constructor(source: CatalogueSource) : super(
bundleOf(
SOURCE_EXTRA to source.id,
),
)
constructor(sourceId: Long) : super(
bundleOf(
SOURCE_EXTRA to sourceId,
),
)
@Suppress("unused")
constructor(bundle: Bundle) : super(bundle)
val sourceId = args.getLong(SOURCE_EXTRA)
@Composable
override fun ComposeContent() {
Navigator(screen = SourceFeedScreen(sourceId))
}
companion object {
const val SOURCE_EXTRA = "source"
}
}

View File

@ -13,19 +13,16 @@ 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 eu.kanade.domain.manga.model.Manga
import eu.kanade.domain.source.interactor.GetRemoteManga
import eu.kanade.presentation.browse.SourceFeedScreen
import eu.kanade.presentation.browse.components.FailedToLoadSavedSearchDialog
import eu.kanade.presentation.browse.components.SourceFeedAddDialog
import eu.kanade.presentation.browse.components.SourceFeedDeleteDialog
import eu.kanade.presentation.util.LocalRouter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
import eu.kanade.tachiyomi.ui.browse.source.browse.SourceFilterSheet
import eu.kanade.tachiyomi.ui.manga.MangaScreen
import eu.kanade.tachiyomi.util.lang.launchUI
@ -47,19 +44,18 @@ class SourceFeedScreen(val sourceId: Long) : Screen {
val screenModel = rememberScreenModel { SourceFeedScreenModel(sourceId) }
val state by screenModel.state.collectAsState()
val navigator = LocalNavigator.currentOrThrow
val router = LocalRouter.currentOrThrow
SourceFeedScreen(
name = screenModel.source.name,
isLoading = state.isLoading,
items = state.items,
onFabClick = if (state.filters.isEmpty()) null else { { filterSheet?.show() } },
onClickBrowse = { onBrowseClick(router, screenModel.source) },
onClickLatest = { onLatestClick(router, screenModel.source) },
onClickSavedSearch = { onSavedSearchClick(router, screenModel.source, it) },
onClickBrowse = { onBrowseClick(navigator, screenModel.source) },
onClickLatest = { onLatestClick(navigator, screenModel.source) },
onClickSavedSearch = { onSavedSearchClick(navigator, screenModel.source, it) },
onClickDelete = screenModel::openDeleteFeed,
onClickManga = { onMangaClick(navigator, it) },
onClickSearch = { onSearchClick(router, screenModel.source, it) },
onClickSearch = { onSearchClick(navigator, screenModel.source, it) },
searchQuery = state.searchQuery,
onSearchQueryChange = screenModel::search,
isIncognitoMode = screenModel.isIncognitoMode,
@ -102,7 +98,7 @@ class SourceFeedScreen(val sourceId: Long) : Screen {
val context = LocalContext.current
LaunchedEffect(state.filters) {
initFilterSheet(state, screenModel, scope, context, router)
initFilterSheet(state, screenModel, scope, context, navigator)
}
}
@ -111,28 +107,28 @@ class SourceFeedScreen(val sourceId: Long) : Screen {
screenModel: SourceFeedScreenModel,
viewScope: CoroutineScope,
context: Context,
router: Router,
navigator: Navigator,
) {
val filterSerializer = FilterSerializer()
filterSheet = SourceFilterSheet(
context,
context = context,
// SY -->
router,
screenModel.source,
emptyList(),
navigator = navigator,
source = screenModel.source,
searches = emptyList(),
// SY <--
onFilterClicked = {
val allDefault = state.filters == screenModel.source.getFilterList()
filterSheet?.dismiss()
if (allDefault) {
onBrowseClick(
router,
navigator,
screenModel.source.id,
state.searchQuery?.nullIfBlank(),
)
} else {
onBrowseClick(
router,
navigator,
screenModel.source.id,
state.searchQuery?.nullIfBlank(),
filters = Json.encodeToString(filterSerializer.serialize(state.filters)),
@ -164,7 +160,7 @@ class SourceFeedScreen(val sourceId: Long) : Screen {
if (!allDefault) {
onBrowseClick(
router,
navigator,
screenModel.source.id,
search = state.searchQuery?.nullIfBlank(),
savedSearch = search.id,
@ -192,23 +188,23 @@ class SourceFeedScreen(val sourceId: Long) : Screen {
navigator.push(MangaScreen(manga.id, true))
}
fun onBrowseClick(router: Router, sourceId: Long, search: String? = null, savedSearch: Long? = null, filters: String? = null) {
router.replaceTopController(BrowseSourceController(sourceId, search, savedSearch = savedSearch, filterList = filters).withFadeTransaction())
fun onBrowseClick(navigator: Navigator, sourceId: Long, search: String? = null, savedSearch: Long? = null, filters: String? = null) {
navigator.replace(BrowseSourceScreen(sourceId, search, savedSearch = savedSearch, filtersJson = filters))
}
private fun onLatestClick(router: Router, source: CatalogueSource) {
router.replaceTopController(BrowseSourceController(source, GetRemoteManga.QUERY_LATEST).withFadeTransaction())
private fun onLatestClick(navigator: Navigator, source: CatalogueSource) {
navigator.replace(BrowseSourceScreen(source.id, GetRemoteManga.QUERY_LATEST))
}
fun onBrowseClick(router: Router, source: CatalogueSource) {
router.replaceTopController(BrowseSourceController(source, GetRemoteManga.QUERY_POPULAR).withFadeTransaction())
fun onBrowseClick(navigator: Navigator, source: CatalogueSource) {
navigator.replace(BrowseSourceScreen(source.id, GetRemoteManga.QUERY_POPULAR))
}
private fun onSavedSearchClick(router: Router, source: CatalogueSource, savedSearch: SavedSearch) {
router.replaceTopController(BrowseSourceController(source, savedSearch = savedSearch.id).withFadeTransaction())
private fun onSavedSearchClick(navigator: Navigator, source: CatalogueSource, savedSearch: SavedSearch) {
navigator.replace(BrowseSourceScreen(source.id, savedSearch = savedSearch.id))
}
private fun onSearchClick(router: Router, source: CatalogueSource, query: String) {
onBrowseClick(router, source.id, query.nullIfBlank())
private fun onSearchClick(navigator: Navigator, source: CatalogueSource, query: String) {
onBrowseClick(navigator, source.id, query.nullIfBlank())
}
}

View File

@ -49,12 +49,6 @@ import xyz.nulldev.ts.api.http.serializer.FilterSerializer
import java.util.concurrent.Executors
import eu.kanade.domain.manga.model.Manga as DomainManga
/**
* Presenter of [SourceFeedController]
* Function calls should be done from here. UI calls should be done from the controller.
*
* @param source the source.
*/
open class SourceFeedScreenModel(
val sourceId: Long,
private val sourceManager: SourceManager = Injekt.get(),

View File

@ -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,
),
)
}
}
}

View File

@ -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)) },
)
}
}

View File

@ -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())
}
}
}

View File

@ -16,7 +16,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.R
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.collectLatest
@ -29,7 +28,6 @@ class CategoryScreen : Screen {
@Composable
override fun Content() {
val context = LocalContext.current
val router = LocalRouter.currentOrThrow
val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { CategoryScreenModel() }
@ -49,12 +47,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) {

View File

@ -1,20 +0,0 @@
package eu.kanade.tachiyomi.ui.category.biometric
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
/**
* Controller to manage the lock times for the biometric lock.
*/
class BiometricTimesController : BasicFullComposeController() {
@Composable
override fun ComposeContent() {
CompositionLocalProvider(LocalRouter provides router) {
Navigator(screen = BiometricTimesScreen())
}
}
}

View File

@ -8,12 +8,12 @@ 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.currentOrThrow
import com.google.android.material.timepicker.MaterialTimePicker
import eu.kanade.presentation.category.BiometricTimesScreen
import eu.kanade.presentation.category.components.CategoryDeleteDialog
import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.util.LocalRouter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.util.system.toast
@ -27,7 +27,7 @@ class BiometricTimesScreen : Screen {
@Composable
override fun Content() {
val context = LocalContext.current
val router = LocalRouter.currentOrThrow
val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { BiometricTimesScreenModel() }
val state by screenModel.state.collectAsState()
@ -43,7 +43,7 @@ class BiometricTimesScreen : Screen {
state = successState,
onClickCreate = { screenModel.showDialog(BiometricTimesDialog.Create) },
onClickDelete = { screenModel.showDialog(BiometricTimesDialog.Delete(it)) },
navigateUp = router::popCurrentController,
navigateUp = navigator::pop,
)
fun showTimePicker(startTime: Duration? = null) {

View File

@ -16,9 +16,6 @@ import kotlinx.coroutines.flow.update
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
/**
* Presenter of [BiometricTimesController]. Used to manage the categories of the library.
*/
class BiometricTimesScreenModel(
private val preferences: SecurityPreferences = Injekt.get(),
) : StateScreenModel<BiometricTimesScreenState>(BiometricTimesScreenState.Loading) {

View File

@ -1,20 +0,0 @@
package eu.kanade.tachiyomi.ui.category.genre
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
/**
* Controller to manage the categories for the users' library.
*/
class SortTagController : BasicFullComposeController() {
@Composable
override fun ComposeContent() {
CompositionLocalProvider(LocalRouter provides router) {
Navigator(screen = SortTagScreen())
}
}
}

View File

@ -8,11 +8,11 @@ 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.currentOrThrow
import eu.kanade.presentation.category.components.CategoryCreateDialog
import eu.kanade.presentation.category.components.CategoryDeleteDialog
import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.util.LocalRouter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.collectLatest
@ -22,7 +22,7 @@ class SortTagScreen : Screen {
@Composable
override fun Content() {
val context = LocalContext.current
val router = LocalRouter.currentOrThrow
val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { SortTagScreenModel() }
val state by screenModel.state.collectAsState()
@ -40,7 +40,7 @@ class SortTagScreen : Screen {
onClickDelete = { screenModel.showDialog(SortTagDialog.Delete(it)) },
onClickMoveUp = screenModel::moveUp,
onClickMoveDown = screenModel::moveDown,
navigateUp = router::popCurrentController,
navigateUp = navigator::pop,
)
when (val dialog = successState.dialog) {

View File

@ -17,9 +17,6 @@ import kotlinx.coroutines.flow.update
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
/**
* Presenter of [SortTagController]. Used to manage the categories of the library.
*/
class SortTagScreenModel(
private val getSortTag: GetSortTag = Injekt.get(),
private val createSortTag: CreateSortTag = Injekt.get(),

View File

@ -1,20 +0,0 @@
package eu.kanade.tachiyomi.ui.category.repos
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
/**
* Controller to manage the categories for the users' library.
*/
class RepoController : BasicFullComposeController() {
@Composable
override fun ComposeContent() {
CompositionLocalProvider(LocalRouter provides router) {
Navigator(screen = RepoScreen())
}
}
}

View File

@ -8,12 +8,12 @@ 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.currentOrThrow
import eu.kanade.presentation.category.SourceRepoScreen
import eu.kanade.presentation.category.components.CategoryCreateDialog
import eu.kanade.presentation.category.components.CategoryDeleteDialog
import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.util.LocalRouter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.collectLatest
@ -23,7 +23,7 @@ class RepoScreen : Screen {
@Composable
override fun Content() {
val context = LocalContext.current
val router = LocalRouter.currentOrThrow
val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { RepoScreenModel() }
val state by screenModel.state.collectAsState()
@ -39,7 +39,7 @@ class RepoScreen : Screen {
state = successState,
onClickCreate = { screenModel.showDialog(RepoDialog.Create) },
onClickDelete = { screenModel.showDialog(RepoDialog.Delete(it)) },
navigateUp = router::popCurrentController,
navigateUp = navigator::pop,
)
when (val dialog = successState.dialog) {

View File

@ -16,9 +16,6 @@ import kotlinx.coroutines.flow.update
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
/**
* Presenter of [RepoController]. Used to manage the repos for the extensions.
*/
class RepoScreenModel(
private val getSourceRepos: GetSourceRepos = Injekt.get(),
private val createSourceRepo: CreateSourceRepo = Injekt.get(),

View File

@ -1,20 +0,0 @@
package eu.kanade.tachiyomi.ui.category.sources
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
/**
* Controller to manage the categories for the users' library.
*/
class SourceCategoryController : BasicFullComposeController() {
@Composable
override fun ComposeContent() {
CompositionLocalProvider(LocalRouter provides router) {
Navigator(screen = SourceCategoryScreen())
}
}
}

View File

@ -8,13 +8,13 @@ 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.currentOrThrow
import eu.kanade.presentation.category.SourceCategoryScreen
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.R
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.collectLatest
@ -24,7 +24,7 @@ class SourceCategoryScreen : Screen {
@Composable
override fun Content() {
val context = LocalContext.current
val router = LocalRouter.currentOrThrow
val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { SourceCategoryScreenModel() }
val state by screenModel.state.collectAsState()
@ -41,7 +41,7 @@ class SourceCategoryScreen : Screen {
onClickCreate = { screenModel.showDialog(SourceCategoryDialog.Create) },
onClickRename = { screenModel.showDialog(SourceCategoryDialog.Rename(it)) },
onClickDelete = { screenModel.showDialog(SourceCategoryDialog.Delete(it)) },
navigateUp = router::popCurrentController,
navigateUp = navigator::pop,
)
when (val dialog = successState.dialog) {

View File

@ -17,9 +17,6 @@ import kotlinx.coroutines.flow.update
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
/**
* Presenter of [SourceCategoryController]. Used to manage the categories of the library.
*/
class SourceCategoryScreenModel(
private val getSourceCategories: GetSourceCategories = Injekt.get(),
private val createSourceCategory: CreateSourceCategory = Injekt.get(),

View File

@ -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)
}
}

View File

@ -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 ->

View File

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

View File

@ -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))

View File

@ -1,34 +1,76 @@
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.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
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.core.prefs.asState
import eu.kanade.domain.chapter.model.Chapter
import eu.kanade.domain.ui.UiPreferences
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
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
object HistoryScreen : Screen {
object HistoryTab : Tab {
private val snackbarHostState = SnackbarHostState()
private val resumeLastChapterReadEvent = Channel<Unit>()
override val options: TabOptions
@Composable
get() {
val isSelected = LocalTabNavigator.current.current.key == key
val image = AnimatedImageVector.animatedVectorResource(R.drawable.anim_history_enter)
return TabOptions(
index = 2u,
title = stringResource(R.string.label_recent_manga),
icon = rememberAnimatedVectorPainter(image, isSelected),
)
}
override suspend fun onReselect(navigator: Navigator) {
resumeLastChapterReadEvent.send(Unit)
}
// SY -->
@Composable
override fun isEnabled(): Boolean {
val scope = rememberCoroutineScope()
return remember {
Injekt.get<UiPreferences>().showNavHistory().asState(scope)
}.value
}
// SY <--
@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 +81,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 +126,12 @@ object HistoryScreen : Screen {
}
}
}
LaunchedEffect(Unit) {
resumeLastChapterReadEvent.consumeAsFlow().collectLatest {
openChapter(context, screenModel.getNextChapter())
}
}
}
suspend fun openChapter(context: Context, chapter: Chapter?) {

View File

@ -0,0 +1,306 @@
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.remember
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.core.prefs.asState
import eu.kanade.core.util.fastFilter
import eu.kanade.domain.library.service.LibraryPreferences
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.components.NavigationBar
import eu.kanade.presentation.components.NavigationRail
import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.util.Tab
import eu.kanade.presentation.util.Transition
import eu.kanade.presentation.util.isTabletUi
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.BrowseTab
import eu.kanade.tachiyomi.ui.history.HistoryTab
import eu.kanade.tachiyomi.ui.library.LibraryTab
import eu.kanade.tachiyomi.ui.manga.MangaScreen
import eu.kanade.tachiyomi.ui.more.MoreTab
import eu.kanade.tachiyomi.ui.updates.UpdatesTab
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
object HomeScreen : Screen {
private val librarySearchEvent = Channel<String>()
private val openTabEvent = Channel<Tab>()
private val showBottomNavEvent = Channel<Boolean>()
private val tabs = listOf(
LibraryTab,
UpdatesTab,
HistoryTab,
BrowseTab(),
MoreTab(),
)
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
// SY -->
val scope = rememberCoroutineScope()
val alwaysShowLabel by remember {
Injekt.get<UiPreferences>().bottomBarLabels().asState(scope)
}
// SY <--
TabNavigator(
tab = LibraryTab,
) { tabNavigator ->
// Provide usable navigator to content screen
CompositionLocalProvider(LocalNavigator provides navigator) {
Row(verticalAlignment = Alignment.CenterVertically) {
if (isTabletUi()) {
NavigationRail {
tabs
// SY -->
.fastFilter { it.isEnabled() }
// SY <--
.fastForEach {
NavigationRailItem(it/* SY --> */, alwaysShowLabel/* SY <-- */)
}
}
}
Scaffold(
bottomBar = {
if (!isTabletUi()) {
val bottomNavVisible by produceState(initialValue = true) {
showBottomNavEvent.receiveAsFlow().collectLatest { value = it }
}
AnimatedVisibility(
visible = bottomNavVisible,
enter = expandVertically(),
exit = shrinkVertically(),
) {
NavigationBar {
tabs
// SY -->
.fastFilter { it.isEnabled() }
// SY <--
.fastForEach {
NavigationBarItem(it/* SY --> */, alwaysShowLabel/* SY <-- */)
}
}
}
}
},
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/* SY --> */, alwaysShowLabel: Boolean/* SY <-- */) {
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 = /* SY --> */alwaysShowLabel, /* SY <-- */
)
}
@Composable
fun NavigationRailItem(tab: eu.kanade.presentation.util.Tab/* SY --> */, alwaysShowLabel: Boolean/* SY <-- */) {
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 = /* SY --> */alwaysShowLabel, /* SY <-- */
)
}
@Composable
private fun NavigationIconItem(tab: eu.kanade.presentation.util.Tab) {
BadgedBox(
badge = {
when {
tab is UpdatesTab -> {
val count by produceState(initialValue = 0) {
val pref = Injekt.get<LibraryPreferences>()
combine(
pref.showUpdatesNavBadge().changes(),
pref.unreadUpdatesCount().changes(),
) { show, count -> if (show) count else 0 }
.collectLatest { value = it }
}
if (count > 0) {
Badge {
val desc = pluralStringResource(
id = R.plurals.notification_chapters_generic,
count = count,
count,
)
Text(
text = count.toString(),
modifier = Modifier.semantics { contentDescription = desc },
)
}
}
}
BrowseTab::class.isInstance(tab) -> {
val count by produceState(initialValue = 0) {
Injekt.get<SourcePreferences>().extensionUpdatesCount().changes()
.collectLatest { value = it }
}
if (count > 0) {
Badge {
val desc = pluralStringResource(
id = R.plurals.update_check_notification_ext_updates,
count = count,
count,
)
Text(
text = count.toString(),
modifier = Modifier.semantics { contentDescription = desc },
)
}
}
}
}
},
) {
Icon(painter = tab.options.icon!!, contentDescription = tab.options.title)
}
}
suspend fun search(query: String) {
librarySearchEvent.send(query)
}
suspend fun openTab(tab: Tab) {
openTabEvent.send(tab)
}
suspend fun showBottomNav(show: Boolean) {
showBottomNavEvent.send(show)
}
sealed class Tab {
data class Library(val mangaIdToOpen: Long? = null) : Tab()
object Updates : Tab()
object History : Tab()
data class Browse(val toExtensions: Boolean = false) : Tab()
data class More(val toDownloads: Boolean) : Tab()
}
}

View File

@ -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)
}

View File

@ -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.GetCategories
import eu.kanade.domain.category.interactor.SetDisplayModeForCategory
@ -31,11 +31,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
@ -48,12 +48,12 @@ 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)
// SY -->
grouping = Grouping(router.activity!!)
grouping = Grouping(activity)
// SY <--
}

View File

@ -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.UnsortedPreferences
import eu.kanade.domain.category.model.Category
import eu.kanade.domain.library.model.LibraryGroup
@ -44,33 +48,48 @@ import eu.kanade.presentation.library.components.SyncFavoritesConfirmDialog
import eu.kanade.presentation.library.components.SyncFavoritesProgressDialog
import eu.kanade.presentation.library.components.SyncFavoritesWarningDialog
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.migration.advanced.design.PreMigrationController
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
import eu.kanade.tachiyomi.ui.category.CategoryController
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationScreen
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.util.system.toast
import eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView
import exh.favorites.FavoritesSyncStatus
import exh.source.MERGED_SOURCE_ID
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
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
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
@ -127,7 +146,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))
}
@ -158,9 +177,9 @@ object LibraryScreen : Screen {
.map { it.manga.id }
screenModel.clearSelection()
if (selectedMangaIds.isNotEmpty()) {
PreMigrationController.navigateToMigration(
PreMigrationScreen.navigateToMigration(
Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
router,
navigator,
selectedMangaIds,
)
} else {
@ -172,66 +191,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
@ -242,7 +258,7 @@ object LibraryScreen : Screen {
onDismissRequest = onDismissRequest,
onEditCategories = {
screenModel.clearSelection()
router.pushController(CategoryController())
navigator.push(CategoryScreen())
},
onConfirm = { include, exclude ->
screenModel.clearSelection()
@ -295,7 +311,7 @@ object LibraryScreen : Screen {
SyncFavoritesProgressDialog(
status = screenModel.favoritesSync.status.collectAsState().value,
setStatusIdle = { screenModel.favoritesSync.status.value = FavoritesSyncStatus.Idle(context) },
openManga = { router.openManga(it.id) },
openManga = { navigator.push(MangaScreen(it.id)) },
)
// SY <--
@ -307,11 +323,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
@ -319,23 +333,19 @@ object LibraryScreen : Screen {
}
LaunchedEffect(Unit) {
launch { queryEvent.collectLatest(screenModel::search) }
launch { requestSettingsSheetEvent.collectLatest { onClickFilter() } }
launch { queryEvent.receiveAsFlow().collect(screenModel::search) }
launch { requestSettingsSheetEvent.receiveAsFlow().collectLatest { onClickFilter() } }
}
}
private fun Router.openManga(mangaId: Long) {
pushController(MangaController(mangaId))
}
// For invoking search from other screen
private val queryEvent = MutableSharedFlow<String>(replay = 1)
fun search(query: String) = queryEvent.tryEmit(query)
private val queryEvent = Channel<String>()
suspend fun search(query: String) = queryEvent.send(query)
// For opening settings sheet in LibraryController
private val requestSettingsSheetEvent = MutableSharedFlow<Unit>()
private val openSettingsSheetEvent_ = MutableSharedFlow<Category>()
val openSettingsSheetEvent = openSettingsSheetEvent_.asSharedFlow()
private suspend fun sendSettingsSheetIntent(category: Category) = openSettingsSheetEvent_.emit(category)
suspend fun requestOpenSettingsSheet() = requestSettingsSheetEvent.emit(Unit)
private val requestSettingsSheetEvent = Channel<Unit>()
private val openSettingsSheetEvent_ = Channel<Category>()
val openSettingsSheetEvent = openSettingsSheetEvent_.receiveAsFlow()
private suspend fun sendSettingsSheetIntent(category: Category) = openSettingsSheetEvent_.send(category)
suspend fun requestOpenSettingsSheet() = requestSettingsSheetEvent.send(Unit)
}

View File

@ -7,80 +7,75 @@ import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.os.Looper
import android.view.Menu
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.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.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.UnsortedPreferences
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.more.settings.screen.ConfigureExhDialog
import eu.kanade.presentation.more.settings.screen.WhatsNewDialog
import eu.kanade.presentation.util.Transition
import eu.kanade.presentation.util.collectAsState
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R
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.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.toast
import eu.kanade.tachiyomi.util.view.setComposeContent
import eu.kanade.tachiyomi.util.view.setNavigationBarTransparentCompat
import exh.EXHMigrations
import exh.eh.EHentaiUpdateWorker
import exh.source.BlacklistedSources
import exh.source.EH_SOURCE_ID
import exh.source.EXH_SOURCE_ID
import exh.uconfig.WarnConfigureDialogController
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
@ -99,24 +94,20 @@ class MainActivity : BaseActivity() {
private val unsortedPreferences: UnsortedPreferences by injectLazy()
// SY <--
lateinit var binding: MainActivityBinding
private lateinit var router: Router
private val startScreenId = R.id.nav_library
private var isConfirmingExit: Boolean = false
private var isHandlingShortcut: Boolean = false
/**
* App bar lift state for backstack
*/
private val backstackLiftState = mutableMapOf<String, Boolean>()
private val chapterCache: ChapterCache by injectLazy()
// To be checked by splash screen. If true then splash screen will be removed.
var ready = false
/**
* Sheet containing filter/sort/display items.
*/
private var settingsSheet: LibrarySettingsSheet? = null
private lateinit var navigator: Navigator
// SY -->
// Idle-until-urgent
private var firstPaint = false
@ -134,6 +125,8 @@ class MainActivity : BaseActivity() {
iuuQueue += task
}
}
private var runExhConfigureDialog by mutableStateOf(false)
// SY <--
override fun onCreate(savedInstanceState: Bundle?) {
@ -164,23 +157,64 @@ 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) {
// SY -->
WhatsNewDialog(onDismissRequest = { showChangelog = false })
// SY <--
}
ConfigureExhDialog(run = runExhConfigureDialog, onRunning = { runExhConfigureDialog = false })
}
val startTime = System.currentTimeMillis()
@ -190,114 +224,26 @@ 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) {
WhatsNewDialogController().showDialog(router)
}
// EXH <--
// SY -->
initWhenIdle {
// Upload settings
if (unsortedPreferences.enableExhentai().get() &&
unsortedPreferences.exhShowSettingsUploadWarning().get()
) {
WarnConfigureDialogController.uploadSettings(router)
runExhConfigureDialog = true
}
// Scheduler uploader job if required
EHentaiUpdateWorker.scheduleBackground(this)
}
// SY <--
} else {
// Restore selected nav item
router.backstack.firstOrNull()?.tag()?.toIntOrNull()?.let {
nav.menu.findItem(it).isChecked = true
}
}
// SY -->
if (!unsortedPreferences.isHentaiEnabled().get()) {
@ -305,40 +251,55 @@ class MainActivity : BaseActivity() {
BlacklistedSources.HIDDEN_SOURCES += EXH_SOURCE_ID
}
// SY -->
}
merge(libraryPreferences.showUpdatesNavBadge().changes(), libraryPreferences.unreadUpdatesCount().changes())
.onEach { setUnreadUpdatesBadge() }
.launchIn(lifecycleScope)
private fun showSettingsSheet(category: Category? = null) {
if (category != null) {
settingsSheet?.show(category)
} else {
lifecycleScope.launch { LibraryTab.requestOpenSettingsSheet() }
}
}
sourcePreferences.extensionUpdatesCount()
.asHotFlow { setExtensionsBadge() }
.launchIn(lifecycleScope)
@Composable
private fun ConfirmExit() {
val scope = rememberCoroutineScope()
val confirmExit by preferences.confirmExit().collectAsState()
var waitingConfirmation by remember { mutableStateOf(false) }
BackHandler(enabled = !waitingConfirmation && confirmExit) {
scope.launch {
waitingConfirmation = true
val toast = toast(R.string.confirm_exit, Toast.LENGTH_LONG)
delay(2.seconds)
toast.cancel()
waitingConfirmation = false
}
}
}
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)
// SY -->
uiPreferences.bottomBarLabels()
.asHotFlow { setNavLabelVisibility() }
.launchIn(lifecycleScope)
// SY <--
}
}
/**
@ -348,16 +309,16 @@ class MainActivity : BaseActivity() {
* after the animation is finished.
*/
private fun setSplashScreenExitAnimation(splashScreen: SplashScreen?) {
val root = findViewById<View>(android.R.id.content)
val setNavbarScrim = {
// Make sure navigation bar is on bottom before we modify it
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, insets ->
ViewCompat.setOnApplyWindowInsetsListener(root) { _, insets ->
if (insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom > 0) {
val elevation = binding.bottomNav?.elevation ?: 0F
window.setNavigationBarTransparentCompat(this@MainActivity, elevation)
window.setNavigationBarTransparentCompat(this@MainActivity, 3.dpToPx.toFloat())
}
insets
}
ViewCompat.requestApplyInsets(binding.root)
ViewCompat.requestApplyInsets(root)
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S && splashScreen != null) {
@ -375,7 +336,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
}
}
@ -403,69 +364,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))
@ -474,32 +379,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
@ -508,20 +400,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 -> {
@ -535,190 +423,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()
// SY -->
binding.bottomNav?.menu?.let { updateNavMenu(it) }
// SY <--
} else {
binding.bottomNav?.slideDown()
}
}
private fun showSideNav(visible: Boolean) {
binding.sideNav?.isVisible = visible
// SY -->
binding.sideNav?.let { updateNavMenu(it.menu) }
// SY <--
}
// SY -->
private fun updateNavMenu(menu: Menu) {
menu.findItem(R.id.nav_updates).isVisible = uiPreferences.showNavUpdates().get()
menu.findItem(R.id.nav_history).isVisible = uiPreferences.showNavHistory().get()
}
// SY <--
private val nav: NavigationBarView
get() = binding.bottomNav ?: binding.sideNav!!
// SY -->
private fun setNavLabelVisibility() {
if (uiPreferences.bottomBarLabels().get()) {
nav.labelVisibilityMode = NavigationBarView.LABEL_VISIBILITY_LABELED
} else {
nav.labelVisibilityMode = NavigationBarView.LABEL_VISIBILITY_SELECTED
}
}
// SY <--
init {
registerSecureActivity(this)
}

View File

@ -1,34 +0,0 @@
package eu.kanade.tachiyomi.ui.main
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.util.AttributeSet
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
import it.gmariotti.changelibs.library.view.ChangeLogRecyclerView
class WhatsNewDialogController : DialogController() {
@Suppress("DEPRECATION")
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
val activity = activity!!
val view = WhatsNewRecyclerView(activity)
return MaterialAlertDialogBuilder(activity)
.setTitle(R.string.whats_new)
.setView(view)
.setPositiveButton(android.R.string.cancel, null)
.create()
}
class WhatsNewRecyclerView(context: Context) : ChangeLogRecyclerView(context) {
override fun initAttrs(attrs: AttributeSet?, defStyle: Int) {
mRowLayoutId = R.layout.changelog_row_layout
mRowHeaderLayoutId = R.layout.changelog_header_layout
mChangeLogFileResourceId = if (BuildConfig.DEBUG /* SY --> */ || isPreviewBuildType/* SY <-- */) R.raw.changelog_debug else R.raw.changelog_release
}
}
}

View File

@ -1,53 +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
import eu.kanade.tachiyomi.ui.browse.source.SourcesScreen
import eu.kanade.tachiyomi.util.system.getSerializableCompat
class MangaController : BasicFullComposeController {
@Suppress("unused")
constructor(bundle: Bundle) : this(bundle.getLong(MANGA_EXTRA))
constructor(
mangaId: Long,
fromSource: Boolean = false,
smartSearchConfig: SourcesScreen.SmartSearchConfig? = null,
) : super(bundleOf(MANGA_EXTRA to mangaId, FROM_SOURCE_EXTRA to fromSource, SMART_SEARCH_CONFIG_EXTRA to smartSearchConfig))
// SY -->
constructor(redirect: MangaInfoScreenModel.EXHRedirect) : super(
bundleOf(MANGA_EXTRA to redirect.mangaId),
)
// SY <--
val mangaId: Long
get() = args.getLong(MANGA_EXTRA)
val fromSource: Boolean
get() = args.getBoolean(FROM_SOURCE_EXTRA)
// SY -->
val smartSearchConfig: SourcesScreen.SmartSearchConfig?
get() = args.getSerializableCompat(SMART_SEARCH_CONFIG_EXTRA)
// SY <--
@Composable
override fun ComposeContent() {
Navigator(screen = MangaScreen(mangaId, fromSource, smartSearchConfig))
}
companion object {
const val FROM_SOURCE_EXTRA = "from_source"
const val MANGA_EXTRA = "manga"
// EXH -->
const val UPDATE_EXTRA = "update"
const val SMART_SEARCH_CONFIG_EXTRA = "smartSearchConfig"
// EXH <--
}
}

View File

@ -17,6 +17,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
@ -29,7 +30,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 com.google.android.material.dialog.MaterialAlertDialogBuilder
import eu.kanade.domain.UnsortedPreferences
import eu.kanade.domain.chapter.model.Chapter
@ -47,7 +47,6 @@ import eu.kanade.presentation.manga.components.DownloadCustomAmountDialog
import eu.kanade.presentation.manga.components.MangaCoverDialog
import eu.kanade.presentation.manga.components.SelectScanlatorsDialog
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.CatalogueSource
@ -55,22 +54,16 @@ import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
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.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationScreen
import eu.kanade.tachiyomi.ui.browse.source.SourcesScreen
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
import eu.kanade.tachiyomi.ui.browse.source.feed.SourceFeedController
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
import eu.kanade.tachiyomi.ui.browse.source.feed.SourceFeedScreen
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
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.merged.EditMergedSettingsDialog
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.chapter.getNextUnread
import eu.kanade.tachiyomi.util.lang.launchUI
@ -78,9 +71,9 @@ import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
import eu.kanade.tachiyomi.util.system.copyToClipboard
import eu.kanade.tachiyomi.util.system.toShareIntent
import eu.kanade.tachiyomi.util.system.toast
import exh.md.similar.MangaDexSimilarController
import exh.pagepreview.PagePreviewController
import exh.recs.RecommendsController
import exh.md.similar.MangaDexSimilarScreen
import exh.pagepreview.PagePreviewScreen
import exh.recs.RecommendsScreen
import exh.source.MERGED_SOURCE_ID
import exh.source.getMainSource
import exh.source.isMdBasedSource
@ -89,12 +82,13 @@ import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class MangaScreen(
private val mangaId: Long,
private val fromSource: Boolean = false,
val fromSource: Boolean = false,
private val smartSearchConfig: SourcesScreen.SmartSearchConfig? = null,
) : Screen {
@ -103,9 +97,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, smartSearchConfig != null) }
val state by screenModel.state.collectAsState()
@ -123,8 +117,8 @@ class MangaScreen(
screenModel.redirectFlow
.take(1)
.onEach {
router.replaceTopController(
MangaController(it).withFadeTransaction(),
navigator.replace(
MangaScreen(it.mangaId),
)
}
.launchIn(this)
@ -135,12 +129,7 @@ class MangaScreen(
state = successState,
snackbarHostState = screenModel.snackbarHostState,
isTabletUi = isTabletUi(),
onBackClicked = {
when {
navigator.canPop -> navigator.pop()
else -> router.popCurrentController()
}
},
onBackClicked = navigator::pop,
onChapterClicked = { openChapter(context, it) },
onDownloadChapter = screenModel::runChapterDownloadActions.takeIf { !successState.source.isLocalOrStub() },
onAddToLibraryClicked = {
@ -158,11 +147,11 @@ class MangaScreen(
// SY <--
onWebViewLongClicked = { copyMangaUrl(context, screenModel.manga, screenModel.source) }.takeIf { isHttpSource },
onTrackingClicked = screenModel::showTrackDialog.takeIf { successState.trackingAvailable },
onTagClicked = { performGenreSearch(router, navigator, 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, navigator, 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() },
@ -171,12 +160,12 @@ class MangaScreen(
onMigrateClicked = { migrateManga(navigator, screenModel.manga!!) }.takeIf { successState.manga.favorite },
onMetadataViewerClicked = { openMetadataViewer(navigator, successState.manga) },
onEditInfoClicked = screenModel::showEditMangaInfoDialog,
onRecommendClicked = { openRecommends(context, router, screenModel.source?.getMainSource(), successState.manga) },
onRecommendClicked = { openRecommends(context, navigator, screenModel.source?.getMainSource(), successState.manga) },
onMergedSettingsClicked = screenModel::showEditMergedSettingsDialog,
onMergeClicked = { openSmartSearch(navigator, successState.manga) },
onMergeWithAnotherClicked = { mergeWithAnother(navigator, context, successState.manga, screenModel::smartSearchMerge) },
onOpenPagePreview = { openPagePreview(context, successState.chapters.getNextUnread(successState.manga), it) },
onMorePreviewsClicked = { openMorePagePreviews(router, successState.manga) },
onMorePreviewsClicked = { openMorePagePreviews(navigator, successState.manga) },
// SY <--
onMultiBookmarkClicked = screenModel::bookmarkChapters,
onMultiMarkAsReadClicked = screenModel::markChaptersRead,
@ -366,52 +355,29 @@ class MangaScreen(
*
* @param query the search query to the parent controller
*/
private fun performSearch(router: Router, navigator: Navigator, 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
}
// SY -->
if (navigator.canPop) {
when (val previousScreen = navigator.items[navigator.items.size - 2]) {
is SourceFeedScreen -> {
navigator.pop()
previousScreen.onBrowseClick(router, previousScreen.sourceId, query)
}
}
return
}
// SY <--
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)
}
// SY -->
is SourceFeedController -> {
router.handleBack()
router.handleBack()
router.pushController(BrowseSourceController(previousController.sourceId, query))
is SourceFeedScreen -> {
navigator.pop()
navigator.replace(BrowseSourceScreen(previousController.sourceId, query))
}
// SY <--
}
@ -422,20 +388,17 @@ class MangaScreen(
*
* @param genreName the search genre to the parent controller
*/
private fun performGenreSearch(router: Router, navigator: Navigator, 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, navigator, genreName, global = false)
performSearch(navigator, genreName, global = false)
}
}
@ -484,8 +447,8 @@ class MangaScreen(
.show()
}
private fun openMorePagePreviews(router: Router, manga: Manga) {
router.pushController(PagePreviewController(manga.id))
private fun openMorePagePreviews(navigator: Navigator, manga: Manga) {
navigator.push(PagePreviewScreen(manga.id))
}
private fun openPagePreview(context: Context, chapter: Chapter?, page: Int) {
@ -527,7 +490,7 @@ class MangaScreen(
// EXH <--
// AZ -->
private fun openRecommends(context: Context, router: Router, source: Source?, manga: Manga) {
private fun openRecommends(context: Context, navigator: Navigator, source: Source?, manga: Manga) {
source ?: return
if (source.isMdBasedSource()) {
MaterialAlertDialogBuilder(context)
@ -541,13 +504,13 @@ class MangaScreen(
) { dialog, index ->
dialog.dismiss()
when (index) {
0 -> router.pushController(MangaDexSimilarController(manga, source as CatalogueSource))
1 -> router.pushController(RecommendsController(manga, source as CatalogueSource))
0 -> navigator.push(MangaDexSimilarScreen(manga.id, source.id))
1 -> navigator.push(RecommendsScreen(manga.id, source.id))
}
}
.show()
} else if (source is CatalogueSource) {
router.pushController(RecommendsController(manga, source))
navigator.push(RecommendsScreen(manga.id, source.id))
}
}
// AZ <--

View File

@ -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/"
}
}

View File

@ -1,32 +1,39 @@
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.domain.ui.UiPreferences
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.history.HistoryController
import eu.kanade.tachiyomi.ui.setting.SettingsMainController
import eu.kanade.tachiyomi.ui.stats.StatsController
import eu.kanade.tachiyomi.ui.updates.UpdatesController
import eu.kanade.tachiyomi.ui.category.CategoryScreen
import eu.kanade.tachiyomi.ui.download.DownloadQueueScreen
import eu.kanade.tachiyomi.ui.history.HistoryTab
import eu.kanade.tachiyomi.ui.setting.SettingsScreen
import eu.kanade.tachiyomi.ui.stats.StatsScreen
import eu.kanade.tachiyomi.ui.updates.UpdatesTab
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.system.isInstalledFromFDroid
import exh.ui.batchadd.BatchAddController
import exh.ui.batchadd.BatchAddScreen
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@ -35,11 +42,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(
@ -53,16 +77,16 @@ object MoreScreen : Screen {
showNavUpdates = screenModel.showNavUpdates,
showNavHistory = screenModel.showNavHistory,
// SY <--
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()) },
// SY -->
onClickBatchAdd = { router.pushController(BatchAddController()) },
onClickUpdates = { router.pushController(UpdatesController()) },
onClickHistory = { router.pushController(HistoryController()) },
onClickBatchAdd = { navigator.push(BatchAddScreen()) },
onClickUpdates = { navigator.push(UpdatesTab) },
onClickHistory = { navigator.push(HistoryTab) },
// SY <--
)
}

View File

@ -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"

View File

@ -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()
},
)
}
}

View File

@ -63,7 +63,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
@ -85,6 +84,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
import eu.kanade.tachiyomi.ui.reader.viewer.pager.VerticalPagerViewer
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer
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
@ -458,7 +458,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)
},
)

View File

@ -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"

View File

@ -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)
}
},

View File

@ -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())
}
}

View File

@ -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,
)
},

View File

@ -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)
}
}

View File

@ -1,29 +1,70 @@
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.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
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.core.prefs.asState
import eu.kanade.domain.ui.UiPreferences
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
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
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)
}
// SY -->
@Composable
override fun isEnabled(): Boolean {
val scope = rememberCoroutineScope()
return remember {
Injekt.get<UiPreferences>().showNavHistory().asState(scope)
}.value
}
// SY <--
object UpdatesScreen : Screen {
@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 +75,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 +118,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

View File

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

View File

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

View File

@ -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()
}

View File

@ -1,6 +1,5 @@
package exh.debug
import android.app.Activity
import android.util.Log
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.Crossfade
@ -30,6 +29,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
@ -43,6 +43,9 @@ import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
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 eu.kanade.core.prefs.PreferenceMutableState
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.Divider
@ -53,8 +56,8 @@ import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
import eu.kanade.presentation.more.settings.widget.TrailingWidgetBuffer
import eu.kanade.presentation.util.plus
import eu.kanade.presentation.util.topSmallPaddingValues
import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
import exh.util.capitalize
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -63,12 +66,17 @@ import kotlin.reflect.KFunction
import kotlin.reflect.KVisibility
import kotlin.reflect.full.declaredFunctions
class SettingsDebugController : BasicFullComposeController() {
class SettingsDebugScreen : Screen {
data class DebugToggle(val name: String, val pref: PreferenceMutableState<Boolean>, val default: Boolean)
@Composable
override fun ComposeContent() {
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val scope = rememberCoroutineScope()
DisposableEffect(Unit) {
onDispose { navigator.pop() }
}
val functions by produceState<List<Pair<KFunction<*>, String>>?>(initialValue = null) {
value = withContext(Dispatchers.Default) {
DebugFunctions::class.declaredFunctions.filter {
@ -82,7 +90,7 @@ class SettingsDebugController : BasicFullComposeController() {
}
val toggles by produceState(initialValue = emptyList()) {
value = withContext(Dispatchers.Default) {
DebugToggles.values().map { DebugToggle(it.name, it.asPref(viewScope), it.default) }
DebugToggles.values().map { DebugToggle(it.name, it.asPref(scope), it.default) }
}
}
Scaffold(
@ -96,7 +104,7 @@ class SettingsDebugController : BasicFullComposeController() {
Crossfade(functions == null) {
when (it) {
true -> LoadingScreen()
false -> FunctionList(paddingValues, functions.orEmpty(), toggles)
false -> FunctionList(paddingValues, functions.orEmpty(), toggles, scope)
}
}
}
@ -107,8 +115,8 @@ class SettingsDebugController : BasicFullComposeController() {
paddingValues: PaddingValues,
functions: List<Pair<KFunction<*>, String>>,
toggles: List<DebugToggle>,
scope: CoroutineScope,
) {
val scope = rememberCoroutineScope()
Box(Modifier.fillMaxSize()) {
var running by remember { mutableStateOf(false) }
var result by remember { mutableStateOf<Pair<String, String>?>(null) }
@ -227,9 +235,4 @@ class SettingsDebugController : BasicFullComposeController() {
)
}
}
override fun onActivityStopped(activity: Activity) {
super.onActivityStopped(activity)
router.popCurrentController()
}
}

View File

@ -4,17 +4,16 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.bluelinelabs.conductor.Router
import cafe.adriel.voyager.navigator.Navigator
import eu.kanade.tachiyomi.databinding.SourceFilterMangadexHeaderBinding
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.online.RandomMangaSource
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.withIOContext
import exh.md.follows.MangaDexFollowsController
import exh.md.follows.MangaDexFollowsScreen
class MangaDexFabHeaderAdapter(val router: Router, val source: CatalogueSource, val onClick: () -> Unit) :
class MangaDexFabHeaderAdapter(val navigator: Navigator, val source: CatalogueSource, val onClick: () -> Unit) :
RecyclerView.Adapter<MangaDexFabHeaderAdapter.SavedSearchesViewHolder>() {
private lateinit var binding: SourceFilterMangadexHeaderBinding
@ -33,7 +32,7 @@ class MangaDexFabHeaderAdapter(val router: Router, val source: CatalogueSource,
inner class SavedSearchesViewHolder(view: View) : RecyclerView.ViewHolder(view) {
fun bind() {
binding.mangadexFollows.setOnClickListener {
router.replaceTopController(MangaDexFollowsController(source).withFadeTransaction())
navigator.replace(MangaDexFollowsScreen(source.id))
onClick()
}
binding.mangadexRandom.setOnClickListener {
@ -41,11 +40,11 @@ class MangaDexFabHeaderAdapter(val router: Router, val source: CatalogueSource,
val randomMangaUrl = withIOContext {
(source as? RandomMangaSource)?.fetchRandomMangaUrl()
}
router.replaceTopController(
BrowseSourceController(
source,
navigator.replace(
BrowseSourceScreen(
source.id,
"id:$randomMangaUrl",
).withFadeTransaction(),
),
)
onClick()
}

View File

@ -1,30 +0,0 @@
package exh.md.follows
import android.os.Bundle
import androidx.compose.runtime.Composable
import androidx.core.os.bundleOf
import cafe.adriel.voyager.navigator.Navigator
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
/**
* Controller that shows the latest manga from the catalogue. Inherit [BrowseSourceController].
*/
class MangaDexFollowsController(bundle: Bundle) : BasicFullComposeController(bundle) {
constructor(source: CatalogueSource) : this(
bundleOf(
SOURCE_ID_KEY to source.id,
),
)
private val sourceId = args.getLong(SOURCE_ID_KEY)
@Composable
override fun ComposeContent() {
Navigator(screen = MangaDexFollowsScreen(sourceId))
}
}
private const val SOURCE_ID_KEY = "source_id"

View File

@ -1,6 +1,5 @@
package exh.md.follows
import androidx.activity.compose.BackHandler
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
@ -23,19 +22,16 @@ import eu.kanade.presentation.browse.components.RemoveMangaDialog
import eu.kanade.presentation.components.ChangeCategoryDialog
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.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel
import eu.kanade.tachiyomi.ui.category.CategoryController
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.ui.category.CategoryScreen
import eu.kanade.tachiyomi.ui.manga.MangaScreen
import eu.kanade.tachiyomi.util.lang.launchIO
class MangaDexFollowsScreen(private val sourceId: Long) : Screen {
@Composable
override fun Content() {
val router = LocalRouter.currentOrThrow
val navigator = LocalNavigator.currentOrThrow
val scope = rememberCoroutineScope()
val haptic = LocalHapticFeedback.current
@ -44,20 +40,13 @@ class MangaDexFollowsScreen(private val sourceId: Long) : Screen {
val snackbarHostState = remember { SnackbarHostState() }
val navigateUp: () -> Unit = {
when {
navigator.canPop -> navigator.pop()
router.backstackSize > 1 -> router.popCurrentController()
}
}
Scaffold(
topBar = { scrollBehavior ->
BrowseSourceSimpleToolbar(
title = stringResource(R.string.mangadex_follows),
displayMode = screenModel.displayMode,
onDisplayModeChange = { screenModel.displayMode = it },
navigateUp = navigateUp,
navigateUp = navigator::pop,
scrollBehavior = scrollBehavior,
)
},
@ -82,7 +71,7 @@ class MangaDexFollowsScreen(private val sourceId: Long) : Screen {
onWebViewClick = null,
onHelpClick = null,
onLocalSourceHelpClick = null,
onMangaClick = { router.pushController(MangaController(it.id, true)) },
onMangaClick = { navigator.push(MangaScreen(it.id, true)) },
onMangaLongClick = { manga ->
scope.launchIO {
val duplicateManga = screenModel.getDuplicateLibraryManga(manga)
@ -109,7 +98,7 @@ class MangaDexFollowsScreen(private val sourceId: Long) : Screen {
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),
)
}
@ -127,7 +116,7 @@ class MangaDexFollowsScreen(private val sourceId: Long) : Screen {
initialSelection = dialog.initialSelection,
onDismissRequest = onDismissRequest,
onEditCategories = {
router.pushController(CategoryController())
navigator.push(CategoryScreen())
},
onConfirm = { include, _ ->
screenModel.changeMangaFavorite(dialog.manga)
@ -137,7 +126,5 @@ class MangaDexFollowsScreen(private val sourceId: Long) : Screen {
}
else -> {}
}
BackHandler(onBack = navigateUp)
}
}

View File

@ -1,7 +1,7 @@
package exh.md.follows
import android.content.Context
import com.bluelinelabs.conductor.Router
import cafe.adriel.voyager.navigator.Navigator
import eu.kanade.domain.manga.model.Manga
import eu.kanade.domain.source.model.SourcePagingSourceType
import eu.kanade.tachiyomi.source.model.FilterList
@ -22,7 +22,7 @@ class MangaDexFollowsScreenModel(sourceId: Long) : BrowseSourceScreenModel(sourc
return map { it to metadata }
}
override fun initFilterSheet(context: Context, router: Router) {
override fun initFilterSheet(context: Context, navigator: Navigator) {
// No-op: we don't allow filtering in recs
}
}

View File

@ -1,34 +0,0 @@
package exh.md.similar
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.ui.browse.source.browse.BrowseSourceController
/**
* Controller that shows the latest manga from the catalogue. Inherit [BrowseSourceController].
*/
class MangaDexSimilarController(bundle: Bundle) : BasicFullComposeController(bundle) {
constructor(manga: Manga, source: CatalogueSource) : this(
bundleOf(
MANGA_ID to manga.id,
SOURCE_ID_KEY to source.id,
),
)
val mangaId = args.getLong(MANGA_ID)
val sourceId = args.getLong(SOURCE_ID_KEY)
@Composable
override fun ComposeContent() {
Navigator(screen = MangaDexSimilarScreen(mangaId, sourceId))
}
}
private const val MANGA_ID = "manga_id"
private const val SOURCE_ID_KEY = "source_id"

View File

@ -17,10 +17,8 @@ import eu.kanade.domain.manga.model.Manga
import eu.kanade.presentation.browse.BrowseSourceContent
import eu.kanade.presentation.browse.components.BrowseSourceSimpleToolbar
import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.util.LocalRouter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.ui.manga.MangaScreen
class MangaDexSimilarScreen(val mangaId: Long, val sourceId: Long) : Screen {
@ -28,26 +26,18 @@ class MangaDexSimilarScreen(val mangaId: Long, val sourceId: Long) : Screen {
override fun Content() {
val screenModel = rememberScreenModel { MangaDexSimilarScreenModel(mangaId, sourceId) }
val state by screenModel.state.collectAsState()
val router = LocalRouter.currentOrThrow
val navigator = LocalNavigator.currentOrThrow
val onMangaClick: (Manga) -> Unit = {
router.pushController(MangaController(it.id, true))
navigator.push(MangaScreen(it.id, true))
}
val snackbarHostState = remember { SnackbarHostState() }
val navigateUp: () -> Unit = {
when {
navigator.canPop -> navigator.pop()
router.backstackSize > 1 -> router.popCurrentController()
}
}
Scaffold(
topBar = { scrollBehavior ->
BrowseSourceSimpleToolbar(
navigateUp = navigateUp,
navigateUp = navigator::pop,
title = stringResource(R.string.similar, screenModel.manga.title),
displayMode = screenModel.displayMode,
onDisplayModeChange = { screenModel.displayMode = it },

View File

@ -1,7 +1,7 @@
package exh.md.similar
import android.content.Context
import com.bluelinelabs.conductor.Router
import cafe.adriel.voyager.navigator.Navigator
import eu.kanade.domain.manga.interactor.GetManga
import eu.kanade.domain.manga.model.Manga
import eu.kanade.domain.source.model.SourcePagingSourceType
@ -16,9 +16,6 @@ import kotlinx.coroutines.runBlocking
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
/**
* Presenter of [MangaDexSimilarController]. Inherit BrowseCataloguePresenter.
*/
class MangaDexSimilarScreenModel(
val mangaId: Long,
sourceId: Long,
@ -35,7 +32,7 @@ class MangaDexSimilarScreenModel(
return map { it to metadata }
}
override fun initFilterSheet(context: Context, router: Router) {
override fun initFilterSheet(context: Context, navigator: Navigator) {
// No-op: we don't allow filtering in recs
}
}

View File

@ -1,26 +0,0 @@
package exh.pagepreview
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 PagePreviewController : BasicFullComposeController {
@Suppress("unused")
constructor(bundle: Bundle? = null) : super(bundle)
constructor(mangaId: Long) : super(
bundleOf(MANGA_ID to mangaId),
)
@Composable
override fun ComposeContent() {
Navigator(screen = PagePreviewScreen(args.getLong(MANGA_ID, -1)))
}
companion object {
const val MANGA_ID = "manga_id"
}
}

View File

@ -7,8 +7,8 @@ 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.util.LocalRouter
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import exh.pagepreview.components.PagePreviewScreen
@ -19,7 +19,7 @@ class PagePreviewScreen(private val mangaId: Long) : Screen {
val screenModel = rememberScreenModel { PagePreviewScreenModel(mangaId) }
val context = LocalContext.current
val state by screenModel.state.collectAsState()
val router = LocalRouter.currentOrThrow
val navigator = LocalNavigator.currentOrThrow
PagePreviewScreen(
state = state,
pageDialogOpen = screenModel.pageDialogOpen,
@ -27,7 +27,7 @@ class PagePreviewScreen(private val mangaId: Long) : Screen {
onOpenPage = { openPage(context, state, it) },
onOpenPageDialog = { screenModel.pageDialogOpen = true },
onDismissPageDialog = { screenModel.pageDialogOpen = false },
navigateUp = router::popCurrentController,
navigateUp = navigator::pop,
)
}

View File

@ -1,34 +0,0 @@
package exh.recs
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.ui.browse.source.browse.BrowseSourceController
/**
* Controller that shows the latest manga from the catalogue. Inherit [BrowseSourceController].
*/
class RecommendsController(bundle: Bundle) : BasicFullComposeController(bundle) {
constructor(manga: Manga, source: CatalogueSource) : this(
bundleOf(
MANGA_ID to manga.id,
SOURCE_ID_KEY to source.id,
),
)
val mangaId = args.getLong(MANGA_ID)
val sourceId = args.getLong(SOURCE_ID_KEY)
@Composable
override fun ComposeContent() {
Navigator(screen = RecommendsScreen(mangaId, sourceId))
}
}
private const val MANGA_ID = "manga_id"
private const val SOURCE_ID_KEY = "source_id"

Some files were not shown because too many files have changed in this diff Show More