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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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">
<aapt:attr name="android:drawable">
<vector
android:width="1080dp"
android:height="1080dp"
android:width="24dp"
android:height="24dp"
android:viewportWidth="1080"
android:viewportHeight="1080">
<group android:name="_R_G">

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

View file

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