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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -68,10 +68,9 @@ fun EpisodeSettingsDialog(
},
)
},
) { contentPadding, page ->
) { page ->
Column(
modifier = Modifier
.padding(contentPadding)
.padding(vertical = TabbedDialogPaddings.Vertical)
.verticalScroll(rememberScrollState()),
) {

View file

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

View file

@ -68,10 +68,9 @@ fun ChapterSettingsDialog(
},
)
},
) { contentPadding, page ->
) { page ->
Column(
modifier = Modifier
.padding(contentPadding)
.padding(vertical = TabbedDialogPaddings.Vertical)
.verticalScroll(rememberScrollState()),
) {

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -42,10 +42,8 @@ fun SourceFilterAnimeDialog(
AdaptiveSheet(
onDismissRequest = onDismissRequest,
) { contentPadding ->
LazyColumn(
contentPadding = contentPadding,
) {
) {
LazyColumn {
stickyHeader {
Row(
modifier = Modifier

View file

@ -42,10 +42,8 @@ fun SourceFilterMangaDialog(
AdaptiveSheet(
onDismissRequest = onDismissRequest,
) { contentPadding ->
LazyColumn(
contentPadding = contentPadding,
) {
) {
LazyColumn {
stickyHeader {
Row(
modifier = Modifier

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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