Last Commit Merged: 152fdec855
This commit is contained in:
LuftVerbot 2023-10-06 18:47:44 +02:00
parent b3c911ea28
commit e99e4c2f41
39 changed files with 570 additions and 467 deletions

View file

@ -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)

View file

@ -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,
)

View file

@ -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

View file

@ -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)
}
} }
} }

View file

@ -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)
} }
} }
} }

View file

@ -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,

View file

@ -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(

View file

@ -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()),
) { ) {

View file

@ -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) }

View file

@ -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()),
) { ) {

View file

@ -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) }

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,
) )

View file

@ -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,

View file

@ -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,

View file

@ -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,
) )

View file

@ -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,

View file

@ -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)
}
}
}

View file

@ -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) }
} }

View file

@ -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)

View file

@ -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() {

View file

@ -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() {

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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,

View file

@ -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 = {

View file

@ -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>

View file

@ -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")
} }
} }

View file

@ -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 {

View file

@ -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"

View file

@ -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" }

View file

@ -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"

View file

@ -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,

View file

@ -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,

View file

@ -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,