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