Merge Voyager screens (#8656)

* Merge Voyager screens

* cleanups
This commit is contained in:
Ivan Iskandar 2022-12-03 10:35:30 +07:00 committed by GitHub
parent 5313a5d5d2
commit 3d66eaea83
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
67 changed files with 1180 additions and 1991 deletions

View File

@ -267,15 +267,12 @@ dependencies {
exclude(group = "androidx.viewpager", module = "viewpager") exclude(group = "androidx.viewpager", module = "viewpager")
} }
implementation(libs.insetter) implementation(libs.insetter)
implementation(libs.markwon) implementation(libs.bundles.richtext)
implementation(libs.aboutLibraries.compose) implementation(libs.aboutLibraries.compose)
implementation(libs.cascade) implementation(libs.cascade)
implementation(libs.bundles.voyager) implementation(libs.bundles.voyager)
implementation(libs.wheelpicker) implementation(libs.wheelpicker)
// Conductor
implementation(libs.conductor)
// FlowBinding // FlowBinding
implementation(libs.flowbinding.android) implementation(libs.flowbinding.android)
@ -328,6 +325,7 @@ tasks {
kotlinOptions.freeCompilerArgs += listOf( kotlinOptions.freeCompilerArgs += listOf(
"-opt-in=coil.annotation.ExperimentalCoilApi", "-opt-in=coil.annotation.ExperimentalCoilApi",
"-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi", "-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi",
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
"-opt-in=androidx.compose.material.ExperimentalMaterialApi", "-opt-in=androidx.compose.material.ExperimentalMaterialApi",
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api", "-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi", "-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.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.animation.expandVertically import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut 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.WindowInsetsSides
import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.shape.ZeroCornerSize import androidx.compose.foundation.shape.ZeroCornerSize
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.BookmarkAdd import androidx.compose.material.icons.outlined.BookmarkAdd
@ -95,7 +96,11 @@ fun MangaBottomActionMenu(
} }
Row( Row(
modifier = Modifier modifier = Modifier
.padding(WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()) .padding(
WindowInsets.navigationBars
.only(WindowInsetsSides.Bottom)
.asPaddingValues(),
)
.padding(horizontal = 8.dp, vertical = 12.dp), .padding(horizontal = 8.dp, vertical = 12.dp),
) { ) {
if (onBookmarkClicked != null) { if (onBookmarkClicked != null) {
@ -213,16 +218,16 @@ private fun RowScope.Button(
fun LibraryBottomActionMenu( fun LibraryBottomActionMenu(
visible: Boolean, visible: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onChangeCategoryClicked: (() -> Unit)?, onChangeCategoryClicked: () -> Unit,
onMarkAsReadClicked: (() -> Unit)?, onMarkAsReadClicked: () -> Unit,
onMarkAsUnreadClicked: (() -> Unit)?, onMarkAsUnreadClicked: () -> Unit,
onDownloadClicked: ((DownloadAction) -> Unit)?, onDownloadClicked: ((DownloadAction) -> Unit)?,
onDeleteClicked: (() -> Unit)?, onDeleteClicked: () -> Unit,
) { ) {
AnimatedVisibility( AnimatedVisibility(
visible = visible, visible = visible,
enter = expandVertically(expandFrom = Alignment.Bottom), enter = expandVertically(animationSpec = tween(delayMillis = 300)),
exit = shrinkVertically(shrinkTowards = Alignment.Bottom), exit = shrinkVertically(animationSpec = tween()),
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
Surface( Surface(
@ -244,10 +249,12 @@ fun LibraryBottomActionMenu(
} }
Row( Row(
modifier = Modifier modifier = Modifier
.navigationBarsPadding() .windowInsetsPadding(
WindowInsets.navigationBars
.only(WindowInsetsSides.Bottom),
)
.padding(horizontal = 8.dp, vertical = 12.dp), .padding(horizontal = 8.dp, vertical = 12.dp),
) { ) {
if (onChangeCategoryClicked != null) {
Button( Button(
title = stringResource(R.string.action_move_category), title = stringResource(R.string.action_move_category),
icon = Icons.Outlined.Label, icon = Icons.Outlined.Label,
@ -255,8 +262,6 @@ fun LibraryBottomActionMenu(
onLongClick = { onLongClickItem(0) }, onLongClick = { onLongClickItem(0) },
onClick = onChangeCategoryClicked, onClick = onChangeCategoryClicked,
) )
}
if (onMarkAsReadClicked != null) {
Button( Button(
title = stringResource(R.string.action_mark_as_read), title = stringResource(R.string.action_mark_as_read),
icon = Icons.Outlined.DoneAll, icon = Icons.Outlined.DoneAll,
@ -264,8 +269,6 @@ fun LibraryBottomActionMenu(
onLongClick = { onLongClickItem(1) }, onLongClick = { onLongClickItem(1) },
onClick = onMarkAsReadClicked, onClick = onMarkAsReadClicked,
) )
}
if (onMarkAsUnreadClicked != null) {
Button( Button(
title = stringResource(R.string.action_mark_as_unread), title = stringResource(R.string.action_mark_as_unread),
icon = Icons.Outlined.RemoveDone, icon = Icons.Outlined.RemoveDone,
@ -273,7 +276,6 @@ fun LibraryBottomActionMenu(
onLongClick = { onLongClickItem(2) }, onLongClick = { onLongClickItem(2) },
onClick = onMarkAsUnreadClicked, onClick = onMarkAsUnreadClicked,
) )
}
if (onDownloadClicked != null) { if (onDownloadClicked != null) {
var downloadExpanded by remember { mutableStateOf(false) } var downloadExpanded by remember { mutableStateOf(false) }
Button( Button(
@ -292,7 +294,6 @@ fun LibraryBottomActionMenu(
) )
} }
} }
if (onDeleteClicked != null) {
Button( Button(
title = stringResource(R.string.action_delete), title = stringResource(R.string.action_delete),
icon = Icons.Outlined.Delete, icon = Icons.Outlined.Delete,
@ -304,4 +305,3 @@ fun LibraryBottomActionMenu(
} }
} }
} }
}

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@ -23,8 +23,8 @@ import eu.kanade.tachiyomi.data.library.LibraryUpdateService
import eu.kanade.tachiyomi.data.updater.AppUpdateService import eu.kanade.tachiyomi.data.updater.AppUpdateService
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.main.MainActivity 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.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.util.Constants
import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.getUriCompat import eu.kanade.tachiyomi.util.storage.getUriCompat
@ -457,7 +457,7 @@ class NotificationReceiver : BroadcastReceiver() {
val newIntent = val newIntent =
Intent(context, MainActivity::class.java).setAction(MainActivity.SHORTCUT_MANGA) Intent(context, MainActivity::class.java).setAction(MainActivity.SHORTCUT_MANGA)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
.putExtra(MangaController.MANGA_EXTRA, manga.id) .putExtra(Constants.MANGA_EXTRA, manga.id)
.putExtra("notificationId", manga.id.hashCode()) .putExtra("notificationId", manga.id.hashCode())
.putExtra("groupId", groupId) .putExtra("groupId", groupId)
return PendingIntent.getActivity(context, manga.id.hashCode(), newIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) 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.R
import eu.kanade.tachiyomi.core.security.SecurityPreferences import eu.kanade.tachiyomi.core.security.SecurityPreferences
import eu.kanade.tachiyomi.ui.main.MainActivity 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.lang.launchIO
import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.util.system.dpToPx
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
@ -136,7 +136,7 @@ class UpdatesGridGlanceWidget : GlanceAppWidget() {
) { ) {
val intent = Intent(LocalContext.current, MainActivity::class.java).apply { val intent = Intent(LocalContext.current, MainActivity::class.java).apply {
action = MainActivity.SHORTCUT_MANGA action = MainActivity.SHORTCUT_MANGA
putExtra(MangaController.MANGA_EXTRA, mangaId) putExtra(Constants.MANGA_EXTRA, mangaId)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)

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

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

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,17 +1,23 @@
package eu.kanade.tachiyomi.ui.browse 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.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext 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.ScreenModel
import cafe.adriel.voyager.core.model.coroutineScope import cafe.adriel.voyager.core.model.coroutineScope
import cafe.adriel.voyager.core.model.rememberScreenModel 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.core.prefs.asState
import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.base.BasePreferences
import eu.kanade.presentation.components.TabbedScreen import eu.kanade.presentation.components.TabbedScreen
import eu.kanade.presentation.util.Tab
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreenModel import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreenModel
import eu.kanade.tachiyomi.ui.browse.extension.extensionsTab import eu.kanade.tachiyomi.ui.browse.extension.extensionsTab
@ -22,9 +28,21 @@ import eu.kanade.tachiyomi.util.storage.DiskUtil
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
data class BrowseScreen( data class BrowseTab(
private val toExtensions: Boolean, private val toExtensions: Boolean = false,
) : Screen { ) : 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 @Composable
override fun Content() { override fun Content() {

View File

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

View File

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

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? = null, source: CatalogueSource, searchQuery: String? = null) : this(
bundleOf(
SOURCE_ID_KEY to source.id,
MANGA_KEY to manga,
SEARCH_QUERY_KEY to searchQuery,
),
)
private var oldManga: Manga = args.getSerializableCompat(MANGA_KEY)!!
private val sourceId = args.getLong(SOURCE_ID_KEY)
private val query = args.getString(SEARCH_QUERY_KEY)
@Composable
override fun ComposeContent() {
Navigator(screen = SourceSearchScreen(oldManga, sourceId, query))
}
}
private const val MANGA_KEY = "oldManga"
private const val SOURCE_ID_KEY = "sourceId"
private const val SEARCH_QUERY_KEY = "searchQuery"

View File

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

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

View File

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

View File

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

View File

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

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

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

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 androidx.recyclerview.widget.LinearLayoutManager
import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.EmptyScreen 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.OverflowMenu
import eu.kanade.presentation.components.Pill import eu.kanade.presentation.components.Pill
import eu.kanade.presentation.components.Scaffold import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.util.LocalRouter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.DownloadService import eu.kanade.tachiyomi.data.download.DownloadService
import eu.kanade.tachiyomi.databinding.DownloadListBinding import eu.kanade.tachiyomi.databinding.DownloadListBinding
@ -66,7 +66,7 @@ object DownloadQueueScreen : Screen {
@Composable @Composable
override fun Content() { override fun Content() {
val context = LocalContext.current val context = LocalContext.current
val router = LocalRouter.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val screenModel = rememberScreenModel { DownloadQueueScreenModel() } val screenModel = rememberScreenModel { DownloadQueueScreenModel() }
val downloadList by screenModel.state.collectAsState() val downloadList by screenModel.state.collectAsState()
@ -121,7 +121,7 @@ object DownloadQueueScreen : Screen {
} }
} }
}, },
navigateUp = router::popCurrentController, navigateUp = navigator::pop,
actions = { actions = {
if (downloadList.isNotEmpty()) { if (downloadList.isNotEmpty()) {
OverflowMenu { closeMenu -> 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.presentation.history.HistoryUiModel
import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.toDateKey import eu.kanade.tachiyomi.util.lang.toDateKey
import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.system.logcat import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel 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) { fun getNextChapterForManga(mangaId: Long, chapterId: Long) {
coroutineScope.launchIO { coroutineScope.launchIO {
sendNextChapterEvent(getNextChapters.await(mangaId, chapterId, onlyUnread = false)) sendNextChapterEvent(getNextChapters.await(mangaId, chapterId, onlyUnread = false))

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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,26 +1,33 @@
package eu.kanade.tachiyomi.ui.more 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.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext 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.ScreenModel
import cafe.adriel.voyager.core.model.coroutineScope import cafe.adriel.voyager.core.model.coroutineScope
import cafe.adriel.voyager.core.model.rememberScreenModel 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.currentOrThrow
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
import cafe.adriel.voyager.navigator.tab.TabOptions
import eu.kanade.core.prefs.asState import eu.kanade.core.prefs.asState
import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.base.BasePreferences
import eu.kanade.presentation.more.MoreScreen 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.DownloadManager
import eu.kanade.tachiyomi.data.download.DownloadService import eu.kanade.tachiyomi.data.download.DownloadService
import eu.kanade.tachiyomi.ui.base.controller.pushController import eu.kanade.tachiyomi.ui.category.CategoryScreen
import eu.kanade.tachiyomi.ui.category.CategoryController import eu.kanade.tachiyomi.ui.download.DownloadQueueScreen
import eu.kanade.tachiyomi.ui.download.DownloadController import eu.kanade.tachiyomi.ui.setting.SettingsScreen
import eu.kanade.tachiyomi.ui.setting.SettingsMainController import eu.kanade.tachiyomi.ui.stats.StatsScreen
import eu.kanade.tachiyomi.ui.stats.StatsController
import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.system.isInstalledFromFDroid import eu.kanade.tachiyomi.util.system.isInstalledFromFDroid
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -31,11 +38,28 @@ import kotlinx.coroutines.flow.combine
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get 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 @Composable
override fun Content() { override fun Content() {
val router = LocalRouter.currentOrThrow
val context = LocalContext.current val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { MoreScreenModel() } val screenModel = rememberScreenModel { MoreScreenModel() }
val downloadQueueState by screenModel.downloadQueueState.collectAsState() val downloadQueueState by screenModel.downloadQueueState.collectAsState()
MoreScreen( MoreScreen(
@ -45,12 +69,12 @@ object MoreScreen : Screen {
incognitoMode = screenModel.incognitoMode, incognitoMode = screenModel.incognitoMode,
onIncognitoModeChange = { screenModel.incognitoMode = it }, onIncognitoModeChange = { screenModel.incognitoMode = it },
isFDroid = context.isInstalledFromFDroid(), isFDroid = context.isInstalledFromFDroid(),
onClickDownloadQueue = { router.pushController(DownloadController()) }, onClickDownloadQueue = { navigator.push(DownloadQueueScreen) },
onClickCategories = { router.pushController(CategoryController()) }, onClickCategories = { navigator.push(CategoryScreen()) },
onClickStats = { router.pushController(StatsController()) }, onClickStats = { navigator.push(StatsScreen()) },
onClickBackupAndRestore = { router.pushController(SettingsMainController.toBackupScreen()) }, onClickBackupAndRestore = { navigator.push(SettingsScreen.toBackupScreen()) },
onClickSettings = { router.pushController(SettingsMainController()) }, onClickSettings = { navigator.push(SettingsScreen.toMainScreen()) },
onClickAbout = { router.pushController(SettingsMainController.toAboutScreen()) }, onClickAbout = { navigator.push(SettingsScreen.toAboutScreen()) },
) )
} }
} }

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

@ -56,7 +56,6 @@ import eu.kanade.tachiyomi.ui.base.delegate.SecureActivityDelegateImpl
import eu.kanade.tachiyomi.ui.base.delegate.ThemingDelegate import eu.kanade.tachiyomi.ui.base.delegate.ThemingDelegate
import eu.kanade.tachiyomi.ui.base.delegate.ThemingDelegateImpl import eu.kanade.tachiyomi.ui.base.delegate.ThemingDelegateImpl
import eu.kanade.tachiyomi.ui.main.MainActivity 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.AddToLibraryFirst
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.Error import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.Error
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.Success import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.Success
@ -71,6 +70,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
import eu.kanade.tachiyomi.ui.webview.WebViewActivity 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.preference.toggle
import eu.kanade.tachiyomi.util.system.applySystemAnimatorScale import eu.kanade.tachiyomi.util.system.applySystemAnimatorScale
import eu.kanade.tachiyomi.util.system.createReaderThemeContext import eu.kanade.tachiyomi.util.system.createReaderThemeContext
@ -375,7 +375,7 @@ class ReaderActivity :
startActivity( startActivity(
Intent(this, MainActivity::class.java).apply { Intent(this, MainActivity::class.java).apply {
action = MainActivity.SHORTCUT_MANGA action = MainActivity.SHORTCUT_MANGA
putExtra(MangaController.MANGA_EXTRA, id) putExtra(Constants.MANGA_EXTRA, id)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) 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.SettingsGeneralScreen
import eu.kanade.presentation.more.settings.screen.SettingsMainScreen import eu.kanade.presentation.more.settings.screen.SettingsMainScreen
import eu.kanade.presentation.util.LocalBackPress import eu.kanade.presentation.util.LocalBackPress
import eu.kanade.presentation.util.LocalRouter
import eu.kanade.presentation.util.Transition import eu.kanade.presentation.util.Transition
import eu.kanade.presentation.util.isTabletUi import eu.kanade.presentation.util.isTabletUi
@ -24,15 +23,8 @@ class SettingsScreen private constructor(
@Composable @Composable
override fun Content() { override fun Content() {
val router = LocalRouter.currentOrThrow
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
if (!isTabletUi()) { if (!isTabletUi()) {
val back: () -> Unit = {
when {
navigator.canPop -> navigator.pop()
router.backstackSize > 1 -> router.handleBack()
}
}
Navigator( Navigator(
screen = if (toBackup) { screen = if (toBackup) {
SettingsBackupScreen SettingsBackupScreen
@ -42,7 +34,7 @@ class SettingsScreen private constructor(
SettingsMainScreen SettingsMainScreen
}, },
content = { content = {
CompositionLocalProvider(LocalBackPress provides back) { CompositionLocalProvider(LocalBackPress provides navigator::pop) {
ScreenTransition( ScreenTransition(
navigator = it, navigator = it,
transition = { Transition.OneWayFade }, transition = { Transition.OneWayFade },
@ -62,7 +54,7 @@ class SettingsScreen private constructor(
) { ) {
TwoPanelBox( TwoPanelBox(
startContent = { startContent = {
CompositionLocalProvider(LocalBackPress provides router::popCurrentController) { CompositionLocalProvider(LocalBackPress provides navigator::pop) {
SettingsMainScreen.Content(twoPane = true) 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.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.uniqueScreenKey import cafe.adriel.voyager.core.screen.uniqueScreenKey
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.LoadingScreen import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.Scaffold import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.more.stats.StatsScreenContent import eu.kanade.presentation.more.stats.StatsScreenContent
import eu.kanade.presentation.more.stats.StatsScreenState import eu.kanade.presentation.more.stats.StatsScreenState
import eu.kanade.presentation.util.LocalRouter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
class StatsScreen : Screen { class StatsScreen : Screen {
@ -23,8 +22,7 @@ class StatsScreen : Screen {
@Composable @Composable
override fun Content() { override fun Content() {
val router = LocalRouter.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val context = LocalContext.current
val screenModel = rememberScreenModel { StatsScreenModel() } val screenModel = rememberScreenModel { StatsScreenModel() }
val state by screenModel.state.collectAsState() val state by screenModel.state.collectAsState()
@ -38,7 +36,7 @@ class StatsScreen : Screen {
topBar = { scrollBehavior -> topBar = { scrollBehavior ->
AppBar( AppBar(
title = stringResource(R.string.label_stats), title = stringResource(R.string.label_stats),
navigateUp = router::popCurrentController, navigateUp = navigator::pop,
scrollBehavior = scrollBehavior, 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,54 @@
package eu.kanade.tachiyomi.ui.updates 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.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import cafe.adriel.voyager.core.model.rememberScreenModel 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.currentOrThrow
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
import cafe.adriel.voyager.navigator.tab.TabOptions
import eu.kanade.presentation.updates.UpdateScreen import eu.kanade.presentation.updates.UpdateScreen
import eu.kanade.presentation.updates.UpdatesDeleteConfirmationDialog 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.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.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.reader.ReaderActivity
import eu.kanade.tachiyomi.ui.updates.UpdatesScreenModel.Event import eu.kanade.tachiyomi.ui.updates.UpdatesScreenModel.Event
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
object UpdatesScreen : Screen { object UpdatesTab : Tab {
override val options: TabOptions
@Composable
get() {
val isSelected = LocalTabNavigator.current.current.key == key
val image = AnimatedImageVector.animatedVectorResource(R.drawable.anim_updates_enter)
return TabOptions(
index = 1u,
title = stringResource(R.string.label_recent_updates),
icon = rememberAnimatedVectorPainter(image, isSelected),
)
}
override suspend fun onReselect(navigator: Navigator) {
navigator.push(DownloadQueueScreen)
}
@Composable @Composable
override fun Content() { override fun Content() {
val context = LocalContext.current val context = LocalContext.current
val router = LocalRouter.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { UpdatesScreenModel() } val screenModel = rememberScreenModel { UpdatesScreenModel() }
val state by screenModel.state.collectAsState() val state by screenModel.state.collectAsState()
@ -34,7 +59,7 @@ object UpdatesScreen : Screen {
downloadedOnlyMode = screenModel.isDownloadOnly, downloadedOnlyMode = screenModel.isDownloadOnly,
lastUpdated = screenModel.lastUpdated, lastUpdated = screenModel.lastUpdated,
relativeTime = screenModel.relativeTime, relativeTime = screenModel.relativeTime,
onClickCover = { item -> router.pushController(MangaController(item.update.mangaId)) }, onClickCover = { item -> navigator.push(MangaScreen(item.update.mangaId)) },
onSelectAll = screenModel::toggleAllSelection, onSelectAll = screenModel::toggleAllSelection,
onInvertSelection = screenModel::invertSelection, onInvertSelection = screenModel::invertSelection,
onUpdateLibrary = screenModel::updateLibrary, onUpdateLibrary = screenModel::updateLibrary,
@ -77,8 +102,9 @@ object UpdatesScreen : Screen {
} }
LaunchedEffect(state.selectionMode) { LaunchedEffect(state.selectionMode) {
(context as? MainActivity)?.showBottomNav(!state.selectionMode) HomeScreen.showBottomNav(!state.selectionMode)
} }
LaunchedEffect(state.isLoading) { LaunchedEffect(state.isLoading) {
if (!state.isLoading) { if (!state.isLoading) {
(context as? MainActivity)?.ready = true (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

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

View File

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

View File

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

View File

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

View File

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