mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-04 08:08:55 +01:00 
			
		
		
		
	@@ -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()
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user