mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-24 13:48:55 +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
|
- name: Build app and run unit tests
|
||||||
uses: gradle/gradle-command-action@v2
|
uses: gradle/gradle-command-action@v2
|
||||||
with:
|
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
|
- name: Build app and run unit tests
|
||||||
uses: gradle/gradle-command-action@v2
|
uses: gradle/gradle-command-action@v2
|
||||||
with:
|
with:
|
||||||
arguments: lintKotlin assembleStandardRelease testStandardReleaseUnitTest
|
arguments: lintKotlin assembleStandardRelease testReleaseUnitTest
|
||||||
|
|
||||||
# Sign APK and create release for tags
|
# Sign APK and create release for tags
|
||||||
|
|
||||||
|
|
|
@ -218,7 +218,6 @@ dependencies {
|
||||||
// Disk
|
// Disk
|
||||||
implementation(libs.disklrucache)
|
implementation(libs.disklrucache)
|
||||||
implementation(libs.unifile)
|
implementation(libs.unifile)
|
||||||
implementation(libs.compress)
|
|
||||||
implementation(libs.junrar)
|
implementation(libs.junrar)
|
||||||
|
|
||||||
// Preferences
|
// Preferences
|
||||||
|
|
5
app/proguard-rules.pro
vendored
5
app/proguard-rules.pro
vendored
|
@ -74,7 +74,4 @@
|
||||||
##---------------End: proguard configuration for kotlinx.serialization ----------
|
##---------------End: proguard configuration for kotlinx.serialization ----------
|
||||||
|
|
||||||
# XmlUtil
|
# XmlUtil
|
||||||
-keep public enum nl.adaptivity.xmlutil.EventType { *; }
|
-keep public enum nl.adaptivity.xmlutil.EventType { *; }
|
||||||
|
|
||||||
# org.apache.commons:commons-compress
|
|
||||||
-keep,allowoptimization class org.apache.commons.compress.archivers.zip.**
|
|
|
@ -11,7 +11,6 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.paging.LoadState
|
import androidx.paging.LoadState
|
||||||
import androidx.paging.compose.LazyPagingItems
|
import androidx.paging.compose.LazyPagingItems
|
||||||
import androidx.paging.compose.itemKey
|
|
||||||
import eu.kanade.presentation.browse.InLibraryBadge
|
import eu.kanade.presentation.browse.InLibraryBadge
|
||||||
import eu.kanade.presentation.browse.manga.components.BrowseSourceLoadingItem
|
import eu.kanade.presentation.browse.manga.components.BrowseSourceLoadingItem
|
||||||
import eu.kanade.presentation.library.CommonEntryItemDefaults
|
import eu.kanade.presentation.library.CommonEntryItemDefaults
|
||||||
|
@ -41,10 +40,7 @@ fun BrowseAnimeSourceComfortableGrid(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
items(
|
items(count = animeList.itemCount) { index ->
|
||||||
count = animeList.itemCount,
|
|
||||||
key = animeList.itemKey { it.value.id },
|
|
||||||
) { index ->
|
|
||||||
val anime by animeList[index]?.collectAsState() ?: return@items
|
val anime by animeList[index]?.collectAsState() ?: return@items
|
||||||
BrowseAnimeSourceComfortableGridItem(
|
BrowseAnimeSourceComfortableGridItem(
|
||||||
anime = anime,
|
anime = anime,
|
||||||
|
|
|
@ -11,7 +11,6 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.paging.LoadState
|
import androidx.paging.LoadState
|
||||||
import androidx.paging.compose.LazyPagingItems
|
import androidx.paging.compose.LazyPagingItems
|
||||||
import androidx.paging.compose.itemKey
|
|
||||||
import eu.kanade.presentation.browse.InLibraryBadge
|
import eu.kanade.presentation.browse.InLibraryBadge
|
||||||
import eu.kanade.presentation.browse.manga.components.BrowseSourceLoadingItem
|
import eu.kanade.presentation.browse.manga.components.BrowseSourceLoadingItem
|
||||||
import eu.kanade.presentation.library.CommonEntryItemDefaults
|
import eu.kanade.presentation.library.CommonEntryItemDefaults
|
||||||
|
@ -41,10 +40,7 @@ fun BrowseAnimeSourceCompactGrid(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
items(
|
items(count = animeList.itemCount) { index ->
|
||||||
count = animeList.itemCount,
|
|
||||||
key = animeList.itemKey { it.value.id },
|
|
||||||
) { index ->
|
|
||||||
val anime by animeList[index]?.collectAsState() ?: return@items
|
val anime by animeList[index]?.collectAsState() ?: return@items
|
||||||
BrowseAnimeSourceCompactGridItem(
|
BrowseAnimeSourceCompactGridItem(
|
||||||
anime = anime,
|
anime = anime,
|
||||||
|
|
|
@ -7,7 +7,6 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.paging.LoadState
|
import androidx.paging.LoadState
|
||||||
import androidx.paging.compose.LazyPagingItems
|
import androidx.paging.compose.LazyPagingItems
|
||||||
import androidx.paging.compose.itemKey
|
|
||||||
import androidx.paging.compose.items
|
import androidx.paging.compose.items
|
||||||
import eu.kanade.presentation.browse.InLibraryBadge
|
import eu.kanade.presentation.browse.InLibraryBadge
|
||||||
import eu.kanade.presentation.browse.manga.components.BrowseSourceLoadingItem
|
import eu.kanade.presentation.browse.manga.components.BrowseSourceLoadingItem
|
||||||
|
@ -35,10 +34,7 @@ fun BrowseAnimeSourceList(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
items(
|
items(count = animeList.itemCount) { index ->
|
||||||
count = animeList.itemCount,
|
|
||||||
key = animeList.itemKey { it.value.id },
|
|
||||||
) { index ->
|
|
||||||
val anime by animeList[index]?.collectAsState() ?: return@items
|
val anime by animeList[index]?.collectAsState() ?: return@items
|
||||||
BrowseAnimeSourceListItem(
|
BrowseAnimeSourceListItem(
|
||||||
anime = anime,
|
anime = anime,
|
||||||
|
|
|
@ -11,7 +11,6 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.paging.LoadState
|
import androidx.paging.LoadState
|
||||||
import androidx.paging.compose.LazyPagingItems
|
import androidx.paging.compose.LazyPagingItems
|
||||||
import androidx.paging.compose.itemKey
|
|
||||||
import eu.kanade.presentation.browse.InLibraryBadge
|
import eu.kanade.presentation.browse.InLibraryBadge
|
||||||
import eu.kanade.presentation.library.CommonEntryItemDefaults
|
import eu.kanade.presentation.library.CommonEntryItemDefaults
|
||||||
import eu.kanade.presentation.library.EntryComfortableGridItem
|
import eu.kanade.presentation.library.EntryComfortableGridItem
|
||||||
|
@ -40,10 +39,7 @@ fun BrowseMangaSourceComfortableGrid(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
items(
|
items(count = mangaList.itemCount) { index ->
|
||||||
count = mangaList.itemCount,
|
|
||||||
key = mangaList.itemKey { it.value.id },
|
|
||||||
) { index ->
|
|
||||||
val manga by mangaList[index]?.collectAsState() ?: return@items
|
val manga by mangaList[index]?.collectAsState() ?: return@items
|
||||||
BrowseMangaSourceComfortableGridItem(
|
BrowseMangaSourceComfortableGridItem(
|
||||||
manga = manga,
|
manga = manga,
|
||||||
|
|
|
@ -11,7 +11,6 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.paging.LoadState
|
import androidx.paging.LoadState
|
||||||
import androidx.paging.compose.LazyPagingItems
|
import androidx.paging.compose.LazyPagingItems
|
||||||
import androidx.paging.compose.itemKey
|
|
||||||
import eu.kanade.presentation.browse.InLibraryBadge
|
import eu.kanade.presentation.browse.InLibraryBadge
|
||||||
import eu.kanade.presentation.library.CommonEntryItemDefaults
|
import eu.kanade.presentation.library.CommonEntryItemDefaults
|
||||||
import eu.kanade.presentation.library.EntryCompactGridItem
|
import eu.kanade.presentation.library.EntryCompactGridItem
|
||||||
|
@ -40,10 +39,7 @@ fun BrowseMangaSourceCompactGrid(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
items(
|
items(count = mangaList.itemCount) { index ->
|
||||||
count = mangaList.itemCount,
|
|
||||||
key = mangaList.itemKey { it.value.id },
|
|
||||||
) { index ->
|
|
||||||
val manga by mangaList[index]?.collectAsState() ?: return@items
|
val manga by mangaList[index]?.collectAsState() ?: return@items
|
||||||
BrowseMangaSourceCompactGridItem(
|
BrowseMangaSourceCompactGridItem(
|
||||||
manga = manga,
|
manga = manga,
|
||||||
|
|
|
@ -7,7 +7,6 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.paging.LoadState
|
import androidx.paging.LoadState
|
||||||
import androidx.paging.compose.LazyPagingItems
|
import androidx.paging.compose.LazyPagingItems
|
||||||
import androidx.paging.compose.itemKey
|
|
||||||
import androidx.paging.compose.items
|
import androidx.paging.compose.items
|
||||||
import eu.kanade.presentation.browse.InLibraryBadge
|
import eu.kanade.presentation.browse.InLibraryBadge
|
||||||
import eu.kanade.presentation.library.CommonEntryItemDefaults
|
import eu.kanade.presentation.library.CommonEntryItemDefaults
|
||||||
|
@ -34,10 +33,7 @@ fun BrowseMangaSourceList(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
items(
|
items(count = mangaList.itemCount) { index ->
|
||||||
count = mangaList.itemCount,
|
|
||||||
key = mangaList.itemKey { it.value.id },
|
|
||||||
) { index ->
|
|
||||||
val manga by mangaList[index]?.collectAsState() ?: return@items
|
val manga by mangaList[index]?.collectAsState() ?: return@items
|
||||||
BrowseMangaSourceListItem(
|
BrowseMangaSourceListItem(
|
||||||
manga = manga,
|
manga = manga,
|
||||||
|
|
|
@ -16,13 +16,15 @@ import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import androidx.compose.ui.window.DialogProperties
|
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.lifecycle.DisposableEffectIgnoringConfiguration
|
||||||
|
import cafe.adriel.voyager.core.screen.Screen
|
||||||
import cafe.adriel.voyager.navigator.Navigator
|
import cafe.adriel.voyager.navigator.Navigator
|
||||||
import cafe.adriel.voyager.transitions.ScreenTransition
|
import cafe.adriel.voyager.transitions.ScreenTransition
|
||||||
import eu.kanade.presentation.util.Screen
|
|
||||||
import eu.kanade.presentation.util.isTabletUi
|
import eu.kanade.presentation.util.isTabletUi
|
||||||
import tachiyomi.presentation.core.components.AdaptiveSheet as AdaptiveSheetImpl
|
import tachiyomi.presentation.core.components.AdaptiveSheet as AdaptiveSheetImpl
|
||||||
|
|
||||||
|
@OptIn(InternalVoyagerApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun NavigatorAdaptiveSheet(
|
fun NavigatorAdaptiveSheet(
|
||||||
screen: Screen,
|
screen: Screen,
|
||||||
|
|
|
@ -20,6 +20,7 @@ import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.PlainTooltipBox
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
@ -203,21 +204,36 @@ fun AppBarActions(
|
||||||
var showMenu by remember { mutableStateOf(false) }
|
var showMenu by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
actions.filterIsInstance<AppBar.Action>().map {
|
actions.filterIsInstance<AppBar.Action>().map {
|
||||||
IconButton(
|
PlainTooltipBox(
|
||||||
onClick = it.onClick,
|
tooltip = { Text(it.title) },
|
||||||
enabled = it.enabled,
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
IconButton(
|
||||||
imageVector = it.icon,
|
onClick = it.onClick,
|
||||||
contentDescription = it.title,
|
enabled = it.enabled,
|
||||||
)
|
modifier = Modifier.tooltipAnchor(),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = it.icon,
|
||||||
|
contentDescription = it.title,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val overflowActions = actions.filterIsInstance<AppBar.OverflowAction>()
|
val overflowActions = actions.filterIsInstance<AppBar.OverflowAction>()
|
||||||
if (overflowActions.isNotEmpty()) {
|
if (overflowActions.isNotEmpty()) {
|
||||||
IconButton(onClick = { showMenu = !showMenu }) {
|
PlainTooltipBox(
|
||||||
Icon(Icons.Outlined.MoreVert, contentDescription = stringResource(R.string.abc_action_menu_overflow_description))
|
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(
|
DropdownMenu(
|
||||||
|
@ -327,17 +343,35 @@ fun SearchToolbar(
|
||||||
if (!searchEnabled) {
|
if (!searchEnabled) {
|
||||||
// Don't show search action
|
// Don't show search action
|
||||||
} else if (searchQuery == null) {
|
} else if (searchQuery == null) {
|
||||||
IconButton(onClick) {
|
PlainTooltipBox(
|
||||||
Icon(Icons.Outlined.Search, contentDescription = stringResource(R.string.action_search))
|
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()) {
|
} else if (searchQuery.isNotEmpty()) {
|
||||||
IconButton(
|
PlainTooltipBox(
|
||||||
onClick = {
|
tooltip = { Text(stringResource(R.string.action_reset)) },
|
||||||
onClick()
|
|
||||||
focusRequester.requestFocus()
|
|
||||||
},
|
|
||||||
) {
|
) {
|
||||||
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.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.wrapContentSize
|
import androidx.compose.foundation.layout.wrapContentSize
|
||||||
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
@ -31,7 +32,6 @@ import kotlinx.coroutines.launch
|
||||||
import tachiyomi.presentation.core.components.HorizontalPager
|
import tachiyomi.presentation.core.components.HorizontalPager
|
||||||
import tachiyomi.presentation.core.components.material.Divider
|
import tachiyomi.presentation.core.components.material.Divider
|
||||||
import tachiyomi.presentation.core.components.material.TabIndicator
|
import tachiyomi.presentation.core.components.material.TabIndicator
|
||||||
import tachiyomi.presentation.core.components.rememberPagerState
|
|
||||||
|
|
||||||
object TabbedDialogPaddings {
|
object TabbedDialogPaddings {
|
||||||
val Horizontal = 24.dp
|
val Horizontal = 24.dp
|
||||||
|
@ -84,7 +84,7 @@ fun TabbedDialog(
|
||||||
|
|
||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
modifier = Modifier.animateContentSize(),
|
modifier = Modifier.animateContentSize(),
|
||||||
count = tabTitles.size,
|
pageCount = tabTitles.size,
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
verticalAlignment = Alignment.Top,
|
verticalAlignment = Alignment.Top,
|
||||||
) { page ->
|
) { page ->
|
||||||
|
|
|
@ -7,6 +7,8 @@ import androidx.compose.foundation.layout.calculateEndPadding
|
||||||
import androidx.compose.foundation.layout.calculateStartPadding
|
import androidx.compose.foundation.layout.calculateStartPadding
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
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.MaterialTheme
|
||||||
import androidx.compose.material3.ScrollableTabRow
|
import androidx.compose.material3.ScrollableTabRow
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
|
@ -25,11 +27,9 @@ import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import tachiyomi.presentation.core.components.HorizontalPager
|
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.Scaffold
|
||||||
import tachiyomi.presentation.core.components.material.TabIndicator
|
import tachiyomi.presentation.core.components.material.TabIndicator
|
||||||
import tachiyomi.presentation.core.components.material.TabText
|
import tachiyomi.presentation.core.components.material.TabText
|
||||||
import tachiyomi.presentation.core.components.rememberPagerState
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TabbedScreen(
|
fun TabbedScreen(
|
||||||
|
@ -105,7 +105,7 @@ fun TabbedScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
count = tabs.size,
|
pageCount = tabs.size,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
state = state,
|
state = state,
|
||||||
verticalAlignment = Alignment.Top,
|
verticalAlignment = Alignment.Top,
|
||||||
|
|
|
@ -26,6 +26,8 @@ import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
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.EntryDownloadDropdownMenu
|
||||||
import eu.kanade.presentation.components.OverflowMenu
|
import eu.kanade.presentation.components.OverflowMenu
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
@ -77,18 +79,20 @@ fun EntryToolbar(
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
if (isActionMode) {
|
if (isActionMode) {
|
||||||
IconButton(onClick = onSelectAll) {
|
AppBarActions(
|
||||||
Icon(
|
listOf(
|
||||||
imageVector = Icons.Outlined.SelectAll,
|
AppBar.Action(
|
||||||
contentDescription = stringResource(R.string.action_select_all),
|
title = stringResource(R.string.action_select_all),
|
||||||
)
|
icon = Icons.Outlined.SelectAll,
|
||||||
}
|
onClick = onSelectAll,
|
||||||
IconButton(onClick = onInvertSelection) {
|
),
|
||||||
Icon(
|
AppBar.Action(
|
||||||
imageVector = Icons.Outlined.FlipToBack,
|
title = stringResource(R.string.action_select_inverse),
|
||||||
contentDescription = stringResource(R.string.action_select_inverse),
|
icon = Icons.Outlined.FlipToBack,
|
||||||
)
|
onClick = onInvertSelection,
|
||||||
}
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
if (onClickDownload != null) {
|
if (onClickDownload != null) {
|
||||||
val (downloadExpanded, onDownloadExpanded) = remember { mutableStateOf(false) }
|
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.StateScreenModel
|
||||||
import cafe.adriel.voyager.core.model.coroutineScope
|
import cafe.adriel.voyager.core.model.coroutineScope
|
||||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||||
|
import cafe.adriel.voyager.core.screen.Screen
|
||||||
import eu.kanade.core.util.asFlow
|
import eu.kanade.core.util.asFlow
|
||||||
import eu.kanade.presentation.components.TabbedDialogPaddings
|
import eu.kanade.presentation.components.TabbedDialogPaddings
|
||||||
import eu.kanade.presentation.util.Screen
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.animesource.model.Video
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadManager
|
import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadManager
|
||||||
|
@ -81,7 +81,7 @@ class EpisodeOptionsDialogScreen(
|
||||||
private val episodeId: Long,
|
private val episodeId: Long,
|
||||||
private val animeId: Long,
|
private val animeId: Long,
|
||||||
private val sourceId: Long,
|
private val sourceId: Long,
|
||||||
) : Screen() {
|
) : Screen {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun Content() {
|
override fun Content() {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package eu.kanade.presentation.library
|
package eu.kanade.presentation.library
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.pager.PagerState
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.ScrollableTabRow
|
import androidx.compose.material3.ScrollableTabRow
|
||||||
import androidx.compose.material3.Tab
|
import androidx.compose.material3.Tab
|
||||||
|
@ -8,7 +9,6 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.presentation.category.visualName
|
import eu.kanade.presentation.category.visualName
|
||||||
import tachiyomi.domain.category.model.Category
|
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.Divider
|
||||||
import tachiyomi.presentation.core.components.material.TabIndicator
|
import tachiyomi.presentation.core.components.material.TabIndicator
|
||||||
import tachiyomi.presentation.core.components.material.TabText
|
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.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
|
import eu.kanade.presentation.components.AppBarActions
|
||||||
import eu.kanade.presentation.components.OverflowMenu
|
import eu.kanade.presentation.components.OverflowMenu
|
||||||
import eu.kanade.presentation.components.SearchToolbar
|
import eu.kanade.presentation.components.SearchToolbar
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
@ -145,12 +146,20 @@ fun LibrarySelectionToolbar(
|
||||||
AppBar(
|
AppBar(
|
||||||
titleContent = { Text(text = "$selectedCount") },
|
titleContent = { Text(text = "$selectedCount") },
|
||||||
actions = {
|
actions = {
|
||||||
IconButton(onClick = onClickSelectAll) {
|
AppBarActions(
|
||||||
Icon(Icons.Outlined.SelectAll, contentDescription = stringResource(R.string.action_select_all))
|
listOf(
|
||||||
}
|
AppBar.Action(
|
||||||
IconButton(onClick = onClickInvertSelection) {
|
title = stringResource(R.string.action_select_all),
|
||||||
Icon(Icons.Outlined.FlipToBack, contentDescription = stringResource(R.string.action_select_inverse))
|
icon = Icons.Outlined.SelectAll,
|
||||||
}
|
onClick = onClickSelectAll,
|
||||||
|
),
|
||||||
|
AppBar.Action(
|
||||||
|
title = stringResource(R.string.action_select_inverse),
|
||||||
|
icon = Icons.Outlined.FlipToBack,
|
||||||
|
onClick = onClickInvertSelection,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
isActionMode = true,
|
isActionMode = true,
|
||||||
onCancelActionMode = onClickUnselectAll,
|
onCancelActionMode = onClickUnselectAll,
|
||||||
|
|
|
@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.calculateEndPadding
|
import androidx.compose.foundation.layout.calculateEndPadding
|
||||||
import androidx.compose.foundation.layout.calculateStartPadding
|
import androidx.compose.foundation.layout.calculateStartPadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
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.anime.LibraryAnime
|
||||||
import tachiyomi.domain.library.model.LibraryDisplayMode
|
import tachiyomi.domain.library.model.LibraryDisplayMode
|
||||||
import tachiyomi.presentation.core.components.material.PullRefresh
|
import tachiyomi.presentation.core.components.material.PullRefresh
|
||||||
import tachiyomi.presentation.core.components.rememberPagerState
|
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -61,8 +61,10 @@ fun AnimeLibraryContent(
|
||||||
var isRefreshing by remember(pagerState.currentPage) { mutableStateOf(false) }
|
var isRefreshing by remember(pagerState.currentPage) { mutableStateOf(false) }
|
||||||
|
|
||||||
if (showPageTabs && categories.size > 1) {
|
if (showPageTabs && categories.size > 1) {
|
||||||
if (categories.size <= pagerState.currentPage) {
|
LaunchedEffect(categories) {
|
||||||
pagerState.currentPage = categories.size - 1
|
if (categories.size <= pagerState.currentPage) {
|
||||||
|
pagerState.scrollToPage(categories.size - 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
LibraryTabs(
|
LibraryTabs(
|
||||||
categories = categories,
|
categories = categories,
|
||||||
|
|
|
@ -3,6 +3,7 @@ package eu.kanade.presentation.library.anime
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.pager.PagerState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
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.anime.LibraryAnime
|
||||||
import tachiyomi.domain.library.model.LibraryDisplayMode
|
import tachiyomi.domain.library.model.LibraryDisplayMode
|
||||||
import tachiyomi.presentation.core.components.HorizontalPager
|
import tachiyomi.presentation.core.components.HorizontalPager
|
||||||
import tachiyomi.presentation.core.components.PagerState
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AnimeLibraryPager(
|
fun AnimeLibraryPager(
|
||||||
|
@ -35,7 +35,7 @@ fun AnimeLibraryPager(
|
||||||
onClickContinueWatching: ((LibraryAnime) -> Unit)?,
|
onClickContinueWatching: ((LibraryAnime) -> Unit)?,
|
||||||
) {
|
) {
|
||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
count = pageCount,
|
pageCount = pageCount,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
state = state,
|
state = state,
|
||||||
verticalAlignment = Alignment.Top,
|
verticalAlignment = Alignment.Top,
|
||||||
|
|
|
@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.calculateEndPadding
|
import androidx.compose.foundation.layout.calculateEndPadding
|
||||||
import androidx.compose.foundation.layout.calculateStartPadding
|
import androidx.compose.foundation.layout.calculateStartPadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
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.manga.LibraryManga
|
||||||
import tachiyomi.domain.library.model.LibraryDisplayMode
|
import tachiyomi.domain.library.model.LibraryDisplayMode
|
||||||
import tachiyomi.presentation.core.components.material.PullRefresh
|
import tachiyomi.presentation.core.components.material.PullRefresh
|
||||||
import tachiyomi.presentation.core.components.rememberPagerState
|
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -61,8 +61,10 @@ fun MangaLibraryContent(
|
||||||
var isRefreshing by remember(pagerState.currentPage) { mutableStateOf(false) }
|
var isRefreshing by remember(pagerState.currentPage) { mutableStateOf(false) }
|
||||||
|
|
||||||
if (showPageTabs && categories.size > 1) {
|
if (showPageTabs && categories.size > 1) {
|
||||||
if (categories.size <= pagerState.currentPage) {
|
LaunchedEffect(categories) {
|
||||||
pagerState.currentPage = categories.size - 1
|
if (categories.size <= pagerState.currentPage) {
|
||||||
|
pagerState.scrollToPage(categories.size - 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
LibraryTabs(
|
LibraryTabs(
|
||||||
categories = categories,
|
categories = categories,
|
||||||
|
|
|
@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.pager.PagerState
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.runtime.Composable
|
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.manga.LibraryManga
|
||||||
import tachiyomi.domain.library.model.LibraryDisplayMode
|
import tachiyomi.domain.library.model.LibraryDisplayMode
|
||||||
import tachiyomi.presentation.core.components.HorizontalPager
|
import tachiyomi.presentation.core.components.HorizontalPager
|
||||||
import tachiyomi.presentation.core.components.PagerState
|
|
||||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||||
import tachiyomi.presentation.core.util.plus
|
import tachiyomi.presentation.core.util.plus
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ fun MangaLibraryPager(
|
||||||
onClickContinueReading: ((LibraryManga) -> Unit)?,
|
onClickContinueReading: ((LibraryManga) -> Unit)?,
|
||||||
) {
|
) {
|
||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
count = pageCount,
|
pageCount = pageCount,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
state = state,
|
state = state,
|
||||||
verticalAlignment = Alignment.Top,
|
verticalAlignment = Alignment.Top,
|
||||||
|
|
|
@ -18,6 +18,7 @@ import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.util.fastForEach
|
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.core.model.rememberScreenModel
|
||||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
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.Screen
|
||||||
import eu.kanade.presentation.util.ioCoroutineScope
|
import eu.kanade.presentation.util.ioCoroutineScope
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||||
import eu.kanade.tachiyomi.util.system.workManager
|
import eu.kanade.tachiyomi.util.system.workManager
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
@ -63,13 +67,17 @@ object WorkerInfoScreen : Screen() {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
IconButton(
|
AppBarActions(
|
||||||
onClick = {
|
listOf(
|
||||||
context.copyToClipboard(title, enqueued + finished + running)
|
AppBar.Action(
|
||||||
},
|
title = stringResource(R.string.action_copy_to_clipboard),
|
||||||
) {
|
icon = Icons.Default.ContentCopy,
|
||||||
Icon(imageVector = Icons.Default.ContentCopy, contentDescription = null)
|
onClick = {
|
||||||
}
|
context.copyToClipboard(title, enqueued + finished + running)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
scrollBehavior = it,
|
scrollBehavior = it,
|
||||||
)
|
)
|
||||||
|
|
|
@ -35,6 +35,7 @@ import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.presentation.util.isTabletUi
|
import eu.kanade.presentation.util.isTabletUi
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChapterNavigator(
|
fun ChapterNavigator(
|
||||||
|
@ -107,7 +108,7 @@ fun ChapterNavigator(
|
||||||
valueRange = 1f..totalPages.toFloat(),
|
valueRange = 1f..totalPages.toFloat(),
|
||||||
steps = totalPages - 2,
|
steps = totalPages - 2,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
onSliderValueChange(it.toInt() - 1)
|
onSliderValueChange(it.roundToInt() - 1)
|
||||||
},
|
},
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,60 +2,59 @@ package eu.kanade.presentation.reader
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
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.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
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.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Info
|
||||||
import androidx.compose.material.icons.outlined.OfflinePin
|
import androidx.compose.material.icons.outlined.OfflinePin
|
||||||
import androidx.compose.material.icons.outlined.Warning
|
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.Icon
|
||||||
import androidx.compose.material3.LocalContentColor
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedCard
|
||||||
import androidx.compose.material3.ProvideTextStyle
|
import androidx.compose.material3.ProvideTextStyle
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.pluralStringResource
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.Placeholder
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
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.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.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.manga.Chapter
|
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.database.models.manga.toDomainChapter
|
||||||
import eu.kanade.tachiyomi.data.download.manga.MangaDownloadManager
|
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
|
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.domain.items.service.calculateChapterGap
|
||||||
import tachiyomi.presentation.core.components.material.SecondaryItemAlpha
|
import tachiyomi.presentation.core.util.ThemePreviews
|
||||||
|
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChapterTransition(
|
fun ChapterTransition(
|
||||||
transition: ChapterTransition,
|
transition: ChapterTransition,
|
||||||
downloadManager: MangaDownloadManager,
|
currChapterDownloaded: Boolean,
|
||||||
manga: Manga?,
|
goingToChapterDownloaded: Boolean,
|
||||||
) {
|
) {
|
||||||
manga ?: return
|
|
||||||
|
|
||||||
val currChapter = transition.from.chapter
|
val currChapter = transition.from.chapter
|
||||||
val currChapterDownloaded = transition.from.pageLoader?.isLocal == true
|
|
||||||
|
|
||||||
val goingToChapter = transition.to?.chapter
|
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) {
|
ProvideTextStyle(MaterialTheme.typography.bodyMedium) {
|
||||||
when (transition) {
|
when (transition) {
|
||||||
|
@ -90,80 +89,289 @@ fun ChapterTransition(
|
||||||
@Composable
|
@Composable
|
||||||
private fun TransitionText(
|
private fun TransitionText(
|
||||||
topLabel: String,
|
topLabel: String,
|
||||||
topChapter: Chapter? = null,
|
topChapter: Chapter?,
|
||||||
topChapterDownloaded: Boolean,
|
topChapterDownloaded: Boolean,
|
||||||
bottomLabel: String,
|
bottomLabel: String,
|
||||||
bottomChapter: Chapter? = null,
|
bottomChapter: Chapter?,
|
||||||
bottomChapterDownloaded: Boolean,
|
bottomChapterDownloaded: Boolean,
|
||||||
fallbackLabel: String,
|
fallbackLabel: String,
|
||||||
chapterGap: Int,
|
chapterGap: Int,
|
||||||
) {
|
) {
|
||||||
val hasTopChapter = topChapter != null
|
Column(
|
||||||
val hasBottomChapter = bottomChapter != null
|
modifier = Modifier
|
||||||
|
.widthIn(max = 460.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
if (topChapter != null) {
|
||||||
|
ChapterText(
|
||||||
|
header = topLabel,
|
||||||
|
name = topChapter.name,
|
||||||
|
scanlator = topChapter.scanlator,
|
||||||
|
downloaded = topChapterDownloaded,
|
||||||
|
)
|
||||||
|
|
||||||
Column {
|
Spacer(Modifier.height(VerticalSpacerSize))
|
||||||
Text(
|
} else {
|
||||||
text = if (hasTopChapter) topLabel else fallbackLabel,
|
NoChapterNotification(
|
||||||
fontWeight = FontWeight.Bold,
|
text = fallbackLabel,
|
||||||
textAlign = if (hasTopChapter) TextAlign.Start else TextAlign.Center,
|
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||||
)
|
)
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
if (bottomChapter != null) {
|
||||||
text = if (hasBottomChapter) bottomLabel else fallbackLabel,
|
if (chapterGap > 0) {
|
||||||
fontWeight = FontWeight.Bold,
|
ChapterGapWarning(
|
||||||
textAlign = if (hasBottomChapter) TextAlign.Start else TextAlign.Center,
|
gapCount = chapterGap,
|
||||||
)
|
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||||
bottomChapter?.let { ChapterText(chapter = it, downloaded = bottomChapterDownloaded) }
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
@Composable
|
||||||
private fun ColumnScope.ChapterText(
|
private fun NoChapterNotification(
|
||||||
chapter: Chapter,
|
text: String,
|
||||||
downloaded: Boolean,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
FlowRow(
|
OutlinedCard(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
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(
|
Icon(
|
||||||
imageVector = Icons.Outlined.OfflinePin,
|
imageVector = Icons.Outlined.Info,
|
||||||
contentDescription = stringResource(R.string.label_downloaded),
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
contentDescription = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(Modifier.width(8.dp))
|
Text(
|
||||||
}
|
text = text,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
Text(chapter.name)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
chapter.scanlator?.let {
|
}
|
||||||
ProvideTextStyle(
|
|
||||||
MaterialTheme.typography.bodyMedium.copy(
|
@Composable
|
||||||
color = LocalContentColor.current.copy(alpha = SecondaryItemAlpha),
|
private fun ChapterGapWarning(
|
||||||
),
|
gapCount: Int,
|
||||||
) {
|
modifier: Modifier = Modifier,
|
||||||
Text(it)
|
) {
|
||||||
|
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) {}
|
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 {
|
abstract class Screen : Screen {
|
||||||
|
|
||||||
override val key: ScreenKey = uniqueScreenKey
|
override val key: ScreenKey = uniqueScreenKey
|
||||||
|
|
|
@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.data.backup
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
|
import androidx.work.BackoffPolicy
|
||||||
import androidx.work.CoroutineWorker
|
import androidx.work.CoroutineWorker
|
||||||
import androidx.work.ExistingPeriodicWorkPolicy
|
import androidx.work.ExistingPeriodicWorkPolicy
|
||||||
import androidx.work.ExistingWorkPolicy
|
import androidx.work.ExistingWorkPolicy
|
||||||
|
@ -22,6 +23,8 @@ import tachiyomi.domain.backup.service.BackupPreferences
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
import kotlin.time.toJavaDuration
|
||||||
|
|
||||||
class BackupCreateJob(private val context: Context, workerParams: WorkerParameters) :
|
class BackupCreateJob(private val context: Context, workerParams: WorkerParameters) :
|
||||||
CoroutineWorker(context, workerParams) {
|
CoroutineWorker(context, workerParams) {
|
||||||
|
@ -29,12 +32,14 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete
|
||||||
private val notifier = BackupNotifier(context)
|
private val notifier = BackupNotifier(context)
|
||||||
|
|
||||||
override suspend fun doWork(): Result {
|
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 backupPreferences = Injekt.get<BackupPreferences>()
|
||||||
val uri = inputData.getString(LOCATION_URI_KEY)?.toUri()
|
val uri = inputData.getString(LOCATION_URI_KEY)?.toUri()
|
||||||
?: backupPreferences.backupsDirectory().get().toUri()
|
?: backupPreferences.backupsDirectory().get().toUri()
|
||||||
val flags = inputData.getInt(BACKUP_FLAGS_KEY, BackupConst.BACKUP_ALL)
|
val flags = inputData.getInt(BACKUP_FLAGS_KEY, BackupConst.BACKUP_ALL)
|
||||||
val isAutoBackup = inputData.getBoolean(IS_AUTO_BACKUP_KEY, true)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setForeground(getForegroundInfo())
|
setForeground(getForegroundInfo())
|
||||||
} catch (e: IllegalStateException) {
|
} catch (e: IllegalStateException) {
|
||||||
|
@ -79,6 +84,7 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete
|
||||||
10,
|
10,
|
||||||
TimeUnit.MINUTES,
|
TimeUnit.MINUTES,
|
||||||
)
|
)
|
||||||
|
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10.minutes.toJavaDuration())
|
||||||
.addTag(TAG_AUTO)
|
.addTag(TAG_AUTO)
|
||||||
.setInputData(
|
.setInputData(
|
||||||
workDataOf(
|
workDataOf(
|
||||||
|
|
|
@ -111,7 +111,6 @@ class BackupManager(
|
||||||
* @param uri path of Uri
|
* @param uri path of Uri
|
||||||
* @param isAutoBackup backup called from scheduled backup job
|
* @param isAutoBackup backup called from scheduled backup job
|
||||||
*/
|
*/
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
suspend fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String {
|
suspend fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String {
|
||||||
if (!context.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
if (!context.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
||||||
throw IllegalStateException(context.getString(R.string.missing_storage_permission))
|
throw IllegalStateException(context.getString(R.string.missing_storage_permission))
|
||||||
|
@ -446,10 +445,10 @@ class BackupManager(
|
||||||
}
|
}
|
||||||
|
|
||||||
internal suspend fun restoreExistingManga(manga: Manga, dbManga: Mangas): Manga {
|
internal suspend fun restoreExistingManga(manga: Manga, dbManga: Mangas): Manga {
|
||||||
var manga = manga.copy(id = dbManga._id)
|
var updatedManga = manga.copy(id = dbManga._id)
|
||||||
manga = manga.copyFrom(dbManga)
|
updatedManga = updatedManga.copyFrom(dbManga)
|
||||||
updateManga(manga)
|
updateManga(updatedManga)
|
||||||
return manga
|
return updatedManga
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -466,10 +465,10 @@ class BackupManager(
|
||||||
}
|
}
|
||||||
|
|
||||||
internal suspend fun restoreExistingAnime(anime: Anime, dbAnime: Animes): Anime {
|
internal suspend fun restoreExistingAnime(anime: Anime, dbAnime: Animes): Anime {
|
||||||
var anime = anime.copy(id = dbAnime._id)
|
var updatedAnime = anime.copy(id = dbAnime._id)
|
||||||
anime = anime.copyFrom(dbAnime)
|
updatedAnime = updatedAnime.copyFrom(dbAnime)
|
||||||
updateAnime(anime)
|
updateAnime(updatedAnime)
|
||||||
return anime
|
return updatedAnime
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -582,7 +581,7 @@ class BackupManager(
|
||||||
dbCategories.firstOrNull { dbCategory ->
|
dbCategories.firstOrNull { dbCategory ->
|
||||||
dbCategory.name == backupCategory.name
|
dbCategory.name == backupCategory.name
|
||||||
}?.let { dbCategory ->
|
}?.let { dbCategory ->
|
||||||
mangaCategoriesToUpdate.add(Pair(manga.id!!, dbCategory.id))
|
mangaCategoriesToUpdate.add(Pair(manga.id, dbCategory.id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -590,7 +589,7 @@ class BackupManager(
|
||||||
// Update database
|
// Update database
|
||||||
if (mangaCategoriesToUpdate.isNotEmpty()) {
|
if (mangaCategoriesToUpdate.isNotEmpty()) {
|
||||||
mangaHandler.await(true) {
|
mangaHandler.await(true) {
|
||||||
mangas_categoriesQueries.deleteMangaCategoryByMangaId(manga.id!!)
|
mangas_categoriesQueries.deleteMangaCategoryByMangaId(manga.id)
|
||||||
mangaCategoriesToUpdate.forEach { (mangaId, categoryId) ->
|
mangaCategoriesToUpdate.forEach { (mangaId, categoryId) ->
|
||||||
mangas_categoriesQueries.insert(mangaId, categoryId)
|
mangas_categoriesQueries.insert(mangaId, categoryId)
|
||||||
}
|
}
|
||||||
|
@ -615,7 +614,7 @@ class BackupManager(
|
||||||
dbCategories.firstOrNull { dbCategory ->
|
dbCategories.firstOrNull { dbCategory ->
|
||||||
dbCategory.name == backupCategory.name
|
dbCategory.name == backupCategory.name
|
||||||
}?.let { dbCategory ->
|
}?.let { dbCategory ->
|
||||||
animeCategoriesToUpdate.add(Pair(anime.id!!, dbCategory.id))
|
animeCategoriesToUpdate.add(Pair(anime.id, dbCategory.id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -623,7 +622,7 @@ class BackupManager(
|
||||||
// Update database
|
// Update database
|
||||||
if (animeCategoriesToUpdate.isNotEmpty()) {
|
if (animeCategoriesToUpdate.isNotEmpty()) {
|
||||||
animeHandler.await(true) {
|
animeHandler.await(true) {
|
||||||
animes_categoriesQueries.deleteAnimeCategoryByAnimeId(anime.id!!)
|
animes_categoriesQueries.deleteAnimeCategoryByAnimeId(anime.id)
|
||||||
animeCategoriesToUpdate.forEach { (animeId, categoryId) ->
|
animeCategoriesToUpdate.forEach { (animeId, categoryId) ->
|
||||||
animes_categoriesQueries.insert(animeId, categoryId)
|
animes_categoriesQueries.insert(animeId, categoryId)
|
||||||
}
|
}
|
||||||
|
@ -730,37 +729,38 @@ class BackupManager(
|
||||||
* @param tracks the track list to restore.
|
* @param tracks the track list to restore.
|
||||||
*/
|
*/
|
||||||
internal suspend fun restoreTracking(manga: Manga, tracks: List<tachiyomi.domain.track.manga.model.MangaTrack>) {
|
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
|
// 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 toUpdate = mutableListOf<Manga_sync>()
|
||||||
val toInsert = mutableListOf<tachiyomi.domain.track.manga.model.MangaTrack>()
|
val toInsert = mutableListOf<tachiyomi.domain.track.manga.model.MangaTrack>()
|
||||||
|
|
||||||
tracks.forEach { track ->
|
tracks
|
||||||
var isInDatabase = false
|
// Fix foreign keys with the current manga id
|
||||||
for (dbTrack in dbTracks) {
|
.map { it.copy(mangaId = manga.id) }
|
||||||
if (track.syncId == dbTrack.sync_id) {
|
.forEach { track ->
|
||||||
// The sync is already in the db, only update its fields
|
var isInDatabase = false
|
||||||
var temp = dbTrack
|
for (dbTrack in dbTracks) {
|
||||||
if (track.remoteId != dbTrack.remote_id) {
|
if (track.syncId == dbTrack.sync_id) {
|
||||||
temp = temp.copy(remote_id = track.remoteId)
|
// 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)
|
if (!isInDatabase) {
|
||||||
}
|
// Insert new sync. Let the db assign the id
|
||||||
temp = temp.copy(last_chapter_read = max(dbTrack.last_chapter_read, track.lastChapterRead))
|
toInsert.add(track.copy(id = 0))
|
||||||
isInDatabase = true
|
|
||||||
toUpdate.add(temp)
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!isInDatabase) {
|
|
||||||
// Insert new sync. Let the db assign the id
|
|
||||||
toInsert.add(track.copy(id = 0))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Update database
|
// Update database
|
||||||
if (toUpdate.isNotEmpty()) {
|
if (toUpdate.isNotEmpty()) {
|
||||||
mangaHandler.await(true) {
|
mangaHandler.await(true) {
|
||||||
|
@ -812,37 +812,38 @@ class BackupManager(
|
||||||
* @param tracks the track list to restore.
|
* @param tracks the track list to restore.
|
||||||
*/
|
*/
|
||||||
internal suspend fun restoreAnimeTracking(anime: Anime, tracks: List<tachiyomi.domain.track.anime.model.AnimeTrack>) {
|
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
|
// 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 toUpdate = mutableListOf<Anime_sync>()
|
||||||
val toInsert = mutableListOf<tachiyomi.domain.track.anime.model.AnimeTrack>()
|
val toInsert = mutableListOf<tachiyomi.domain.track.anime.model.AnimeTrack>()
|
||||||
|
|
||||||
tracks.forEach { track ->
|
tracks
|
||||||
var isInDatabase = false
|
// Fix foreign keys with the current manga id
|
||||||
for (dbTrack in dbTracks) {
|
.map { it.copy(animeId = anime.id) }
|
||||||
if (track.syncId == dbTrack.sync_id) {
|
.forEach { track ->
|
||||||
// The sync is already in the db, only update its fields
|
var isInDatabase = false
|
||||||
var temp = dbTrack
|
for (dbTrack in dbTracks) {
|
||||||
if (track.remoteId != dbTrack.remote_id) {
|
if (track.syncId == dbTrack.sync_id) {
|
||||||
temp = temp.copy(remote_id = track.remoteId)
|
// 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)
|
if (!isInDatabase) {
|
||||||
}
|
// Insert new sync. Let the db assign the id
|
||||||
temp = temp.copy(last_episode_seen = max(dbTrack.last_episode_seen, track.lastEpisodeSeen))
|
toInsert.add(track.copy(id = 0))
|
||||||
isInDatabase = true
|
|
||||||
toUpdate.add(temp)
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!isInDatabase) {
|
|
||||||
// Insert new sync. Let the db assign the id
|
|
||||||
toInsert.add(track.copy(id = 0))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Update database
|
// Update database
|
||||||
if (toUpdate.isNotEmpty()) {
|
if (toUpdate.isNotEmpty()) {
|
||||||
animeHandler.await(true) {
|
animeHandler.await(true) {
|
||||||
|
@ -891,22 +892,22 @@ class BackupManager(
|
||||||
val dbChapters = mangaHandler.awaitList { chaptersQueries.getChaptersByMangaId(manga.id!!) }
|
val dbChapters = mangaHandler.awaitList { chaptersQueries.getChaptersByMangaId(manga.id!!) }
|
||||||
|
|
||||||
val processed = chapters.map { chapter ->
|
val processed = chapters.map { chapter ->
|
||||||
var chapter = chapter
|
var updatedChapter = chapter
|
||||||
val dbChapter = dbChapters.find { it.url == chapter.url }
|
val dbChapter = dbChapters.find { it.url == updatedChapter.url }
|
||||||
if (dbChapter != null) {
|
if (dbChapter != null) {
|
||||||
chapter = chapter.copy(id = dbChapter._id)
|
updatedChapter = updatedChapter.copy(id = dbChapter._id)
|
||||||
chapter = chapter.copyFrom(dbChapter)
|
updatedChapter = updatedChapter.copyFrom(dbChapter)
|
||||||
if (dbChapter.read && !chapter.read) {
|
if (dbChapter.read && !updatedChapter.read) {
|
||||||
chapter = chapter.copy(read = dbChapter.read, lastPageRead = dbChapter.last_page_read)
|
updatedChapter = updatedChapter.copy(read = true, lastPageRead = dbChapter.last_page_read)
|
||||||
} else if (chapter.lastPageRead == 0L && dbChapter.last_page_read != 0L) {
|
} 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) {
|
if (!updatedChapter.bookmark && dbChapter.bookmark) {
|
||||||
chapter = chapter.copy(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 }
|
val newChapters = processed.groupBy { it.id > 0 }
|
||||||
|
@ -918,22 +919,22 @@ class BackupManager(
|
||||||
val dbEpisodes = animeHandler.awaitList { episodesQueries.getEpisodesByAnimeId(anime.id!!) }
|
val dbEpisodes = animeHandler.awaitList { episodesQueries.getEpisodesByAnimeId(anime.id!!) }
|
||||||
|
|
||||||
val processed = episodes.map { episode ->
|
val processed = episodes.map { episode ->
|
||||||
var episode = episode
|
var updatedEpisode = episode
|
||||||
val dbEpisode = dbEpisodes.find { it.url == episode.url }
|
val dbEpisode = dbEpisodes.find { it.url == updatedEpisode.url }
|
||||||
if (dbEpisode != null) {
|
if (dbEpisode != null) {
|
||||||
episode = episode.copy(id = dbEpisode._id)
|
updatedEpisode = updatedEpisode.copy(id = dbEpisode._id)
|
||||||
episode = episode.copyFrom(dbEpisode)
|
updatedEpisode = updatedEpisode.copyFrom(dbEpisode)
|
||||||
if (dbEpisode.seen && !episode.seen) {
|
if (dbEpisode.seen && !updatedEpisode.seen) {
|
||||||
episode = episode.copy(seen = dbEpisode.seen, lastSecondSeen = dbEpisode.last_second_seen)
|
updatedEpisode = updatedEpisode.copy(seen = true, lastSecondSeen = dbEpisode.last_second_seen)
|
||||||
} else if (episode.lastSecondSeen == 0L && dbEpisode.last_second_seen != 0L) {
|
} else if (updatedEpisode.lastSecondSeen == 0L && dbEpisode.last_second_seen != 0L) {
|
||||||
episode = episode.copy(lastSecondSeen = dbEpisode.last_second_seen)
|
updatedEpisode = updatedEpisode.copy(lastSecondSeen = dbEpisode.last_second_seen)
|
||||||
}
|
}
|
||||||
if (!episode.bookmark && dbEpisode.bookmark) {
|
if (!updatedEpisode.bookmark && dbEpisode.bookmark) {
|
||||||
episode = episode.copy(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 }
|
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
|
* Updates a list of chapters with known database ids
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -28,6 +28,7 @@ data class BackupAnimeTracking(
|
||||||
@ProtoNumber(11) var finishedWatchingDate: Long = 0,
|
@ProtoNumber(11) var finishedWatchingDate: Long = 0,
|
||||||
@ProtoNumber(100) var mediaId: Long = 0,
|
@ProtoNumber(100) var mediaId: Long = 0,
|
||||||
) {
|
) {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
fun getTrackingImpl(): AnimeTrack {
|
fun getTrackingImpl(): AnimeTrack {
|
||||||
return AnimeTrack(
|
return AnimeTrack(
|
||||||
id = -1,
|
id = -1,
|
||||||
|
|
|
@ -29,6 +29,7 @@ data class BackupTracking(
|
||||||
@ProtoNumber(100) var mediaId: Long = 0,
|
@ProtoNumber(100) var mediaId: Long = 0,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
fun getTrackingImpl(): MangaTrack {
|
fun getTrackingImpl(): MangaTrack {
|
||||||
return MangaTrack(
|
return MangaTrack(
|
||||||
id = -1,
|
id = -1,
|
||||||
|
|
|
@ -118,7 +118,6 @@ class AnimeCoverScreenModel(
|
||||||
fun editCover(context: Context, data: Uri) {
|
fun editCover(context: Context, data: Uri) {
|
||||||
val anime = state.value ?: return
|
val anime = state.value ?: return
|
||||||
coroutineScope.launchIO {
|
coroutineScope.launchIO {
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
context.contentResolver.openInputStream(data)?.use {
|
context.contentResolver.openInputStream(data)?.use {
|
||||||
try {
|
try {
|
||||||
anime.editCover(Injekt.get(), it, updateAnime, coverCache)
|
anime.editCover(Injekt.get(), it, updateAnime, coverCache)
|
||||||
|
|
|
@ -628,8 +628,8 @@ class AnimeInfoScreenModel(
|
||||||
downloadEpisodes(episodes, false, video)
|
downloadEpisodes(episodes, false, video)
|
||||||
}
|
}
|
||||||
if (!isFavorited && !successState.hasPromptedToAddBefore) {
|
if (!isFavorited && !successState.hasPromptedToAddBefore) {
|
||||||
updateSuccessState { successState ->
|
updateSuccessState { state ->
|
||||||
successState.copy(hasPromptedToAddBefore = true)
|
state.copy(hasPromptedToAddBefore = true)
|
||||||
}
|
}
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
val result = snackbarHostState.showSnackbar(
|
val result = snackbarHostState.showSnackbar(
|
||||||
|
|
|
@ -118,7 +118,6 @@ class MangaCoverScreenModel(
|
||||||
fun editCover(context: Context, data: Uri) {
|
fun editCover(context: Context, data: Uri) {
|
||||||
val manga = state.value ?: return
|
val manga = state.value ?: return
|
||||||
coroutineScope.launchIO {
|
coroutineScope.launchIO {
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
context.contentResolver.openInputStream(data)?.use {
|
context.contentResolver.openInputStream(data)?.use {
|
||||||
try {
|
try {
|
||||||
manga.editCover(Injekt.get(), it, updateManga, coverCache)
|
manga.editCover(Injekt.get(), it, updateManga, coverCache)
|
||||||
|
|
|
@ -622,8 +622,8 @@ class MangaInfoScreenModel(
|
||||||
downloadChapters(chapters)
|
downloadChapters(chapters)
|
||||||
}
|
}
|
||||||
if (!isFavorited && !successState.hasPromptedToAddBefore) {
|
if (!isFavorited && !successState.hasPromptedToAddBefore) {
|
||||||
updateSuccessState { successState ->
|
updateSuccessState { state ->
|
||||||
successState.copy(hasPromptedToAddBefore = true)
|
state.copy(hasPromptedToAddBefore = true)
|
||||||
}
|
}
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
val result = snackbarHostState.showSnackbar(
|
val result = snackbarHostState.showSnackbar(
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
package eu.kanade.tachiyomi.ui.reader.loader
|
package eu.kanade.tachiyomi.ui.reader.loader
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import com.github.junrar.Archive
|
import com.github.junrar.Archive
|
||||||
import com.github.junrar.rarfile.FileHeader
|
import com.github.junrar.rarfile.FileHeader
|
||||||
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
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.File
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.PipedInputStream
|
import java.io.PipedInputStream
|
||||||
|
@ -15,36 +16,30 @@ import java.io.PipedOutputStream
|
||||||
*/
|
*/
|
||||||
internal class RarPageLoader(file: File) : PageLoader() {
|
internal class RarPageLoader(file: File) : PageLoader() {
|
||||||
|
|
||||||
private val context: Application by injectLazy()
|
private val rar = Archive(file)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override var isLocal: Boolean = true
|
override var isLocal: Boolean = true
|
||||||
|
|
||||||
override suspend fun getPages(): List<ReaderPage> {
|
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() {
|
override fun recycle() {
|
||||||
super.recycle()
|
super.recycle()
|
||||||
tmpDir.deleteRecursively()
|
rar.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,52 +1,46 @@
|
||||||
package eu.kanade.tachiyomi.ui.reader.loader
|
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 eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||||
import org.apache.commons.compress.archivers.zip.ZipFile
|
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
||||||
import org.apache.commons.compress.utils.SeekableInMemoryByteChannel
|
import tachiyomi.core.util.system.ImageUtil
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.io.ByteArrayOutputStream
|
|
||||||
import java.io.File
|
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.
|
* Loader used to load a chapter from a .zip or .cbz file.
|
||||||
*/
|
*/
|
||||||
internal class ZipPageLoader(file: File) : PageLoader() {
|
internal class ZipPageLoader(file: File) : PageLoader() {
|
||||||
|
|
||||||
private val context: Application by injectLazy()
|
private val zip = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
private val tmpDir = File(context.externalCacheDir, "reader_${file.hashCode()}").also {
|
ZipFile(file, StandardCharsets.ISO_8859_1)
|
||||||
it.deleteRecursively()
|
} else {
|
||||||
it.mkdirs()
|
ZipFile(file)
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override var isLocal: Boolean = true
|
override var isLocal: Boolean = true
|
||||||
|
|
||||||
override suspend fun getPages(): List<ReaderPage> {
|
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() {
|
override fun recycle() {
|
||||||
super.recycle()
|
super.recycle()
|
||||||
tmpDir.deleteRecursively()
|
zip.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,35 +2,70 @@ package eu.kanade.tachiyomi.ui.reader.viewer
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.widget.FrameLayout
|
import androidx.compose.material3.LocalContentColor
|
||||||
import androidx.compose.ui.platform.ComposeView
|
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.reader.ChapterTransition
|
||||||
|
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||||
import eu.kanade.tachiyomi.data.download.manga.MangaDownloadManager
|
import eu.kanade.tachiyomi.data.download.manga.MangaDownloadManager
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
|
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
|
||||||
import eu.kanade.tachiyomi.util.view.setComposeContent
|
|
||||||
import tachiyomi.domain.entries.manga.model.Manga
|
import tachiyomi.domain.entries.manga.model.Manga
|
||||||
|
|
||||||
class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
|
class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
|
||||||
FrameLayout(context, attrs) {
|
AbstractComposeView(context, attrs) {
|
||||||
|
|
||||||
|
private var data: Data? by mutableStateOf(null)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
|
layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun bind(transition: ChapterTransition, downloadManager: MangaDownloadManager, manga: Manga?) {
|
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()
|
@Composable
|
||||||
|
override fun Content() {
|
||||||
val transitionView = ComposeView(context).apply {
|
data?.let {
|
||||||
setComposeContent {
|
TachiyomiTheme {
|
||||||
ChapterTransition(
|
CompositionLocalProvider(
|
||||||
transition = transition,
|
LocalTextStyle provides MaterialTheme.typography.bodySmall,
|
||||||
downloadManager = downloadManager,
|
LocalContentColor provides MaterialTheme.colorScheme.onBackground,
|
||||||
manga = manga,
|
) {
|
||||||
)
|
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")
|
@Suppress("OverridingDeprecatedMember")
|
||||||
class StubAnimeSource(
|
class StubAnimeSource(
|
||||||
override val id: Long,
|
override val id: Long,
|
||||||
override val name: String,
|
|
||||||
override val lang: String,
|
override val lang: String,
|
||||||
|
override val name: String,
|
||||||
) : AnimeSource {
|
) : AnimeSource {
|
||||||
|
|
||||||
val isInvalid: Boolean = name.isBlank() || lang.isBlank()
|
val isInvalid: Boolean = name.isBlank() || lang.isBlank()
|
||||||
|
|
|
@ -9,8 +9,8 @@ import rx.Observable
|
||||||
@Suppress("OverridingDeprecatedMember")
|
@Suppress("OverridingDeprecatedMember")
|
||||||
class StubMangaSource(
|
class StubMangaSource(
|
||||||
override val id: Long,
|
override val id: Long,
|
||||||
override val name: String,
|
|
||||||
override val lang: String,
|
override val lang: String,
|
||||||
|
override val name: String,
|
||||||
) : MangaSource {
|
) : MangaSource {
|
||||||
|
|
||||||
val isInvalid: Boolean = name.isBlank() || lang.isBlank()
|
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"
|
biometricktx = "androidx.biometric:biometric-ktx:1.2.0-alpha05"
|
||||||
constraintlayout = "androidx.constraintlayout:constraintlayout:2.1.4"
|
constraintlayout = "androidx.constraintlayout:constraintlayout:2.1.4"
|
||||||
coordinatorlayout = "androidx.coordinatorlayout:coordinatorlayout:1.2.0"
|
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"
|
splashscreen = "androidx.core:core-splashscreen:1.0.0-alpha02"
|
||||||
recyclerview = "androidx.recyclerview:recyclerview:1.3.0"
|
recyclerview = "androidx.recyclerview:recyclerview:1.3.0"
|
||||||
viewpager = "androidx.viewpager:viewpager:1.1.0-alpha01"
|
viewpager = "androidx.viewpager:viewpager:1.1.0-alpha01"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
[versions]
|
[versions]
|
||||||
compiler = "1.4.4"
|
compiler = "1.4.7"
|
||||||
compose-bom = "2023.03.00"
|
compose-bom = "2023.03.00"
|
||||||
accompanist = "0.30.1"
|
accompanist = "0.30.1"
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
[versions]
|
[versions]
|
||||||
kotlin_version = "1.8.10"
|
kotlin_version = "1.8.21"
|
||||||
serialization_version = "1.5.0"
|
serialization_version = "1.5.0"
|
||||||
xml_serialization_version = "0.85.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" }
|
reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin_version" }
|
||||||
gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", 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-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core" }
|
||||||
coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android" }
|
coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android" }
|
||||||
coroutines-guava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava" }
|
coroutines-guava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava" }
|
||||||
|
|
|
@ -6,7 +6,7 @@ shizuku_version = "12.2.0"
|
||||||
sqlite = "2.3.1"
|
sqlite = "2.3.1"
|
||||||
sqldelight = "1.5.5"
|
sqldelight = "1.5.5"
|
||||||
leakcanary = "2.10"
|
leakcanary = "2.10"
|
||||||
voyager = "1.0.0-rc07"
|
voyager = "1.0.0-rc06"
|
||||||
richtext = "0.16.0"
|
richtext = "0.16.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
|
@ -31,7 +31,6 @@ jsoup = "org.jsoup:jsoup:1.16.1"
|
||||||
|
|
||||||
disklrucache = "com.jakewharton:disklrucache:2.0.2"
|
disklrucache = "com.jakewharton:disklrucache:2.0.2"
|
||||||
unifile = "com.github.tachiyomiorg:unifile:17bec43"
|
unifile = "com.github.tachiyomiorg:unifile:17bec43"
|
||||||
compress = "org.apache.commons:commons-compress:1.23.0"
|
|
||||||
junrar = "com.github.junrar:junrar:7.5.4"
|
junrar = "com.github.junrar:junrar:7.5.4"
|
||||||
|
|
||||||
sqlite-framework = { module = "androidx.sqlite:sqlite-framework", version.ref = "sqlite" }
|
sqlite-framework = { module = "androidx.sqlite:sqlite-framework", version.ref = "sqlite" }
|
||||||
|
@ -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-commonmark = { module = "com.halilibo.compose-richtext:richtext-commonmark", version.ref = "richtext" }
|
||||||
richtext-m3 = { module = "com.halilibo.compose-richtext:richtext-ui-material3", 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-core = "com.github.arkon.FlexibleAdapter:flexible-adapter:c8013533"
|
||||||
flexible-adapter-ui = "com.github.arkon.FlexibleAdapter:flexible-adapter-ui:c8013533"
|
flexible-adapter-ui = "com.github.arkon.FlexibleAdapter:flexible-adapter-ui:c8013533"
|
||||||
photoview = "com.github.chrisbanes:PhotoView:2.3.0"
|
photoview = "com.github.chrisbanes:PhotoView:2.3.0"
|
||||||
directionalviewpager = "com.github.tachiyomiorg:DirectionalViewPager:1.0.0"
|
directionalviewpager = "com.github.tachiyomiorg:DirectionalViewPager:1.0.0"
|
||||||
insetter = "dev.chrisbanes.insetter:insetter:0.6.1"
|
insetter = "dev.chrisbanes.insetter:insetter:0.6.1"
|
||||||
compose-cascade = "me.saket.cascade:cascade-compose:2.0.0-rc02"
|
compose-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"
|
compose-simpleicons = "br.com.devsrsouza.compose.icons.android:simple-icons:1.0.0"
|
||||||
|
|
||||||
logcat = "com.squareup.logcat:logcat:0.1"
|
logcat = "com.squareup.logcat:logcat:0.1"
|
||||||
|
@ -84,11 +83,11 @@ sqldelight-android-paging = { module = "com.squareup.sqldelight:android-paging3-
|
||||||
sqldelight-gradle = { module = "com.squareup.sqldelight:gradle-plugin", version.ref = "sqldelight" }
|
sqldelight-gradle = { module = "com.squareup.sqldelight:gradle-plugin", version.ref = "sqldelight" }
|
||||||
|
|
||||||
junit = "org.junit.jupiter:junit-jupiter:5.9.3"
|
junit = "org.junit.jupiter:junit-jupiter:5.9.3"
|
||||||
kotest-assertions = "io.kotest:kotest-assertions-core:5.6.1"
|
kotest-assertions = "io.kotest:kotest-assertions-core:5.6.2"
|
||||||
|
|
||||||
voyager-navigator = { module = "ca.gosyer:voyager-navigator", version.ref = "voyager" }
|
voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" }
|
||||||
voyager-tab-navigator = { module = "ca.gosyer:voyager-tab-navigator", version.ref = "voyager" }
|
voyager-tab-navigator = { module = "cafe.adriel.voyager:voyager-tab-navigator", version.ref = "voyager" }
|
||||||
voyager-transitions = { module = "ca.gosyer:voyager-transitions", version.ref = "voyager" }
|
voyager-transitions = { module = "cafe.adriel.voyager:voyager-transitions", version.ref = "voyager" }
|
||||||
|
|
||||||
kotlinter = "org.jmailen.gradle:kotlinter-gradle:3.13.0"
|
kotlinter = "org.jmailen.gradle:kotlinter-gradle:3.13.0"
|
||||||
|
|
||||||
|
|
|
@ -1,300 +1,59 @@
|
||||||
package tachiyomi.presentation.core.components
|
package tachiyomi.presentation.core.components
|
||||||
|
|
||||||
import androidx.compose.foundation.gestures.FlingBehavior
|
|
||||||
import androidx.compose.foundation.gestures.Orientation
|
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.PaddingValues
|
||||||
import androidx.compose.foundation.layout.wrapContentSize
|
import androidx.compose.foundation.pager.PageSize
|
||||||
import androidx.compose.foundation.lazy.LazyListItemInfo
|
import androidx.compose.foundation.pager.PagerDefaults
|
||||||
import androidx.compose.foundation.lazy.LazyListLayoutInfo
|
import androidx.compose.foundation.pager.PagerSnapDistance
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.pager.PagerState
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.runtime.Composable
|
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.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.setValue
|
||||||
import androidx.compose.runtime.snapshotFlow
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.Density
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
import androidx.compose.ui.util.fastForEach
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.util.fastMaxBy
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.util.fastSumBy
|
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
||||||
import kotlin.math.abs
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Horizontal Pager with custom SnapFlingBehavior for a more natural swipe feeling
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun HorizontalPager(
|
fun HorizontalPager(
|
||||||
count: Int,
|
pageCount: Int,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
state: PagerState = rememberPagerState(),
|
state: PagerState = rememberPagerState(),
|
||||||
key: ((page: Int) -> Any)? = null,
|
contentPadding: PaddingValues = PaddingValues(0.dp),
|
||||||
contentPadding: PaddingValues = PaddingValues(),
|
pageSize: PageSize = PageSize.Fill,
|
||||||
|
beyondBoundsPageCount: Int = 0,
|
||||||
|
pageSpacing: Dp = 0.dp,
|
||||||
verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
|
verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
|
||||||
userScrollEnabled: Boolean = true,
|
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(
|
androidx.compose.foundation.pager.HorizontalPager(
|
||||||
count = count,
|
pageCount = pageCount,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
state = state,
|
state = state,
|
||||||
isVertical = false,
|
|
||||||
key = key,
|
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
|
pageSize = pageSize,
|
||||||
|
beyondBoundsPageCount = beyondBoundsPageCount,
|
||||||
|
pageSpacing = pageSpacing,
|
||||||
verticalAlignment = verticalAlignment,
|
verticalAlignment = verticalAlignment,
|
||||||
|
flingBehavior = PagerDefaults.flingBehavior(
|
||||||
|
state = state,
|
||||||
|
pagerSnapDistance = PagerSnapDistance.atMost(0),
|
||||||
|
),
|
||||||
userScrollEnabled = userScrollEnabled,
|
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