mirror of
https://github.com/mihonapp/mihon.git
synced 2024-12-24 18:08:24 +01:00
parent
5313a5d5d2
commit
3d66eaea83
@ -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",
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -1,10 +1,7 @@
|
||||
package eu.kanade.presentation.more
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.CloudOff
|
||||
import androidx.compose.material.icons.outlined.GetApp
|
||||
@ -29,8 +26,7 @@ import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
|
||||
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.more.DownloadQueueState
|
||||
import eu.kanade.tachiyomi.ui.more.MoreController
|
||||
import eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView
|
||||
import eu.kanade.tachiyomi.util.Constants
|
||||
|
||||
@Composable
|
||||
fun MoreScreen(
|
||||
@ -50,10 +46,7 @@ fun MoreScreen(
|
||||
val uriHandler = LocalUriHandler.current
|
||||
|
||||
ScrollbarLazyColumn(
|
||||
modifier = Modifier.statusBarsPadding(),
|
||||
contentPadding = TachiyomiBottomNavigationView.withBottomNavPadding(
|
||||
WindowInsets.navigationBars.asPaddingValues(),
|
||||
),
|
||||
modifier = Modifier.systemBarsPadding(),
|
||||
) {
|
||||
if (isFDroid) {
|
||||
item {
|
||||
@ -169,7 +162,7 @@ fun MoreScreen(
|
||||
TextPreferenceWidget(
|
||||
title = stringResource(R.string.label_help),
|
||||
icon = Icons.Outlined.HelpOutline,
|
||||
onPreferenceClick = { uriHandler.openUri(MoreController.URL_HELP) },
|
||||
onPreferenceClick = { uriHandler.openUri(Constants.URL_HELP) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
144
app/src/main/java/eu/kanade/presentation/more/NewUpdateScreen.kt
Normal file
144
app/src/main/java/eu/kanade/presentation/more/NewUpdateScreen.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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))
|
||||
|
@ -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) {}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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())}"
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
@ -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())
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.base.controller
|
||||
|
||||
interface RootController
|
@ -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"
|
@ -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() {
|
@ -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()
|
||||
|
@ -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))
|
||||
}
|
||||
},
|
||||
)
|
||||
|
@ -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"
|
@ -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))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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"
|
@ -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)
|
||||
}
|
||||
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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 ->
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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))
|
||||
|
@ -1,34 +1,60 @@
|
||||
package eu.kanade.tachiyomi.ui.history
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.animation.graphics.res.animatedVectorResource
|
||||
import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
|
||||
import androidx.compose.animation.graphics.vector.AnimatedImageVector
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
import cafe.adriel.voyager.core.screen.Screen
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.Navigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
|
||||
import cafe.adriel.voyager.navigator.tab.TabOptions
|
||||
import eu.kanade.domain.chapter.model.Chapter
|
||||
import eu.kanade.presentation.history.HistoryScreen
|
||||
import eu.kanade.presentation.history.components.HistoryDeleteAllDialog
|
||||
import eu.kanade.presentation.history.components.HistoryDeleteDialog
|
||||
import eu.kanade.presentation.util.LocalRouter
|
||||
import eu.kanade.presentation.util.Tab
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.base.controller.pushController
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaScreen
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.consumeAsFlow
|
||||
|
||||
object HistoryScreen : Screen {
|
||||
object HistoryTab : Tab {
|
||||
|
||||
private val snackbarHostState = SnackbarHostState()
|
||||
|
||||
private val resumeLastChapterReadEvent = Channel<Unit>()
|
||||
|
||||
override val options: TabOptions
|
||||
@Composable
|
||||
get() {
|
||||
val isSelected = LocalTabNavigator.current.current.key == key
|
||||
val image = AnimatedImageVector.animatedVectorResource(R.drawable.anim_history_enter)
|
||||
return TabOptions(
|
||||
index = 2u,
|
||||
title = stringResource(R.string.label_recent_manga),
|
||||
icon = rememberAnimatedVectorPainter(image, isSelected),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun onReselect(navigator: Navigator) {
|
||||
resumeLastChapterReadEvent.send(Unit)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val router = LocalRouter.currentOrThrow
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val context = LocalContext.current
|
||||
val screenModel = rememberScreenModel { HistoryScreenModel() }
|
||||
val state by screenModel.state.collectAsState()
|
||||
@ -39,7 +65,7 @@ object HistoryScreen : Screen {
|
||||
incognitoMode = screenModel.isIncognitoMode,
|
||||
downloadedOnlyMode = screenModel.isDownloadOnly,
|
||||
onSearchQueryChange = screenModel::updateSearchQuery,
|
||||
onClickCover = { router.pushController(MangaController(it)) },
|
||||
onClickCover = { navigator.push(MangaScreen(it)) },
|
||||
onClickResume = screenModel::getNextChapterForManga,
|
||||
onDialogChange = screenModel::setDialog,
|
||||
)
|
||||
@ -84,6 +110,12 @@ object HistoryScreen : Screen {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
resumeLastChapterReadEvent.consumeAsFlow().collectLatest {
|
||||
openChapter(context, screenModel.getNextChapter())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun openChapter(context: Context, chapter: Chapter?) {
|
288
app/src/main/java/eu/kanade/tachiyomi/ui/home/HomeScreen.kt
Normal file
288
app/src/main/java/eu/kanade/tachiyomi/ui/home/HomeScreen.kt
Normal 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()
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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/"
|
||||
}
|
||||
}
|
@ -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()) },
|
||||
)
|
||||
}
|
||||
}
|
@ -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"
|
@ -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()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
@ -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)
|
||||
},
|
||||
)
|
||||
|
@ -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"
|
@ -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)
|
||||
}
|
||||
},
|
||||
|
@ -1,13 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.stats
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import cafe.adriel.voyager.navigator.Navigator
|
||||
import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
|
||||
|
||||
class StatsController : BasicFullComposeController() {
|
||||
|
||||
@Composable
|
||||
override fun ComposeContent() {
|
||||
Navigator(screen = StatsScreen())
|
||||
}
|
||||
}
|
@ -3,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,
|
||||
)
|
||||
},
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -1,29 +1,54 @@
|
||||
package eu.kanade.tachiyomi.ui.updates
|
||||
|
||||
import androidx.compose.animation.graphics.res.animatedVectorResource
|
||||
import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
|
||||
import androidx.compose.animation.graphics.vector.AnimatedImageVector
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
import cafe.adriel.voyager.core.screen.Screen
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.Navigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
|
||||
import cafe.adriel.voyager.navigator.tab.TabOptions
|
||||
import eu.kanade.presentation.updates.UpdateScreen
|
||||
import eu.kanade.presentation.updates.UpdatesDeleteConfirmationDialog
|
||||
import eu.kanade.presentation.util.LocalRouter
|
||||
import eu.kanade.presentation.util.Tab
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.base.controller.pushController
|
||||
import eu.kanade.tachiyomi.ui.download.DownloadQueueScreen
|
||||
import eu.kanade.tachiyomi.ui.home.HomeScreen
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaScreen
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
import eu.kanade.tachiyomi.ui.updates.UpdatesScreenModel.Event
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
|
||||
object UpdatesScreen : Screen {
|
||||
object UpdatesTab : Tab {
|
||||
|
||||
override val options: TabOptions
|
||||
@Composable
|
||||
get() {
|
||||
val isSelected = LocalTabNavigator.current.current.key == key
|
||||
val image = AnimatedImageVector.animatedVectorResource(R.drawable.anim_updates_enter)
|
||||
return TabOptions(
|
||||
index = 1u,
|
||||
title = stringResource(R.string.label_recent_updates),
|
||||
icon = rememberAnimatedVectorPainter(image, isSelected),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun onReselect(navigator: Navigator) {
|
||||
navigator.push(DownloadQueueScreen)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val context = LocalContext.current
|
||||
val router = LocalRouter.currentOrThrow
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val screenModel = rememberScreenModel { UpdatesScreenModel() }
|
||||
val state by screenModel.state.collectAsState()
|
||||
|
||||
@ -34,7 +59,7 @@ object UpdatesScreen : Screen {
|
||||
downloadedOnlyMode = screenModel.isDownloadOnly,
|
||||
lastUpdated = screenModel.lastUpdated,
|
||||
relativeTime = screenModel.relativeTime,
|
||||
onClickCover = { item -> router.pushController(MangaController(item.update.mangaId)) },
|
||||
onClickCover = { item -> navigator.push(MangaScreen(item.update.mangaId)) },
|
||||
onSelectAll = screenModel::toggleAllSelection,
|
||||
onInvertSelection = screenModel::invertSelection,
|
||||
onUpdateLibrary = screenModel::updateLibrary,
|
||||
@ -77,8 +102,9 @@ object UpdatesScreen : Screen {
|
||||
}
|
||||
|
||||
LaunchedEffect(state.selectionMode) {
|
||||
(context as? MainActivity)?.showBottomNav(!state.selectionMode)
|
||||
HomeScreen.showBottomNav(!state.selectionMode)
|
||||
}
|
||||
|
||||
LaunchedEffect(state.isLoading) {
|
||||
if (!state.isLoading) {
|
||||
(context as? MainActivity)?.ready = true
|
7
app/src/main/java/eu/kanade/tachiyomi/util/Constants.kt
Normal file
7
app/src/main/java/eu/kanade/tachiyomi/util/Constants.kt
Normal 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"
|
||||
}
|
@ -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() }),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
package eu.kanade.tachiyomi.widget
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import com.bluelinelabs.conductor.ChangeHandlerFrameLayout
|
||||
|
||||
/**
|
||||
* [ChangeHandlerFrameLayout] with the ability to draw behind the header sibling in [CoordinatorLayout].
|
||||
* The layout behavior of this view is set to [TachiyomiScrollingViewBehavior] and should not be changed.
|
||||
*/
|
||||
class TachiyomiChangeHandlerFrameLayout(
|
||||
context: Context,
|
||||
attrs: AttributeSet,
|
||||
) : ChangeHandlerFrameLayout(context, attrs), CoordinatorLayout.AttachedBehavior {
|
||||
|
||||
/**
|
||||
* If true, this view will draw behind the header sibling.
|
||||
*
|
||||
* @see TachiyomiScrollingViewBehavior.shouldHeaderOverlap
|
||||
*/
|
||||
var overlapHeader = false
|
||||
set(value) {
|
||||
if (field != value) {
|
||||
field = value
|
||||
(layoutParams as? CoordinatorLayout.LayoutParams)?.behavior = behavior.apply {
|
||||
shouldHeaderOverlap = value
|
||||
}
|
||||
if (!value) {
|
||||
// The behavior doesn't reset translationY when shouldHeaderOverlap is false
|
||||
translationY = 0F
|
||||
}
|
||||
forceLayout()
|
||||
}
|
||||
}
|
||||
|
||||
fun enableScrollingBehavior(enable: Boolean) {
|
||||
(layoutParams as? CoordinatorLayout.LayoutParams)?.behavior = if (enable) {
|
||||
behavior.apply {
|
||||
shouldHeaderOverlap = overlapHeader
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
if (!enable) {
|
||||
// The behavior doesn't reset translationY when shouldHeaderOverlap is false
|
||||
translationY = 0F
|
||||
}
|
||||
forceLayout()
|
||||
}
|
||||
|
||||
override fun getBehavior() = TachiyomiScrollingViewBehavior()
|
||||
}
|
@ -2,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">
|
||||
|
@ -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>
|
@ -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>
|
@ -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" }
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user