mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-24 21:58:34 +03:00
parent
b3c911ea28
commit
e99e4c2f41
39 changed files with 570 additions and 467 deletions
|
@ -187,7 +187,6 @@ dependencies {
|
||||||
implementation(androidx.appcompat)
|
implementation(androidx.appcompat)
|
||||||
implementation(androidx.biometricktx)
|
implementation(androidx.biometricktx)
|
||||||
implementation(androidx.constraintlayout)
|
implementation(androidx.constraintlayout)
|
||||||
implementation(androidx.coordinatorlayout)
|
|
||||||
implementation(androidx.corektx)
|
implementation(androidx.corektx)
|
||||||
implementation(androidx.splashscreen)
|
implementation(androidx.splashscreen)
|
||||||
implementation(androidx.recyclerview)
|
implementation(androidx.recyclerview)
|
||||||
|
@ -236,7 +235,6 @@ dependencies {
|
||||||
// UI libraries
|
// UI libraries
|
||||||
implementation(libs.material)
|
implementation(libs.material)
|
||||||
implementation(libs.flexible.adapter.core)
|
implementation(libs.flexible.adapter.core)
|
||||||
implementation(libs.flexible.adapter.ui)
|
|
||||||
implementation(libs.photoview)
|
implementation(libs.photoview)
|
||||||
implementation(libs.directionalviewpager) {
|
implementation(libs.directionalviewpager) {
|
||||||
exclude(group = "androidx.viewpager", module = "viewpager")
|
exclude(group = "androidx.viewpager", module = "viewpager")
|
||||||
|
@ -245,7 +243,6 @@ dependencies {
|
||||||
implementation(libs.bundles.richtext)
|
implementation(libs.bundles.richtext)
|
||||||
implementation(libs.aboutLibraries.compose)
|
implementation(libs.aboutLibraries.compose)
|
||||||
implementation(libs.bundles.voyager)
|
implementation(libs.bundles.voyager)
|
||||||
implementation(libs.compose.cascade)
|
|
||||||
implementation(libs.compose.materialmotion)
|
implementation(libs.compose.materialmotion)
|
||||||
implementation(libs.compose.simpleicons)
|
implementation(libs.compose.simpleicons)
|
||||||
|
|
||||||
|
|
|
@ -4,13 +4,7 @@ import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.animation.with
|
import androidx.compose.animation.togetherWith
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
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.only
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
@ -20,7 +14,7 @@ import cafe.adriel.voyager.core.annotation.InternalVoyagerApi
|
||||||
import cafe.adriel.voyager.core.lifecycle.DisposableEffectIgnoringConfiguration
|
import cafe.adriel.voyager.core.lifecycle.DisposableEffectIgnoringConfiguration
|
||||||
import cafe.adriel.voyager.core.screen.Screen
|
import cafe.adriel.voyager.core.screen.Screen
|
||||||
import cafe.adriel.voyager.navigator.Navigator
|
import cafe.adriel.voyager.navigator.Navigator
|
||||||
import cafe.adriel.voyager.transitions.ScreenTransition
|
import eu.kanade.presentation.util.ScreenTransition
|
||||||
import eu.kanade.presentation.util.isTabletUi
|
import eu.kanade.presentation.util.isTabletUi
|
||||||
import tachiyomi.presentation.core.components.AdaptiveSheet as AdaptiveSheetImpl
|
import tachiyomi.presentation.core.components.AdaptiveSheet as AdaptiveSheetImpl
|
||||||
|
|
||||||
|
@ -43,7 +37,7 @@ fun NavigatorAdaptiveSheet(
|
||||||
ScreenTransition(
|
ScreenTransition(
|
||||||
navigator = sheetNavigator,
|
navigator = sheetNavigator,
|
||||||
transition = {
|
transition = {
|
||||||
fadeIn(animationSpec = tween(220, delayMillis = 90)) with
|
fadeIn(animationSpec = tween(220, delayMillis = 90)) togetherWith
|
||||||
fadeOut(animationSpec = tween(90))
|
fadeOut(animationSpec = tween(90))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -79,15 +73,13 @@ fun AdaptiveSheet(
|
||||||
tonalElevation: Dp = 1.dp,
|
tonalElevation: Dp = 1.dp,
|
||||||
enableSwipeDismiss: Boolean = true,
|
enableSwipeDismiss: Boolean = true,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
content: @Composable (PaddingValues) -> Unit,
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
val isTabletUi = isTabletUi()
|
val isTabletUi = isTabletUi()
|
||||||
|
|
||||||
Dialog(
|
Dialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
properties = DialogProperties(
|
properties = dialogProperties,
|
||||||
usePlatformDefaultWidth = false,
|
|
||||||
decorFitsSystemWindows = false,
|
|
||||||
),
|
|
||||||
) {
|
) {
|
||||||
AdaptiveSheetImpl(
|
AdaptiveSheetImpl(
|
||||||
isTabletUi = isTabletUi,
|
isTabletUi = isTabletUi,
|
||||||
|
@ -95,12 +87,12 @@ fun AdaptiveSheet(
|
||||||
enableSwipeDismiss = enableSwipeDismiss,
|
enableSwipeDismiss = enableSwipeDismiss,
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
) {
|
) {
|
||||||
val contentPadding = if (isTabletUi) {
|
content()
|
||||||
PaddingValues()
|
|
||||||
} else {
|
|
||||||
WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()
|
|
||||||
}
|
|
||||||
content(contentPadding)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val dialogProperties = DialogProperties(
|
||||||
|
usePlatformDefaultWidth = false,
|
||||||
|
decorFitsSystemWindows = false,
|
||||||
|
)
|
||||||
|
|
|
@ -19,6 +19,7 @@ import androidx.compose.material.icons.outlined.Search
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.LocalContentColor
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.PlainTooltipBox
|
import androidx.compose.material3.PlainTooltipBox
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
@ -37,6 +38,7 @@ import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
|
@ -210,10 +212,11 @@ fun AppBarActions(
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = it.onClick,
|
onClick = it.onClick,
|
||||||
enabled = it.enabled,
|
enabled = it.enabled,
|
||||||
modifier = Modifier.tooltipAnchor(),
|
modifier = Modifier.tooltipTrigger(),
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = it.icon,
|
imageVector = it.icon,
|
||||||
|
tint = it.iconTint ?: LocalContentColor.current,
|
||||||
contentDescription = it.title,
|
contentDescription = it.title,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -227,7 +230,7 @@ fun AppBarActions(
|
||||||
) {
|
) {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = { showMenu = !showMenu },
|
onClick = { showMenu = !showMenu },
|
||||||
modifier = Modifier.tooltipAnchor(),
|
modifier = Modifier.tooltipTrigger(),
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Outlined.MoreVert,
|
Icons.Outlined.MoreVert,
|
||||||
|
@ -348,7 +351,7 @@ fun SearchToolbar(
|
||||||
) {
|
) {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = Modifier.tooltipAnchor(),
|
modifier = Modifier.tooltipTrigger(),
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Outlined.Search,
|
Icons.Outlined.Search,
|
||||||
|
@ -365,7 +368,7 @@ fun SearchToolbar(
|
||||||
onClick()
|
onClick()
|
||||||
focusRequester.requestFocus()
|
focusRequester.requestFocus()
|
||||||
},
|
},
|
||||||
modifier = Modifier.tooltipAnchor(),
|
modifier = Modifier.tooltipTrigger(),
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Outlined.Close,
|
Icons.Outlined.Close,
|
||||||
|
@ -390,6 +393,7 @@ sealed interface AppBar {
|
||||||
data class Action(
|
data class Action(
|
||||||
val title: String,
|
val title: String,
|
||||||
val icon: ImageVector,
|
val icon: ImageVector,
|
||||||
|
val iconTint: Color? = null,
|
||||||
val onClick: () -> Unit,
|
val onClick: () -> Unit,
|
||||||
val enabled: Boolean = true,
|
val enabled: Boolean = true,
|
||||||
) : AppBarAction
|
) : AppBarAction
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
package eu.kanade.presentation.components
|
package eu.kanade.presentation.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.sizeIn
|
import androidx.compose.foundation.layout.sizeIn
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.MoreVert
|
import androidx.compose.material.icons.outlined.ArrowLeft
|
||||||
|
import androidx.compose.material.icons.outlined.ArrowRight
|
||||||
import androidx.compose.material.icons.outlined.RadioButtonChecked
|
import androidx.compose.material.icons.outlined.RadioButtonChecked
|
||||||
import androidx.compose.material.icons.outlined.RadioButtonUnchecked
|
import androidx.compose.material.icons.outlined.RadioButtonUnchecked
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
@ -17,13 +16,13 @@ import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.DpOffset
|
import androidx.compose.ui.unit.DpOffset
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.PopupProperties
|
import androidx.compose.ui.window.PopupProperties
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import me.saket.cascade.CascadeColumnScope
|
|
||||||
import me.saket.cascade.CascadeDropdownMenu
|
|
||||||
import androidx.compose.material3.DropdownMenu as ComposeDropdownMenu
|
import androidx.compose.material3.DropdownMenu as ComposeDropdownMenu
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -72,25 +71,28 @@ fun RadioMenuItem(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun OverflowMenu(
|
fun NestedMenuItem(
|
||||||
content: @Composable CascadeColumnScope.(() -> Unit) -> Unit,
|
text: @Composable () -> Unit,
|
||||||
|
children: @Composable ColumnScope.(() -> Unit) -> Unit,
|
||||||
) {
|
) {
|
||||||
var moreExpanded by remember { mutableStateOf(false) }
|
var nestedExpanded by remember { mutableStateOf(false) }
|
||||||
val closeMenu = { moreExpanded = false }
|
val closeMenu = { nestedExpanded = false }
|
||||||
|
val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
|
||||||
|
|
||||||
Box {
|
DropdownMenuItem(
|
||||||
IconButton(onClick = { moreExpanded = !moreExpanded }) {
|
text = text,
|
||||||
|
onClick = { nestedExpanded = true },
|
||||||
|
trailingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.MoreVert,
|
imageVector = if (isLtr) Icons.Outlined.ArrowRight else Icons.Outlined.ArrowLeft,
|
||||||
contentDescription = stringResource(R.string.abc_action_menu_overflow_description),
|
contentDescription = null,
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
CascadeDropdownMenu(
|
)
|
||||||
expanded = moreExpanded,
|
DropdownMenu(
|
||||||
|
expanded = nestedExpanded,
|
||||||
onDismissRequest = closeMenu,
|
onDismissRequest = closeMenu,
|
||||||
offset = DpOffset(8.dp, (-56).dp),
|
|
||||||
) {
|
) {
|
||||||
content(closeMenu)
|
children(closeMenu)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ import androidx.compose.animation.animateContentSize
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.wrapContentSize
|
import androidx.compose.foundation.layout.wrapContentSize
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
|
@ -43,13 +42,13 @@ fun TabbedDialog(
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
tabTitles: List<String>,
|
tabTitles: List<String>,
|
||||||
tabOverflowMenuContent: (@Composable ColumnScope.(() -> Unit) -> Unit)? = null,
|
tabOverflowMenuContent: (@Composable ColumnScope.(() -> Unit) -> Unit)? = null,
|
||||||
content: @Composable (PaddingValues, Int) -> Unit,
|
content: @Composable (Int) -> Unit,
|
||||||
) {
|
) {
|
||||||
AdaptiveSheet(
|
AdaptiveSheet(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
) { contentPadding ->
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val pagerState = rememberPagerState()
|
val pagerState = rememberPagerState { tabTitles.size }
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
Row {
|
Row {
|
||||||
|
@ -84,11 +83,10 @@ fun TabbedDialog(
|
||||||
|
|
||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
modifier = Modifier.animateContentSize(),
|
modifier = Modifier.animateContentSize(),
|
||||||
pageCount = tabTitles.size,
|
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
verticalAlignment = Alignment.Top,
|
verticalAlignment = Alignment.Top,
|
||||||
) { page ->
|
) { page ->
|
||||||
content(contentPadding, page)
|
content(page)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,7 @@ fun TabbedScreen(
|
||||||
startIndex: Int? = null,
|
startIndex: Int? = null,
|
||||||
mangaSearchQuery: String? = null,
|
mangaSearchQuery: String? = null,
|
||||||
onChangeMangaSearchQuery: (String?) -> Unit = {},
|
onChangeMangaSearchQuery: (String?) -> Unit = {},
|
||||||
state: PagerState = rememberPagerState(),
|
state: PagerState = rememberPagerState { tabs.size },
|
||||||
scrollable: Boolean = false,
|
scrollable: Boolean = false,
|
||||||
animeSearchQuery: String? = null,
|
animeSearchQuery: String? = null,
|
||||||
onChangeAnimeSearchQuery: (String?) -> Unit = {},
|
onChangeAnimeSearchQuery: (String?) -> Unit = {},
|
||||||
|
@ -105,7 +105,6 @@ fun TabbedScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
pageCount = tabs.size,
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
state = state,
|
state = state,
|
||||||
verticalAlignment = Alignment.Top,
|
verticalAlignment = Alignment.Top,
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package eu.kanade.presentation.entries
|
package eu.kanade.presentation.entries
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.ArrowBack
|
import androidx.compose.material.icons.outlined.ArrowBack
|
||||||
|
@ -9,7 +8,6 @@ import androidx.compose.material.icons.outlined.Download
|
||||||
import androidx.compose.material.icons.outlined.FilterList
|
import androidx.compose.material.icons.outlined.FilterList
|
||||||
import androidx.compose.material.icons.outlined.FlipToBack
|
import androidx.compose.material.icons.outlined.FlipToBack
|
||||||
import androidx.compose.material.icons.outlined.SelectAll
|
import androidx.compose.material.icons.outlined.SelectAll
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.LocalContentColor
|
import androidx.compose.material3.LocalContentColor
|
||||||
|
@ -19,8 +17,10 @@ import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.surfaceColorAtElevation
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
@ -29,7 +29,6 @@ import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.components.AppBarActions
|
import eu.kanade.presentation.components.AppBarActions
|
||||||
import eu.kanade.presentation.components.EntryDownloadDropdownMenu
|
import eu.kanade.presentation.components.EntryDownloadDropdownMenu
|
||||||
import eu.kanade.presentation.components.OverflowMenu
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import tachiyomi.presentation.core.theme.active
|
import tachiyomi.presentation.core.theme.active
|
||||||
|
|
||||||
|
@ -94,16 +93,9 @@ fun EntryToolbar(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
var downloadExpanded by remember { mutableStateOf(false) }
|
||||||
if (onClickDownload != null) {
|
if (onClickDownload != null) {
|
||||||
val (downloadExpanded, onDownloadExpanded) = remember { mutableStateOf(false) }
|
val onDismissRequest = { downloadExpanded = false }
|
||||||
Box {
|
|
||||||
IconButton(onClick = { onDownloadExpanded(!downloadExpanded) }) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Outlined.Download,
|
|
||||||
contentDescription = stringResource(R.string.manga_download),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val onDismissRequest = { onDownloadExpanded(false) }
|
|
||||||
EntryDownloadDropdownMenu(
|
EntryDownloadDropdownMenu(
|
||||||
expanded = downloadExpanded,
|
expanded = downloadExpanded,
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
|
@ -111,67 +103,75 @@ fun EntryToolbar(
|
||||||
isManga = isManga,
|
isManga = isManga,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
val filterTint = if (hasFilters) MaterialTheme.colorScheme.active else LocalContentColor.current
|
val filterTint = if (hasFilters) MaterialTheme.colorScheme.active else LocalContentColor.current
|
||||||
IconButton(onClick = onClickFilter) {
|
AppBarActions(
|
||||||
Icon(Icons.Outlined.FilterList, contentDescription = stringResource(R.string.action_filter), tint = filterTint)
|
actions = buildList {
|
||||||
|
if (onClickDownload != null) {
|
||||||
|
add(
|
||||||
|
AppBar.Action(
|
||||||
|
title = stringResource(R.string.manga_download),
|
||||||
|
icon = Icons.Outlined.Download,
|
||||||
|
onClick = { downloadExpanded = !downloadExpanded },
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
add(
|
||||||
OverflowMenu { closeMenu ->
|
AppBar.Action(
|
||||||
DropdownMenuItem(
|
title = stringResource(R.string.action_filter),
|
||||||
text = { Text(text = stringResource(R.string.action_webview_refresh)) },
|
icon = Icons.Outlined.FilterList,
|
||||||
onClick = {
|
iconTint = filterTint,
|
||||||
onClickRefresh()
|
onClick = onClickFilter,
|
||||||
closeMenu()
|
),
|
||||||
},
|
)
|
||||||
|
add(
|
||||||
|
AppBar.OverflowAction(
|
||||||
|
title = stringResource(R.string.action_webview_refresh),
|
||||||
|
onClick = onClickRefresh,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
if (onClickEditCategory != null) {
|
if (onClickEditCategory != null) {
|
||||||
DropdownMenuItem(
|
add(
|
||||||
text = { Text(text = stringResource(R.string.action_edit_categories)) },
|
AppBar.OverflowAction(
|
||||||
onClick = {
|
title = stringResource(R.string.action_edit_categories),
|
||||||
onClickEditCategory()
|
onClick = onClickEditCategory,
|
||||||
closeMenu()
|
),
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (onClickMigrate != null) {
|
if (onClickMigrate != null) {
|
||||||
DropdownMenuItem(
|
add(
|
||||||
text = { Text(text = stringResource(R.string.action_migrate)) },
|
AppBar.OverflowAction(
|
||||||
onClick = {
|
title = stringResource(R.string.action_migrate),
|
||||||
onClickMigrate()
|
onClick = onClickMigrate,
|
||||||
closeMenu()
|
),
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (changeAnimeSkipIntro != null) {
|
if (changeAnimeSkipIntro != null) {
|
||||||
DropdownMenuItem(
|
add(
|
||||||
text = { Text(text = stringResource(R.string.action_change_intro_length)) },
|
AppBar.OverflowAction(
|
||||||
onClick = {
|
title = stringResource(R.string.action_change_intro_length),
|
||||||
changeAnimeSkipIntro()
|
onClick = changeAnimeSkipIntro,
|
||||||
closeMenu()
|
),
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (onClickShare != null) {
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(text = stringResource(R.string.action_share)) },
|
|
||||||
onClick = {
|
|
||||||
onClickShare()
|
|
||||||
closeMenu()
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (onClickSettings != null) {
|
if (onClickSettings != null) {
|
||||||
DropdownMenuItem(
|
add(
|
||||||
text = { Text(text = stringResource(R.string.settings)) },
|
AppBar.OverflowAction(
|
||||||
onClick = {
|
title = stringResource(R.string.settings),
|
||||||
onClickSettings()
|
onClick = onClickSettings,
|
||||||
closeMenu()
|
),
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (onClickShare != null) {
|
||||||
|
add(
|
||||||
|
AppBar.OverflowAction(
|
||||||
|
title = stringResource(R.string.action_share),
|
||||||
|
onClick = onClickShare,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
|
|
@ -68,10 +68,9 @@ fun EpisodeSettingsDialog(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { contentPadding, page ->
|
) { page ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(contentPadding)
|
|
||||||
.padding(vertical = TabbedDialogPaddings.Vertical)
|
.padding(vertical = TabbedDialogPaddings.Vertical)
|
||||||
.verticalScroll(rememberScrollState()),
|
.verticalScroll(rememberScrollState()),
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -43,6 +43,8 @@ import coil.imageLoader
|
||||||
import coil.request.CachePolicy
|
import coil.request.CachePolicy
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import coil.size.Size
|
import coil.size.Size
|
||||||
|
import eu.kanade.presentation.components.AppBar
|
||||||
|
import eu.kanade.presentation.components.AppBarActions
|
||||||
import eu.kanade.presentation.components.DropdownMenu
|
import eu.kanade.presentation.components.DropdownMenu
|
||||||
import eu.kanade.presentation.entries.EditCoverAction
|
import eu.kanade.presentation.entries.EditCoverAction
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
@ -88,18 +90,24 @@ fun AnimeCoverDialog(
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
ActionsPill {
|
ActionsPill {
|
||||||
IconButton(onClick = onShareClick) {
|
AppBarActions(
|
||||||
Icon(
|
actions = buildList {
|
||||||
imageVector = Icons.Outlined.Share,
|
add(
|
||||||
contentDescription = stringResource(R.string.action_share),
|
AppBar.Action(
|
||||||
|
title = stringResource(R.string.action_share),
|
||||||
|
icon = Icons.Outlined.Share,
|
||||||
|
onClick = onShareClick,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
add(
|
||||||
IconButton(onClick = onSaveClick) {
|
AppBar.Action(
|
||||||
Icon(
|
title = stringResource(R.string.action_save),
|
||||||
imageVector = Icons.Outlined.Save,
|
icon = Icons.Outlined.Save,
|
||||||
contentDescription = stringResource(R.string.action_save),
|
onClick = onSaveClick,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
|
||||||
if (onEditClick != null) {
|
if (onEditClick != null) {
|
||||||
Box {
|
Box {
|
||||||
var expanded by remember { mutableStateOf(false) }
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
|
|
@ -68,10 +68,9 @@ fun ChapterSettingsDialog(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { contentPadding, page ->
|
) { page ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(contentPadding)
|
|
||||||
.padding(vertical = TabbedDialogPaddings.Vertical)
|
.padding(vertical = TabbedDialogPaddings.Vertical)
|
||||||
.verticalScroll(rememberScrollState()),
|
.verticalScroll(rememberScrollState()),
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -43,6 +43,8 @@ import coil.imageLoader
|
||||||
import coil.request.CachePolicy
|
import coil.request.CachePolicy
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import coil.size.Size
|
import coil.size.Size
|
||||||
|
import eu.kanade.presentation.components.AppBar
|
||||||
|
import eu.kanade.presentation.components.AppBarActions
|
||||||
import eu.kanade.presentation.components.DropdownMenu
|
import eu.kanade.presentation.components.DropdownMenu
|
||||||
import eu.kanade.presentation.entries.EditCoverAction
|
import eu.kanade.presentation.entries.EditCoverAction
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
@ -88,18 +90,24 @@ fun MangaCoverDialog(
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
ActionsPill {
|
ActionsPill {
|
||||||
IconButton(onClick = onShareClick) {
|
AppBarActions(
|
||||||
Icon(
|
actions = buildList {
|
||||||
imageVector = Icons.Outlined.Share,
|
add(
|
||||||
contentDescription = stringResource(R.string.action_share),
|
AppBar.Action(
|
||||||
|
title = stringResource(R.string.action_share),
|
||||||
|
icon = Icons.Outlined.Share,
|
||||||
|
onClick = onShareClick,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
add(
|
||||||
IconButton(onClick = onSaveClick) {
|
AppBar.Action(
|
||||||
Icon(
|
title = stringResource(R.string.action_save),
|
||||||
imageVector = Icons.Outlined.Save,
|
icon = Icons.Outlined.Save,
|
||||||
contentDescription = stringResource(R.string.action_save),
|
onClick = onSaveClick,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
|
||||||
if (onEditClick != null) {
|
if (onEditClick != null) {
|
||||||
Box {
|
Box {
|
||||||
var expanded by remember { mutableStateOf(false) }
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
|
|
@ -6,9 +6,6 @@ import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.FilterList
|
import androidx.compose.material.icons.outlined.FilterList
|
||||||
import androidx.compose.material.icons.outlined.FlipToBack
|
import androidx.compose.material.icons.outlined.FlipToBack
|
||||||
import androidx.compose.material.icons.outlined.SelectAll
|
import androidx.compose.material.icons.outlined.SelectAll
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.LocalContentColor
|
import androidx.compose.material3.LocalContentColor
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
@ -22,7 +19,6 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.components.AppBarActions
|
import eu.kanade.presentation.components.AppBarActions
|
||||||
import eu.kanade.presentation.components.OverflowMenu
|
|
||||||
import eu.kanade.presentation.components.SearchToolbar
|
import eu.kanade.presentation.components.SearchToolbar
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import tachiyomi.presentation.core.components.Pill
|
import tachiyomi.presentation.core.components.Pill
|
||||||
|
@ -102,33 +98,28 @@ fun LibraryRegularToolbar(
|
||||||
onChangeSearchQuery = onSearchQueryChange,
|
onChangeSearchQuery = onSearchQueryChange,
|
||||||
actions = {
|
actions = {
|
||||||
val filterTint = if (hasFilters) MaterialTheme.colorScheme.active else LocalContentColor.current
|
val filterTint = if (hasFilters) MaterialTheme.colorScheme.active else LocalContentColor.current
|
||||||
IconButton(onClick = onClickFilter) {
|
AppBarActions(
|
||||||
Icon(Icons.Outlined.FilterList, contentDescription = stringResource(R.string.action_filter), tint = filterTint)
|
listOf(
|
||||||
}
|
AppBar.Action(
|
||||||
|
title = stringResource(R.string.action_filter),
|
||||||
OverflowMenu { closeMenu ->
|
icon = Icons.Outlined.FilterList,
|
||||||
DropdownMenuItem(
|
iconTint = filterTint,
|
||||||
text = { Text(text = stringResource(R.string.action_update_library)) },
|
onClick = onClickFilter,
|
||||||
onClick = {
|
),
|
||||||
onClickGlobalUpdate()
|
AppBar.OverflowAction(
|
||||||
closeMenu()
|
title = stringResource(R.string.action_update_library),
|
||||||
},
|
onClick = onClickGlobalUpdate,
|
||||||
|
),
|
||||||
|
AppBar.OverflowAction(
|
||||||
|
title = stringResource(R.string.action_update_category),
|
||||||
|
onClick = onClickRefresh,
|
||||||
|
),
|
||||||
|
AppBar.OverflowAction(
|
||||||
|
title = stringResource(R.string.action_open_random_manga),
|
||||||
|
onClick = onClickOpenRandomEntry,
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(text = stringResource(R.string.action_update_category)) },
|
|
||||||
onClick = {
|
|
||||||
onClickRefresh()
|
|
||||||
closeMenu()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(text = stringResource(R.string.action_open_random_manga)) },
|
|
||||||
onClick = {
|
|
||||||
onClickOpenRandomEntry()
|
|
||||||
closeMenu()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
navigateUp = navigateUp,
|
navigateUp = navigateUp,
|
||||||
|
|
|
@ -55,7 +55,7 @@ fun AnimeLibraryContent(
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
val coercedCurrentPage = remember { currentPage().coerceAtMost(categories.lastIndex) }
|
val coercedCurrentPage = remember { currentPage().coerceAtMost(categories.lastIndex) }
|
||||||
val pagerState = rememberPagerState(coercedCurrentPage)
|
val pagerState = rememberPagerState(coercedCurrentPage) { categories.size }
|
||||||
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
var isRefreshing by remember(pagerState.currentPage) { mutableStateOf(false) }
|
var isRefreshing by remember(pagerState.currentPage) { mutableStateOf(false) }
|
||||||
|
@ -99,7 +99,6 @@ fun AnimeLibraryContent(
|
||||||
AnimeLibraryPager(
|
AnimeLibraryPager(
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
contentPadding = PaddingValues(bottom = contentPadding.calculateBottomPadding()),
|
contentPadding = PaddingValues(bottom = contentPadding.calculateBottomPadding()),
|
||||||
pageCount = categories.size,
|
|
||||||
hasActiveFilters = hasActiveFilters,
|
hasActiveFilters = hasActiveFilters,
|
||||||
selectedAnime = selection,
|
selectedAnime = selection,
|
||||||
searchQuery = searchQuery,
|
searchQuery = searchQuery,
|
||||||
|
|
|
@ -22,7 +22,6 @@ import tachiyomi.presentation.core.components.HorizontalPager
|
||||||
fun AnimeLibraryPager(
|
fun AnimeLibraryPager(
|
||||||
state: PagerState,
|
state: PagerState,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
pageCount: Int,
|
|
||||||
hasActiveFilters: Boolean,
|
hasActiveFilters: Boolean,
|
||||||
selectedAnime: List<LibraryAnime>,
|
selectedAnime: List<LibraryAnime>,
|
||||||
searchQuery: String?,
|
searchQuery: String?,
|
||||||
|
@ -35,7 +34,6 @@ fun AnimeLibraryPager(
|
||||||
onClickContinueWatching: ((LibraryAnime) -> Unit)?,
|
onClickContinueWatching: ((LibraryAnime) -> Unit)?,
|
||||||
) {
|
) {
|
||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
pageCount = pageCount,
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
state = state,
|
state = state,
|
||||||
verticalAlignment = Alignment.Top,
|
verticalAlignment = Alignment.Top,
|
||||||
|
|
|
@ -54,10 +54,9 @@ fun AnimeLibrarySettingsDialog(
|
||||||
stringResource(R.string.action_sort),
|
stringResource(R.string.action_sort),
|
||||||
stringResource(R.string.action_display),
|
stringResource(R.string.action_display),
|
||||||
),
|
),
|
||||||
) { contentPadding, page ->
|
) { page ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(contentPadding)
|
|
||||||
.padding(vertical = TabbedDialogPaddings.Vertical)
|
.padding(vertical = TabbedDialogPaddings.Vertical)
|
||||||
.verticalScroll(rememberScrollState()),
|
.verticalScroll(rememberScrollState()),
|
||||||
) {
|
) {
|
||||||
|
@ -220,23 +219,25 @@ private fun ColumnScope.DisplayPage(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val columns by columnPreference.changes().collectAsState(initial = 0)
|
val columns by columnPreference.collectAsState()
|
||||||
Column {
|
Column(modifier = Modifier.weight(0.5f)) {
|
||||||
Text(
|
Text(
|
||||||
stringResource(id = R.string.pref_library_columns),
|
stringResource(id = R.string.pref_library_columns),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
)
|
)
|
||||||
|
Text(
|
||||||
if (columns > 0) {
|
if (columns > 0) {
|
||||||
Text(stringResource(id = R.string.pref_library_columns_per_row, columns))
|
stringResource(id = R.string.pref_library_columns_per_row, columns)
|
||||||
} else {
|
} else {
|
||||||
Text(stringResource(id = R.string.label_default))
|
stringResource(id = R.string.label_default)
|
||||||
}
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Slider(
|
Slider(
|
||||||
value = columns.toFloat(),
|
value = columns.toFloat(),
|
||||||
onValueChange = { columnPreference.set(it.toInt()) },
|
onValueChange = { columnPreference.set(it.toInt()) },
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1.5f),
|
||||||
valueRange = 0f..10f,
|
valueRange = 0f..10f,
|
||||||
steps = 10,
|
steps = 10,
|
||||||
)
|
)
|
||||||
|
|
|
@ -55,7 +55,7 @@ fun MangaLibraryContent(
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
val coercedCurrentPage = remember { currentPage().coerceAtMost(categories.lastIndex) }
|
val coercedCurrentPage = remember { currentPage().coerceAtMost(categories.lastIndex) }
|
||||||
val pagerState = rememberPagerState(coercedCurrentPage)
|
val pagerState = rememberPagerState(coercedCurrentPage) { categories.size }
|
||||||
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
var isRefreshing by remember(pagerState.currentPage) { mutableStateOf(false) }
|
var isRefreshing by remember(pagerState.currentPage) { mutableStateOf(false) }
|
||||||
|
@ -99,7 +99,6 @@ fun MangaLibraryContent(
|
||||||
MangaLibraryPager(
|
MangaLibraryPager(
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
contentPadding = PaddingValues(bottom = contentPadding.calculateBottomPadding()),
|
contentPadding = PaddingValues(bottom = contentPadding.calculateBottomPadding()),
|
||||||
pageCount = categories.size,
|
|
||||||
hasActiveFilters = hasActiveFilters,
|
hasActiveFilters = hasActiveFilters,
|
||||||
selectedManga = selection,
|
selectedManga = selection,
|
||||||
searchQuery = searchQuery,
|
searchQuery = searchQuery,
|
||||||
|
|
|
@ -31,7 +31,6 @@ import tachiyomi.presentation.core.util.plus
|
||||||
fun MangaLibraryPager(
|
fun MangaLibraryPager(
|
||||||
state: PagerState,
|
state: PagerState,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
pageCount: Int,
|
|
||||||
hasActiveFilters: Boolean,
|
hasActiveFilters: Boolean,
|
||||||
selectedManga: List<LibraryManga>,
|
selectedManga: List<LibraryManga>,
|
||||||
searchQuery: String?,
|
searchQuery: String?,
|
||||||
|
@ -44,7 +43,6 @@ fun MangaLibraryPager(
|
||||||
onClickContinueReading: ((LibraryManga) -> Unit)?,
|
onClickContinueReading: ((LibraryManga) -> Unit)?,
|
||||||
) {
|
) {
|
||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
pageCount = pageCount,
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
state = state,
|
state = state,
|
||||||
verticalAlignment = Alignment.Top,
|
verticalAlignment = Alignment.Top,
|
||||||
|
|
|
@ -54,10 +54,9 @@ fun MangaLibrarySettingsDialog(
|
||||||
stringResource(R.string.action_sort),
|
stringResource(R.string.action_sort),
|
||||||
stringResource(R.string.action_display),
|
stringResource(R.string.action_display),
|
||||||
),
|
),
|
||||||
) { contentPadding, page ->
|
) { page ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(contentPadding)
|
|
||||||
.padding(vertical = TabbedDialogPaddings.Vertical)
|
.padding(vertical = TabbedDialogPaddings.Vertical)
|
||||||
.verticalScroll(rememberScrollState()),
|
.verticalScroll(rememberScrollState()),
|
||||||
) {
|
) {
|
||||||
|
@ -219,23 +218,25 @@ private fun ColumnScope.DisplayPage(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val columns by columnPreference.changes().collectAsState(initial = 0)
|
val columns by columnPreference.collectAsState()
|
||||||
Column {
|
Column(modifier = Modifier.weight(0.5f)) {
|
||||||
Text(
|
Text(
|
||||||
stringResource(id = R.string.pref_library_columns),
|
stringResource(id = R.string.pref_library_columns),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
)
|
)
|
||||||
|
Text(
|
||||||
if (columns > 0) {
|
if (columns > 0) {
|
||||||
Text(stringResource(id = R.string.pref_library_columns_per_row, columns))
|
stringResource(id = R.string.pref_library_columns_per_row, columns)
|
||||||
} else {
|
} else {
|
||||||
Text(stringResource(id = R.string.label_default))
|
stringResource(id = R.string.label_default)
|
||||||
}
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Slider(
|
Slider(
|
||||||
value = columns.toFloat(),
|
value = columns.toFloat(),
|
||||||
onValueChange = { columnPreference.set(it.toInt()) },
|
onValueChange = { columnPreference.set(it.toInt()) },
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1.5f),
|
||||||
valueRange = 0f..10f,
|
valueRange = 0f..10f,
|
||||||
steps = 10,
|
steps = 10,
|
||||||
)
|
)
|
||||||
|
|
|
@ -19,6 +19,7 @@ import androidx.compose.material.minimumInteractiveComponentSize
|
||||||
import androidx.compose.material3.DatePicker
|
import androidx.compose.material3.DatePicker
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.RadioButton
|
import androidx.compose.material3.RadioButton
|
||||||
|
import androidx.compose.material3.SelectableDates
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.rememberDatePickerState
|
import androidx.compose.material3.rememberDatePickerState
|
||||||
|
@ -140,13 +141,14 @@ fun TrackScoreSelector(
|
||||||
fun TrackDateSelector(
|
fun TrackDateSelector(
|
||||||
title: String,
|
title: String,
|
||||||
initialSelectedDateMillis: Long,
|
initialSelectedDateMillis: Long,
|
||||||
dateValidator: (Long) -> Boolean,
|
selectableDates: SelectableDates,
|
||||||
onConfirm: (Long) -> Unit,
|
onConfirm: (Long) -> Unit,
|
||||||
onRemove: (() -> Unit)?,
|
onRemove: (() -> Unit)?,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val pickerState = rememberDatePickerState(
|
val pickerState = rememberDatePickerState(
|
||||||
initialSelectedDateMillis = initialSelectedDateMillis,
|
initialSelectedDateMillis = initialSelectedDateMillis,
|
||||||
|
selectableDates = selectableDates,
|
||||||
)
|
)
|
||||||
AlertDialogContent(
|
AlertDialogContent(
|
||||||
modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars),
|
modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars),
|
||||||
|
@ -155,7 +157,6 @@ fun TrackDateSelector(
|
||||||
Column {
|
Column {
|
||||||
DatePicker(
|
DatePicker(
|
||||||
state = pickerState,
|
state = pickerState,
|
||||||
dateValidator = dateValidator,
|
|
||||||
title = null,
|
title = null,
|
||||||
headline = null,
|
headline = null,
|
||||||
showModeToggle = false,
|
showModeToggle = false,
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
package eu.kanade.presentation.util
|
package eu.kanade.presentation.util
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||||
|
import androidx.compose.animation.ContentTransform
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.ProvidableCompositionLocal
|
import androidx.compose.runtime.ProvidableCompositionLocal
|
||||||
import androidx.compose.runtime.staticCompositionLocalOf
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
import cafe.adriel.voyager.core.model.ScreenModel
|
import cafe.adriel.voyager.core.model.ScreenModel
|
||||||
import cafe.adriel.voyager.core.model.ScreenModelStore
|
import cafe.adriel.voyager.core.model.ScreenModelStore
|
||||||
import cafe.adriel.voyager.core.screen.Screen
|
import cafe.adriel.voyager.core.screen.Screen
|
||||||
|
@ -10,7 +14,7 @@ import cafe.adriel.voyager.core.screen.ScreenKey
|
||||||
import cafe.adriel.voyager.core.screen.uniqueScreenKey
|
import cafe.adriel.voyager.core.screen.uniqueScreenKey
|
||||||
import cafe.adriel.voyager.core.stack.StackEvent
|
import cafe.adriel.voyager.core.stack.StackEvent
|
||||||
import cafe.adriel.voyager.navigator.Navigator
|
import cafe.adriel.voyager.navigator.Navigator
|
||||||
import cafe.adriel.voyager.transitions.ScreenTransition
|
import cafe.adriel.voyager.transitions.ScreenTransitionContent
|
||||||
import kotlinx.coroutines.CoroutineName
|
import kotlinx.coroutines.CoroutineName
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -65,3 +69,22 @@ fun DefaultNavigatorScreenTransition(navigator: Navigator) {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ScreenTransition(
|
||||||
|
navigator: Navigator,
|
||||||
|
transition: AnimatedContentTransitionScope<Screen>.() -> ContentTransform,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
content: ScreenTransitionContent = { it.Content() },
|
||||||
|
) {
|
||||||
|
AnimatedContent(
|
||||||
|
targetState = navigator.lastItem,
|
||||||
|
transitionSpec = transition,
|
||||||
|
modifier = modifier,
|
||||||
|
label = "",
|
||||||
|
) { screen ->
|
||||||
|
navigator.saveableState("transition", screen) {
|
||||||
|
content(screen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -114,7 +114,7 @@ fun WebViewScreenContent(
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
val webClient = remember {
|
val webClient = remember {
|
||||||
object : AccompanistWebViewClient() {
|
object : AccompanistWebViewClient() {
|
||||||
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
|
override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) {
|
||||||
super.onPageStarted(view, url, favicon)
|
super.onPageStarted(view, url, favicon)
|
||||||
url?.let { onUrlChange(it) }
|
url?.let { onUrlChange(it) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,6 @@ import tachiyomi.domain.backup.service.BackupPreferences
|
||||||
import tachiyomi.domain.entries.TriStateFilter
|
import tachiyomi.domain.entries.TriStateFilter
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences
|
import tachiyomi.domain.library.service.LibraryPreferences
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_NON_COMPLETED
|
import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_NON_COMPLETED
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
|
@ -150,7 +149,6 @@ object Migrations {
|
||||||
// Force MAL log out due to login flow change
|
// Force MAL log out due to login flow change
|
||||||
// v52: switched from scraping to WebView
|
// v52: switched from scraping to WebView
|
||||||
// v53: switched from WebView to OAuth
|
// v53: switched from WebView to OAuth
|
||||||
val trackManager = Injekt.get<TrackManager>()
|
|
||||||
if (trackManager.myAnimeList.isLogged) {
|
if (trackManager.myAnimeList.isLogged) {
|
||||||
trackManager.myAnimeList.logout()
|
trackManager.myAnimeList.logout()
|
||||||
context.toast(R.string.myanimelist_relogin)
|
context.toast(R.string.myanimelist_relogin)
|
||||||
|
|
|
@ -12,7 +12,6 @@ import com.arthenica.ffmpegkit.Level
|
||||||
import com.arthenica.ffmpegkit.LogCallback
|
import com.arthenica.ffmpegkit.LogCallback
|
||||||
import com.arthenica.ffmpegkit.SessionState
|
import com.arthenica.ffmpegkit.SessionState
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import com.jakewharton.rxrelay.PublishRelay
|
|
||||||
import eu.kanade.domain.items.episode.model.toSEpisode
|
import eu.kanade.domain.items.episode.model.toSEpisode
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.animesource.UnmeteredSource
|
import eu.kanade.tachiyomi.animesource.UnmeteredSource
|
||||||
|
@ -26,16 +25,26 @@ import eu.kanade.tachiyomi.data.notification.NotificationHandler
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.storage.saveTo
|
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||||
import eu.kanade.tachiyomi.util.storage.toFFmpegString
|
import eu.kanade.tachiyomi.util.storage.toFFmpegString
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.transformLatest
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.supervisorScope
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Subscription
|
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.schedulers.Schedulers
|
|
||||||
import rx.subjects.PublishSubject
|
import rx.subjects.PublishSubject
|
||||||
import tachiyomi.core.util.lang.launchIO
|
import tachiyomi.core.util.lang.launchIO
|
||||||
import tachiyomi.core.util.lang.launchNow
|
import tachiyomi.core.util.lang.launchNow
|
||||||
|
@ -52,6 +61,7 @@ import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class is the one in charge of downloading episodes.
|
* This class is the one in charge of downloading episodes.
|
||||||
|
@ -87,15 +97,8 @@ class AnimeDownloader(
|
||||||
*/
|
*/
|
||||||
private val notifier by lazy { AnimeDownloadNotifier(context) }
|
private val notifier by lazy { AnimeDownloadNotifier(context) }
|
||||||
|
|
||||||
/**
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
* AnimeDownloader subscription.
|
private var downloaderJob: Job? = null
|
||||||
*/
|
|
||||||
private var subscription: Subscription? = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Relay to send a list of downloads to the downloader.
|
|
||||||
*/
|
|
||||||
private val downloadsRelay = PublishRelay.create<List<AnimeDownload>>()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Preference for user's choice of external downloader
|
* Preference for user's choice of external downloader
|
||||||
|
@ -106,7 +109,7 @@ class AnimeDownloader(
|
||||||
* Whether the downloader is running.
|
* Whether the downloader is running.
|
||||||
*/
|
*/
|
||||||
val isRunning: Boolean
|
val isRunning: Boolean
|
||||||
get() = subscription != null
|
get() = downloaderJob?.isActive ?: false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the downloader is paused
|
* Whether the downloader is paused
|
||||||
|
@ -134,18 +137,17 @@ class AnimeDownloader(
|
||||||
* @return true if the downloader is started, false otherwise.
|
* @return true if the downloader is started, false otherwise.
|
||||||
*/
|
*/
|
||||||
fun start(): Boolean {
|
fun start(): Boolean {
|
||||||
if (subscription != null || queueState.value.isEmpty()) {
|
if (isRunning || queueState.value.isEmpty()) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeSubscription()
|
|
||||||
|
|
||||||
val pending = queueState.value.filter { it.status != AnimeDownload.State.DOWNLOADED }
|
val pending = queueState.value.filter { it.status != AnimeDownload.State.DOWNLOADED }
|
||||||
pending.forEach { if (it.status != AnimeDownload.State.QUEUE) it.status = AnimeDownload.State.QUEUE }
|
pending.forEach { if (it.status != AnimeDownload.State.QUEUE) it.status = AnimeDownload.State.QUEUE }
|
||||||
|
|
||||||
isPaused = false
|
isPaused = false
|
||||||
|
|
||||||
downloadsRelay.call(pending)
|
launchDownloaderJob()
|
||||||
|
|
||||||
return pending.isNotEmpty()
|
return pending.isNotEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -153,7 +155,7 @@ class AnimeDownloader(
|
||||||
* Stops the downloader.
|
* Stops the downloader.
|
||||||
*/
|
*/
|
||||||
fun stop(reason: String? = null) {
|
fun stop(reason: String? = null) {
|
||||||
destroySubscription()
|
cancelDownloaderJob()
|
||||||
queueState.value
|
queueState.value
|
||||||
.filter { it.status == AnimeDownload.State.DOWNLOADING }
|
.filter { it.status == AnimeDownload.State.DOWNLOADING }
|
||||||
.forEach { it.status = AnimeDownload.State.ERROR }
|
.forEach { it.status = AnimeDownload.State.ERROR }
|
||||||
|
@ -181,7 +183,7 @@ class AnimeDownloader(
|
||||||
* Pauses the downloader
|
* Pauses the downloader
|
||||||
*/
|
*/
|
||||||
fun pause() {
|
fun pause() {
|
||||||
destroySubscription()
|
cancelDownloaderJob()
|
||||||
queueState.value
|
queueState.value
|
||||||
.filter { it.status == AnimeDownload.State.DOWNLOADING }
|
.filter { it.status == AnimeDownload.State.DOWNLOADING }
|
||||||
.forEach { it.status = AnimeDownload.State.QUEUE }
|
.forEach { it.status = AnimeDownload.State.QUEUE }
|
||||||
|
@ -192,7 +194,7 @@ class AnimeDownloader(
|
||||||
* Removes everything from the queue.
|
* Removes everything from the queue.
|
||||||
*/
|
*/
|
||||||
fun clearQueue() {
|
fun clearQueue() {
|
||||||
destroySubscription()
|
cancelDownloaderJob()
|
||||||
|
|
||||||
_clearQueue()
|
_clearQueue()
|
||||||
notifier.dismissProgress()
|
notifier.dismissProgress()
|
||||||
|
@ -201,52 +203,72 @@ class AnimeDownloader(
|
||||||
/**
|
/**
|
||||||
* Prepares the subscriptions to start downloading.
|
* Prepares the subscriptions to start downloading.
|
||||||
*/
|
*/
|
||||||
private fun initializeSubscription() {
|
private fun launchDownloaderJob() {
|
||||||
// Unsubscribe the previous subscription if it exists
|
if (isRunning) return
|
||||||
destroySubscription()
|
|
||||||
|
|
||||||
subscription = downloadsRelay.flatMapIterable { it }
|
downloaderJob = scope.launch {
|
||||||
// Concurrently download from 3 different sources
|
val activeDownloadsFlow = queueState.transformLatest { queue ->
|
||||||
|
while (true) {
|
||||||
|
val activeDownloads = queue.asSequence()
|
||||||
|
.filter { it.status.value <= AnimeDownload.State.DOWNLOADING.value } // Ignore completed downloads, leave them in the queue
|
||||||
.groupBy { it.source }
|
.groupBy { it.source }
|
||||||
.flatMap(
|
.toList().take(5) // Concurrently download from 5 different sources
|
||||||
{ bySource ->
|
.map { (_, downloads) -> downloads.first() }
|
||||||
bySource.flatMap(
|
emit(activeDownloads)
|
||||||
{ download ->
|
|
||||||
|
if (activeDownloads.isEmpty()) break
|
||||||
|
// Suspend until a download enters the ERROR state
|
||||||
|
val activeDownloadsErroredFlow =
|
||||||
|
combine(activeDownloads.map(AnimeDownload::statusFlow)) { states ->
|
||||||
|
states.contains(AnimeDownload.State.ERROR)
|
||||||
|
}.filter { it }
|
||||||
|
activeDownloadsErroredFlow.first()
|
||||||
|
}
|
||||||
|
}.distinctUntilChanged()
|
||||||
|
|
||||||
|
// Use supervisorScope to cancel child jobs when the downloader job is cancelled
|
||||||
|
supervisorScope {
|
||||||
|
val downloadJobs = mutableMapOf<AnimeDownload, Job>()
|
||||||
|
|
||||||
|
activeDownloadsFlow.collectLatest { activeDownloads ->
|
||||||
|
val downloadJobsToStop = downloadJobs.filter { it.key !in activeDownloads }
|
||||||
|
downloadJobsToStop.forEach { (download, job) ->
|
||||||
|
job.cancel()
|
||||||
|
downloadJobs.remove(download)
|
||||||
|
}
|
||||||
|
|
||||||
|
val downloadsToStart = activeDownloads.filter { it !in downloadJobs }
|
||||||
|
downloadsToStart.forEach { download ->
|
||||||
|
downloadJobs[download] = launchDownloadJob(download)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.launchDownloadJob(download: AnimeDownload) = launchIO {
|
||||||
|
try {
|
||||||
downloadEpisode(download)
|
downloadEpisode(download)
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
},
|
|
||||||
if (sourceManager.get(bySource.key.id) is UnmeteredSource) {
|
|
||||||
downloadPreferences.numberOfDownloads().get()
|
|
||||||
} else {
|
|
||||||
1
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
5,
|
|
||||||
)
|
|
||||||
.subscribe(
|
|
||||||
{
|
|
||||||
// Remove successful download from queue
|
// Remove successful download from queue
|
||||||
if (it.status == AnimeDownload.State.DOWNLOADED) {
|
if (download.status == AnimeDownload.State.DOWNLOADED) {
|
||||||
removeFromQueue(it)
|
removeFromQueue(download)
|
||||||
}
|
}
|
||||||
if (areAllAnimeDownloadsFinished()) {
|
if (areAllAnimeDownloadsFinished()) {
|
||||||
stop()
|
stop()
|
||||||
}
|
}
|
||||||
},
|
} catch (e: Throwable) {
|
||||||
{ error ->
|
if (e is CancellationException) throw e
|
||||||
logcat(LogPriority.ERROR, error)
|
logcat(LogPriority.ERROR, e)
|
||||||
notifier.onError(error.message)
|
notifier.onError(e.message)
|
||||||
stop()
|
stop()
|
||||||
},
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Destroys the downloader subscriptions.
|
* Destroys the downloader subscriptions.
|
||||||
*/
|
*/
|
||||||
private fun destroySubscription() {
|
private fun cancelDownloaderJob() {
|
||||||
isFFmpegRunning = false
|
isFFmpegRunning = false
|
||||||
FFmpegKitConfig.getSessions().filter {
|
FFmpegKitConfig.getSessions().filter {
|
||||||
it.isFFmpeg && (it.state == SessionState.CREATED || it.state == SessionState.RUNNING)
|
it.isFFmpeg && (it.state == SessionState.CREATED || it.state == SessionState.RUNNING)
|
||||||
|
@ -254,8 +276,8 @@ class AnimeDownloader(
|
||||||
it.cancel()
|
it.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
subscription?.unsubscribe()
|
downloaderJob?.cancel()
|
||||||
subscription = null
|
downloaderJob = null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -272,17 +294,13 @@ class AnimeDownloader(
|
||||||
|
|
||||||
val source = sourceManager.get(anime.source) as? AnimeHttpSource ?: return@launchIO
|
val source = sourceManager.get(anime.source) as? AnimeHttpSource ?: return@launchIO
|
||||||
val wasEmpty = queueState.value.isEmpty()
|
val wasEmpty = queueState.value.isEmpty()
|
||||||
// Called in background thread, the operation can be slow with SAF.
|
val episodesWithoutDir = episodes
|
||||||
val episodesWithoutDir = async {
|
|
||||||
episodes
|
|
||||||
// Filter out those already downloaded.
|
// Filter out those already downloaded.
|
||||||
.filter { provider.findEpisodeDir(it.name, it.scanlator, anime.title, source) == null }
|
.filter { provider.findEpisodeDir(it.name, it.scanlator, anime.title, source) == null }
|
||||||
// Add episodes to queue from the start.
|
// Add chapters to queue from the start.
|
||||||
.sortedByDescending { it.sourceOrder }
|
.sortedByDescending { it.sourceOrder }
|
||||||
}
|
|
||||||
|
|
||||||
// Runs in main thread (synchronization needed).
|
val episodesToQueue = episodesWithoutDir
|
||||||
val episodesToQueue = episodesWithoutDir.await()
|
|
||||||
// Filter out those already enqueued.
|
// Filter out those already enqueued.
|
||||||
.filter { episode -> queueState.value.none { it.episode.id == episode.id } }
|
.filter { episode -> queueState.value.none { it.episode.id == episode.id } }
|
||||||
// Create a download for each one.
|
// Create a download for each one.
|
||||||
|
@ -291,11 +309,6 @@ class AnimeDownloader(
|
||||||
if (episodesToQueue.isNotEmpty()) {
|
if (episodesToQueue.isNotEmpty()) {
|
||||||
addAllToQueue(episodesToQueue)
|
addAllToQueue(episodesToQueue)
|
||||||
|
|
||||||
if (isRunning) {
|
|
||||||
// Send the list of downloads to the downloader.
|
|
||||||
downloadsRelay.call(episodesToQueue)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start downloader if needed
|
// Start downloader if needed
|
||||||
if (autoStart && wasEmpty) {
|
if (autoStart && wasEmpty) {
|
||||||
val queuedDownloads = queueState.value.count { it: AnimeDownload -> it.source !is UnmeteredSource }
|
val queuedDownloads = queueState.value.count { it: AnimeDownload -> it.source !is UnmeteredSource }
|
||||||
|
@ -515,7 +528,12 @@ class AnimeDownloader(
|
||||||
}
|
}
|
||||||
|
|
||||||
var duration = 0L
|
var duration = 0L
|
||||||
|
var nextLineIsDuration = false
|
||||||
val logCallback = LogCallback { log ->
|
val logCallback = LogCallback { log ->
|
||||||
|
if (nextLineIsDuration) {
|
||||||
|
parseDuration(log.message)?.let { duration = it }
|
||||||
|
nextLineIsDuration = false
|
||||||
|
}
|
||||||
if (log.level <= Level.AV_LOG_WARNING) log.message?.let { logcat { it } }
|
if (log.level <= Level.AV_LOG_WARNING) log.message?.let { logcat { it } }
|
||||||
if (duration != 0L && log.message.startsWith("frame=")) {
|
if (duration != 0L && log.message.startsWith("frame=")) {
|
||||||
val outTime = log.message
|
val outTime = log.message
|
||||||
|
@ -806,7 +824,7 @@ class AnimeDownloader(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private inline fun removeFromQueueByPredicate(predicate: (AnimeDownload) -> Boolean) {
|
private inline fun removeFromQueueIf(predicate: (AnimeDownload) -> Boolean) {
|
||||||
_queueState.update { queue ->
|
_queueState.update { queue ->
|
||||||
val downloads = queue.filter { predicate(it) }
|
val downloads = queue.filter { predicate(it) }
|
||||||
store.removeAll(downloads)
|
store.removeAll(downloads)
|
||||||
|
@ -821,11 +839,11 @@ class AnimeDownloader(
|
||||||
|
|
||||||
fun removeFromQueue(episodes: List<Episode>) {
|
fun removeFromQueue(episodes: List<Episode>) {
|
||||||
val episodeIds = episodes.map { it.id }
|
val episodeIds = episodes.map { it.id }
|
||||||
removeFromQueueByPredicate { it.episode.id in episodeIds }
|
removeFromQueueIf { it.episode.id in episodeIds }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeFromQueue(anime: Anime) {
|
fun removeFromQueue(anime: Anime) {
|
||||||
removeFromQueueByPredicate { it.anime.id == anime.id }
|
removeFromQueueIf { it.anime.id == anime.id }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun _clearQueue() {
|
private fun _clearQueue() {
|
||||||
|
|
|
@ -4,7 +4,6 @@ import android.content.Context
|
||||||
import aniyomi.util.DataSaver
|
import aniyomi.util.DataSaver
|
||||||
import aniyomi.util.DataSaver.Companion.getImage
|
import aniyomi.util.DataSaver.Companion.getImage
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import com.jakewharton.rxrelay.PublishRelay
|
|
||||||
import eu.kanade.domain.entries.manga.model.getComicInfo
|
import eu.kanade.domain.entries.manga.model.getComicInfo
|
||||||
import eu.kanade.domain.items.chapter.model.toSChapter
|
import eu.kanade.domain.items.chapter.model.toSChapter
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
|
@ -20,26 +19,31 @@ import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil.NOMEDIA_FILE
|
import eu.kanade.tachiyomi.util.storage.DiskUtil.NOMEDIA_FILE
|
||||||
import eu.kanade.tachiyomi.util.storage.saveTo
|
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asFlow
|
import kotlinx.coroutines.flow.asFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.flatMapMerge
|
import kotlinx.coroutines.flow.flatMapMerge
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.retryWhen
|
import kotlinx.coroutines.flow.retryWhen
|
||||||
|
import kotlinx.coroutines.flow.transformLatest
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.supervisorScope
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import nl.adaptivity.xmlutil.serialization.XML
|
import nl.adaptivity.xmlutil.serialization.XML
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Observable
|
|
||||||
import rx.Subscription
|
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
|
||||||
import rx.schedulers.Schedulers
|
|
||||||
import tachiyomi.core.metadata.comicinfo.COMIC_INFO_FILE
|
import tachiyomi.core.metadata.comicinfo.COMIC_INFO_FILE
|
||||||
import tachiyomi.core.metadata.comicinfo.ComicInfo
|
import tachiyomi.core.metadata.comicinfo.ComicInfo
|
||||||
import tachiyomi.core.util.lang.awaitSingle
|
import tachiyomi.core.util.lang.awaitSingle
|
||||||
|
@ -99,21 +103,14 @@ class MangaDownloader(
|
||||||
*/
|
*/
|
||||||
private val notifier by lazy { MangaDownloadNotifier(context) }
|
private val notifier by lazy { MangaDownloadNotifier(context) }
|
||||||
|
|
||||||
/**
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
* Downloader subscription.
|
private var downloaderJob: Job? = null
|
||||||
*/
|
|
||||||
private var subscription: Subscription? = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Relay to send a list of downloads to the downloader.
|
|
||||||
*/
|
|
||||||
private val downloadsRelay = PublishRelay.create<List<MangaDownload>>()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the downloader is running.
|
* Whether the downloader is running.
|
||||||
*/
|
*/
|
||||||
val isRunning: Boolean
|
val isRunning: Boolean
|
||||||
get() = subscription != null
|
get() = downloaderJob?.isActive ?: false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the downloader is paused
|
* Whether the downloader is paused
|
||||||
|
@ -135,18 +132,17 @@ class MangaDownloader(
|
||||||
* @return true if the downloader is started, false otherwise.
|
* @return true if the downloader is started, false otherwise.
|
||||||
*/
|
*/
|
||||||
fun start(): Boolean {
|
fun start(): Boolean {
|
||||||
if (subscription != null || queueState.value.isEmpty()) {
|
if (isRunning || queueState.value.isEmpty()) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeSubscription()
|
|
||||||
|
|
||||||
val pending = queueState.value.filter { it.status != MangaDownload.State.DOWNLOADED }
|
val pending = queueState.value.filter { it.status != MangaDownload.State.DOWNLOADED }
|
||||||
pending.forEach { if (it.status != MangaDownload.State.QUEUE) it.status = MangaDownload.State.QUEUE }
|
pending.forEach { if (it.status != MangaDownload.State.QUEUE) it.status = MangaDownload.State.QUEUE }
|
||||||
|
|
||||||
isPaused = false
|
isPaused = false
|
||||||
|
|
||||||
downloadsRelay.call(pending)
|
launchDownloaderJob()
|
||||||
|
|
||||||
return pending.isNotEmpty()
|
return pending.isNotEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -154,7 +150,7 @@ class MangaDownloader(
|
||||||
* Stops the downloader.
|
* Stops the downloader.
|
||||||
*/
|
*/
|
||||||
fun stop(reason: String? = null) {
|
fun stop(reason: String? = null) {
|
||||||
destroySubscription()
|
cancelDownloaderJob()
|
||||||
queueState.value
|
queueState.value
|
||||||
.filter { it.status == MangaDownload.State.DOWNLOADING }
|
.filter { it.status == MangaDownload.State.DOWNLOADING }
|
||||||
.forEach { it.status = MangaDownload.State.ERROR }
|
.forEach { it.status = MangaDownload.State.ERROR }
|
||||||
|
@ -182,7 +178,7 @@ class MangaDownloader(
|
||||||
* Pauses the downloader
|
* Pauses the downloader
|
||||||
*/
|
*/
|
||||||
fun pause() {
|
fun pause() {
|
||||||
destroySubscription()
|
cancelDownloaderJob()
|
||||||
queueState.value
|
queueState.value
|
||||||
.filter { it.status == MangaDownload.State.DOWNLOADING }
|
.filter { it.status == MangaDownload.State.DOWNLOADING }
|
||||||
.forEach { it.status = MangaDownload.State.QUEUE }
|
.forEach { it.status = MangaDownload.State.QUEUE }
|
||||||
|
@ -193,7 +189,7 @@ class MangaDownloader(
|
||||||
* Removes everything from the queue.
|
* Removes everything from the queue.
|
||||||
*/
|
*/
|
||||||
fun clearQueue() {
|
fun clearQueue() {
|
||||||
destroySubscription()
|
cancelDownloaderJob()
|
||||||
|
|
||||||
_clearQueue()
|
_clearQueue()
|
||||||
notifier.dismissProgress()
|
notifier.dismissProgress()
|
||||||
|
@ -202,49 +198,74 @@ class MangaDownloader(
|
||||||
/**
|
/**
|
||||||
* Prepares the subscriptions to start downloading.
|
* Prepares the subscriptions to start downloading.
|
||||||
*/
|
*/
|
||||||
private fun initializeSubscription() {
|
private fun launchDownloaderJob() {
|
||||||
if (subscription != null) return
|
if (isRunning) return
|
||||||
|
|
||||||
subscription = downloadsRelay.concatMapIterable { it }
|
downloaderJob = scope.launch {
|
||||||
// Concurrently download from 5 different sources
|
val activeDownloadsFlow = queueState.transformLatest { queue ->
|
||||||
|
while (true) {
|
||||||
|
val activeDownloads = queue.asSequence()
|
||||||
|
.filter { it.status.value <= MangaDownload.State.DOWNLOADING.value } // Ignore completed downloads, leave them in the queue
|
||||||
.groupBy { it.source }
|
.groupBy { it.source }
|
||||||
.flatMap(
|
.toList().take(5) // Concurrently download from 5 different sources
|
||||||
{ bySource ->
|
.map { (_, downloads) -> downloads.first() }
|
||||||
bySource.concatMap { download ->
|
emit(activeDownloads)
|
||||||
Observable.fromCallable {
|
|
||||||
runBlocking { downloadChapter(download) }
|
if (activeDownloads.isEmpty()) break
|
||||||
download
|
// Suspend until a download enters the ERROR state
|
||||||
}.subscribeOn(Schedulers.io())
|
val activeDownloadsErroredFlow =
|
||||||
|
combine(activeDownloads.map(MangaDownload::statusFlow)) { states ->
|
||||||
|
states.contains(MangaDownload.State.ERROR)
|
||||||
|
}.filter { it }
|
||||||
|
activeDownloadsErroredFlow.first()
|
||||||
}
|
}
|
||||||
},
|
}.distinctUntilChanged()
|
||||||
5,
|
|
||||||
)
|
// Use supervisorScope to cancel child jobs when the downloader job is cancelled
|
||||||
.onBackpressureLatest()
|
supervisorScope {
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
val downloadJobs = mutableMapOf<MangaDownload, Job>()
|
||||||
.subscribe(
|
|
||||||
{
|
activeDownloadsFlow.collectLatest { activeDownloads ->
|
||||||
|
val downloadJobsToStop = downloadJobs.filter { it.key !in activeDownloads }
|
||||||
|
downloadJobsToStop.forEach { (download, job) ->
|
||||||
|
job.cancel()
|
||||||
|
downloadJobs.remove(download)
|
||||||
|
}
|
||||||
|
|
||||||
|
val downloadsToStart = activeDownloads.filter { it !in downloadJobs }
|
||||||
|
downloadsToStart.forEach { download ->
|
||||||
|
downloadJobs[download] = launchDownloadJob(download)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.launchDownloadJob(download: MangaDownload) = launchIO {
|
||||||
|
try {
|
||||||
|
downloadChapter(download)
|
||||||
|
|
||||||
// Remove successful download from queue
|
// Remove successful download from queue
|
||||||
if (it.status == MangaDownload.State.DOWNLOADED) {
|
if (download.status == MangaDownload.State.DOWNLOADED) {
|
||||||
removeFromQueue(it)
|
removeFromQueue(download)
|
||||||
}
|
}
|
||||||
if (areAllDownloadsFinished()) {
|
if (areAllDownloadsFinished()) {
|
||||||
stop()
|
stop()
|
||||||
}
|
}
|
||||||
},
|
} catch (e: Throwable) {
|
||||||
{ error ->
|
if (e is CancellationException) throw e
|
||||||
logcat(LogPriority.ERROR, error)
|
logcat(LogPriority.ERROR, e)
|
||||||
notifier.onError(error.message)
|
notifier.onError(e.message)
|
||||||
stop()
|
stop()
|
||||||
},
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Destroys the downloader subscriptions.
|
* Destroys the downloader subscriptions.
|
||||||
*/
|
*/
|
||||||
private fun destroySubscription() {
|
private fun cancelDownloaderJob() {
|
||||||
subscription?.unsubscribe()
|
downloaderJob?.cancel()
|
||||||
subscription = null
|
downloaderJob = null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -261,17 +282,13 @@ class MangaDownloader(
|
||||||
|
|
||||||
val source = sourceManager.get(manga.source) as? HttpSource ?: return@launchIO
|
val source = sourceManager.get(manga.source) as? HttpSource ?: return@launchIO
|
||||||
val wasEmpty = queueState.value.isEmpty()
|
val wasEmpty = queueState.value.isEmpty()
|
||||||
// Called in background thread, the operation can be slow with SAF.
|
val chaptersWithoutDir = chapters
|
||||||
val chaptersWithoutDir = async {
|
|
||||||
chapters
|
|
||||||
// Filter out those already downloaded.
|
// Filter out those already downloaded.
|
||||||
.filter { provider.findChapterDir(it.name, it.scanlator, manga.title, source) == null }
|
.filter { provider.findChapterDir(it.name, it.scanlator, manga.title, source) == null }
|
||||||
// Add chapters to queue from the start.
|
// Add chapters to queue from the start.
|
||||||
.sortedByDescending { it.sourceOrder }
|
.sortedByDescending { it.sourceOrder }
|
||||||
}
|
|
||||||
|
|
||||||
// Runs in main thread (synchronization needed).
|
val chaptersToQueue = chaptersWithoutDir
|
||||||
val chaptersToQueue = chaptersWithoutDir.await()
|
|
||||||
// Filter out those already enqueued.
|
// Filter out those already enqueued.
|
||||||
.filter { chapter -> queueState.value.none { it.chapter.id == chapter.id } }
|
.filter { chapter -> queueState.value.none { it.chapter.id == chapter.id } }
|
||||||
// Create a download for each one.
|
// Create a download for each one.
|
||||||
|
@ -280,11 +297,6 @@ class MangaDownloader(
|
||||||
if (chaptersToQueue.isNotEmpty()) {
|
if (chaptersToQueue.isNotEmpty()) {
|
||||||
addAllToQueue(chaptersToQueue)
|
addAllToQueue(chaptersToQueue)
|
||||||
|
|
||||||
if (isRunning) {
|
|
||||||
// Send the list of downloads to the downloader.
|
|
||||||
downloadsRelay.call(chaptersToQueue)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start downloader if needed
|
// Start downloader if needed
|
||||||
if (autoStart && wasEmpty) {
|
if (autoStart && wasEmpty) {
|
||||||
val queuedDownloads = queueState.value.count { it: MangaDownload -> it.source !is UnmeteredSource }
|
val queuedDownloads = queueState.value.count { it: MangaDownload -> it.source !is UnmeteredSource }
|
||||||
|
@ -665,7 +677,7 @@ class MangaDownloader(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private inline fun removeFromQueueByPredicate(predicate: (MangaDownload) -> Boolean) {
|
private inline fun removeFromQueueIf(predicate: (MangaDownload) -> Boolean) {
|
||||||
_queueState.update { queue ->
|
_queueState.update { queue ->
|
||||||
val downloads = queue.filter { predicate(it) }
|
val downloads = queue.filter { predicate(it) }
|
||||||
store.removeAll(downloads)
|
store.removeAll(downloads)
|
||||||
|
@ -680,11 +692,11 @@ class MangaDownloader(
|
||||||
|
|
||||||
fun removeFromQueue(chapters: List<Chapter>) {
|
fun removeFromQueue(chapters: List<Chapter>) {
|
||||||
val chapterIds = chapters.map { it.id }
|
val chapterIds = chapters.map { it.id }
|
||||||
removeFromQueueByPredicate { it.chapter.id in chapterIds }
|
removeFromQueueIf { it.chapter.id in chapterIds }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeFromQueue(manga: Manga) {
|
fun removeFromQueue(manga: Manga) {
|
||||||
removeFromQueueByPredicate { it.manga.id == manga.id }
|
removeFromQueueIf { it.manga.id == manga.id }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun _clearQueue() {
|
private fun _clearQueue() {
|
||||||
|
|
|
@ -22,9 +22,10 @@ data class MangaDownload(
|
||||||
val source: HttpSource,
|
val source: HttpSource,
|
||||||
val manga: Manga,
|
val manga: Manga,
|
||||||
val chapter: Chapter,
|
val chapter: Chapter,
|
||||||
var pages: List<Page>? = null,
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
var pages: List<Page>? = null
|
||||||
|
|
||||||
val totalProgress: Int
|
val totalProgress: Int
|
||||||
get() = pages?.sumOf(Page::progress) ?: 0
|
get() = pages?.sumOf(Page::progress) ?: 0
|
||||||
|
|
||||||
|
|
|
@ -42,10 +42,8 @@ fun SourceFilterAnimeDialog(
|
||||||
|
|
||||||
AdaptiveSheet(
|
AdaptiveSheet(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
) { contentPadding ->
|
|
||||||
LazyColumn(
|
|
||||||
contentPadding = contentPadding,
|
|
||||||
) {
|
) {
|
||||||
|
LazyColumn {
|
||||||
stickyHeader {
|
stickyHeader {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
|
@ -42,10 +42,8 @@ fun SourceFilterMangaDialog(
|
||||||
|
|
||||||
AdaptiveSheet(
|
AdaptiveSheet(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
) { contentPadding ->
|
|
||||||
LazyColumn(
|
|
||||||
contentPadding = contentPadding,
|
|
||||||
) {
|
) {
|
||||||
|
LazyColumn {
|
||||||
stickyHeader {
|
stickyHeader {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
|
@ -14,6 +14,7 @@ import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.FilledTonalButton
|
import androidx.compose.material3.FilledTonalButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.SelectableDates
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
@ -425,6 +426,62 @@ private data class TrackDateSelectorScreen(
|
||||||
private val start: Boolean,
|
private val start: Boolean,
|
||||||
) : Screen() {
|
) : Screen() {
|
||||||
|
|
||||||
|
private val selectableDates = object : SelectableDates {
|
||||||
|
override fun isSelectableDate(utcTimeMillis: Long): Boolean {
|
||||||
|
val dateToCheck = Instant.ofEpochMilli(utcTimeMillis)
|
||||||
|
.atZone(ZoneOffset.systemDefault())
|
||||||
|
.toLocalDate()
|
||||||
|
|
||||||
|
if (dateToCheck > LocalDate.now()) {
|
||||||
|
// Disallow future dates
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (start && track.finishDate > 0) {
|
||||||
|
// Disallow start date to be set later than finish date
|
||||||
|
val dateFinished = Instant.ofEpochMilli(track.finishDate)
|
||||||
|
.atZone(ZoneId.systemDefault())
|
||||||
|
.toLocalDate()
|
||||||
|
dateToCheck <= dateFinished
|
||||||
|
} else if (!start && track.startDate > 0) {
|
||||||
|
// Disallow end date to be set earlier than start date
|
||||||
|
val dateStarted = Instant.ofEpochMilli(track.startDate)
|
||||||
|
.atZone(ZoneId.systemDefault())
|
||||||
|
.toLocalDate()
|
||||||
|
dateToCheck >= dateStarted
|
||||||
|
} else {
|
||||||
|
// Nothing set before
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isSelectableYear(year: Int): Boolean {
|
||||||
|
if (year > LocalDate.now().year) {
|
||||||
|
// Disallow future dates
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (start && track.finishDate > 0) {
|
||||||
|
// Disallow start date to be set later than finish date
|
||||||
|
val dateFinished = Instant.ofEpochMilli(track.finishDate)
|
||||||
|
.atZone(ZoneId.systemDefault())
|
||||||
|
.toLocalDate()
|
||||||
|
.year
|
||||||
|
year <= dateFinished
|
||||||
|
} else if (!start && track.startDate > 0) {
|
||||||
|
// Disallow end date to be set earlier than start date
|
||||||
|
val dateStarted = Instant.ofEpochMilli(track.startDate)
|
||||||
|
.atZone(ZoneId.systemDefault())
|
||||||
|
.toLocalDate()
|
||||||
|
.year
|
||||||
|
year >= dateStarted
|
||||||
|
} else {
|
||||||
|
// Nothing set before
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun Content() {
|
override fun Content() {
|
||||||
val navigator = LocalNavigator.currentOrThrow
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
|
@ -448,33 +505,7 @@ private data class TrackDateSelectorScreen(
|
||||||
stringResource(R.string.track_finished_reading_date)
|
stringResource(R.string.track_finished_reading_date)
|
||||||
},
|
},
|
||||||
initialSelectedDateMillis = sm.initialSelection,
|
initialSelectedDateMillis = sm.initialSelection,
|
||||||
dateValidator = { utcMillis ->
|
selectableDates = selectableDates,
|
||||||
val dateToCheck = Instant.ofEpochMilli(utcMillis)
|
|
||||||
.atZone(ZoneOffset.systemDefault())
|
|
||||||
.toLocalDate()
|
|
||||||
|
|
||||||
if (dateToCheck > LocalDate.now()) {
|
|
||||||
// Disallow future dates
|
|
||||||
return@TrackDateSelector false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (start && track.finishDate > 0) {
|
|
||||||
// Disallow start date to be set later than finish date
|
|
||||||
val dateFinished = Instant.ofEpochMilli(track.finishDate)
|
|
||||||
.atZone(ZoneId.systemDefault())
|
|
||||||
.toLocalDate()
|
|
||||||
dateToCheck <= dateFinished
|
|
||||||
} else if (!start && track.startDate > 0) {
|
|
||||||
// Disallow end date to be set earlier than start date
|
|
||||||
val dateStarted = Instant.ofEpochMilli(track.startDate)
|
|
||||||
.atZone(ZoneId.systemDefault())
|
|
||||||
.toLocalDate()
|
|
||||||
dateToCheck >= dateStarted
|
|
||||||
} else {
|
|
||||||
// Nothing set before
|
|
||||||
true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onConfirm = { sm.setDate(it); navigator.pop() },
|
onConfirm = { sm.setDate(it); navigator.pop() },
|
||||||
onRemove = { sm.confirmRemoveDate(navigator) }.takeIf { canRemove },
|
onRemove = { sm.confirmRemoveDate(navigator) }.takeIf { canRemove },
|
||||||
onDismissRequest = navigator::pop,
|
onDismissRequest = navigator::pop,
|
||||||
|
|
|
@ -14,6 +14,7 @@ import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.FilledTonalButton
|
import androidx.compose.material3.FilledTonalButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.SelectableDates
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
@ -424,6 +425,62 @@ private data class TrackDateSelectorScreen(
|
||||||
private val start: Boolean,
|
private val start: Boolean,
|
||||||
) : Screen() {
|
) : Screen() {
|
||||||
|
|
||||||
|
private val selectableDates = object : SelectableDates {
|
||||||
|
override fun isSelectableDate(utcTimeMillis: Long): Boolean {
|
||||||
|
val dateToCheck = Instant.ofEpochMilli(utcTimeMillis)
|
||||||
|
.atZone(ZoneOffset.systemDefault())
|
||||||
|
.toLocalDate()
|
||||||
|
|
||||||
|
if (dateToCheck > LocalDate.now()) {
|
||||||
|
// Disallow future dates
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (start && track.finishDate > 0) {
|
||||||
|
// Disallow start date to be set later than finish date
|
||||||
|
val dateFinished = Instant.ofEpochMilli(track.finishDate)
|
||||||
|
.atZone(ZoneId.systemDefault())
|
||||||
|
.toLocalDate()
|
||||||
|
dateToCheck <= dateFinished
|
||||||
|
} else if (!start && track.startDate > 0) {
|
||||||
|
// Disallow end date to be set earlier than start date
|
||||||
|
val dateStarted = Instant.ofEpochMilli(track.startDate)
|
||||||
|
.atZone(ZoneId.systemDefault())
|
||||||
|
.toLocalDate()
|
||||||
|
dateToCheck >= dateStarted
|
||||||
|
} else {
|
||||||
|
// Nothing set before
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isSelectableYear(year: Int): Boolean {
|
||||||
|
if (year > LocalDate.now().year) {
|
||||||
|
// Disallow future dates
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (start && track.finishDate > 0) {
|
||||||
|
// Disallow start date to be set later than finish date
|
||||||
|
val dateFinished = Instant.ofEpochMilli(track.finishDate)
|
||||||
|
.atZone(ZoneId.systemDefault())
|
||||||
|
.toLocalDate()
|
||||||
|
.year
|
||||||
|
year <= dateFinished
|
||||||
|
} else if (!start && track.startDate > 0) {
|
||||||
|
// Disallow end date to be set earlier than start date
|
||||||
|
val dateStarted = Instant.ofEpochMilli(track.startDate)
|
||||||
|
.atZone(ZoneId.systemDefault())
|
||||||
|
.toLocalDate()
|
||||||
|
.year
|
||||||
|
year >= dateStarted
|
||||||
|
} else {
|
||||||
|
// Nothing set before
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun Content() {
|
override fun Content() {
|
||||||
val navigator = LocalNavigator.currentOrThrow
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
|
@ -447,33 +504,7 @@ private data class TrackDateSelectorScreen(
|
||||||
stringResource(R.string.track_finished_reading_date)
|
stringResource(R.string.track_finished_reading_date)
|
||||||
},
|
},
|
||||||
initialSelectedDateMillis = sm.initialSelection,
|
initialSelectedDateMillis = sm.initialSelection,
|
||||||
dateValidator = { utcMillis ->
|
selectableDates = selectableDates,
|
||||||
val dateToCheck = Instant.ofEpochMilli(utcMillis)
|
|
||||||
.atZone(ZoneOffset.systemDefault())
|
|
||||||
.toLocalDate()
|
|
||||||
|
|
||||||
if (dateToCheck > LocalDate.now()) {
|
|
||||||
// Disallow future dates
|
|
||||||
return@TrackDateSelector false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (start && track.finishDate > 0) {
|
|
||||||
// Disallow start date to be set later than finish date
|
|
||||||
val dateFinished = Instant.ofEpochMilli(track.finishDate)
|
|
||||||
.atZone(ZoneId.systemDefault())
|
|
||||||
.toLocalDate()
|
|
||||||
dateToCheck <= dateFinished
|
|
||||||
} else if (!start && track.startDate > 0) {
|
|
||||||
// Disallow end date to be set earlier than start date
|
|
||||||
val dateStarted = Instant.ofEpochMilli(track.startDate)
|
|
||||||
.atZone(ZoneId.systemDefault())
|
|
||||||
.toLocalDate()
|
|
||||||
dateToCheck >= dateStarted
|
|
||||||
} else {
|
|
||||||
// Nothing set before
|
|
||||||
true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onConfirm = { sm.setDate(it); navigator.pop() },
|
onConfirm = { sm.setDate(it); navigator.pop() },
|
||||||
onRemove = { sm.confirmRemoveDate(navigator) }.takeIf { canRemove },
|
onRemove = { sm.confirmRemoveDate(navigator) }.takeIf { canRemove },
|
||||||
onDismissRequest = navigator::pop,
|
onDismissRequest = navigator::pop,
|
||||||
|
|
|
@ -5,7 +5,7 @@ import androidx.compose.animation.AnimatedContent
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.expandVertically
|
import androidx.compose.animation.expandVertically
|
||||||
import androidx.compose.animation.shrinkVertically
|
import androidx.compose.animation.shrinkVertically
|
||||||
import androidx.compose.animation.with
|
import androidx.compose.animation.togetherWith
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
@ -155,7 +155,7 @@ object HomeScreen : Screen() {
|
||||||
AnimatedContent(
|
AnimatedContent(
|
||||||
targetState = tabNavigator.current,
|
targetState = tabNavigator.current,
|
||||||
transitionSpec = {
|
transitionSpec = {
|
||||||
materialFadeThroughIn(initialScale = 1f, durationMillis = TabFadeDuration) with
|
materialFadeThroughIn(initialScale = 1f, durationMillis = TabFadeDuration) togetherWith
|
||||||
materialFadeThroughOut(durationMillis = TabFadeDuration)
|
materialFadeThroughOut(durationMillis = TabFadeDuration)
|
||||||
},
|
},
|
||||||
content = {
|
content = {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -138,4 +138,4 @@
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:visibility="gone" />
|
android:visibility="gone" />
|
||||||
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
</FrameLayout>
|
||||||
|
|
|
@ -17,6 +17,6 @@ class NetworkPreferences(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun defaultUserAgent(): Preference<String> {
|
fun defaultUserAgent(): Preference<String> {
|
||||||
return preferenceStore.getString("default_user_agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:110.0) Gecko/20100101 Firefox/110.0")
|
return preferenceStore.getString("default_user_agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:110.0) Gecko/20100101 Firefox/111.0")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import tachiyomi.core.util.system.logcat
|
||||||
object WebViewUtil {
|
object WebViewUtil {
|
||||||
const val SPOOF_PACKAGE_NAME = "org.chromium.chrome"
|
const val SPOOF_PACKAGE_NAME = "org.chromium.chrome"
|
||||||
|
|
||||||
const val MINIMUM_WEBVIEW_VERSION = 108
|
const val MINIMUM_WEBVIEW_VERSION = 109
|
||||||
|
|
||||||
fun supportsWebView(context: Context): Boolean {
|
fun supportsWebView(context: Context): Boolean {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -9,10 +9,9 @@ annotation = "androidx.annotation:annotation:1.7.0-alpha02"
|
||||||
appcompat = "androidx.appcompat:appcompat:1.6.1"
|
appcompat = "androidx.appcompat:appcompat:1.6.1"
|
||||||
biometricktx = "androidx.biometric:biometric-ktx:1.2.0-alpha05"
|
biometricktx = "androidx.biometric:biometric-ktx:1.2.0-alpha05"
|
||||||
constraintlayout = "androidx.constraintlayout:constraintlayout:2.1.4"
|
constraintlayout = "androidx.constraintlayout:constraintlayout:2.1.4"
|
||||||
coordinatorlayout = "androidx.coordinatorlayout:coordinatorlayout:1.2.0"
|
corektx = "androidx.core:core-ktx:1.11.0-beta01"
|
||||||
corektx = "androidx.core:core-ktx:1.11.0-alpha04"
|
|
||||||
splashscreen = "androidx.core:core-splashscreen:1.0.0-alpha02"
|
splashscreen = "androidx.core:core-splashscreen:1.0.0-alpha02"
|
||||||
recyclerview = "androidx.recyclerview:recyclerview:1.3.0"
|
recyclerview = "androidx.recyclerview:recyclerview:1.3.1-rc01"
|
||||||
viewpager = "androidx.viewpager:viewpager:1.1.0-alpha01"
|
viewpager = "androidx.viewpager:viewpager:1.1.0-alpha01"
|
||||||
glance = "androidx.glance:glance-appwidget:1.0.0-alpha03"
|
glance = "androidx.glance:glance-appwidget:1.0.0-alpha03"
|
||||||
profileinstaller = "androidx.profileinstaller:profileinstaller:1.3.1"
|
profileinstaller = "androidx.profileinstaller:profileinstaller:1.3.1"
|
||||||
|
@ -26,7 +25,7 @@ work-runtime = "androidx.work:work-runtime-ktx:2.8.1"
|
||||||
guava = "com.google.guava:guava:31.1-android"
|
guava = "com.google.guava:guava:31.1-android"
|
||||||
|
|
||||||
paging-runtime = "androidx.paging:paging-runtime:3.1.1"
|
paging-runtime = "androidx.paging:paging-runtime:3.1.1"
|
||||||
paging-compose = "androidx.paging:paging-compose:1.0.0-alpha19"
|
paging-compose = "androidx.paging:paging-compose:1.0.0-alpha20"
|
||||||
|
|
||||||
benchmark-macro = "androidx.benchmark:benchmark-macro-junit4:1.1.1"
|
benchmark-macro = "androidx.benchmark:benchmark-macro-junit4:1.1.1"
|
||||||
test-ext = "androidx.test.ext:junit-ktx:1.1.5"
|
test-ext = "androidx.test.ext:junit-ktx:1.1.5"
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
[versions]
|
[versions]
|
||||||
compiler = "1.4.7"
|
compiler = "1.4.7"
|
||||||
compose-bom = "2023.03.00"
|
compose-bom = "2023.04.00-alpha04"
|
||||||
accompanist = "0.30.1"
|
accompanist = "0.31.2-alpha"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
activity = "androidx.activity:activity-compose:1.7.1"
|
activity = "androidx.activity:activity-compose:1.7.2"
|
||||||
bom = { group = "dev.chrisbanes.compose", name = "compose-bom", version.ref = "compose-bom" }
|
bom = { group = "dev.chrisbanes.compose", name = "compose-bom", version.ref = "compose-bom" }
|
||||||
foundation = { module = "androidx.compose.foundation:foundation" }
|
foundation = { module = "androidx.compose.foundation:foundation" }
|
||||||
animation = { module = "androidx.compose.animation:animation" }
|
animation = { module = "androidx.compose.animation:animation" }
|
||||||
|
|
|
@ -5,7 +5,7 @@ coil_version = "2.3.0"
|
||||||
shizuku_version = "12.2.0"
|
shizuku_version = "12.2.0"
|
||||||
sqlite = "2.3.1"
|
sqlite = "2.3.1"
|
||||||
sqldelight = "1.5.5"
|
sqldelight = "1.5.5"
|
||||||
leakcanary = "2.10"
|
leakcanary = "2.11"
|
||||||
voyager = "1.0.0-rc06"
|
voyager = "1.0.0-rc06"
|
||||||
richtext = "0.16.0"
|
richtext = "0.16.0"
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ junrar = "com.github.junrar:junrar:7.5.4"
|
||||||
|
|
||||||
sqlite-framework = { module = "androidx.sqlite:sqlite-framework", version.ref = "sqlite" }
|
sqlite-framework = { module = "androidx.sqlite:sqlite-framework", version.ref = "sqlite" }
|
||||||
sqlite-ktx = { module = "androidx.sqlite:sqlite-ktx", version.ref = "sqlite" }
|
sqlite-ktx = { module = "androidx.sqlite:sqlite-ktx", version.ref = "sqlite" }
|
||||||
sqlite-android = "com.github.requery:sqlite-android:3.41.1"
|
sqlite-android = "com.github.requery:sqlite-android:3.42.0"
|
||||||
|
|
||||||
preferencektx = "androidx.preference:preference-ktx:1.2.0"
|
preferencektx = "androidx.preference:preference-ktx:1.2.0"
|
||||||
|
|
||||||
|
@ -55,12 +55,10 @@ richtext-m3 = { module = "com.halilibo.compose-richtext:richtext-ui-material3",
|
||||||
|
|
||||||
material = "com.google.android.material:material:1.9.0"
|
material = "com.google.android.material:material:1.9.0"
|
||||||
flexible-adapter-core = "com.github.arkon.FlexibleAdapter:flexible-adapter:c8013533"
|
flexible-adapter-core = "com.github.arkon.FlexibleAdapter:flexible-adapter:c8013533"
|
||||||
flexible-adapter-ui = "com.github.arkon.FlexibleAdapter:flexible-adapter-ui:c8013533"
|
|
||||||
photoview = "com.github.chrisbanes:PhotoView:2.3.0"
|
photoview = "com.github.chrisbanes:PhotoView:2.3.0"
|
||||||
directionalviewpager = "com.github.tachiyomiorg:DirectionalViewPager:1.0.0"
|
directionalviewpager = "com.github.tachiyomiorg:DirectionalViewPager:1.0.0"
|
||||||
insetter = "dev.chrisbanes.insetter:insetter:0.6.1"
|
insetter = "dev.chrisbanes.insetter:insetter:0.6.1"
|
||||||
compose-cascade = "me.saket.cascade:cascade-compose:2.0.0-rc02"
|
compose-materialmotion = "io.github.fornewid:material-motion-compose-core:1.0.2"
|
||||||
compose-materialmotion = "io.github.fornewid:material-motion-compose-core:0.12.3"
|
|
||||||
compose-simpleicons = "br.com.devsrsouza.compose.icons.android:simple-icons:1.0.0"
|
compose-simpleicons = "br.com.devsrsouza.compose.icons.android:simple-icons:1.0.0"
|
||||||
|
|
||||||
logcat = "com.squareup.logcat:logcat:0.1"
|
logcat = "com.squareup.logcat:logcat:0.1"
|
||||||
|
@ -84,6 +82,7 @@ sqldelight-gradle = { module = "com.squareup.sqldelight:gradle-plugin", version.
|
||||||
|
|
||||||
junit = "org.junit.jupiter:junit-jupiter:5.9.3"
|
junit = "org.junit.jupiter:junit-jupiter:5.9.3"
|
||||||
kotest-assertions = "io.kotest:kotest-assertions-core:5.6.2"
|
kotest-assertions = "io.kotest:kotest-assertions-core:5.6.2"
|
||||||
|
mockk = "io.mockk:mockk:1.13.5"
|
||||||
|
|
||||||
voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" }
|
voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" }
|
||||||
voyager-tab-navigator = { module = "cafe.adriel.voyager:voyager-tab-navigator", version.ref = "voyager" }
|
voyager-tab-navigator = { module = "cafe.adriel.voyager:voyager-tab-navigator", version.ref = "voyager" }
|
||||||
|
@ -91,8 +90,6 @@ voyager-transitions = { module = "cafe.adriel.voyager:voyager-transitions", vers
|
||||||
|
|
||||||
kotlinter = "org.jmailen.gradle:kotlinter-gradle:3.13.0"
|
kotlinter = "org.jmailen.gradle:kotlinter-gradle:3.13.0"
|
||||||
|
|
||||||
mockk = "io.mockk:mockk:1.13.5"
|
|
||||||
|
|
||||||
aniyomi-mpv = "com.github.aniyomiorg:aniyomi-mpv-lib:1.10.n"
|
aniyomi-mpv = "com.github.aniyomiorg:aniyomi-mpv-lib:1.10.n"
|
||||||
ffmpeg-kit = "com.github.jmir1:ffmpeg-kit:1.10"
|
ffmpeg-kit = "com.github.jmir1:ffmpeg-kit:1.10"
|
||||||
arthenica-smartexceptions = "com.arthenica:smart-exception-java:0.1.1"
|
arthenica-smartexceptions = "com.arthenica:smart-exception-java:0.1.1"
|
||||||
|
|
|
@ -4,9 +4,9 @@ import androidx.compose.foundation.gestures.Orientation
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.pager.PageSize
|
import androidx.compose.foundation.pager.PageSize
|
||||||
import androidx.compose.foundation.pager.PagerDefaults
|
import androidx.compose.foundation.pager.PagerDefaults
|
||||||
|
import androidx.compose.foundation.pager.PagerScope
|
||||||
import androidx.compose.foundation.pager.PagerSnapDistance
|
import androidx.compose.foundation.pager.PagerSnapDistance
|
||||||
import androidx.compose.foundation.pager.PagerState
|
import androidx.compose.foundation.pager.PagerState
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
@ -21,9 +21,8 @@ import androidx.compose.ui.unit.dp
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun HorizontalPager(
|
fun HorizontalPager(
|
||||||
pageCount: Int,
|
state: PagerState,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
state: PagerState = rememberPagerState(),
|
|
||||||
contentPadding: PaddingValues = PaddingValues(0.dp),
|
contentPadding: PaddingValues = PaddingValues(0.dp),
|
||||||
pageSize: PageSize = PageSize.Fill,
|
pageSize: PageSize = PageSize.Fill,
|
||||||
beyondBoundsPageCount: Int = 0,
|
beyondBoundsPageCount: Int = 0,
|
||||||
|
@ -35,12 +34,11 @@ fun HorizontalPager(
|
||||||
pageNestedScrollConnection: NestedScrollConnection = PagerDefaults.pageNestedScrollConnection(
|
pageNestedScrollConnection: NestedScrollConnection = PagerDefaults.pageNestedScrollConnection(
|
||||||
Orientation.Horizontal,
|
Orientation.Horizontal,
|
||||||
),
|
),
|
||||||
pageContent: @Composable (page: Int) -> Unit,
|
pageContent: @Composable PagerScope.(page: Int) -> Unit,
|
||||||
) {
|
) {
|
||||||
androidx.compose.foundation.pager.HorizontalPager(
|
androidx.compose.foundation.pager.HorizontalPager(
|
||||||
pageCount = pageCount,
|
|
||||||
modifier = modifier,
|
|
||||||
state = state,
|
state = state,
|
||||||
|
modifier = modifier,
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
pageSize = pageSize,
|
pageSize = pageSize,
|
||||||
beyondBoundsPageCount = beyondBoundsPageCount,
|
beyondBoundsPageCount = beyondBoundsPageCount,
|
||||||
|
|
|
@ -45,6 +45,7 @@ import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.util.fastFirstOrNull
|
import androidx.compose.ui.util.fastFirstOrNull
|
||||||
import androidx.compose.ui.util.fastForEach
|
import androidx.compose.ui.util.fastForEach
|
||||||
import androidx.compose.ui.util.fastMaxBy
|
import androidx.compose.ui.util.fastMaxBy
|
||||||
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
@ -60,6 +61,7 @@ import kotlin.math.roundToInt
|
||||||
*
|
*
|
||||||
* Set key with [STICKY_HEADER_KEY_PREFIX] prefix to any sticky header item in the list.
|
* Set key with [STICKY_HEADER_KEY_PREFIX] prefix to any sticky header item in the list.
|
||||||
*/
|
*/
|
||||||
|
@OptIn(FlowPreview::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun VerticalFastScroller(
|
fun VerticalFastScroller(
|
||||||
listState: LazyListState,
|
listState: LazyListState,
|
||||||
|
@ -217,6 +219,7 @@ private fun rememberColumnWidthSums(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(FlowPreview::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun VerticalGridFastScroller(
|
fun VerticalGridFastScroller(
|
||||||
state: LazyGridState,
|
state: LazyGridState,
|
||||||
|
|
|
@ -64,6 +64,7 @@ import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.util.fastFirstOrNull
|
import androidx.compose.ui.util.fastFirstOrNull
|
||||||
import androidx.compose.ui.util.fastSumBy
|
import androidx.compose.ui.util.fastSumBy
|
||||||
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
@ -174,6 +175,7 @@ private fun ContentDrawScope.onDrawScrollbar(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(FlowPreview::class)
|
||||||
private fun Modifier.drawScrollbar(
|
private fun Modifier.drawScrollbar(
|
||||||
orientation: Orientation,
|
orientation: Orientation,
|
||||||
reverseScrolling: Boolean,
|
reverseScrolling: Boolean,
|
||||||
|
|
Loading…
Reference in a new issue