diff --git a/.github/workflows/build_pull_request.yml b/.github/workflows/build_pull_request.yml index 077eb0251e..b80a88639c 100644 --- a/.github/workflows/build_pull_request.yml +++ b/.github/workflows/build_pull_request.yml @@ -37,4 +37,4 @@ jobs: - name: Build app and run unit tests uses: gradle/gradle-command-action@v2 with: - arguments: lintKotlin assembleStandardRelease testStandardReleaseUnitTest \ No newline at end of file + arguments: lintKotlin assembleStandardRelease testReleaseUnitTest \ No newline at end of file diff --git a/.github/workflows/build_push.yml b/.github/workflows/build_push.yml index 6d7f714606..11ed33f08b 100644 --- a/.github/workflows/build_push.yml +++ b/.github/workflows/build_push.yml @@ -42,7 +42,7 @@ jobs: - name: Build app and run unit tests uses: gradle/gradle-command-action@v2 with: - arguments: lintKotlin assembleStandardRelease testStandardReleaseUnitTest + arguments: lintKotlin assembleStandardRelease testReleaseUnitTest # Sign APK and create release for tags diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b294e6b490..2bae5afc2b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -218,7 +218,6 @@ dependencies { // Disk implementation(libs.disklrucache) implementation(libs.unifile) - implementation(libs.compress) implementation(libs.junrar) // Preferences diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index faccc95759..343be8d652 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -74,7 +74,4 @@ ##---------------End: proguard configuration for kotlinx.serialization ---------- # XmlUtil --keep public enum nl.adaptivity.xmlutil.EventType { *; } - -# org.apache.commons:commons-compress --keep,allowoptimization class org.apache.commons.compress.archivers.zip.** +-keep public enum nl.adaptivity.xmlutil.EventType { *; } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/presentation/browse/anime/components/BrowseAnimeSourceComfortableGrid.kt b/app/src/main/java/eu/kanade/presentation/browse/anime/components/BrowseAnimeSourceComfortableGrid.kt index 42685be877..6de029ec7a 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/anime/components/BrowseAnimeSourceComfortableGrid.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/anime/components/BrowseAnimeSourceComfortableGrid.kt @@ -11,7 +11,6 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.unit.dp import androidx.paging.LoadState import androidx.paging.compose.LazyPagingItems -import androidx.paging.compose.itemKey import eu.kanade.presentation.browse.InLibraryBadge import eu.kanade.presentation.browse.manga.components.BrowseSourceLoadingItem import eu.kanade.presentation.library.CommonEntryItemDefaults @@ -41,10 +40,7 @@ fun BrowseAnimeSourceComfortableGrid( } } - items( - count = animeList.itemCount, - key = animeList.itemKey { it.value.id }, - ) { index -> + items(count = animeList.itemCount) { index -> val anime by animeList[index]?.collectAsState() ?: return@items BrowseAnimeSourceComfortableGridItem( anime = anime, diff --git a/app/src/main/java/eu/kanade/presentation/browse/anime/components/BrowseAnimeSourceCompactGrid.kt b/app/src/main/java/eu/kanade/presentation/browse/anime/components/BrowseAnimeSourceCompactGrid.kt index 423ba74636..6f59175053 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/anime/components/BrowseAnimeSourceCompactGrid.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/anime/components/BrowseAnimeSourceCompactGrid.kt @@ -11,7 +11,6 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.unit.dp import androidx.paging.LoadState import androidx.paging.compose.LazyPagingItems -import androidx.paging.compose.itemKey import eu.kanade.presentation.browse.InLibraryBadge import eu.kanade.presentation.browse.manga.components.BrowseSourceLoadingItem import eu.kanade.presentation.library.CommonEntryItemDefaults @@ -41,10 +40,7 @@ fun BrowseAnimeSourceCompactGrid( } } - items( - count = animeList.itemCount, - key = animeList.itemKey { it.value.id }, - ) { index -> + items(count = animeList.itemCount) { index -> val anime by animeList[index]?.collectAsState() ?: return@items BrowseAnimeSourceCompactGridItem( anime = anime, diff --git a/app/src/main/java/eu/kanade/presentation/browse/anime/components/BrowseAnimeSourceList.kt b/app/src/main/java/eu/kanade/presentation/browse/anime/components/BrowseAnimeSourceList.kt index 5474dc9562..63cc8f112f 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/anime/components/BrowseAnimeSourceList.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/anime/components/BrowseAnimeSourceList.kt @@ -7,7 +7,6 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.unit.dp import androidx.paging.LoadState import androidx.paging.compose.LazyPagingItems -import androidx.paging.compose.itemKey import androidx.paging.compose.items import eu.kanade.presentation.browse.InLibraryBadge import eu.kanade.presentation.browse.manga.components.BrowseSourceLoadingItem @@ -35,10 +34,7 @@ fun BrowseAnimeSourceList( } } - items( - count = animeList.itemCount, - key = animeList.itemKey { it.value.id }, - ) { index -> + items(count = animeList.itemCount) { index -> val anime by animeList[index]?.collectAsState() ?: return@items BrowseAnimeSourceListItem( anime = anime, diff --git a/app/src/main/java/eu/kanade/presentation/browse/manga/components/BrowseMangaSourceComfortableGrid.kt b/app/src/main/java/eu/kanade/presentation/browse/manga/components/BrowseMangaSourceComfortableGrid.kt index 7062549f28..6c2ff2c899 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/manga/components/BrowseMangaSourceComfortableGrid.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/manga/components/BrowseMangaSourceComfortableGrid.kt @@ -11,7 +11,6 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.unit.dp import androidx.paging.LoadState import androidx.paging.compose.LazyPagingItems -import androidx.paging.compose.itemKey import eu.kanade.presentation.browse.InLibraryBadge import eu.kanade.presentation.library.CommonEntryItemDefaults import eu.kanade.presentation.library.EntryComfortableGridItem @@ -40,10 +39,7 @@ fun BrowseMangaSourceComfortableGrid( } } - items( - count = mangaList.itemCount, - key = mangaList.itemKey { it.value.id }, - ) { index -> + items(count = mangaList.itemCount) { index -> val manga by mangaList[index]?.collectAsState() ?: return@items BrowseMangaSourceComfortableGridItem( manga = manga, diff --git a/app/src/main/java/eu/kanade/presentation/browse/manga/components/BrowseMangaSourceCompactGrid.kt b/app/src/main/java/eu/kanade/presentation/browse/manga/components/BrowseMangaSourceCompactGrid.kt index 313b30bccf..83c8a5c6f6 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/manga/components/BrowseMangaSourceCompactGrid.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/manga/components/BrowseMangaSourceCompactGrid.kt @@ -11,7 +11,6 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.unit.dp import androidx.paging.LoadState import androidx.paging.compose.LazyPagingItems -import androidx.paging.compose.itemKey import eu.kanade.presentation.browse.InLibraryBadge import eu.kanade.presentation.library.CommonEntryItemDefaults import eu.kanade.presentation.library.EntryCompactGridItem @@ -40,10 +39,7 @@ fun BrowseMangaSourceCompactGrid( } } - items( - count = mangaList.itemCount, - key = mangaList.itemKey { it.value.id }, - ) { index -> + items(count = mangaList.itemCount) { index -> val manga by mangaList[index]?.collectAsState() ?: return@items BrowseMangaSourceCompactGridItem( manga = manga, diff --git a/app/src/main/java/eu/kanade/presentation/browse/manga/components/BrowseMangaSourceList.kt b/app/src/main/java/eu/kanade/presentation/browse/manga/components/BrowseMangaSourceList.kt index a57825f478..7576939f26 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/manga/components/BrowseMangaSourceList.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/manga/components/BrowseMangaSourceList.kt @@ -7,7 +7,6 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.unit.dp import androidx.paging.LoadState import androidx.paging.compose.LazyPagingItems -import androidx.paging.compose.itemKey import androidx.paging.compose.items import eu.kanade.presentation.browse.InLibraryBadge import eu.kanade.presentation.library.CommonEntryItemDefaults @@ -34,10 +33,7 @@ fun BrowseMangaSourceList( } } - items( - count = mangaList.itemCount, - key = mangaList.itemKey { it.value.id }, - ) { index -> + items(count = mangaList.itemCount) { index -> val manga by mangaList[index]?.collectAsState() ?: return@items BrowseMangaSourceListItem( manga = manga, diff --git a/app/src/main/java/eu/kanade/presentation/components/AdaptiveSheet.kt b/app/src/main/java/eu/kanade/presentation/components/AdaptiveSheet.kt index 0746f62c28..08a200b505 100644 --- a/app/src/main/java/eu/kanade/presentation/components/AdaptiveSheet.kt +++ b/app/src/main/java/eu/kanade/presentation/components/AdaptiveSheet.kt @@ -16,13 +16,15 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties +import cafe.adriel.voyager.core.annotation.InternalVoyagerApi import cafe.adriel.voyager.core.lifecycle.DisposableEffectIgnoringConfiguration +import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.transitions.ScreenTransition -import eu.kanade.presentation.util.Screen import eu.kanade.presentation.util.isTabletUi import tachiyomi.presentation.core.components.AdaptiveSheet as AdaptiveSheetImpl +@OptIn(InternalVoyagerApi::class) @Composable fun NavigatorAdaptiveSheet( screen: Screen, diff --git a/app/src/main/java/eu/kanade/presentation/components/AppBar.kt b/app/src/main/java/eu/kanade/presentation/components/AppBar.kt index 2aefa606a9..0f76763180 100644 --- a/app/src/main/java/eu/kanade/presentation/components/AppBar.kt +++ b/app/src/main/java/eu/kanade/presentation/components/AppBar.kt @@ -20,6 +20,7 @@ import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.PlainTooltipBox import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults @@ -203,21 +204,36 @@ fun AppBarActions( var showMenu by remember { mutableStateOf(false) } actions.filterIsInstance().map { - IconButton( - onClick = it.onClick, - enabled = it.enabled, + PlainTooltipBox( + tooltip = { Text(it.title) }, ) { - Icon( - imageVector = it.icon, - contentDescription = it.title, - ) + IconButton( + onClick = it.onClick, + enabled = it.enabled, + modifier = Modifier.tooltipAnchor(), + ) { + Icon( + imageVector = it.icon, + contentDescription = it.title, + ) + } } } val overflowActions = actions.filterIsInstance() if (overflowActions.isNotEmpty()) { - IconButton(onClick = { showMenu = !showMenu }) { - Icon(Icons.Outlined.MoreVert, contentDescription = stringResource(R.string.abc_action_menu_overflow_description)) + PlainTooltipBox( + tooltip = { Text(stringResource(R.string.abc_action_menu_overflow_description)) }, + ) { + IconButton( + onClick = { showMenu = !showMenu }, + modifier = Modifier.tooltipAnchor(), + ) { + Icon( + Icons.Outlined.MoreVert, + contentDescription = stringResource(R.string.abc_action_menu_overflow_description), + ) + } } DropdownMenu( @@ -327,17 +343,35 @@ fun SearchToolbar( if (!searchEnabled) { // Don't show search action } else if (searchQuery == null) { - IconButton(onClick) { - Icon(Icons.Outlined.Search, contentDescription = stringResource(R.string.action_search)) + PlainTooltipBox( + tooltip = { Text(stringResource(R.string.action_search)) }, + ) { + IconButton( + onClick = onClick, + modifier = Modifier.tooltipAnchor(), + ) { + Icon( + Icons.Outlined.Search, + contentDescription = stringResource(R.string.action_search), + ) + } } } else if (searchQuery.isNotEmpty()) { - IconButton( - onClick = { - onClick() - focusRequester.requestFocus() - }, + PlainTooltipBox( + tooltip = { Text(stringResource(R.string.action_reset)) }, ) { - Icon(Icons.Outlined.Close, contentDescription = stringResource(R.string.action_reset)) + IconButton( + onClick = { + onClick() + focusRequester.requestFocus() + }, + modifier = Modifier.tooltipAnchor(), + ) { + Icon( + Icons.Outlined.Close, + contentDescription = stringResource(R.string.action_reset), + ) + } } } } diff --git a/app/src/main/java/eu/kanade/presentation/components/TabbedDialog.kt b/app/src/main/java/eu/kanade/presentation/components/TabbedDialog.kt index 31f306b736..698916caba 100644 --- a/app/src/main/java/eu/kanade/presentation/components/TabbedDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/components/TabbedDialog.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material3.Icon @@ -31,7 +32,6 @@ import kotlinx.coroutines.launch import tachiyomi.presentation.core.components.HorizontalPager import tachiyomi.presentation.core.components.material.Divider import tachiyomi.presentation.core.components.material.TabIndicator -import tachiyomi.presentation.core.components.rememberPagerState object TabbedDialogPaddings { val Horizontal = 24.dp @@ -84,7 +84,7 @@ fun TabbedDialog( HorizontalPager( modifier = Modifier.animateContentSize(), - count = tabTitles.size, + pageCount = tabTitles.size, state = pagerState, verticalAlignment = Alignment.Top, ) { page -> diff --git a/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt b/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt index 372b048444..08858e5c21 100644 --- a/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt @@ -7,6 +7,8 @@ import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ScrollableTabRow import androidx.compose.material3.SnackbarHost @@ -25,11 +27,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch import tachiyomi.presentation.core.components.HorizontalPager -import tachiyomi.presentation.core.components.PagerState import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.TabIndicator import tachiyomi.presentation.core.components.material.TabText -import tachiyomi.presentation.core.components.rememberPagerState @Composable fun TabbedScreen( @@ -105,7 +105,7 @@ fun TabbedScreen( } HorizontalPager( - count = tabs.size, + pageCount = tabs.size, modifier = Modifier.fillMaxSize(), state = state, verticalAlignment = Alignment.Top, diff --git a/app/src/main/java/eu/kanade/presentation/entries/EntryToolbar.kt b/app/src/main/java/eu/kanade/presentation/entries/EntryToolbar.kt index bde9786b46..9e780190f5 100644 --- a/app/src/main/java/eu/kanade/presentation/entries/EntryToolbar.kt +++ b/app/src/main/java/eu/kanade/presentation/entries/EntryToolbar.kt @@ -26,6 +26,8 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import eu.kanade.presentation.components.AppBar +import eu.kanade.presentation.components.AppBarActions import eu.kanade.presentation.components.EntryDownloadDropdownMenu import eu.kanade.presentation.components.OverflowMenu import eu.kanade.tachiyomi.R @@ -77,18 +79,20 @@ fun EntryToolbar( }, actions = { if (isActionMode) { - IconButton(onClick = onSelectAll) { - Icon( - imageVector = Icons.Outlined.SelectAll, - contentDescription = stringResource(R.string.action_select_all), - ) - } - IconButton(onClick = onInvertSelection) { - Icon( - imageVector = Icons.Outlined.FlipToBack, - contentDescription = stringResource(R.string.action_select_inverse), - ) - } + AppBarActions( + listOf( + AppBar.Action( + title = stringResource(R.string.action_select_all), + icon = Icons.Outlined.SelectAll, + onClick = onSelectAll, + ), + AppBar.Action( + title = stringResource(R.string.action_select_inverse), + icon = Icons.Outlined.FlipToBack, + onClick = onInvertSelection, + ), + ), + ) } else { if (onClickDownload != null) { val (downloadExpanded, onDownloadExpanded) = remember { mutableStateOf(false) } diff --git a/app/src/main/java/eu/kanade/presentation/entries/anime/EpisodeOptionsDialogScreen.kt b/app/src/main/java/eu/kanade/presentation/entries/anime/EpisodeOptionsDialogScreen.kt index 84b657f869..8ea702c0fa 100644 --- a/app/src/main/java/eu/kanade/presentation/entries/anime/EpisodeOptionsDialogScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/entries/anime/EpisodeOptionsDialogScreen.kt @@ -49,9 +49,9 @@ import androidx.compose.ui.unit.dp import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.coroutineScope import cafe.adriel.voyager.core.model.rememberScreenModel +import cafe.adriel.voyager.core.screen.Screen import eu.kanade.core.util.asFlow import eu.kanade.presentation.components.TabbedDialogPaddings -import eu.kanade.presentation.util.Screen import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadManager @@ -81,7 +81,7 @@ class EpisodeOptionsDialogScreen( private val episodeId: Long, private val animeId: Long, private val sourceId: Long, -) : Screen() { +) : Screen { @Composable override fun Content() { diff --git a/app/src/main/java/eu/kanade/presentation/library/LibraryTabs.kt b/app/src/main/java/eu/kanade/presentation/library/LibraryTabs.kt index bc77c451b4..936dfd1382 100644 --- a/app/src/main/java/eu/kanade/presentation/library/LibraryTabs.kt +++ b/app/src/main/java/eu/kanade/presentation/library/LibraryTabs.kt @@ -1,6 +1,7 @@ package eu.kanade.presentation.library import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.pager.PagerState import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ScrollableTabRow import androidx.compose.material3.Tab @@ -8,7 +9,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.unit.dp import eu.kanade.presentation.category.visualName import tachiyomi.domain.category.model.Category -import tachiyomi.presentation.core.components.PagerState import tachiyomi.presentation.core.components.material.Divider import tachiyomi.presentation.core.components.material.TabIndicator import tachiyomi.presentation.core.components.material.TabText diff --git a/app/src/main/java/eu/kanade/presentation/library/LibraryToolbar.kt b/app/src/main/java/eu/kanade/presentation/library/LibraryToolbar.kt index 02e7f38827..5ff127be6b 100644 --- a/app/src/main/java/eu/kanade/presentation/library/LibraryToolbar.kt +++ b/app/src/main/java/eu/kanade/presentation/library/LibraryToolbar.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.sp import eu.kanade.presentation.components.AppBar +import eu.kanade.presentation.components.AppBarActions import eu.kanade.presentation.components.OverflowMenu import eu.kanade.presentation.components.SearchToolbar import eu.kanade.tachiyomi.R @@ -145,12 +146,20 @@ fun LibrarySelectionToolbar( AppBar( titleContent = { Text(text = "$selectedCount") }, actions = { - IconButton(onClick = onClickSelectAll) { - Icon(Icons.Outlined.SelectAll, contentDescription = stringResource(R.string.action_select_all)) - } - IconButton(onClick = onClickInvertSelection) { - Icon(Icons.Outlined.FlipToBack, contentDescription = stringResource(R.string.action_select_inverse)) - } + AppBarActions( + listOf( + AppBar.Action( + title = stringResource(R.string.action_select_all), + icon = Icons.Outlined.SelectAll, + onClick = onClickSelectAll, + ), + AppBar.Action( + title = stringResource(R.string.action_select_inverse), + icon = Icons.Outlined.FlipToBack, + onClick = onClickInvertSelection, + ), + ), + ) }, isActionMode = true, onCancelActionMode = onClickUnselectAll, diff --git a/app/src/main/java/eu/kanade/presentation/library/anime/AnimeLibraryContent.kt b/app/src/main/java/eu/kanade/presentation/library/anime/AnimeLibraryContent.kt index 11c6fe6fc9..908448bf65 100644 --- a/app/src/main/java/eu/kanade/presentation/library/anime/AnimeLibraryContent.kt +++ b/app/src/main/java/eu/kanade/presentation/library/anime/AnimeLibraryContent.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -23,7 +24,6 @@ import tachiyomi.domain.category.model.Category import tachiyomi.domain.library.anime.LibraryAnime import tachiyomi.domain.library.model.LibraryDisplayMode import tachiyomi.presentation.core.components.material.PullRefresh -import tachiyomi.presentation.core.components.rememberPagerState import kotlin.time.Duration.Companion.seconds @Composable @@ -61,8 +61,10 @@ fun AnimeLibraryContent( var isRefreshing by remember(pagerState.currentPage) { mutableStateOf(false) } if (showPageTabs && categories.size > 1) { - if (categories.size <= pagerState.currentPage) { - pagerState.currentPage = categories.size - 1 + LaunchedEffect(categories) { + if (categories.size <= pagerState.currentPage) { + pagerState.scrollToPage(categories.size - 1) + } } LibraryTabs( categories = categories, diff --git a/app/src/main/java/eu/kanade/presentation/library/anime/AnimeLibraryPager.kt b/app/src/main/java/eu/kanade/presentation/library/anime/AnimeLibraryPager.kt index b4a3289a99..79a5e219fd 100644 --- a/app/src/main/java/eu/kanade/presentation/library/anime/AnimeLibraryPager.kt +++ b/app/src/main/java/eu/kanade/presentation/library/anime/AnimeLibraryPager.kt @@ -3,6 +3,7 @@ package eu.kanade.presentation.library.anime import android.content.res.Configuration import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.pager.PagerState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -16,7 +17,6 @@ import eu.kanade.tachiyomi.ui.library.anime.AnimeLibraryItem import tachiyomi.domain.library.anime.LibraryAnime import tachiyomi.domain.library.model.LibraryDisplayMode import tachiyomi.presentation.core.components.HorizontalPager -import tachiyomi.presentation.core.components.PagerState @Composable fun AnimeLibraryPager( @@ -35,7 +35,7 @@ fun AnimeLibraryPager( onClickContinueWatching: ((LibraryAnime) -> Unit)?, ) { HorizontalPager( - count = pageCount, + pageCount = pageCount, modifier = Modifier.fillMaxSize(), state = state, verticalAlignment = Alignment.Top, diff --git a/app/src/main/java/eu/kanade/presentation/library/manga/MangaLibraryContent.kt b/app/src/main/java/eu/kanade/presentation/library/manga/MangaLibraryContent.kt index db60bbf825..ae0ecc9316 100644 --- a/app/src/main/java/eu/kanade/presentation/library/manga/MangaLibraryContent.kt +++ b/app/src/main/java/eu/kanade/presentation/library/manga/MangaLibraryContent.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -23,7 +24,6 @@ import tachiyomi.domain.category.model.Category import tachiyomi.domain.library.manga.LibraryManga import tachiyomi.domain.library.model.LibraryDisplayMode import tachiyomi.presentation.core.components.material.PullRefresh -import tachiyomi.presentation.core.components.rememberPagerState import kotlin.time.Duration.Companion.seconds @Composable @@ -61,8 +61,10 @@ fun MangaLibraryContent( var isRefreshing by remember(pagerState.currentPage) { mutableStateOf(false) } if (showPageTabs && categories.size > 1) { - if (categories.size <= pagerState.currentPage) { - pagerState.currentPage = categories.size - 1 + LaunchedEffect(categories) { + if (categories.size <= pagerState.currentPage) { + pagerState.scrollToPage(categories.size - 1) + } } LibraryTabs( categories = categories, diff --git a/app/src/main/java/eu/kanade/presentation/library/manga/MangaLibraryPager.kt b/app/src/main/java/eu/kanade/presentation/library/manga/MangaLibraryPager.kt index 7b717c1df0..030fbee312 100644 --- a/app/src/main/java/eu/kanade/presentation/library/manga/MangaLibraryPager.kt +++ b/app/src/main/java/eu/kanade/presentation/library/manga/MangaLibraryPager.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable @@ -23,7 +24,6 @@ import eu.kanade.tachiyomi.ui.library.manga.MangaLibraryItem import tachiyomi.domain.library.manga.LibraryManga import tachiyomi.domain.library.model.LibraryDisplayMode import tachiyomi.presentation.core.components.HorizontalPager -import tachiyomi.presentation.core.components.PagerState import tachiyomi.presentation.core.screens.EmptyScreen import tachiyomi.presentation.core.util.plus @@ -44,7 +44,7 @@ fun MangaLibraryPager( onClickContinueReading: ((LibraryManga) -> Unit)?, ) { HorizontalPager( - count = pageCount, + pageCount = pageCount, modifier = Modifier.fillMaxSize(), state = state, verticalAlignment = Alignment.Top, diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/WorkerInfoScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/WorkerInfoScreen.kt index 8f533fa01d..8b16084c68 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/WorkerInfoScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/WorkerInfoScreen.kt @@ -18,6 +18,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEach @@ -28,8 +29,11 @@ import cafe.adriel.voyager.core.model.ScreenModel import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow +import eu.kanade.presentation.components.AppBar +import eu.kanade.presentation.components.AppBarActions import eu.kanade.presentation.util.Screen import eu.kanade.presentation.util.ioCoroutineScope +import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.workManager import kotlinx.coroutines.flow.SharingStarted @@ -63,13 +67,17 @@ object WorkerInfoScreen : Screen() { } }, actions = { - IconButton( - onClick = { - context.copyToClipboard(title, enqueued + finished + running) - }, - ) { - Icon(imageVector = Icons.Default.ContentCopy, contentDescription = null) - } + AppBarActions( + listOf( + AppBar.Action( + title = stringResource(R.string.action_copy_to_clipboard), + icon = Icons.Default.ContentCopy, + onClick = { + context.copyToClipboard(title, enqueued + finished + running) + }, + ), + ), + ) }, scrollBehavior = it, ) diff --git a/app/src/main/java/eu/kanade/presentation/reader/ChapterNavigator.kt b/app/src/main/java/eu/kanade/presentation/reader/ChapterNavigator.kt index fbd1f2fae9..1d9118555a 100644 --- a/app/src/main/java/eu/kanade/presentation/reader/ChapterNavigator.kt +++ b/app/src/main/java/eu/kanade/presentation/reader/ChapterNavigator.kt @@ -35,6 +35,7 @@ import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import eu.kanade.presentation.util.isTabletUi import eu.kanade.tachiyomi.R +import kotlin.math.roundToInt @Composable fun ChapterNavigator( @@ -107,7 +108,7 @@ fun ChapterNavigator( valueRange = 1f..totalPages.toFloat(), steps = totalPages - 2, onValueChange = { - onSliderValueChange(it.toInt() - 1) + onSliderValueChange(it.roundToInt() - 1) }, interactionSource = interactionSource, ) diff --git a/app/src/main/java/eu/kanade/presentation/reader/ChapterTransition.kt b/app/src/main/java/eu/kanade/presentation/reader/ChapterTransition.kt index 6a51764e8c..978fc3d102 100644 --- a/app/src/main/java/eu/kanade/presentation/reader/ChapterTransition.kt +++ b/app/src/main/java/eu/kanade/presentation/reader/ChapterTransition.kt @@ -2,60 +2,59 @@ package eu.kanade.presentation.reader import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.text.InlineTextContent +import androidx.compose.foundation.text.appendInlineContent import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Info import androidx.compose.material.icons.outlined.OfflinePin import androidx.compose.material.icons.outlined.Warning +import androidx.compose.material3.CardColors +import androidx.compose.material3.CardDefaults import androidx.compose.material3.Icon -import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard import androidx.compose.material3.ProvideTextStyle +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.Placeholder +import androidx.compose.ui.text.PlaceholderVerticalAlign +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.google.common.io.Files.append +import eu.kanade.presentation.theme.TachiyomiTheme import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.manga.Chapter +import eu.kanade.tachiyomi.data.database.models.manga.ChapterImpl import eu.kanade.tachiyomi.data.database.models.manga.toDomainChapter -import eu.kanade.tachiyomi.data.download.manga.MangaDownloadManager import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition -import tachiyomi.domain.entries.manga.model.Manga +import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import tachiyomi.domain.items.service.calculateChapterGap -import tachiyomi.presentation.core.components.material.SecondaryItemAlpha +import tachiyomi.presentation.core.util.ThemePreviews +import tachiyomi.presentation.core.util.secondaryItemAlpha @Composable fun ChapterTransition( transition: ChapterTransition, - downloadManager: MangaDownloadManager, - manga: Manga?, + currChapterDownloaded: Boolean, + goingToChapterDownloaded: Boolean, ) { - manga ?: return - val currChapter = transition.from.chapter - val currChapterDownloaded = transition.from.pageLoader?.isLocal == true - val goingToChapter = transition.to?.chapter - val goingToChapterDownloaded = if (goingToChapter != null) { - downloadManager.isChapterDownloaded( - goingToChapter.name, - goingToChapter.scanlator, - manga.title, - manga.source, - skipCache = true, - ) - } else { - false - } ProvideTextStyle(MaterialTheme.typography.bodyMedium) { when (transition) { @@ -90,80 +89,289 @@ fun ChapterTransition( @Composable private fun TransitionText( topLabel: String, - topChapter: Chapter? = null, + topChapter: Chapter?, topChapterDownloaded: Boolean, bottomLabel: String, - bottomChapter: Chapter? = null, + bottomChapter: Chapter?, bottomChapterDownloaded: Boolean, fallbackLabel: String, chapterGap: Int, ) { - val hasTopChapter = topChapter != null - val hasBottomChapter = bottomChapter != null + Column( + modifier = Modifier + .widthIn(max = 460.dp) + .fillMaxWidth(), + ) { + if (topChapter != null) { + ChapterText( + header = topLabel, + name = topChapter.name, + scanlator = topChapter.scanlator, + downloaded = topChapterDownloaded, + ) - Column { - Text( - text = if (hasTopChapter) topLabel else fallbackLabel, - fontWeight = FontWeight.Bold, - textAlign = if (hasTopChapter) TextAlign.Start else TextAlign.Center, - ) - topChapter?.let { ChapterText(chapter = it, downloaded = topChapterDownloaded) } - - Spacer(Modifier.height(16.dp)) - - if (chapterGap > 0) { - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - imageVector = Icons.Outlined.Warning, - tint = MaterialTheme.colorScheme.error, - contentDescription = null, - ) + Spacer(Modifier.height(VerticalSpacerSize)) + } else { + NoChapterNotification( + text = fallbackLabel, + modifier = Modifier.align(Alignment.CenterHorizontally), + ) + } - Text(text = pluralStringResource(R.plurals.missing_chapters_warning, count = chapterGap, chapterGap)) + if (bottomChapter != null) { + if (chapterGap > 0) { + ChapterGapWarning( + gapCount = chapterGap, + modifier = Modifier.align(Alignment.CenterHorizontally), + ) } - Spacer(Modifier.height(16.dp)) - } + Spacer(Modifier.height(VerticalSpacerSize)) - Text( - text = if (hasBottomChapter) bottomLabel else fallbackLabel, - fontWeight = FontWeight.Bold, - textAlign = if (hasBottomChapter) TextAlign.Start else TextAlign.Center, - ) - bottomChapter?.let { ChapterText(chapter = it, downloaded = bottomChapterDownloaded) } + ChapterText( + header = bottomLabel, + name = bottomChapter.name, + scanlator = bottomChapter.scanlator, + downloaded = bottomChapterDownloaded, + ) + } else { + NoChapterNotification( + text = fallbackLabel, + modifier = Modifier.align(Alignment.CenterHorizontally), + ) + } } } @Composable -private fun ColumnScope.ChapterText( - chapter: Chapter, - downloaded: Boolean, +private fun NoChapterNotification( + text: String, + modifier: Modifier = Modifier, ) { - FlowRow( - verticalAlignment = Alignment.CenterVertically, + OutlinedCard( + modifier = modifier, + colors = CardColor, ) { - if (downloaded) { + Row( + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 12.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { Icon( - imageVector = Icons.Outlined.OfflinePin, - contentDescription = stringResource(R.string.label_downloaded), + imageVector = Icons.Outlined.Info, + tint = MaterialTheme.colorScheme.primary, + contentDescription = null, ) - Spacer(Modifier.width(8.dp)) + Text( + text = text, + style = MaterialTheme.typography.bodyMedium, + ) } + } +} - Text(chapter.name) +@Composable +private fun ChapterGapWarning( + gapCount: Int, + modifier: Modifier = Modifier, +) { + OutlinedCard( + modifier = modifier, + colors = CardColor, + ) { + Row( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = Icons.Outlined.Warning, + tint = MaterialTheme.colorScheme.error, + contentDescription = null, + ) + + Text( + text = pluralStringResource(R.plurals.missing_chapters_warning, count = gapCount, gapCount), + style = MaterialTheme.typography.bodyMedium, + ) + } } +} + +@Composable +private fun ChapterHeaderText( + text: String, + modifier: Modifier = Modifier, +) { + Text( + text = text, + modifier = modifier, + style = MaterialTheme.typography.titleMedium, + ) +} + +@Composable +private fun ChapterText( + header: String, + name: String, + scanlator: String?, + downloaded: Boolean, +) { + Column { + ChapterHeaderText( + text = header, + modifier = Modifier.padding(bottom = 4.dp), + ) - chapter.scanlator?.let { - ProvideTextStyle( - MaterialTheme.typography.bodyMedium.copy( - color = LocalContentColor.current.copy(alpha = SecondaryItemAlpha), + 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), + ) + }, ), - ) { - Text(it) + ) + + 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, + ) } } } diff --git a/app/src/main/java/eu/kanade/presentation/util/Navigator.kt b/app/src/main/java/eu/kanade/presentation/util/Navigator.kt index 0e48e32cb6..ddf1000537 100644 --- a/app/src/main/java/eu/kanade/presentation/util/Navigator.kt +++ b/app/src/main/java/eu/kanade/presentation/util/Navigator.kt @@ -31,8 +31,6 @@ abstract class Tab : cafe.adriel.voyager.navigator.tab.Tab { open suspend fun onReselect(navigator: Navigator) {} } -// TODO: this prevents crashes in nested navigators with transitions not being disposed -// properly. Go back to using vanilla Voyager Screens once fixed upstream. abstract class Screen : Screen { override val key: ScreenKey = uniqueScreenKey diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateJob.kt index 752e0384bf..074abc6b4f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateJob.kt @@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.data.backup import android.content.Context import android.net.Uri import androidx.core.net.toUri +import androidx.work.BackoffPolicy import androidx.work.CoroutineWorker import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.ExistingWorkPolicy @@ -22,6 +23,8 @@ import tachiyomi.domain.backup.service.BackupPreferences import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.util.concurrent.TimeUnit +import kotlin.time.Duration.Companion.minutes +import kotlin.time.toJavaDuration class BackupCreateJob(private val context: Context, workerParams: WorkerParameters) : CoroutineWorker(context, workerParams) { @@ -29,12 +32,14 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete private val notifier = BackupNotifier(context) override suspend fun doWork(): Result { + val isAutoBackup = inputData.getBoolean(IS_AUTO_BACKUP_KEY, true) + + if (isAutoBackup && BackupRestoreJob.isRunning(context)) return Result.retry() + val backupPreferences = Injekt.get() val uri = inputData.getString(LOCATION_URI_KEY)?.toUri() ?: backupPreferences.backupsDirectory().get().toUri() val flags = inputData.getInt(BACKUP_FLAGS_KEY, BackupConst.BACKUP_ALL) - val isAutoBackup = inputData.getBoolean(IS_AUTO_BACKUP_KEY, true) - try { setForeground(getForegroundInfo()) } catch (e: IllegalStateException) { @@ -79,6 +84,7 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete 10, TimeUnit.MINUTES, ) + .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10.minutes.toJavaDuration()) .addTag(TAG_AUTO) .setInputData( workDataOf( diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt index ddba9acaf6..0228ca2238 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt @@ -111,7 +111,6 @@ class BackupManager( * @param uri path of Uri * @param isAutoBackup backup called from scheduled backup job */ - @Suppress("BlockingMethodInNonBlockingContext") suspend fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String { if (!context.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { throw IllegalStateException(context.getString(R.string.missing_storage_permission)) @@ -446,10 +445,10 @@ class BackupManager( } internal suspend fun restoreExistingManga(manga: Manga, dbManga: Mangas): Manga { - var manga = manga.copy(id = dbManga._id) - manga = manga.copyFrom(dbManga) - updateManga(manga) - return manga + var updatedManga = manga.copy(id = dbManga._id) + updatedManga = updatedManga.copyFrom(dbManga) + updateManga(updatedManga) + return updatedManga } /** @@ -466,10 +465,10 @@ class BackupManager( } internal suspend fun restoreExistingAnime(anime: Anime, dbAnime: Animes): Anime { - var anime = anime.copy(id = dbAnime._id) - anime = anime.copyFrom(dbAnime) - updateAnime(anime) - return anime + var updatedAnime = anime.copy(id = dbAnime._id) + updatedAnime = updatedAnime.copyFrom(dbAnime) + updateAnime(updatedAnime) + return updatedAnime } /** @@ -582,7 +581,7 @@ class BackupManager( dbCategories.firstOrNull { dbCategory -> dbCategory.name == backupCategory.name }?.let { dbCategory -> - mangaCategoriesToUpdate.add(Pair(manga.id!!, dbCategory.id)) + mangaCategoriesToUpdate.add(Pair(manga.id, dbCategory.id)) } } } @@ -590,7 +589,7 @@ class BackupManager( // Update database if (mangaCategoriesToUpdate.isNotEmpty()) { mangaHandler.await(true) { - mangas_categoriesQueries.deleteMangaCategoryByMangaId(manga.id!!) + mangas_categoriesQueries.deleteMangaCategoryByMangaId(manga.id) mangaCategoriesToUpdate.forEach { (mangaId, categoryId) -> mangas_categoriesQueries.insert(mangaId, categoryId) } @@ -615,7 +614,7 @@ class BackupManager( dbCategories.firstOrNull { dbCategory -> dbCategory.name == backupCategory.name }?.let { dbCategory -> - animeCategoriesToUpdate.add(Pair(anime.id!!, dbCategory.id)) + animeCategoriesToUpdate.add(Pair(anime.id, dbCategory.id)) } } } @@ -623,7 +622,7 @@ class BackupManager( // Update database if (animeCategoriesToUpdate.isNotEmpty()) { animeHandler.await(true) { - animes_categoriesQueries.deleteAnimeCategoryByAnimeId(anime.id!!) + animes_categoriesQueries.deleteAnimeCategoryByAnimeId(anime.id) animeCategoriesToUpdate.forEach { (animeId, categoryId) -> animes_categoriesQueries.insert(animeId, categoryId) } @@ -730,37 +729,38 @@ class BackupManager( * @param tracks the track list to restore. */ internal suspend fun restoreTracking(manga: Manga, tracks: List) { - // Fix foreign keys with the current manga id - val tracks = tracks.map { it.copy(mangaId = manga.id!!) } - // Get tracks from database - val dbTracks = mangaHandler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id!!) } + val dbTracks = mangaHandler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id) } val toUpdate = mutableListOf() val toInsert = mutableListOf() - tracks.forEach { track -> - var isInDatabase = false - for (dbTrack in dbTracks) { - if (track.syncId == dbTrack.sync_id) { - // The sync is already in the db, only update its fields - var temp = dbTrack - if (track.remoteId != dbTrack.remote_id) { - temp = temp.copy(remote_id = track.remoteId) - } - if (track.libraryId != dbTrack.library_id) { - temp = temp.copy(library_id = track.libraryId) + tracks + // Fix foreign keys with the current manga id + .map { it.copy(mangaId = manga.id) } + .forEach { track -> + var isInDatabase = false + for (dbTrack in dbTracks) { + if (track.syncId == dbTrack.sync_id) { + // The sync is already in the db, only update its fields + var temp = dbTrack + if (track.remoteId != dbTrack.remote_id) { + temp = temp.copy(remote_id = track.remoteId) + } + if (track.libraryId != dbTrack.library_id) { + temp = temp.copy(library_id = track.libraryId) + } + temp = temp.copy(last_chapter_read = max(dbTrack.last_chapter_read, track.lastChapterRead)) + isInDatabase = true + toUpdate.add(temp) + break } - temp = temp.copy(last_chapter_read = max(dbTrack.last_chapter_read, track.lastChapterRead)) - isInDatabase = true - toUpdate.add(temp) - break + } + if (!isInDatabase) { + // Insert new sync. Let the db assign the id + toInsert.add(track.copy(id = 0)) } } - if (!isInDatabase) { - // Insert new sync. Let the db assign the id - toInsert.add(track.copy(id = 0)) - } - } + // Update database if (toUpdate.isNotEmpty()) { mangaHandler.await(true) { @@ -812,37 +812,38 @@ class BackupManager( * @param tracks the track list to restore. */ internal suspend fun restoreAnimeTracking(anime: Anime, tracks: List) { - // Fix foreign keys with the current anime id - val tracks = tracks.map { it.copy(animeId = anime.id!!) } - // Get tracks from database - val dbTracks = animeHandler.awaitList { anime_syncQueries.getTracksByAnimeId(anime.id!!) } + val dbTracks = animeHandler.awaitList { anime_syncQueries.getTracksByAnimeId(anime.id) } val toUpdate = mutableListOf() val toInsert = mutableListOf() - tracks.forEach { track -> - var isInDatabase = false - for (dbTrack in dbTracks) { - if (track.syncId == dbTrack.sync_id) { - // The sync is already in the db, only update its fields - var temp = dbTrack - if (track.remoteId != dbTrack.remote_id) { - temp = temp.copy(remote_id = track.remoteId) - } - if (track.libraryId != dbTrack.library_id) { - temp = temp.copy(library_id = track.libraryId) + tracks + // Fix foreign keys with the current manga id + .map { it.copy(animeId = anime.id) } + .forEach { track -> + var isInDatabase = false + for (dbTrack in dbTracks) { + if (track.syncId == dbTrack.sync_id) { + // The sync is already in the db, only update its fields + var temp = dbTrack + if (track.remoteId != dbTrack.remote_id) { + temp = temp.copy(remote_id = track.remoteId) + } + if (track.libraryId != dbTrack.library_id) { + temp = temp.copy(library_id = track.libraryId) + } + temp = temp.copy(last_episode_seen = max(dbTrack.last_episode_seen, track.lastEpisodeSeen)) + isInDatabase = true + toUpdate.add(temp) + break } - temp = temp.copy(last_episode_seen = max(dbTrack.last_episode_seen, track.lastEpisodeSeen)) - isInDatabase = true - toUpdate.add(temp) - break + } + if (!isInDatabase) { + // Insert new sync. Let the db assign the id + toInsert.add(track.copy(id = 0)) } } - if (!isInDatabase) { - // Insert new sync. Let the db assign the id - toInsert.add(track.copy(id = 0)) - } - } + // Update database if (toUpdate.isNotEmpty()) { animeHandler.await(true) { @@ -891,22 +892,22 @@ class BackupManager( val dbChapters = mangaHandler.awaitList { chaptersQueries.getChaptersByMangaId(manga.id!!) } val processed = chapters.map { chapter -> - var chapter = chapter - val dbChapter = dbChapters.find { it.url == chapter.url } + var updatedChapter = chapter + val dbChapter = dbChapters.find { it.url == updatedChapter.url } if (dbChapter != null) { - chapter = chapter.copy(id = dbChapter._id) - chapter = chapter.copyFrom(dbChapter) - if (dbChapter.read && !chapter.read) { - chapter = chapter.copy(read = dbChapter.read, lastPageRead = dbChapter.last_page_read) + updatedChapter = updatedChapter.copy(id = dbChapter._id) + updatedChapter = updatedChapter.copyFrom(dbChapter) + if (dbChapter.read && !updatedChapter.read) { + updatedChapter = updatedChapter.copy(read = true, lastPageRead = dbChapter.last_page_read) } else if (chapter.lastPageRead == 0L && dbChapter.last_page_read != 0L) { - chapter = chapter.copy(lastPageRead = dbChapter.last_page_read) + updatedChapter = updatedChapter.copy(lastPageRead = dbChapter.last_page_read) } - if (!chapter.bookmark && dbChapter.bookmark) { - chapter = chapter.copy(bookmark = dbChapter.bookmark) + if (!updatedChapter.bookmark && dbChapter.bookmark) { + updatedChapter = updatedChapter.copy(bookmark = true) } } - chapter.copy(mangaId = manga.id ?: -1) + updatedChapter.copy(mangaId = manga.id ?: -1) } val newChapters = processed.groupBy { it.id > 0 } @@ -918,22 +919,22 @@ class BackupManager( val dbEpisodes = animeHandler.awaitList { episodesQueries.getEpisodesByAnimeId(anime.id!!) } val processed = episodes.map { episode -> - var episode = episode - val dbEpisode = dbEpisodes.find { it.url == episode.url } + var updatedEpisode = episode + val dbEpisode = dbEpisodes.find { it.url == updatedEpisode.url } if (dbEpisode != null) { - episode = episode.copy(id = dbEpisode._id) - episode = episode.copyFrom(dbEpisode) - if (dbEpisode.seen && !episode.seen) { - episode = episode.copy(seen = dbEpisode.seen, lastSecondSeen = dbEpisode.last_second_seen) - } else if (episode.lastSecondSeen == 0L && dbEpisode.last_second_seen != 0L) { - episode = episode.copy(lastSecondSeen = dbEpisode.last_second_seen) + updatedEpisode = updatedEpisode.copy(id = dbEpisode._id) + updatedEpisode = updatedEpisode.copyFrom(dbEpisode) + if (dbEpisode.seen && !updatedEpisode.seen) { + updatedEpisode = updatedEpisode.copy(seen = true, lastSecondSeen = dbEpisode.last_second_seen) + } else if (updatedEpisode.lastSecondSeen == 0L && dbEpisode.last_second_seen != 0L) { + updatedEpisode = updatedEpisode.copy(lastSecondSeen = dbEpisode.last_second_seen) } - if (!episode.bookmark && dbEpisode.bookmark) { - episode = episode.copy(bookmark = dbEpisode.bookmark) + if (!updatedEpisode.bookmark && dbEpisode.bookmark) { + updatedEpisode = updatedEpisode.copy(bookmark = true) } } - episode.copy(animeId = anime.id ?: -1) + updatedEpisode.copy(animeId = anime.id ?: -1) } val newEpisodes = processed.groupBy { it.id > 0 } @@ -1126,55 +1127,6 @@ class BackupManager( } } - /** - * Updates a list of chapters - */ - private suspend fun updateChapters(chapters: List) { - 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) { - 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 */ diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupAnimeTracking.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupAnimeTracking.kt index 050631691a..538f3513df 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupAnimeTracking.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupAnimeTracking.kt @@ -28,6 +28,7 @@ data class BackupAnimeTracking( @ProtoNumber(11) var finishedWatchingDate: Long = 0, @ProtoNumber(100) var mediaId: Long = 0, ) { + @Suppress("DEPRECATION") fun getTrackingImpl(): AnimeTrack { return AnimeTrack( id = -1, diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupTracking.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupTracking.kt index 28115d33ab..a16dea52ef 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupTracking.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupTracking.kt @@ -29,6 +29,7 @@ data class BackupTracking( @ProtoNumber(100) var mediaId: Long = 0, ) { + @Suppress("DEPRECATION") fun getTrackingImpl(): MangaTrack { return MangaTrack( id = -1, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/entries/anime/AnimeCoverScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/entries/anime/AnimeCoverScreenModel.kt index 6817300985..83fe1290b2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/entries/anime/AnimeCoverScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/entries/anime/AnimeCoverScreenModel.kt @@ -118,7 +118,6 @@ class AnimeCoverScreenModel( fun editCover(context: Context, data: Uri) { val anime = state.value ?: return coroutineScope.launchIO { - @Suppress("BlockingMethodInNonBlockingContext") context.contentResolver.openInputStream(data)?.use { try { anime.editCover(Injekt.get(), it, updateAnime, coverCache) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/entries/anime/AnimeScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/entries/anime/AnimeScreenModel.kt index 94e1bbc223..7f6e29bff5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/entries/anime/AnimeScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/entries/anime/AnimeScreenModel.kt @@ -628,8 +628,8 @@ class AnimeInfoScreenModel( downloadEpisodes(episodes, false, video) } if (!isFavorited && !successState.hasPromptedToAddBefore) { - updateSuccessState { successState -> - successState.copy(hasPromptedToAddBefore = true) + updateSuccessState { state -> + state.copy(hasPromptedToAddBefore = true) } coroutineScope.launch { val result = snackbarHostState.showSnackbar( diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/entries/manga/MangaCoverScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/entries/manga/MangaCoverScreenModel.kt index ae67d3dfb4..3dbff634bc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/entries/manga/MangaCoverScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/entries/manga/MangaCoverScreenModel.kt @@ -118,7 +118,6 @@ class MangaCoverScreenModel( fun editCover(context: Context, data: Uri) { val manga = state.value ?: return coroutineScope.launchIO { - @Suppress("BlockingMethodInNonBlockingContext") context.contentResolver.openInputStream(data)?.use { try { manga.editCover(Injekt.get(), it, updateManga, coverCache) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/entries/manga/MangaScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/entries/manga/MangaScreenModel.kt index e59e78ee82..720405c67d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/entries/manga/MangaScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/entries/manga/MangaScreenModel.kt @@ -622,8 +622,8 @@ class MangaInfoScreenModel( downloadChapters(chapters) } if (!isFavorited && !successState.hasPromptedToAddBefore) { - updateSuccessState { successState -> - successState.copy(hasPromptedToAddBefore = true) + updateSuccessState { state -> + state.copy(hasPromptedToAddBefore = true) } coroutineScope.launch { val result = snackbarHostState.showSnackbar( diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt index 7231a45a03..056319d4e6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt @@ -1,10 +1,11 @@ package eu.kanade.tachiyomi.ui.reader.loader -import android.app.Application import com.github.junrar.Archive import com.github.junrar.rarfile.FileHeader +import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.ReaderPage -import uy.kohesive.injekt.injectLazy +import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder +import tachiyomi.core.util.system.ImageUtil import java.io.File import java.io.InputStream import java.io.PipedInputStream @@ -15,36 +16,30 @@ import java.io.PipedOutputStream */ internal class RarPageLoader(file: File) : PageLoader() { - private val context: Application by injectLazy() - private val tmpDir = File(context.externalCacheDir, "reader_${file.hashCode()}").also { - it.deleteRecursively() - it.mkdirs() - } - - init { - Archive(file).use { rar -> - rar.fileHeaders.asSequence() - .filterNot { it.isDirectory } - .forEach { header -> - val pageOutputStream = File(tmpDir, header.fileName.substringAfterLast("/")) - .also { it.createNewFile() } - .outputStream() - getStream(rar, header).use { - it.copyTo(pageOutputStream) - } - } - } - } + private val rar = Archive(file) override var isLocal: Boolean = true override suspend fun getPages(): List { - return DirectoryPageLoader(tmpDir).getPages() + return rar.fileHeaders.asSequence() + .filter { !it.isDirectory && ImageUtil.isImage(it.fileName) { rar.getInputStream(it) } } + .sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) } + .mapIndexed { i, header -> + ReaderPage(i).apply { + stream = { getStream(rar, header) } + status = Page.State.READY + } + } + .toList() + } + + override suspend fun loadPage(page: ReaderPage) { + check(!isRecycled) } override fun recycle() { super.recycle() - tmpDir.deleteRecursively() + rar.close() } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt index c10552b4ee..e04fe78e6a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt @@ -1,52 +1,46 @@ package eu.kanade.tachiyomi.ui.reader.loader -import android.app.Application +import android.os.Build +import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.ReaderPage -import org.apache.commons.compress.archivers.zip.ZipFile -import org.apache.commons.compress.utils.SeekableInMemoryByteChannel -import uy.kohesive.injekt.injectLazy -import java.io.ByteArrayOutputStream +import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder +import tachiyomi.core.util.system.ImageUtil import java.io.File -import java.io.FileInputStream +import java.nio.charset.StandardCharsets +import java.util.zip.ZipFile /** * Loader used to load a chapter from a .zip or .cbz file. */ internal class ZipPageLoader(file: File) : PageLoader() { - private val context: Application by injectLazy() - private val tmpDir = File(context.externalCacheDir, "reader_${file.hashCode()}").also { - it.deleteRecursively() - it.mkdirs() + private val zip = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + ZipFile(file, StandardCharsets.ISO_8859_1) + } else { + ZipFile(file) } - init { - ByteArrayOutputStream().use { byteArrayOutputStream -> - FileInputStream(file).use { it.copyTo(byteArrayOutputStream) } + override var isLocal: Boolean = true - 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 suspend fun getPages(): List { + 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 var isLocal: Boolean = true - - override suspend fun getPages(): List { - return DirectoryPageLoader(tmpDir).getPages() + override suspend fun loadPage(page: ReaderPage) { + check(!isRecycled) } override fun recycle() { super.recycle() - tmpDir.deleteRecursively() + zip.close() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderTransitionView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderTransitionView.kt index 055fa603b3..81b7bb13fe 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderTransitionView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderTransitionView.kt @@ -2,35 +2,70 @@ package eu.kanade.tachiyomi.ui.reader.viewer import android.content.Context import android.util.AttributeSet -import android.widget.FrameLayout -import androidx.compose.ui.platform.ComposeView +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.AbstractComposeView import eu.kanade.presentation.reader.ChapterTransition +import eu.kanade.presentation.theme.TachiyomiTheme import eu.kanade.tachiyomi.data.download.manga.MangaDownloadManager import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition -import eu.kanade.tachiyomi.util.view.setComposeContent import tachiyomi.domain.entries.manga.model.Manga class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : - FrameLayout(context, attrs) { + AbstractComposeView(context, attrs) { + + private var data: Data? by mutableStateOf(null) init { layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT) } fun bind(transition: ChapterTransition, downloadManager: MangaDownloadManager, manga: Manga?) { - manga ?: return - - removeAllViews() + 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 + } + } - val transitionView = ComposeView(context).apply { - setComposeContent { - ChapterTransition( - transition = transition, - downloadManager = downloadManager, - manga = manga, - ) + @Composable + override fun Content() { + data?.let { + TachiyomiTheme { + CompositionLocalProvider( + LocalTextStyle provides MaterialTheme.typography.bodySmall, + LocalContentColor provides MaterialTheme.colorScheme.onBackground, + ) { + ChapterTransition( + transition = it.transition, + currChapterDownloaded = it.currChapterDownloaded, + goingToChapterDownloaded = it.goingToChapterDownloaded, + ) + } } } - addView(transitionView) } + private data class Data( + val transition: ChapterTransition, + val currChapterDownloaded: Boolean, + val goingToChapterDownloaded: Boolean, + ) } diff --git a/domain/src/main/java/tachiyomi/domain/source/anime/model/StubAnimeSource.kt b/domain/src/main/java/tachiyomi/domain/source/anime/model/StubAnimeSource.kt index c2f35f8f02..33218ba122 100644 --- a/domain/src/main/java/tachiyomi/domain/source/anime/model/StubAnimeSource.kt +++ b/domain/src/main/java/tachiyomi/domain/source/anime/model/StubAnimeSource.kt @@ -8,8 +8,8 @@ import eu.kanade.tachiyomi.animesource.model.Video @Suppress("OverridingDeprecatedMember") class StubAnimeSource( override val id: Long, - override val name: String, override val lang: String, + override val name: String, ) : AnimeSource { val isInvalid: Boolean = name.isBlank() || lang.isBlank() diff --git a/domain/src/main/java/tachiyomi/domain/source/manga/model/StubMangaSource.kt b/domain/src/main/java/tachiyomi/domain/source/manga/model/StubMangaSource.kt index ab015424ad..01f266c36b 100644 --- a/domain/src/main/java/tachiyomi/domain/source/manga/model/StubMangaSource.kt +++ b/domain/src/main/java/tachiyomi/domain/source/manga/model/StubMangaSource.kt @@ -9,8 +9,8 @@ import rx.Observable @Suppress("OverridingDeprecatedMember") class StubMangaSource( override val id: Long, - override val name: String, override val lang: String, + override val name: String, ) : MangaSource { val isInvalid: Boolean = name.isBlank() || lang.isBlank() diff --git a/gradle/androidx.versions.toml b/gradle/androidx.versions.toml index 87400bf0fc..7e9f3cb6ee 100644 --- a/gradle/androidx.versions.toml +++ b/gradle/androidx.versions.toml @@ -10,7 +10,7 @@ appcompat = "androidx.appcompat:appcompat:1.6.1" biometricktx = "androidx.biometric:biometric-ktx:1.2.0-alpha05" constraintlayout = "androidx.constraintlayout:constraintlayout:2.1.4" coordinatorlayout = "androidx.coordinatorlayout:coordinatorlayout:1.2.0" -corektx = "androidx.core:core-ktx:1.11.0-alpha03" +corektx = "androidx.core:core-ktx:1.11.0-alpha04" splashscreen = "androidx.core:core-splashscreen:1.0.0-alpha02" recyclerview = "androidx.recyclerview:recyclerview:1.3.0" viewpager = "androidx.viewpager:viewpager:1.1.0-alpha01" diff --git a/gradle/compose.versions.toml b/gradle/compose.versions.toml index f347f3503b..3264abb3e6 100644 --- a/gradle/compose.versions.toml +++ b/gradle/compose.versions.toml @@ -1,5 +1,5 @@ [versions] -compiler = "1.4.4" +compiler = "1.4.7" compose-bom = "2023.03.00" accompanist = "0.30.1" diff --git a/gradle/kotlinx.versions.toml b/gradle/kotlinx.versions.toml index 4e0ac36f38..3a81629c80 100644 --- a/gradle/kotlinx.versions.toml +++ b/gradle/kotlinx.versions.toml @@ -1,5 +1,5 @@ [versions] -kotlin_version = "1.8.10" +kotlin_version = "1.8.21" serialization_version = "1.5.0" xml_serialization_version = "0.85.0" @@ -7,7 +7,7 @@ xml_serialization_version = "0.85.0" reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin_version" } gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin_version" } -coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version = "1.6.4" } +coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version = "1.7.1" } coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core" } coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android" } coroutines-guava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava" } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dae7aa8755..f2a0d759ff 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,7 +6,7 @@ shizuku_version = "12.2.0" sqlite = "2.3.1" sqldelight = "1.5.5" leakcanary = "2.10" -voyager = "1.0.0-rc07" +voyager = "1.0.0-rc06" richtext = "0.16.0" [libraries] @@ -31,7 +31,6 @@ jsoup = "org.jsoup:jsoup:1.16.1" disklrucache = "com.jakewharton:disklrucache:2.0.2" unifile = "com.github.tachiyomiorg:unifile:17bec43" -compress = "org.apache.commons:commons-compress:1.23.0" junrar = "com.github.junrar:junrar:7.5.4" sqlite-framework = { module = "androidx.sqlite:sqlite-framework", version.ref = "sqlite" } @@ -54,14 +53,14 @@ natural-comparator = "com.github.gpanther:java-nat-sort:natural-comparator-1.1" richtext-commonmark = { module = "com.halilibo.compose-richtext:richtext-commonmark", version.ref = "richtext" } richtext-m3 = { module = "com.halilibo.compose-richtext:richtext-ui-material3", version.ref = "richtext" } -material = "com.google.android.material:material:1.8.0" +material = "com.google.android.material:material:1.9.0" flexible-adapter-core = "com.github.arkon.FlexibleAdapter:flexible-adapter:c8013533" flexible-adapter-ui = "com.github.arkon.FlexibleAdapter:flexible-adapter-ui:c8013533" photoview = "com.github.chrisbanes:PhotoView:2.3.0" directionalviewpager = "com.github.tachiyomiorg:DirectionalViewPager:1.0.0" insetter = "dev.chrisbanes.insetter:insetter:0.6.1" compose-cascade = "me.saket.cascade:cascade-compose:2.0.0-rc02" -compose-materialmotion = "io.github.fornewid:material-motion-compose-core:0.12.1" +compose-materialmotion = "io.github.fornewid:material-motion-compose-core:0.12.3" compose-simpleicons = "br.com.devsrsouza.compose.icons.android:simple-icons:1.0.0" logcat = "com.squareup.logcat:logcat:0.1" @@ -84,11 +83,11 @@ sqldelight-android-paging = { module = "com.squareup.sqldelight:android-paging3- sqldelight-gradle = { module = "com.squareup.sqldelight:gradle-plugin", version.ref = "sqldelight" } junit = "org.junit.jupiter:junit-jupiter:5.9.3" -kotest-assertions = "io.kotest:kotest-assertions-core:5.6.1" +kotest-assertions = "io.kotest:kotest-assertions-core:5.6.2" -voyager-navigator = { module = "ca.gosyer:voyager-navigator", version.ref = "voyager" } -voyager-tab-navigator = { module = "ca.gosyer:voyager-tab-navigator", version.ref = "voyager" } -voyager-transitions = { module = "ca.gosyer:voyager-transitions", version.ref = "voyager" } +voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" } +voyager-tab-navigator = { module = "cafe.adriel.voyager:voyager-tab-navigator", version.ref = "voyager" } +voyager-transitions = { module = "cafe.adriel.voyager:voyager-transitions", version.ref = "voyager" } kotlinter = "org.jmailen.gradle:kotlinter-gradle:3.13.0" diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/Pager.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/Pager.kt index 942e074dbb..c62b315c84 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/Pager.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/Pager.kt @@ -1,300 +1,59 @@ package tachiyomi.presentation.core.components -import androidx.compose.foundation.gestures.FlingBehavior import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider -import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.wrapContentSize -import androidx.compose.foundation.lazy.LazyListItemInfo -import androidx.compose.foundation.lazy.LazyListLayoutInfo -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.pager.PageSize +import androidx.compose.foundation.pager.PagerDefaults +import androidx.compose.foundation.pager.PagerSnapDistance +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.Stable -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.Saver -import androidx.compose.runtime.saveable.listSaver -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.Density -import androidx.compose.ui.util.fastForEach -import androidx.compose.ui.util.fastMaxBy -import androidx.compose.ui.util.fastSumBy -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlin.math.abs +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +/** + * Horizontal Pager with custom SnapFlingBehavior for a more natural swipe feeling + */ @Composable fun HorizontalPager( - count: Int, + pageCount: Int, modifier: Modifier = Modifier, state: PagerState = rememberPagerState(), - key: ((page: Int) -> Any)? = null, - contentPadding: PaddingValues = PaddingValues(), + contentPadding: PaddingValues = PaddingValues(0.dp), + pageSize: PageSize = PageSize.Fill, + beyondBoundsPageCount: Int = 0, + pageSpacing: Dp = 0.dp, verticalAlignment: Alignment.Vertical = Alignment.CenterVertically, userScrollEnabled: Boolean = true, - content: @Composable BoxScope.(page: Int) -> Unit, + reverseLayout: Boolean = false, + key: ((index: Int) -> Any)? = null, + pageNestedScrollConnection: NestedScrollConnection = PagerDefaults.pageNestedScrollConnection( + Orientation.Horizontal, + ), + pageContent: @Composable (page: Int) -> Unit, ) { - Pager( - count = count, + androidx.compose.foundation.pager.HorizontalPager( + pageCount = pageCount, modifier = modifier, state = state, - isVertical = false, - key = key, contentPadding = contentPadding, + pageSize = pageSize, + beyondBoundsPageCount = beyondBoundsPageCount, + pageSpacing = pageSpacing, verticalAlignment = verticalAlignment, + flingBehavior = PagerDefaults.flingBehavior( + state = state, + pagerSnapDistance = PagerSnapDistance.atMost(0), + ), userScrollEnabled = userScrollEnabled, - content = content, + reverseLayout = reverseLayout, + key = key, + pageNestedScrollConnection = pageNestedScrollConnection, + pageContent = pageContent, ) } - -@Composable -private fun Pager( - count: Int, - modifier: Modifier, - state: PagerState, - isVertical: Boolean, - key: ((page: Int) -> Any)?, - contentPadding: PaddingValues, - userScrollEnabled: Boolean, - verticalAlignment: Alignment.Vertical = Alignment.CenterVertically, - horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally, - content: @Composable BoxScope.(page: Int) -> Unit, -) { - LaunchedEffect(count) { - state.currentPage = minOf(count - 1, state.currentPage).coerceAtLeast(0) - } - - LaunchedEffect(state) { - snapshotFlow { state.mostVisiblePageLayoutInfo?.index } - .distinctUntilChanged() - .collect { state.updateCurrentPageBasedOnLazyListState() } - } - - if (isVertical) { - LazyColumn( - modifier = modifier, - state = state.lazyListState, - contentPadding = contentPadding, - horizontalAlignment = horizontalAlignment, - verticalArrangement = Arrangement.aligned(verticalAlignment), - userScrollEnabled = userScrollEnabled, - flingBehavior = rememberLazyListSnapFlingBehavior(lazyListState = state.lazyListState), - ) { - items( - count = count, - key = key, - ) { page -> - Box( - modifier = Modifier - .fillParentMaxHeight() - .wrapContentSize(), - ) { - content(this, page) - } - } - } - } else { - LazyRow( - modifier = modifier, - state = state.lazyListState, - contentPadding = contentPadding, - verticalAlignment = verticalAlignment, - horizontalArrangement = Arrangement.aligned(horizontalAlignment), - userScrollEnabled = userScrollEnabled, - flingBehavior = rememberLazyListSnapFlingBehavior(lazyListState = state.lazyListState), - ) { - items( - count = count, - key = key, - ) { page -> - Box( - modifier = Modifier - .fillParentMaxWidth() - .wrapContentSize(), - ) { - content(this, page) - } - } - } - } -} - -@Composable -fun rememberPagerState( - initialPage: Int = 0, -) = rememberSaveable(saver = PagerState.Saver) { - PagerState(currentPage = initialPage) -} - -@Stable -class PagerState( - currentPage: Int = 0, -) { - init { check(currentPage >= 0) { "currentPage cannot be less than zero" } } - - val lazyListState = LazyListState(firstVisibleItemIndex = currentPage) - - private val pageSize: Int - get() = visiblePages.firstOrNull()?.size ?: 0 - - private var _currentPage by mutableStateOf(currentPage) - - private val layoutInfo: LazyListLayoutInfo - get() = lazyListState.layoutInfo - - private val visiblePages: List - 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 = 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 { - 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