mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-21 12:17:12 +03:00
parent
2ebf477bd1
commit
f705e19182
44 changed files with 661 additions and 681 deletions
2
.github/workflows/build_pull_request.yml
vendored
2
.github/workflows/build_pull_request.yml
vendored
|
@ -37,4 +37,4 @@ jobs:
|
|||
- name: Build app and run unit tests
|
||||
uses: gradle/gradle-command-action@v2
|
||||
with:
|
||||
arguments: lintKotlin assembleStandardRelease testStandardReleaseUnitTest
|
||||
arguments: lintKotlin assembleStandardRelease testReleaseUnitTest
|
2
.github/workflows/build_push.yml
vendored
2
.github/workflows/build_push.yml
vendored
|
@ -42,7 +42,7 @@ jobs:
|
|||
- name: Build app and run unit tests
|
||||
uses: gradle/gradle-command-action@v2
|
||||
with:
|
||||
arguments: lintKotlin assembleStandardRelease testStandardReleaseUnitTest
|
||||
arguments: lintKotlin assembleStandardRelease testReleaseUnitTest
|
||||
|
||||
# Sign APK and create release for tags
|
||||
|
||||
|
|
|
@ -218,7 +218,6 @@ dependencies {
|
|||
// Disk
|
||||
implementation(libs.disklrucache)
|
||||
implementation(libs.unifile)
|
||||
implementation(libs.compress)
|
||||
implementation(libs.junrar)
|
||||
|
||||
// Preferences
|
||||
|
|
5
app/proguard-rules.pro
vendored
5
app/proguard-rules.pro
vendored
|
@ -74,7 +74,4 @@
|
|||
##---------------End: proguard configuration for kotlinx.serialization ----------
|
||||
|
||||
# XmlUtil
|
||||
-keep public enum nl.adaptivity.xmlutil.EventType { *; }
|
||||
|
||||
# org.apache.commons:commons-compress
|
||||
-keep,allowoptimization class org.apache.commons.compress.archivers.zip.**
|
||||
-keep public enum nl.adaptivity.xmlutil.EventType { *; }
|
|
@ -11,7 +11,6 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import androidx.paging.compose.itemKey
|
||||
import eu.kanade.presentation.browse.InLibraryBadge
|
||||
import eu.kanade.presentation.browse.manga.components.BrowseSourceLoadingItem
|
||||
import eu.kanade.presentation.library.CommonEntryItemDefaults
|
||||
|
@ -41,10 +40,7 @@ fun BrowseAnimeSourceComfortableGrid(
|
|||
}
|
||||
}
|
||||
|
||||
items(
|
||||
count = animeList.itemCount,
|
||||
key = animeList.itemKey { it.value.id },
|
||||
) { index ->
|
||||
items(count = animeList.itemCount) { index ->
|
||||
val anime by animeList[index]?.collectAsState() ?: return@items
|
||||
BrowseAnimeSourceComfortableGridItem(
|
||||
anime = anime,
|
||||
|
|
|
@ -11,7 +11,6 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import androidx.paging.compose.itemKey
|
||||
import eu.kanade.presentation.browse.InLibraryBadge
|
||||
import eu.kanade.presentation.browse.manga.components.BrowseSourceLoadingItem
|
||||
import eu.kanade.presentation.library.CommonEntryItemDefaults
|
||||
|
@ -41,10 +40,7 @@ fun BrowseAnimeSourceCompactGrid(
|
|||
}
|
||||
}
|
||||
|
||||
items(
|
||||
count = animeList.itemCount,
|
||||
key = animeList.itemKey { it.value.id },
|
||||
) { index ->
|
||||
items(count = animeList.itemCount) { index ->
|
||||
val anime by animeList[index]?.collectAsState() ?: return@items
|
||||
BrowseAnimeSourceCompactGridItem(
|
||||
anime = anime,
|
||||
|
|
|
@ -7,7 +7,6 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import androidx.paging.compose.itemKey
|
||||
import androidx.paging.compose.items
|
||||
import eu.kanade.presentation.browse.InLibraryBadge
|
||||
import eu.kanade.presentation.browse.manga.components.BrowseSourceLoadingItem
|
||||
|
@ -35,10 +34,7 @@ fun BrowseAnimeSourceList(
|
|||
}
|
||||
}
|
||||
|
||||
items(
|
||||
count = animeList.itemCount,
|
||||
key = animeList.itemKey { it.value.id },
|
||||
) { index ->
|
||||
items(count = animeList.itemCount) { index ->
|
||||
val anime by animeList[index]?.collectAsState() ?: return@items
|
||||
BrowseAnimeSourceListItem(
|
||||
anime = anime,
|
||||
|
|
|
@ -11,7 +11,6 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import androidx.paging.compose.itemKey
|
||||
import eu.kanade.presentation.browse.InLibraryBadge
|
||||
import eu.kanade.presentation.library.CommonEntryItemDefaults
|
||||
import eu.kanade.presentation.library.EntryComfortableGridItem
|
||||
|
@ -40,10 +39,7 @@ fun BrowseMangaSourceComfortableGrid(
|
|||
}
|
||||
}
|
||||
|
||||
items(
|
||||
count = mangaList.itemCount,
|
||||
key = mangaList.itemKey { it.value.id },
|
||||
) { index ->
|
||||
items(count = mangaList.itemCount) { index ->
|
||||
val manga by mangaList[index]?.collectAsState() ?: return@items
|
||||
BrowseMangaSourceComfortableGridItem(
|
||||
manga = manga,
|
||||
|
|
|
@ -11,7 +11,6 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import androidx.paging.compose.itemKey
|
||||
import eu.kanade.presentation.browse.InLibraryBadge
|
||||
import eu.kanade.presentation.library.CommonEntryItemDefaults
|
||||
import eu.kanade.presentation.library.EntryCompactGridItem
|
||||
|
@ -40,10 +39,7 @@ fun BrowseMangaSourceCompactGrid(
|
|||
}
|
||||
}
|
||||
|
||||
items(
|
||||
count = mangaList.itemCount,
|
||||
key = mangaList.itemKey { it.value.id },
|
||||
) { index ->
|
||||
items(count = mangaList.itemCount) { index ->
|
||||
val manga by mangaList[index]?.collectAsState() ?: return@items
|
||||
BrowseMangaSourceCompactGridItem(
|
||||
manga = manga,
|
||||
|
|
|
@ -7,7 +7,6 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import androidx.paging.compose.itemKey
|
||||
import androidx.paging.compose.items
|
||||
import eu.kanade.presentation.browse.InLibraryBadge
|
||||
import eu.kanade.presentation.library.CommonEntryItemDefaults
|
||||
|
@ -34,10 +33,7 @@ fun BrowseMangaSourceList(
|
|||
}
|
||||
}
|
||||
|
||||
items(
|
||||
count = mangaList.itemCount,
|
||||
key = mangaList.itemKey { it.value.id },
|
||||
) { index ->
|
||||
items(count = mangaList.itemCount) { index ->
|
||||
val manga by mangaList[index]?.collectAsState() ?: return@items
|
||||
BrowseMangaSourceListItem(
|
||||
manga = manga,
|
||||
|
|
|
@ -16,13 +16,15 @@ import androidx.compose.ui.unit.Dp
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
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.Screen
|
||||
import eu.kanade.presentation.util.isTabletUi
|
||||
import tachiyomi.presentation.core.components.AdaptiveSheet as AdaptiveSheetImpl
|
||||
|
||||
@OptIn(InternalVoyagerApi::class)
|
||||
@Composable
|
||||
fun NavigatorAdaptiveSheet(
|
||||
screen: Screen,
|
||||
|
|
|
@ -20,6 +20,7 @@ import androidx.compose.material3.DropdownMenuItem
|
|||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.PlainTooltipBox
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
|
@ -203,21 +204,36 @@ fun AppBarActions(
|
|||
var showMenu by remember { mutableStateOf(false) }
|
||||
|
||||
actions.filterIsInstance<AppBar.Action>().map {
|
||||
IconButton(
|
||||
onClick = it.onClick,
|
||||
enabled = it.enabled,
|
||||
PlainTooltipBox(
|
||||
tooltip = { Text(it.title) },
|
||||
) {
|
||||
Icon(
|
||||
imageVector = it.icon,
|
||||
contentDescription = it.title,
|
||||
)
|
||||
IconButton(
|
||||
onClick = it.onClick,
|
||||
enabled = it.enabled,
|
||||
modifier = Modifier.tooltipAnchor(),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = it.icon,
|
||||
contentDescription = it.title,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val overflowActions = actions.filterIsInstance<AppBar.OverflowAction>()
|
||||
if (overflowActions.isNotEmpty()) {
|
||||
IconButton(onClick = { showMenu = !showMenu }) {
|
||||
Icon(Icons.Outlined.MoreVert, contentDescription = stringResource(R.string.abc_action_menu_overflow_description))
|
||||
PlainTooltipBox(
|
||||
tooltip = { Text(stringResource(R.string.abc_action_menu_overflow_description)) },
|
||||
) {
|
||||
IconButton(
|
||||
onClick = { showMenu = !showMenu },
|
||||
modifier = Modifier.tooltipAnchor(),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Outlined.MoreVert,
|
||||
contentDescription = stringResource(R.string.abc_action_menu_overflow_description),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
|
@ -327,17 +343,35 @@ fun SearchToolbar(
|
|||
if (!searchEnabled) {
|
||||
// Don't show search action
|
||||
} else if (searchQuery == null) {
|
||||
IconButton(onClick) {
|
||||
Icon(Icons.Outlined.Search, contentDescription = stringResource(R.string.action_search))
|
||||
PlainTooltipBox(
|
||||
tooltip = { Text(stringResource(R.string.action_search)) },
|
||||
) {
|
||||
IconButton(
|
||||
onClick = onClick,
|
||||
modifier = Modifier.tooltipAnchor(),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Outlined.Search,
|
||||
contentDescription = stringResource(R.string.action_search),
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (searchQuery.isNotEmpty()) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
onClick()
|
||||
focusRequester.requestFocus()
|
||||
},
|
||||
PlainTooltipBox(
|
||||
tooltip = { Text(stringResource(R.string.action_reset)) },
|
||||
) {
|
||||
Icon(Icons.Outlined.Close, contentDescription = stringResource(R.string.action_reset))
|
||||
IconButton(
|
||||
onClick = {
|
||||
onClick()
|
||||
focusRequester.requestFocus()
|
||||
},
|
||||
modifier = Modifier.tooltipAnchor(),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Outlined.Close,
|
||||
contentDescription = stringResource(R.string.action_reset),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ 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
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material3.Icon
|
||||
|
@ -31,7 +32,6 @@ import kotlinx.coroutines.launch
|
|||
import tachiyomi.presentation.core.components.HorizontalPager
|
||||
import tachiyomi.presentation.core.components.material.Divider
|
||||
import tachiyomi.presentation.core.components.material.TabIndicator
|
||||
import tachiyomi.presentation.core.components.rememberPagerState
|
||||
|
||||
object TabbedDialogPaddings {
|
||||
val Horizontal = 24.dp
|
||||
|
@ -84,7 +84,7 @@ fun TabbedDialog(
|
|||
|
||||
HorizontalPager(
|
||||
modifier = Modifier.animateContentSize(),
|
||||
count = tabTitles.size,
|
||||
pageCount = tabTitles.size,
|
||||
state = pagerState,
|
||||
verticalAlignment = Alignment.Top,
|
||||
) { page ->
|
||||
|
|
|
@ -7,6 +7,8 @@ import androidx.compose.foundation.layout.calculateEndPadding
|
|||
import androidx.compose.foundation.layout.calculateStartPadding
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ScrollableTabRow
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
|
@ -25,11 +27,9 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.launch
|
||||
import tachiyomi.presentation.core.components.HorizontalPager
|
||||
import tachiyomi.presentation.core.components.PagerState
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.components.material.TabIndicator
|
||||
import tachiyomi.presentation.core.components.material.TabText
|
||||
import tachiyomi.presentation.core.components.rememberPagerState
|
||||
|
||||
@Composable
|
||||
fun TabbedScreen(
|
||||
|
@ -105,7 +105,7 @@ fun TabbedScreen(
|
|||
}
|
||||
|
||||
HorizontalPager(
|
||||
count = tabs.size,
|
||||
pageCount = tabs.size,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
state = state,
|
||||
verticalAlignment = Alignment.Top,
|
||||
|
|
|
@ -26,6 +26,8 @@ import androidx.compose.ui.draw.alpha
|
|||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
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
|
||||
|
@ -77,18 +79,20 @@ fun EntryToolbar(
|
|||
},
|
||||
actions = {
|
||||
if (isActionMode) {
|
||||
IconButton(onClick = onSelectAll) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.SelectAll,
|
||||
contentDescription = stringResource(R.string.action_select_all),
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onInvertSelection) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.FlipToBack,
|
||||
contentDescription = stringResource(R.string.action_select_inverse),
|
||||
)
|
||||
}
|
||||
AppBarActions(
|
||||
listOf(
|
||||
AppBar.Action(
|
||||
title = stringResource(R.string.action_select_all),
|
||||
icon = Icons.Outlined.SelectAll,
|
||||
onClick = onSelectAll,
|
||||
),
|
||||
AppBar.Action(
|
||||
title = stringResource(R.string.action_select_inverse),
|
||||
icon = Icons.Outlined.FlipToBack,
|
||||
onClick = onInvertSelection,
|
||||
),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
if (onClickDownload != null) {
|
||||
val (downloadExpanded, onDownloadExpanded) = remember { mutableStateOf(false) }
|
||||
|
|
|
@ -49,9 +49,9 @@ import androidx.compose.ui.unit.dp
|
|||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.coroutineScope
|
||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
import cafe.adriel.voyager.core.screen.Screen
|
||||
import eu.kanade.core.util.asFlow
|
||||
import eu.kanade.presentation.components.TabbedDialogPaddings
|
||||
import eu.kanade.presentation.util.Screen
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadManager
|
||||
|
@ -81,7 +81,7 @@ class EpisodeOptionsDialogScreen(
|
|||
private val episodeId: Long,
|
||||
private val animeId: Long,
|
||||
private val sourceId: Long,
|
||||
) : Screen() {
|
||||
) : Screen {
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package eu.kanade.presentation.library
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ScrollableTabRow
|
||||
import androidx.compose.material3.Tab
|
||||
|
@ -8,7 +9,6 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.category.visualName
|
||||
import tachiyomi.domain.category.model.Category
|
||||
import tachiyomi.presentation.core.components.PagerState
|
||||
import tachiyomi.presentation.core.components.material.Divider
|
||||
import tachiyomi.presentation.core.components.material.TabIndicator
|
||||
import tachiyomi.presentation.core.components.material.TabText
|
||||
|
|
|
@ -21,6 +21,7 @@ import androidx.compose.ui.res.stringResource
|
|||
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
|
||||
|
@ -145,12 +146,20 @@ fun LibrarySelectionToolbar(
|
|||
AppBar(
|
||||
titleContent = { Text(text = "$selectedCount") },
|
||||
actions = {
|
||||
IconButton(onClick = onClickSelectAll) {
|
||||
Icon(Icons.Outlined.SelectAll, contentDescription = stringResource(R.string.action_select_all))
|
||||
}
|
||||
IconButton(onClick = onClickInvertSelection) {
|
||||
Icon(Icons.Outlined.FlipToBack, contentDescription = stringResource(R.string.action_select_inverse))
|
||||
}
|
||||
AppBarActions(
|
||||
listOf(
|
||||
AppBar.Action(
|
||||
title = stringResource(R.string.action_select_all),
|
||||
icon = Icons.Outlined.SelectAll,
|
||||
onClick = onClickSelectAll,
|
||||
),
|
||||
AppBar.Action(
|
||||
title = stringResource(R.string.action_select_inverse),
|
||||
icon = Icons.Outlined.FlipToBack,
|
||||
onClick = onClickInvertSelection,
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
isActionMode = true,
|
||||
onCancelActionMode = onClickUnselectAll,
|
||||
|
|
|
@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.PaddingValues
|
|||
import androidx.compose.foundation.layout.calculateEndPadding
|
||||
import androidx.compose.foundation.layout.calculateStartPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
|
@ -23,7 +24,6 @@ import tachiyomi.domain.category.model.Category
|
|||
import tachiyomi.domain.library.anime.LibraryAnime
|
||||
import tachiyomi.domain.library.model.LibraryDisplayMode
|
||||
import tachiyomi.presentation.core.components.material.PullRefresh
|
||||
import tachiyomi.presentation.core.components.rememberPagerState
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@Composable
|
||||
|
@ -61,8 +61,10 @@ fun AnimeLibraryContent(
|
|||
var isRefreshing by remember(pagerState.currentPage) { mutableStateOf(false) }
|
||||
|
||||
if (showPageTabs && categories.size > 1) {
|
||||
if (categories.size <= pagerState.currentPage) {
|
||||
pagerState.currentPage = categories.size - 1
|
||||
LaunchedEffect(categories) {
|
||||
if (categories.size <= pagerState.currentPage) {
|
||||
pagerState.scrollToPage(categories.size - 1)
|
||||
}
|
||||
}
|
||||
LibraryTabs(
|
||||
categories = categories,
|
||||
|
|
|
@ -3,6 +3,7 @@ package eu.kanade.presentation.library.anime
|
|||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
|
@ -16,7 +17,6 @@ import eu.kanade.tachiyomi.ui.library.anime.AnimeLibraryItem
|
|||
import tachiyomi.domain.library.anime.LibraryAnime
|
||||
import tachiyomi.domain.library.model.LibraryDisplayMode
|
||||
import tachiyomi.presentation.core.components.HorizontalPager
|
||||
import tachiyomi.presentation.core.components.PagerState
|
||||
|
||||
@Composable
|
||||
fun AnimeLibraryPager(
|
||||
|
@ -35,7 +35,7 @@ fun AnimeLibraryPager(
|
|||
onClickContinueWatching: ((LibraryAnime) -> Unit)?,
|
||||
) {
|
||||
HorizontalPager(
|
||||
count = pageCount,
|
||||
pageCount = pageCount,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
state = state,
|
||||
verticalAlignment = Alignment.Top,
|
||||
|
|
|
@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.PaddingValues
|
|||
import androidx.compose.foundation.layout.calculateEndPadding
|
||||
import androidx.compose.foundation.layout.calculateStartPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
|
@ -23,7 +24,6 @@ import tachiyomi.domain.category.model.Category
|
|||
import tachiyomi.domain.library.manga.LibraryManga
|
||||
import tachiyomi.domain.library.model.LibraryDisplayMode
|
||||
import tachiyomi.presentation.core.components.material.PullRefresh
|
||||
import tachiyomi.presentation.core.components.rememberPagerState
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@Composable
|
||||
|
@ -61,8 +61,10 @@ fun MangaLibraryContent(
|
|||
var isRefreshing by remember(pagerState.currentPage) { mutableStateOf(false) }
|
||||
|
||||
if (showPageTabs && categories.size > 1) {
|
||||
if (categories.size <= pagerState.currentPage) {
|
||||
pagerState.currentPage = categories.size - 1
|
||||
LaunchedEffect(categories) {
|
||||
if (categories.size <= pagerState.currentPage) {
|
||||
pagerState.scrollToPage(categories.size - 1)
|
||||
}
|
||||
}
|
||||
LibraryTabs(
|
||||
categories = categories,
|
||||
|
|
|
@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.PaddingValues
|
|||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.runtime.Composable
|
||||
|
@ -23,7 +24,6 @@ import eu.kanade.tachiyomi.ui.library.manga.MangaLibraryItem
|
|||
import tachiyomi.domain.library.manga.LibraryManga
|
||||
import tachiyomi.domain.library.model.LibraryDisplayMode
|
||||
import tachiyomi.presentation.core.components.HorizontalPager
|
||||
import tachiyomi.presentation.core.components.PagerState
|
||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||
import tachiyomi.presentation.core.util.plus
|
||||
|
||||
|
@ -44,7 +44,7 @@ fun MangaLibraryPager(
|
|||
onClickContinueReading: ((LibraryManga) -> Unit)?,
|
||||
) {
|
||||
HorizontalPager(
|
||||
count = pageCount,
|
||||
pageCount = pageCount,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
state = state,
|
||||
verticalAlignment = Alignment.Top,
|
||||
|
|
|
@ -18,6 +18,7 @@ import androidx.compose.runtime.collectAsState
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
|
@ -28,8 +29,11 @@ import cafe.adriel.voyager.core.model.ScreenModel
|
|||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.AppBarActions
|
||||
import eu.kanade.presentation.util.Screen
|
||||
import eu.kanade.presentation.util.ioCoroutineScope
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import eu.kanade.tachiyomi.util.system.workManager
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
|
@ -63,13 +67,17 @@ object WorkerInfoScreen : Screen() {
|
|||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(
|
||||
onClick = {
|
||||
context.copyToClipboard(title, enqueued + finished + running)
|
||||
},
|
||||
) {
|
||||
Icon(imageVector = Icons.Default.ContentCopy, contentDescription = null)
|
||||
}
|
||||
AppBarActions(
|
||||
listOf(
|
||||
AppBar.Action(
|
||||
title = stringResource(R.string.action_copy_to_clipboard),
|
||||
icon = Icons.Default.ContentCopy,
|
||||
onClick = {
|
||||
context.copyToClipboard(title, enqueued + finished + running)
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
scrollBehavior = it,
|
||||
)
|
||||
|
|
|
@ -35,6 +35,7 @@ import androidx.compose.ui.unit.LayoutDirection
|
|||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.util.isTabletUi
|
||||
import eu.kanade.tachiyomi.R
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun ChapterNavigator(
|
||||
|
@ -107,7 +108,7 @@ fun ChapterNavigator(
|
|||
valueRange = 1f..totalPages.toFloat(),
|
||||
steps = totalPages - 2,
|
||||
onValueChange = {
|
||||
onSliderValueChange(it.toInt() - 1)
|
||||
onSliderValueChange(it.roundToInt() - 1)
|
||||
},
|
||||
interactionSource = interactionSource,
|
||||
)
|
||||
|
|
|
@ -2,60 +2,59 @@ package eu.kanade.presentation.reader
|
|||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.text.InlineTextContent
|
||||
import androidx.compose.foundation.text.appendInlineContent
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Info
|
||||
import androidx.compose.material.icons.outlined.OfflinePin
|
||||
import androidx.compose.material.icons.outlined.Warning
|
||||
import androidx.compose.material3.CardColors
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedCard
|
||||
import androidx.compose.material3.ProvideTextStyle
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.Placeholder
|
||||
import androidx.compose.ui.text.PlaceholderVerticalAlign
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.google.common.io.Files.append
|
||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.manga.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.manga.ChapterImpl
|
||||
import eu.kanade.tachiyomi.data.database.models.manga.toDomainChapter
|
||||
import eu.kanade.tachiyomi.data.download.manga.MangaDownloadManager
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
|
||||
import tachiyomi.domain.entries.manga.model.Manga
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||
import tachiyomi.domain.items.service.calculateChapterGap
|
||||
import tachiyomi.presentation.core.components.material.SecondaryItemAlpha
|
||||
import tachiyomi.presentation.core.util.ThemePreviews
|
||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
||||
|
||||
@Composable
|
||||
fun ChapterTransition(
|
||||
transition: ChapterTransition,
|
||||
downloadManager: MangaDownloadManager,
|
||||
manga: Manga?,
|
||||
currChapterDownloaded: Boolean,
|
||||
goingToChapterDownloaded: Boolean,
|
||||
) {
|
||||
manga ?: return
|
||||
|
||||
val currChapter = transition.from.chapter
|
||||
val currChapterDownloaded = transition.from.pageLoader?.isLocal == true
|
||||
|
||||
val goingToChapter = transition.to?.chapter
|
||||
val goingToChapterDownloaded = if (goingToChapter != null) {
|
||||
downloadManager.isChapterDownloaded(
|
||||
goingToChapter.name,
|
||||
goingToChapter.scanlator,
|
||||
manga.title,
|
||||
manga.source,
|
||||
skipCache = true,
|
||||
)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
ProvideTextStyle(MaterialTheme.typography.bodyMedium) {
|
||||
when (transition) {
|
||||
|
@ -90,80 +89,289 @@ fun ChapterTransition(
|
|||
@Composable
|
||||
private fun TransitionText(
|
||||
topLabel: String,
|
||||
topChapter: Chapter? = null,
|
||||
topChapter: Chapter?,
|
||||
topChapterDownloaded: Boolean,
|
||||
bottomLabel: String,
|
||||
bottomChapter: Chapter? = null,
|
||||
bottomChapter: Chapter?,
|
||||
bottomChapterDownloaded: Boolean,
|
||||
fallbackLabel: String,
|
||||
chapterGap: Int,
|
||||
) {
|
||||
val hasTopChapter = topChapter != null
|
||||
val hasBottomChapter = bottomChapter != null
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.widthIn(max = 460.dp)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
if (topChapter != null) {
|
||||
ChapterText(
|
||||
header = topLabel,
|
||||
name = topChapter.name,
|
||||
scanlator = topChapter.scanlator,
|
||||
downloaded = topChapterDownloaded,
|
||||
)
|
||||
|
||||
Column {
|
||||
Text(
|
||||
text = if (hasTopChapter) topLabel else fallbackLabel,
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = if (hasTopChapter) TextAlign.Start else TextAlign.Center,
|
||||
)
|
||||
topChapter?.let { ChapterText(chapter = it, downloaded = topChapterDownloaded) }
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
if (chapterGap > 0) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Warning,
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
contentDescription = null,
|
||||
)
|
||||
|
||||
Text(text = pluralStringResource(R.plurals.missing_chapters_warning, count = chapterGap, chapterGap))
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Spacer(Modifier.height(VerticalSpacerSize))
|
||||
} else {
|
||||
NoChapterNotification(
|
||||
text = fallbackLabel,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = if (hasBottomChapter) bottomLabel else fallbackLabel,
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = if (hasBottomChapter) TextAlign.Start else TextAlign.Center,
|
||||
)
|
||||
bottomChapter?.let { ChapterText(chapter = it, downloaded = bottomChapterDownloaded) }
|
||||
if (bottomChapter != null) {
|
||||
if (chapterGap > 0) {
|
||||
ChapterGapWarning(
|
||||
gapCount = chapterGap,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(VerticalSpacerSize))
|
||||
|
||||
ChapterText(
|
||||
header = bottomLabel,
|
||||
name = bottomChapter.name,
|
||||
scanlator = bottomChapter.scanlator,
|
||||
downloaded = bottomChapterDownloaded,
|
||||
)
|
||||
} else {
|
||||
NoChapterNotification(
|
||||
text = fallbackLabel,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.ChapterText(
|
||||
chapter: Chapter,
|
||||
downloaded: Boolean,
|
||||
private fun NoChapterNotification(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
FlowRow(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
OutlinedCard(
|
||||
modifier = modifier,
|
||||
colors = CardColor,
|
||||
) {
|
||||
if (downloaded) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.OfflinePin,
|
||||
contentDescription = stringResource(R.string.label_downloaded),
|
||||
imageVector = Icons.Outlined.Info,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
contentDescription = null,
|
||||
)
|
||||
|
||||
Spacer(Modifier.width(8.dp))
|
||||
}
|
||||
|
||||
Text(chapter.name)
|
||||
}
|
||||
|
||||
chapter.scanlator?.let {
|
||||
ProvideTextStyle(
|
||||
MaterialTheme.typography.bodyMedium.copy(
|
||||
color = LocalContentColor.current.copy(alpha = SecondaryItemAlpha),
|
||||
),
|
||||
) {
|
||||
Text(it)
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChapterGapWarning(
|
||||
gapCount: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
OutlinedCard(
|
||||
modifier = modifier,
|
||||
colors = CardColor,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Warning,
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
contentDescription = null,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = pluralStringResource(R.plurals.missing_chapters_warning, count = gapCount, gapCount),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChapterHeaderText(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
modifier = modifier,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChapterText(
|
||||
header: String,
|
||||
name: String,
|
||||
scanlator: String?,
|
||||
downloaded: Boolean,
|
||||
) {
|
||||
Column {
|
||||
ChapterHeaderText(
|
||||
text = header,
|
||||
modifier = Modifier.padding(bottom = 4.dp),
|
||||
)
|
||||
|
||||
Text(
|
||||
text = buildAnnotatedString {
|
||||
if (downloaded) {
|
||||
appendInlineContent(DownloadedIconContentId)
|
||||
append(' ')
|
||||
}
|
||||
append(name)
|
||||
},
|
||||
fontSize = 20.sp,
|
||||
maxLines = 5,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
inlineContent = mapOf(
|
||||
DownloadedIconContentId to InlineTextContent(
|
||||
Placeholder(
|
||||
width = 22.sp,
|
||||
height = 22.sp,
|
||||
placeholderVerticalAlign = PlaceholderVerticalAlign.Center,
|
||||
),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.OfflinePin,
|
||||
contentDescription = stringResource(R.string.label_downloaded),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
scanlator?.let {
|
||||
Text(
|
||||
text = it,
|
||||
modifier = Modifier
|
||||
.secondaryItemAlpha()
|
||||
.padding(top = 2.dp),
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val CardColor: CardColors
|
||||
@Composable
|
||||
get() = CardDefaults.outlinedCardColors(
|
||||
containerColor = Color.Transparent,
|
||||
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
|
||||
private val VerticalSpacerSize = 24.dp
|
||||
private const val DownloadedIconContentId = "downloaded"
|
||||
|
||||
private fun previewChapter(name: String, scanlator: String, chapterNumber: Float) = ChapterImpl().apply {
|
||||
this.name = name
|
||||
this.scanlator = scanlator
|
||||
this.chapter_number = chapterNumber
|
||||
|
||||
this.id = 0
|
||||
this.manga_id = 0
|
||||
this.url = ""
|
||||
}
|
||||
private val FakeChapter = previewChapter(
|
||||
name = "Vol.1, Ch.1 - Fake Chapter Title",
|
||||
scanlator = "Scanlator Name",
|
||||
chapterNumber = 1f,
|
||||
)
|
||||
private val FakeGapChapter = previewChapter(
|
||||
name = "Vol.5, Ch.44 - Fake Gap Chapter Title",
|
||||
scanlator = "Scanlator Name",
|
||||
chapterNumber = 44f,
|
||||
)
|
||||
private val FakeChapterLongTitle = previewChapter(
|
||||
name = "Vol.1, Ch.0 - The Mundane Musings of a Metafictional Manga: A Chapter About a Chapter, Featuring" +
|
||||
" an Absurdly Long Title and a Surprisingly Normal Day in the Lives of Our Heroes, as They Grapple with the " +
|
||||
"Daily Challenges of Existence, from Paying Rent to Finding Love, All While Navigating the Strange World of " +
|
||||
"Fictional Realities and Reality-Bending Fiction, Where the Fourth Wall is Always in Danger of Being Broken " +
|
||||
"and the Line Between Author and Character is Forever Blurred.",
|
||||
scanlator = "Long Long Funny Scanlator Sniper Group Name Reborn",
|
||||
chapterNumber = 1f,
|
||||
)
|
||||
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
private fun TransitionTextPreview() {
|
||||
TachiyomiTheme {
|
||||
Surface(modifier = Modifier.padding(48.dp)) {
|
||||
ChapterTransition(
|
||||
transition = ChapterTransition.Next(ReaderChapter(FakeChapter), ReaderChapter(FakeChapter)),
|
||||
currChapterDownloaded = false,
|
||||
goingToChapterDownloaded = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
private fun TransitionTextLongTitlePreview() {
|
||||
TachiyomiTheme {
|
||||
Surface(modifier = Modifier.padding(48.dp)) {
|
||||
ChapterTransition(
|
||||
transition = ChapterTransition.Next(ReaderChapter(FakeChapterLongTitle), ReaderChapter(FakeChapter)),
|
||||
currChapterDownloaded = true,
|
||||
goingToChapterDownloaded = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
private fun TransitionTextWithGapPreview() {
|
||||
TachiyomiTheme {
|
||||
Surface(modifier = Modifier.padding(48.dp)) {
|
||||
ChapterTransition(
|
||||
transition = ChapterTransition.Next(ReaderChapter(FakeChapter), ReaderChapter(FakeGapChapter)),
|
||||
currChapterDownloaded = true,
|
||||
goingToChapterDownloaded = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
private fun TransitionTextNoNextPreview() {
|
||||
TachiyomiTheme {
|
||||
Surface(modifier = Modifier.padding(48.dp)) {
|
||||
ChapterTransition(
|
||||
transition = ChapterTransition.Next(ReaderChapter(FakeChapter), null),
|
||||
currChapterDownloaded = true,
|
||||
goingToChapterDownloaded = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
private fun TransitionTextNoPreviousPreview() {
|
||||
TachiyomiTheme {
|
||||
Surface(modifier = Modifier.padding(48.dp)) {
|
||||
ChapterTransition(
|
||||
transition = ChapterTransition.Prev(ReaderChapter(FakeChapter), null),
|
||||
currChapterDownloaded = true,
|
||||
goingToChapterDownloaded = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,8 +31,6 @@ abstract class Tab : cafe.adriel.voyager.navigator.tab.Tab {
|
|||
open suspend fun onReselect(navigator: Navigator) {}
|
||||
}
|
||||
|
||||
// TODO: this prevents crashes in nested navigators with transitions not being disposed
|
||||
// properly. Go back to using vanilla Voyager Screens once fixed upstream.
|
||||
abstract class Screen : Screen {
|
||||
|
||||
override val key: ScreenKey = uniqueScreenKey
|
||||
|
|
|
@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.data.backup
|
|||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import androidx.work.BackoffPolicy
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
|
@ -22,6 +23,8 @@ import tachiyomi.domain.backup.service.BackupPreferences
|
|||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
import kotlin.time.toJavaDuration
|
||||
|
||||
class BackupCreateJob(private val context: Context, workerParams: WorkerParameters) :
|
||||
CoroutineWorker(context, workerParams) {
|
||||
|
@ -29,12 +32,14 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete
|
|||
private val notifier = BackupNotifier(context)
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
val isAutoBackup = inputData.getBoolean(IS_AUTO_BACKUP_KEY, true)
|
||||
|
||||
if (isAutoBackup && BackupRestoreJob.isRunning(context)) return Result.retry()
|
||||
|
||||
val backupPreferences = Injekt.get<BackupPreferences>()
|
||||
val uri = inputData.getString(LOCATION_URI_KEY)?.toUri()
|
||||
?: backupPreferences.backupsDirectory().get().toUri()
|
||||
val flags = inputData.getInt(BACKUP_FLAGS_KEY, BackupConst.BACKUP_ALL)
|
||||
val isAutoBackup = inputData.getBoolean(IS_AUTO_BACKUP_KEY, true)
|
||||
|
||||
try {
|
||||
setForeground(getForegroundInfo())
|
||||
} catch (e: IllegalStateException) {
|
||||
|
@ -79,6 +84,7 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete
|
|||
10,
|
||||
TimeUnit.MINUTES,
|
||||
)
|
||||
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10.minutes.toJavaDuration())
|
||||
.addTag(TAG_AUTO)
|
||||
.setInputData(
|
||||
workDataOf(
|
||||
|
|
|
@ -111,7 +111,6 @@ class BackupManager(
|
|||
* @param uri path of Uri
|
||||
* @param isAutoBackup backup called from scheduled backup job
|
||||
*/
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
suspend fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String {
|
||||
if (!context.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
||||
throw IllegalStateException(context.getString(R.string.missing_storage_permission))
|
||||
|
@ -446,10 +445,10 @@ class BackupManager(
|
|||
}
|
||||
|
||||
internal suspend fun restoreExistingManga(manga: Manga, dbManga: Mangas): Manga {
|
||||
var manga = manga.copy(id = dbManga._id)
|
||||
manga = manga.copyFrom(dbManga)
|
||||
updateManga(manga)
|
||||
return manga
|
||||
var updatedManga = manga.copy(id = dbManga._id)
|
||||
updatedManga = updatedManga.copyFrom(dbManga)
|
||||
updateManga(updatedManga)
|
||||
return updatedManga
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -466,10 +465,10 @@ class BackupManager(
|
|||
}
|
||||
|
||||
internal suspend fun restoreExistingAnime(anime: Anime, dbAnime: Animes): Anime {
|
||||
var anime = anime.copy(id = dbAnime._id)
|
||||
anime = anime.copyFrom(dbAnime)
|
||||
updateAnime(anime)
|
||||
return anime
|
||||
var updatedAnime = anime.copy(id = dbAnime._id)
|
||||
updatedAnime = updatedAnime.copyFrom(dbAnime)
|
||||
updateAnime(updatedAnime)
|
||||
return updatedAnime
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -582,7 +581,7 @@ class BackupManager(
|
|||
dbCategories.firstOrNull { dbCategory ->
|
||||
dbCategory.name == backupCategory.name
|
||||
}?.let { dbCategory ->
|
||||
mangaCategoriesToUpdate.add(Pair(manga.id!!, dbCategory.id))
|
||||
mangaCategoriesToUpdate.add(Pair(manga.id, dbCategory.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -590,7 +589,7 @@ class BackupManager(
|
|||
// Update database
|
||||
if (mangaCategoriesToUpdate.isNotEmpty()) {
|
||||
mangaHandler.await(true) {
|
||||
mangas_categoriesQueries.deleteMangaCategoryByMangaId(manga.id!!)
|
||||
mangas_categoriesQueries.deleteMangaCategoryByMangaId(manga.id)
|
||||
mangaCategoriesToUpdate.forEach { (mangaId, categoryId) ->
|
||||
mangas_categoriesQueries.insert(mangaId, categoryId)
|
||||
}
|
||||
|
@ -615,7 +614,7 @@ class BackupManager(
|
|||
dbCategories.firstOrNull { dbCategory ->
|
||||
dbCategory.name == backupCategory.name
|
||||
}?.let { dbCategory ->
|
||||
animeCategoriesToUpdate.add(Pair(anime.id!!, dbCategory.id))
|
||||
animeCategoriesToUpdate.add(Pair(anime.id, dbCategory.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -623,7 +622,7 @@ class BackupManager(
|
|||
// Update database
|
||||
if (animeCategoriesToUpdate.isNotEmpty()) {
|
||||
animeHandler.await(true) {
|
||||
animes_categoriesQueries.deleteAnimeCategoryByAnimeId(anime.id!!)
|
||||
animes_categoriesQueries.deleteAnimeCategoryByAnimeId(anime.id)
|
||||
animeCategoriesToUpdate.forEach { (animeId, categoryId) ->
|
||||
animes_categoriesQueries.insert(animeId, categoryId)
|
||||
}
|
||||
|
@ -730,37 +729,38 @@ class BackupManager(
|
|||
* @param tracks the track list to restore.
|
||||
*/
|
||||
internal suspend fun restoreTracking(manga: Manga, tracks: List<tachiyomi.domain.track.manga.model.MangaTrack>) {
|
||||
// Fix foreign keys with the current manga id
|
||||
val tracks = tracks.map { it.copy(mangaId = manga.id!!) }
|
||||
|
||||
// Get tracks from database
|
||||
val dbTracks = mangaHandler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id!!) }
|
||||
val dbTracks = mangaHandler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id) }
|
||||
val toUpdate = mutableListOf<Manga_sync>()
|
||||
val toInsert = mutableListOf<tachiyomi.domain.track.manga.model.MangaTrack>()
|
||||
|
||||
tracks.forEach { track ->
|
||||
var isInDatabase = false
|
||||
for (dbTrack in dbTracks) {
|
||||
if (track.syncId == dbTrack.sync_id) {
|
||||
// The sync is already in the db, only update its fields
|
||||
var temp = dbTrack
|
||||
if (track.remoteId != dbTrack.remote_id) {
|
||||
temp = temp.copy(remote_id = track.remoteId)
|
||||
tracks
|
||||
// Fix foreign keys with the current manga id
|
||||
.map { it.copy(mangaId = manga.id) }
|
||||
.forEach { track ->
|
||||
var isInDatabase = false
|
||||
for (dbTrack in dbTracks) {
|
||||
if (track.syncId == dbTrack.sync_id) {
|
||||
// The sync is already in the db, only update its fields
|
||||
var temp = dbTrack
|
||||
if (track.remoteId != dbTrack.remote_id) {
|
||||
temp = temp.copy(remote_id = track.remoteId)
|
||||
}
|
||||
if (track.libraryId != dbTrack.library_id) {
|
||||
temp = temp.copy(library_id = track.libraryId)
|
||||
}
|
||||
temp = temp.copy(last_chapter_read = max(dbTrack.last_chapter_read, track.lastChapterRead))
|
||||
isInDatabase = true
|
||||
toUpdate.add(temp)
|
||||
break
|
||||
}
|
||||
if (track.libraryId != dbTrack.library_id) {
|
||||
temp = temp.copy(library_id = track.libraryId)
|
||||
}
|
||||
temp = temp.copy(last_chapter_read = max(dbTrack.last_chapter_read, track.lastChapterRead))
|
||||
isInDatabase = true
|
||||
toUpdate.add(temp)
|
||||
break
|
||||
}
|
||||
if (!isInDatabase) {
|
||||
// Insert new sync. Let the db assign the id
|
||||
toInsert.add(track.copy(id = 0))
|
||||
}
|
||||
}
|
||||
if (!isInDatabase) {
|
||||
// Insert new sync. Let the db assign the id
|
||||
toInsert.add(track.copy(id = 0))
|
||||
}
|
||||
}
|
||||
|
||||
// Update database
|
||||
if (toUpdate.isNotEmpty()) {
|
||||
mangaHandler.await(true) {
|
||||
|
@ -812,37 +812,38 @@ class BackupManager(
|
|||
* @param tracks the track list to restore.
|
||||
*/
|
||||
internal suspend fun restoreAnimeTracking(anime: Anime, tracks: List<tachiyomi.domain.track.anime.model.AnimeTrack>) {
|
||||
// Fix foreign keys with the current anime id
|
||||
val tracks = tracks.map { it.copy(animeId = anime.id!!) }
|
||||
|
||||
// Get tracks from database
|
||||
val dbTracks = animeHandler.awaitList { anime_syncQueries.getTracksByAnimeId(anime.id!!) }
|
||||
val dbTracks = animeHandler.awaitList { anime_syncQueries.getTracksByAnimeId(anime.id) }
|
||||
val toUpdate = mutableListOf<Anime_sync>()
|
||||
val toInsert = mutableListOf<tachiyomi.domain.track.anime.model.AnimeTrack>()
|
||||
|
||||
tracks.forEach { track ->
|
||||
var isInDatabase = false
|
||||
for (dbTrack in dbTracks) {
|
||||
if (track.syncId == dbTrack.sync_id) {
|
||||
// The sync is already in the db, only update its fields
|
||||
var temp = dbTrack
|
||||
if (track.remoteId != dbTrack.remote_id) {
|
||||
temp = temp.copy(remote_id = track.remoteId)
|
||||
tracks
|
||||
// Fix foreign keys with the current manga id
|
||||
.map { it.copy(animeId = anime.id) }
|
||||
.forEach { track ->
|
||||
var isInDatabase = false
|
||||
for (dbTrack in dbTracks) {
|
||||
if (track.syncId == dbTrack.sync_id) {
|
||||
// The sync is already in the db, only update its fields
|
||||
var temp = dbTrack
|
||||
if (track.remoteId != dbTrack.remote_id) {
|
||||
temp = temp.copy(remote_id = track.remoteId)
|
||||
}
|
||||
if (track.libraryId != dbTrack.library_id) {
|
||||
temp = temp.copy(library_id = track.libraryId)
|
||||
}
|
||||
temp = temp.copy(last_episode_seen = max(dbTrack.last_episode_seen, track.lastEpisodeSeen))
|
||||
isInDatabase = true
|
||||
toUpdate.add(temp)
|
||||
break
|
||||
}
|
||||
if (track.libraryId != dbTrack.library_id) {
|
||||
temp = temp.copy(library_id = track.libraryId)
|
||||
}
|
||||
temp = temp.copy(last_episode_seen = max(dbTrack.last_episode_seen, track.lastEpisodeSeen))
|
||||
isInDatabase = true
|
||||
toUpdate.add(temp)
|
||||
break
|
||||
}
|
||||
if (!isInDatabase) {
|
||||
// Insert new sync. Let the db assign the id
|
||||
toInsert.add(track.copy(id = 0))
|
||||
}
|
||||
}
|
||||
if (!isInDatabase) {
|
||||
// Insert new sync. Let the db assign the id
|
||||
toInsert.add(track.copy(id = 0))
|
||||
}
|
||||
}
|
||||
|
||||
// Update database
|
||||
if (toUpdate.isNotEmpty()) {
|
||||
animeHandler.await(true) {
|
||||
|
@ -891,22 +892,22 @@ class BackupManager(
|
|||
val dbChapters = mangaHandler.awaitList { chaptersQueries.getChaptersByMangaId(manga.id!!) }
|
||||
|
||||
val processed = chapters.map { chapter ->
|
||||
var chapter = chapter
|
||||
val dbChapter = dbChapters.find { it.url == chapter.url }
|
||||
var updatedChapter = chapter
|
||||
val dbChapter = dbChapters.find { it.url == updatedChapter.url }
|
||||
if (dbChapter != null) {
|
||||
chapter = chapter.copy(id = dbChapter._id)
|
||||
chapter = chapter.copyFrom(dbChapter)
|
||||
if (dbChapter.read && !chapter.read) {
|
||||
chapter = chapter.copy(read = dbChapter.read, lastPageRead = dbChapter.last_page_read)
|
||||
updatedChapter = updatedChapter.copy(id = dbChapter._id)
|
||||
updatedChapter = updatedChapter.copyFrom(dbChapter)
|
||||
if (dbChapter.read && !updatedChapter.read) {
|
||||
updatedChapter = updatedChapter.copy(read = true, lastPageRead = dbChapter.last_page_read)
|
||||
} else if (chapter.lastPageRead == 0L && dbChapter.last_page_read != 0L) {
|
||||
chapter = chapter.copy(lastPageRead = dbChapter.last_page_read)
|
||||
updatedChapter = updatedChapter.copy(lastPageRead = dbChapter.last_page_read)
|
||||
}
|
||||
if (!chapter.bookmark && dbChapter.bookmark) {
|
||||
chapter = chapter.copy(bookmark = dbChapter.bookmark)
|
||||
if (!updatedChapter.bookmark && dbChapter.bookmark) {
|
||||
updatedChapter = updatedChapter.copy(bookmark = true)
|
||||
}
|
||||
}
|
||||
|
||||
chapter.copy(mangaId = manga.id ?: -1)
|
||||
updatedChapter.copy(mangaId = manga.id ?: -1)
|
||||
}
|
||||
|
||||
val newChapters = processed.groupBy { it.id > 0 }
|
||||
|
@ -918,22 +919,22 @@ class BackupManager(
|
|||
val dbEpisodes = animeHandler.awaitList { episodesQueries.getEpisodesByAnimeId(anime.id!!) }
|
||||
|
||||
val processed = episodes.map { episode ->
|
||||
var episode = episode
|
||||
val dbEpisode = dbEpisodes.find { it.url == episode.url }
|
||||
var updatedEpisode = episode
|
||||
val dbEpisode = dbEpisodes.find { it.url == updatedEpisode.url }
|
||||
if (dbEpisode != null) {
|
||||
episode = episode.copy(id = dbEpisode._id)
|
||||
episode = episode.copyFrom(dbEpisode)
|
||||
if (dbEpisode.seen && !episode.seen) {
|
||||
episode = episode.copy(seen = dbEpisode.seen, lastSecondSeen = dbEpisode.last_second_seen)
|
||||
} else if (episode.lastSecondSeen == 0L && dbEpisode.last_second_seen != 0L) {
|
||||
episode = episode.copy(lastSecondSeen = dbEpisode.last_second_seen)
|
||||
updatedEpisode = updatedEpisode.copy(id = dbEpisode._id)
|
||||
updatedEpisode = updatedEpisode.copyFrom(dbEpisode)
|
||||
if (dbEpisode.seen && !updatedEpisode.seen) {
|
||||
updatedEpisode = updatedEpisode.copy(seen = true, lastSecondSeen = dbEpisode.last_second_seen)
|
||||
} else if (updatedEpisode.lastSecondSeen == 0L && dbEpisode.last_second_seen != 0L) {
|
||||
updatedEpisode = updatedEpisode.copy(lastSecondSeen = dbEpisode.last_second_seen)
|
||||
}
|
||||
if (!episode.bookmark && dbEpisode.bookmark) {
|
||||
episode = episode.copy(bookmark = dbEpisode.bookmark)
|
||||
if (!updatedEpisode.bookmark && dbEpisode.bookmark) {
|
||||
updatedEpisode = updatedEpisode.copy(bookmark = true)
|
||||
}
|
||||
}
|
||||
|
||||
episode.copy(animeId = anime.id ?: -1)
|
||||
updatedEpisode.copy(animeId = anime.id ?: -1)
|
||||
}
|
||||
|
||||
val newEpisodes = processed.groupBy { it.id > 0 }
|
||||
|
@ -1126,55 +1127,6 @@ class BackupManager(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a list of chapters
|
||||
*/
|
||||
private suspend fun updateChapters(chapters: List<tachiyomi.domain.items.chapter.model.Chapter>) {
|
||||
mangaHandler.await(true) {
|
||||
chapters.forEach { chapter ->
|
||||
chaptersQueries.update(
|
||||
chapter.mangaId,
|
||||
chapter.url,
|
||||
chapter.name,
|
||||
chapter.scanlator,
|
||||
chapter.read.toLong(),
|
||||
chapter.bookmark.toLong(),
|
||||
chapter.lastPageRead,
|
||||
chapter.chapterNumber.toDouble(),
|
||||
chapter.sourceOrder,
|
||||
chapter.dateFetch,
|
||||
chapter.dateUpload,
|
||||
chapter.id,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a list of episodes
|
||||
*/
|
||||
private suspend fun updateEpisodes(episodes: List<tachiyomi.domain.items.episode.model.Episode>) {
|
||||
animeHandler.await(true) {
|
||||
episodes.forEach { episode ->
|
||||
episodesQueries.update(
|
||||
episode.animeId,
|
||||
episode.url,
|
||||
episode.name,
|
||||
episode.scanlator,
|
||||
episode.seen.toLong(),
|
||||
episode.bookmark.toLong(),
|
||||
episode.lastSecondSeen,
|
||||
episode.totalSeconds,
|
||||
episode.episodeNumber.toDouble(),
|
||||
episode.sourceOrder,
|
||||
episode.dateFetch,
|
||||
episode.dateUpload,
|
||||
episode.id,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a list of chapters with known database ids
|
||||
*/
|
||||
|
|
|
@ -28,6 +28,7 @@ data class BackupAnimeTracking(
|
|||
@ProtoNumber(11) var finishedWatchingDate: Long = 0,
|
||||
@ProtoNumber(100) var mediaId: Long = 0,
|
||||
) {
|
||||
@Suppress("DEPRECATION")
|
||||
fun getTrackingImpl(): AnimeTrack {
|
||||
return AnimeTrack(
|
||||
id = -1,
|
||||
|
|
|
@ -29,6 +29,7 @@ data class BackupTracking(
|
|||
@ProtoNumber(100) var mediaId: Long = 0,
|
||||
) {
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
fun getTrackingImpl(): MangaTrack {
|
||||
return MangaTrack(
|
||||
id = -1,
|
||||
|
|
|
@ -118,7 +118,6 @@ class AnimeCoverScreenModel(
|
|||
fun editCover(context: Context, data: Uri) {
|
||||
val anime = state.value ?: return
|
||||
coroutineScope.launchIO {
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
context.contentResolver.openInputStream(data)?.use {
|
||||
try {
|
||||
anime.editCover(Injekt.get(), it, updateAnime, coverCache)
|
||||
|
|
|
@ -628,8 +628,8 @@ class AnimeInfoScreenModel(
|
|||
downloadEpisodes(episodes, false, video)
|
||||
}
|
||||
if (!isFavorited && !successState.hasPromptedToAddBefore) {
|
||||
updateSuccessState { successState ->
|
||||
successState.copy(hasPromptedToAddBefore = true)
|
||||
updateSuccessState { state ->
|
||||
state.copy(hasPromptedToAddBefore = true)
|
||||
}
|
||||
coroutineScope.launch {
|
||||
val result = snackbarHostState.showSnackbar(
|
||||
|
|
|
@ -118,7 +118,6 @@ class MangaCoverScreenModel(
|
|||
fun editCover(context: Context, data: Uri) {
|
||||
val manga = state.value ?: return
|
||||
coroutineScope.launchIO {
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
context.contentResolver.openInputStream(data)?.use {
|
||||
try {
|
||||
manga.editCover(Injekt.get(), it, updateManga, coverCache)
|
||||
|
|
|
@ -622,8 +622,8 @@ class MangaInfoScreenModel(
|
|||
downloadChapters(chapters)
|
||||
}
|
||||
if (!isFavorited && !successState.hasPromptedToAddBefore) {
|
||||
updateSuccessState { successState ->
|
||||
successState.copy(hasPromptedToAddBefore = true)
|
||||
updateSuccessState { state ->
|
||||
state.copy(hasPromptedToAddBefore = true)
|
||||
}
|
||||
coroutineScope.launch {
|
||||
val result = snackbarHostState.showSnackbar(
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
package eu.kanade.tachiyomi.ui.reader.loader
|
||||
|
||||
import android.app.Application
|
||||
import com.github.junrar.Archive
|
||||
import com.github.junrar.rarfile.FileHeader
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
||||
import tachiyomi.core.util.system.ImageUtil
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.io.PipedInputStream
|
||||
|
@ -15,36 +16,30 @@ import java.io.PipedOutputStream
|
|||
*/
|
||||
internal class RarPageLoader(file: File) : PageLoader() {
|
||||
|
||||
private val context: Application by injectLazy()
|
||||
private val tmpDir = File(context.externalCacheDir, "reader_${file.hashCode()}").also {
|
||||
it.deleteRecursively()
|
||||
it.mkdirs()
|
||||
}
|
||||
|
||||
init {
|
||||
Archive(file).use { rar ->
|
||||
rar.fileHeaders.asSequence()
|
||||
.filterNot { it.isDirectory }
|
||||
.forEach { header ->
|
||||
val pageOutputStream = File(tmpDir, header.fileName.substringAfterLast("/"))
|
||||
.also { it.createNewFile() }
|
||||
.outputStream()
|
||||
getStream(rar, header).use {
|
||||
it.copyTo(pageOutputStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private val rar = Archive(file)
|
||||
|
||||
override var isLocal: Boolean = true
|
||||
|
||||
override suspend fun getPages(): List<ReaderPage> {
|
||||
return DirectoryPageLoader(tmpDir).getPages()
|
||||
return rar.fileHeaders.asSequence()
|
||||
.filter { !it.isDirectory && ImageUtil.isImage(it.fileName) { rar.getInputStream(it) } }
|
||||
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
|
||||
.mapIndexed { i, header ->
|
||||
ReaderPage(i).apply {
|
||||
stream = { getStream(rar, header) }
|
||||
status = Page.State.READY
|
||||
}
|
||||
}
|
||||
.toList()
|
||||
}
|
||||
|
||||
override suspend fun loadPage(page: ReaderPage) {
|
||||
check(!isRecycled)
|
||||
}
|
||||
|
||||
override fun recycle() {
|
||||
super.recycle()
|
||||
tmpDir.deleteRecursively()
|
||||
rar.close()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,52 +1,46 @@
|
|||
package eu.kanade.tachiyomi.ui.reader.loader
|
||||
|
||||
import android.app.Application
|
||||
import android.os.Build
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import org.apache.commons.compress.archivers.zip.ZipFile
|
||||
import org.apache.commons.compress.utils.SeekableInMemoryByteChannel
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.ByteArrayOutputStream
|
||||
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
||||
import tachiyomi.core.util.system.ImageUtil
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
/**
|
||||
* Loader used to load a chapter from a .zip or .cbz file.
|
||||
*/
|
||||
internal class ZipPageLoader(file: File) : PageLoader() {
|
||||
|
||||
private val context: Application by injectLazy()
|
||||
private val tmpDir = File(context.externalCacheDir, "reader_${file.hashCode()}").also {
|
||||
it.deleteRecursively()
|
||||
it.mkdirs()
|
||||
}
|
||||
|
||||
init {
|
||||
ByteArrayOutputStream().use { byteArrayOutputStream ->
|
||||
FileInputStream(file).use { it.copyTo(byteArrayOutputStream) }
|
||||
|
||||
ZipFile(SeekableInMemoryByteChannel(byteArrayOutputStream.toByteArray())).use { zip ->
|
||||
zip.entries.asSequence()
|
||||
.filterNot { it.isDirectory }
|
||||
.forEach { entry ->
|
||||
File(tmpDir, entry.name.substringAfterLast("/"))
|
||||
.also { it.createNewFile() }
|
||||
.outputStream().use { pageOutputStream ->
|
||||
zip.getInputStream(entry).copyTo(pageOutputStream)
|
||||
pageOutputStream.flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private val zip = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
ZipFile(file, StandardCharsets.ISO_8859_1)
|
||||
} else {
|
||||
ZipFile(file)
|
||||
}
|
||||
|
||||
override var isLocal: Boolean = true
|
||||
|
||||
override suspend fun getPages(): List<ReaderPage> {
|
||||
return DirectoryPageLoader(tmpDir).getPages()
|
||||
return zip.entries().asSequence()
|
||||
.filter { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
|
||||
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
||||
.mapIndexed { i, entry ->
|
||||
ReaderPage(i).apply {
|
||||
stream = { zip.getInputStream(entry) }
|
||||
status = Page.State.READY
|
||||
}
|
||||
}
|
||||
.toList()
|
||||
}
|
||||
|
||||
override suspend fun loadPage(page: ReaderPage) {
|
||||
check(!isRecycled)
|
||||
}
|
||||
|
||||
override fun recycle() {
|
||||
super.recycle()
|
||||
tmpDir.deleteRecursively()
|
||||
zip.close()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,35 +2,70 @@ package eu.kanade.tachiyomi.ui.reader.viewer
|
|||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.widget.FrameLayout
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.AbstractComposeView
|
||||
import eu.kanade.presentation.reader.ChapterTransition
|
||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||
import eu.kanade.tachiyomi.data.download.manga.MangaDownloadManager
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
|
||||
import eu.kanade.tachiyomi.util.view.setComposeContent
|
||||
import tachiyomi.domain.entries.manga.model.Manga
|
||||
|
||||
class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
|
||||
FrameLayout(context, attrs) {
|
||||
AbstractComposeView(context, attrs) {
|
||||
|
||||
private var data: Data? by mutableStateOf(null)
|
||||
|
||||
init {
|
||||
layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
|
||||
}
|
||||
|
||||
fun bind(transition: ChapterTransition, downloadManager: MangaDownloadManager, manga: Manga?) {
|
||||
manga ?: return
|
||||
data = if (manga != null) {
|
||||
Data(
|
||||
transition = transition,
|
||||
currChapterDownloaded = transition.from.pageLoader?.isLocal == true,
|
||||
goingToChapterDownloaded = transition.to?.chapter?.let { goingToChapter ->
|
||||
downloadManager.isChapterDownloaded(
|
||||
chapterName = goingToChapter.name,
|
||||
chapterScanlator = goingToChapter.scanlator,
|
||||
mangaTitle = manga.title,
|
||||
sourceId = manga.source,
|
||||
skipCache = true,
|
||||
)
|
||||
} ?: false,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
removeAllViews()
|
||||
|
||||
val transitionView = ComposeView(context).apply {
|
||||
setComposeContent {
|
||||
ChapterTransition(
|
||||
transition = transition,
|
||||
downloadManager = downloadManager,
|
||||
manga = manga,
|
||||
)
|
||||
@Composable
|
||||
override fun Content() {
|
||||
data?.let {
|
||||
TachiyomiTheme {
|
||||
CompositionLocalProvider(
|
||||
LocalTextStyle provides MaterialTheme.typography.bodySmall,
|
||||
LocalContentColor provides MaterialTheme.colorScheme.onBackground,
|
||||
) {
|
||||
ChapterTransition(
|
||||
transition = it.transition,
|
||||
currChapterDownloaded = it.currChapterDownloaded,
|
||||
goingToChapterDownloaded = it.goingToChapterDownloaded,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
addView(transitionView)
|
||||
}
|
||||
private data class Data(
|
||||
val transition: ChapterTransition,
|
||||
val currChapterDownloaded: Boolean,
|
||||
val goingToChapterDownloaded: Boolean,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -8,8 +8,8 @@ import eu.kanade.tachiyomi.animesource.model.Video
|
|||
@Suppress("OverridingDeprecatedMember")
|
||||
class StubAnimeSource(
|
||||
override val id: Long,
|
||||
override val name: String,
|
||||
override val lang: String,
|
||||
override val name: String,
|
||||
) : AnimeSource {
|
||||
|
||||
val isInvalid: Boolean = name.isBlank() || lang.isBlank()
|
||||
|
|
|
@ -9,8 +9,8 @@ import rx.Observable
|
|||
@Suppress("OverridingDeprecatedMember")
|
||||
class StubMangaSource(
|
||||
override val id: Long,
|
||||
override val name: String,
|
||||
override val lang: String,
|
||||
override val name: String,
|
||||
) : MangaSource {
|
||||
|
||||
val isInvalid: Boolean = name.isBlank() || lang.isBlank()
|
||||
|
|
|
@ -10,7 +10,7 @@ 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-alpha03"
|
||||
corektx = "androidx.core:core-ktx:1.11.0-alpha04"
|
||||
splashscreen = "androidx.core:core-splashscreen:1.0.0-alpha02"
|
||||
recyclerview = "androidx.recyclerview:recyclerview:1.3.0"
|
||||
viewpager = "androidx.viewpager:viewpager:1.1.0-alpha01"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
[versions]
|
||||
compiler = "1.4.4"
|
||||
compiler = "1.4.7"
|
||||
compose-bom = "2023.03.00"
|
||||
accompanist = "0.30.1"
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
[versions]
|
||||
kotlin_version = "1.8.10"
|
||||
kotlin_version = "1.8.21"
|
||||
serialization_version = "1.5.0"
|
||||
xml_serialization_version = "0.85.0"
|
||||
|
||||
|
@ -7,7 +7,7 @@ xml_serialization_version = "0.85.0"
|
|||
reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin_version" }
|
||||
gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin_version" }
|
||||
|
||||
coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version = "1.6.4" }
|
||||
coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version = "1.7.1" }
|
||||
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core" }
|
||||
coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android" }
|
||||
coroutines-guava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava" }
|
||||
|
|
|
@ -6,7 +6,7 @@ shizuku_version = "12.2.0"
|
|||
sqlite = "2.3.1"
|
||||
sqldelight = "1.5.5"
|
||||
leakcanary = "2.10"
|
||||
voyager = "1.0.0-rc07"
|
||||
voyager = "1.0.0-rc06"
|
||||
richtext = "0.16.0"
|
||||
|
||||
[libraries]
|
||||
|
@ -31,7 +31,6 @@ jsoup = "org.jsoup:jsoup:1.16.1"
|
|||
|
||||
disklrucache = "com.jakewharton:disklrucache:2.0.2"
|
||||
unifile = "com.github.tachiyomiorg:unifile:17bec43"
|
||||
compress = "org.apache.commons:commons-compress:1.23.0"
|
||||
junrar = "com.github.junrar:junrar:7.5.4"
|
||||
|
||||
sqlite-framework = { module = "androidx.sqlite:sqlite-framework", version.ref = "sqlite" }
|
||||
|
@ -54,14 +53,14 @@ natural-comparator = "com.github.gpanther:java-nat-sort:natural-comparator-1.1"
|
|||
richtext-commonmark = { module = "com.halilibo.compose-richtext:richtext-commonmark", version.ref = "richtext" }
|
||||
richtext-m3 = { module = "com.halilibo.compose-richtext:richtext-ui-material3", version.ref = "richtext" }
|
||||
|
||||
material = "com.google.android.material:material:1.8.0"
|
||||
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.1"
|
||||
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"
|
||||
|
||||
logcat = "com.squareup.logcat:logcat:0.1"
|
||||
|
@ -84,11 +83,11 @@ sqldelight-android-paging = { module = "com.squareup.sqldelight:android-paging3-
|
|||
sqldelight-gradle = { module = "com.squareup.sqldelight:gradle-plugin", version.ref = "sqldelight" }
|
||||
|
||||
junit = "org.junit.jupiter:junit-jupiter:5.9.3"
|
||||
kotest-assertions = "io.kotest:kotest-assertions-core:5.6.1"
|
||||
kotest-assertions = "io.kotest:kotest-assertions-core:5.6.2"
|
||||
|
||||
voyager-navigator = { module = "ca.gosyer:voyager-navigator", version.ref = "voyager" }
|
||||
voyager-tab-navigator = { module = "ca.gosyer:voyager-tab-navigator", version.ref = "voyager" }
|
||||
voyager-transitions = { module = "ca.gosyer:voyager-transitions", 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-transitions = { module = "cafe.adriel.voyager:voyager-transitions", version.ref = "voyager" }
|
||||
|
||||
kotlinter = "org.jmailen.gradle:kotlinter-gradle:3.13.0"
|
||||
|
||||
|
|
|
@ -1,300 +1,59 @@
|
|||
package tachiyomi.presentation.core.components
|
||||
|
||||
import androidx.compose.foundation.gestures.FlingBehavior
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider
|
||||
import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.foundation.lazy.LazyListItemInfo
|
||||
import androidx.compose.foundation.lazy.LazyListLayoutInfo
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.pager.PageSize
|
||||
import androidx.compose.foundation.pager.PagerDefaults
|
||||
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.LaunchedEffect
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import androidx.compose.runtime.saveable.listSaver
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import androidx.compose.ui.util.fastMaxBy
|
||||
import androidx.compose.ui.util.fastSumBy
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlin.math.abs
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/**
|
||||
* Horizontal Pager with custom SnapFlingBehavior for a more natural swipe feeling
|
||||
*/
|
||||
@Composable
|
||||
fun HorizontalPager(
|
||||
count: Int,
|
||||
pageCount: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
state: PagerState = rememberPagerState(),
|
||||
key: ((page: Int) -> Any)? = null,
|
||||
contentPadding: PaddingValues = PaddingValues(),
|
||||
contentPadding: PaddingValues = PaddingValues(0.dp),
|
||||
pageSize: PageSize = PageSize.Fill,
|
||||
beyondBoundsPageCount: Int = 0,
|
||||
pageSpacing: Dp = 0.dp,
|
||||
verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
|
||||
userScrollEnabled: Boolean = true,
|
||||
content: @Composable BoxScope.(page: Int) -> Unit,
|
||||
reverseLayout: Boolean = false,
|
||||
key: ((index: Int) -> Any)? = null,
|
||||
pageNestedScrollConnection: NestedScrollConnection = PagerDefaults.pageNestedScrollConnection(
|
||||
Orientation.Horizontal,
|
||||
),
|
||||
pageContent: @Composable (page: Int) -> Unit,
|
||||
) {
|
||||
Pager(
|
||||
count = count,
|
||||
androidx.compose.foundation.pager.HorizontalPager(
|
||||
pageCount = pageCount,
|
||||
modifier = modifier,
|
||||
state = state,
|
||||
isVertical = false,
|
||||
key = key,
|
||||
contentPadding = contentPadding,
|
||||
pageSize = pageSize,
|
||||
beyondBoundsPageCount = beyondBoundsPageCount,
|
||||
pageSpacing = pageSpacing,
|
||||
verticalAlignment = verticalAlignment,
|
||||
flingBehavior = PagerDefaults.flingBehavior(
|
||||
state = state,
|
||||
pagerSnapDistance = PagerSnapDistance.atMost(0),
|
||||
),
|
||||
userScrollEnabled = userScrollEnabled,
|
||||
content = content,
|
||||
reverseLayout = reverseLayout,
|
||||
key = key,
|
||||
pageNestedScrollConnection = pageNestedScrollConnection,
|
||||
pageContent = pageContent,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Pager(
|
||||
count: Int,
|
||||
modifier: Modifier,
|
||||
state: PagerState,
|
||||
isVertical: Boolean,
|
||||
key: ((page: Int) -> Any)?,
|
||||
contentPadding: PaddingValues,
|
||||
userScrollEnabled: Boolean,
|
||||
verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
|
||||
horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
|
||||
content: @Composable BoxScope.(page: Int) -> Unit,
|
||||
) {
|
||||
LaunchedEffect(count) {
|
||||
state.currentPage = minOf(count - 1, state.currentPage).coerceAtLeast(0)
|
||||
}
|
||||
|
||||
LaunchedEffect(state) {
|
||||
snapshotFlow { state.mostVisiblePageLayoutInfo?.index }
|
||||
.distinctUntilChanged()
|
||||
.collect { state.updateCurrentPageBasedOnLazyListState() }
|
||||
}
|
||||
|
||||
if (isVertical) {
|
||||
LazyColumn(
|
||||
modifier = modifier,
|
||||
state = state.lazyListState,
|
||||
contentPadding = contentPadding,
|
||||
horizontalAlignment = horizontalAlignment,
|
||||
verticalArrangement = Arrangement.aligned(verticalAlignment),
|
||||
userScrollEnabled = userScrollEnabled,
|
||||
flingBehavior = rememberLazyListSnapFlingBehavior(lazyListState = state.lazyListState),
|
||||
) {
|
||||
items(
|
||||
count = count,
|
||||
key = key,
|
||||
) { page ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillParentMaxHeight()
|
||||
.wrapContentSize(),
|
||||
) {
|
||||
content(this, page)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LazyRow(
|
||||
modifier = modifier,
|
||||
state = state.lazyListState,
|
||||
contentPadding = contentPadding,
|
||||
verticalAlignment = verticalAlignment,
|
||||
horizontalArrangement = Arrangement.aligned(horizontalAlignment),
|
||||
userScrollEnabled = userScrollEnabled,
|
||||
flingBehavior = rememberLazyListSnapFlingBehavior(lazyListState = state.lazyListState),
|
||||
) {
|
||||
items(
|
||||
count = count,
|
||||
key = key,
|
||||
) { page ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillParentMaxWidth()
|
||||
.wrapContentSize(),
|
||||
) {
|
||||
content(this, page)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberPagerState(
|
||||
initialPage: Int = 0,
|
||||
) = rememberSaveable(saver = PagerState.Saver) {
|
||||
PagerState(currentPage = initialPage)
|
||||
}
|
||||
|
||||
@Stable
|
||||
class PagerState(
|
||||
currentPage: Int = 0,
|
||||
) {
|
||||
init { check(currentPage >= 0) { "currentPage cannot be less than zero" } }
|
||||
|
||||
val lazyListState = LazyListState(firstVisibleItemIndex = currentPage)
|
||||
|
||||
private val pageSize: Int
|
||||
get() = visiblePages.firstOrNull()?.size ?: 0
|
||||
|
||||
private var _currentPage by mutableStateOf(currentPage)
|
||||
|
||||
private val layoutInfo: LazyListLayoutInfo
|
||||
get() = lazyListState.layoutInfo
|
||||
|
||||
private val visiblePages: List<LazyListItemInfo>
|
||||
get() = layoutInfo.visibleItemsInfo
|
||||
|
||||
var currentPage: Int
|
||||
get() = _currentPage
|
||||
set(value) {
|
||||
if (value != _currentPage) {
|
||||
_currentPage = value
|
||||
}
|
||||
}
|
||||
|
||||
val mostVisiblePageLayoutInfo: LazyListItemInfo?
|
||||
get() {
|
||||
val layoutInfo = lazyListState.layoutInfo
|
||||
return layoutInfo.visibleItemsInfo.fastMaxBy {
|
||||
val start = maxOf(it.offset, 0)
|
||||
val end = minOf(
|
||||
it.offset + it.size,
|
||||
layoutInfo.viewportEndOffset - layoutInfo.afterContentPadding,
|
||||
)
|
||||
end - start
|
||||
}
|
||||
}
|
||||
|
||||
private val closestPageToSnappedPosition: LazyListItemInfo?
|
||||
get() = visiblePages.fastMaxBy {
|
||||
-abs(
|
||||
calculateDistanceToDesiredSnapPosition(
|
||||
layoutInfo,
|
||||
it,
|
||||
SnapAlignmentStartToStart,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val currentPageOffsetFraction: Float by derivedStateOf {
|
||||
val currentPagePositionOffset = closestPageToSnappedPosition?.offset ?: 0
|
||||
val pageUsedSpace = pageSize.toFloat()
|
||||
if (pageUsedSpace == 0f) {
|
||||
// Default to 0 when there's no info about the page size yet.
|
||||
0f
|
||||
} else {
|
||||
((-currentPagePositionOffset) / (pageUsedSpace)).coerceIn(
|
||||
MinPageOffset,
|
||||
MaxPageOffset,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateCurrentPageBasedOnLazyListState() {
|
||||
mostVisiblePageLayoutInfo?.let {
|
||||
currentPage = it.index
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun animateScrollToPage(page: Int) {
|
||||
lazyListState.animateScrollToItem(index = page)
|
||||
}
|
||||
|
||||
suspend fun scrollToPage(page: Int) {
|
||||
lazyListState.scrollToItem(index = page)
|
||||
updateCurrentPageBasedOnLazyListState()
|
||||
}
|
||||
|
||||
companion object {
|
||||
val Saver: Saver<PagerState, *> = listSaver(
|
||||
save = { listOf(it.currentPage) },
|
||||
restore = { PagerState(it[0]) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private const val MinPageOffset = -0.5f
|
||||
private const val MaxPageOffset = 0.5f
|
||||
internal val SnapAlignmentStartToStart: (layoutSize: Float, itemSize: Float) -> Float =
|
||||
{ _, _ -> 0f }
|
||||
|
||||
// https://android.googlesource.com/platform/frameworks/support/+/refs/changes/78/2160778/35/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/LazyListSnapLayoutInfoProvider.kt
|
||||
private fun lazyListSnapLayoutInfoProvider(
|
||||
lazyListState: LazyListState,
|
||||
positionInLayout: (layoutSize: Float, itemSize: Float) -> Float = { layoutSize, itemSize ->
|
||||
layoutSize / 2f - itemSize / 2f
|
||||
},
|
||||
) = object : SnapLayoutInfoProvider {
|
||||
|
||||
private val layoutInfo: LazyListLayoutInfo
|
||||
get() = lazyListState.layoutInfo
|
||||
|
||||
// Single page snapping is the default
|
||||
override fun Density.calculateApproachOffset(initialVelocity: Float): Float = 0f
|
||||
|
||||
override fun Density.calculateSnappingOffsetBounds(): ClosedFloatingPointRange<Float> {
|
||||
var lowerBoundOffset = Float.NEGATIVE_INFINITY
|
||||
var upperBoundOffset = Float.POSITIVE_INFINITY
|
||||
|
||||
layoutInfo.visibleItemsInfo.fastForEach { item ->
|
||||
val offset =
|
||||
calculateDistanceToDesiredSnapPosition(layoutInfo, item, positionInLayout)
|
||||
|
||||
// Find item that is closest to the center
|
||||
if (offset <= 0 && offset > lowerBoundOffset) {
|
||||
lowerBoundOffset = offset
|
||||
}
|
||||
|
||||
// Find item that is closest to center, but after it
|
||||
if (offset >= 0 && offset < upperBoundOffset) {
|
||||
upperBoundOffset = offset
|
||||
}
|
||||
}
|
||||
|
||||
return lowerBoundOffset.rangeTo(upperBoundOffset)
|
||||
}
|
||||
|
||||
override fun Density.calculateSnapStepSize(): Float = with(layoutInfo) {
|
||||
if (visibleItemsInfo.isNotEmpty()) {
|
||||
visibleItemsInfo.fastSumBy { it.size } / visibleItemsInfo.size.toFloat()
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberLazyListSnapFlingBehavior(lazyListState: LazyListState): FlingBehavior {
|
||||
val snappingLayout = remember(lazyListState) { lazyListSnapLayoutInfoProvider(lazyListState) }
|
||||
return rememberSnapFlingBehavior(snappingLayout)
|
||||
}
|
||||
|
||||
private fun calculateDistanceToDesiredSnapPosition(
|
||||
layoutInfo: LazyListLayoutInfo,
|
||||
item: LazyListItemInfo,
|
||||
positionInLayout: (layoutSize: Float, itemSize: Float) -> Float,
|
||||
): Float {
|
||||
val containerSize =
|
||||
with(layoutInfo) { singleAxisViewportSize - beforeContentPadding - afterContentPadding }
|
||||
|
||||
val desiredDistance =
|
||||
positionInLayout(containerSize.toFloat(), item.size.toFloat())
|
||||
|
||||
val itemCurrentPosition = item.offset
|
||||
return itemCurrentPosition - desiredDistance
|
||||
}
|
||||
|
||||
private val LazyListLayoutInfo.singleAxisViewportSize: Int
|
||||
get() = if (orientation == Orientation.Vertical) viewportSize.height else viewportSize.width
|
||||
|
|
Loading…
Reference in a new issue