From 5ceae3116bd7c184a470fe1e67a6ea615edd0487 Mon Sep 17 00:00:00 2001 From: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com> Date: Wed, 4 Oct 2023 20:24:51 +0200 Subject: [PATCH] merge2 Last Commit Merged: https://github.com/tachiyomiorg/tachiyomi/commit/9a10656bf07a7dd35400fa6e42dd0e4889ddb177 --- app/build.gradle.kts | 1 + .../GetLanguagesWithAnimeSources.kt | 6 +- .../GetLanguagesWithMangaSources.kt | 6 +- .../browse/anime/AnimeSourcesFilterScreen.kt | 27 +-- .../BrowseAnimeSourceComfortableGrid.kt | 6 +- .../BrowseAnimeSourceCompactGrid.kt | 6 +- .../anime/components/BrowseAnimeSourceList.kt | 9 +- .../browse/manga/MangaSourcesFilterScreen.kt | 27 +-- .../BrowseMangaSourceComfortableGrid.kt | 6 +- .../BrowseMangaSourceCompactGrid.kt | 6 +- .../manga/components/BrowseMangaSourceList.kt | 9 +- .../anime/AnimeLibrarySettingsDialog.kt | 7 +- .../manga/MangaLibrarySettingsDialog.kt | 7 +- .../presentation/reader/ChapterNavigator.kt | 121 +++++++++++++ .../presentation/reader/PageIndicatorText.kt | 44 +++++ .../library/anime/AnimeLibraryUpdateJob.kt | 2 +- .../library/manga/MangaLibraryUpdateJob.kt | 2 +- .../source/AnimeSourcesFilterScreenModel.kt | 3 +- .../source/MangaSourcesFilterScreenModel.kt | 3 +- .../ui/entries/anime/AnimeScreenModel.kt | 6 +- .../ui/entries/manga/MangaScreenModel.kt | 6 +- .../anime/AnimeLibrarySettingsScreenModel.kt | 5 +- .../manga/MangaLibrarySettingsScreenModel.kt | 5 +- .../ui/reader/PageIndicatorTextView.kt | 52 ------ .../tachiyomi/ui/reader/ReaderActivity.kt | 159 +++++------------- .../tachiyomi/ui/reader/ReaderSlider.kt | 30 ---- .../tachiyomi/ui/reader/ReaderViewModel.kt | 26 ++- .../ui/reader/loader/ZipPageLoader.kt | 41 ++--- .../setting/ReaderReadingModeSettings.kt | 2 +- .../ui/reader/setting/ReadingModeType.kt | 4 +- .../viewer/{BaseViewer.kt => Viewer.kt} | 2 +- .../ui/reader/viewer/pager/PagerViewer.kt | 6 +- .../ui/reader/viewer/webtoon/WebtoonViewer.kt | 6 +- .../util/system/ContextExtensions.kt | 14 -- .../tachiyomi/util/system/LocaleHelper.kt | 16 +- app/src/main/res/layout/reader_activity.xml | 82 +-------- gradle/androidx.versions.toml | 6 +- gradle/libs.versions.toml | 1 + 38 files changed, 374 insertions(+), 393 deletions(-) create mode 100644 app/src/main/java/eu/kanade/presentation/reader/ChapterNavigator.kt create mode 100644 app/src/main/java/eu/kanade/presentation/reader/PageIndicatorText.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/PageIndicatorTextView.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderSlider.kt rename app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/{BaseViewer.kt => Viewer.kt} (98%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2bae5afc2..b294e6b49 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -218,6 +218,7 @@ dependencies { // Disk implementation(libs.disklrucache) implementation(libs.unifile) + implementation(libs.compress) implementation(libs.junrar) // Preferences diff --git a/app/src/main/java/eu/kanade/domain/source/anime/interactor/GetLanguagesWithAnimeSources.kt b/app/src/main/java/eu/kanade/domain/source/anime/interactor/GetLanguagesWithAnimeSources.kt index 3b5564bbf..5d82e7eaf 100644 --- a/app/src/main/java/eu/kanade/domain/source/anime/interactor/GetLanguagesWithAnimeSources.kt +++ b/app/src/main/java/eu/kanade/domain/source/anime/interactor/GetLanguagesWithAnimeSources.kt @@ -6,13 +6,14 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import tachiyomi.domain.source.anime.model.AnimeSource import tachiyomi.domain.source.anime.repository.AnimeSourceRepository +import java.util.SortedMap class GetLanguagesWithAnimeSources( private val repository: AnimeSourceRepository, private val preferences: SourcePreferences, ) { - fun subscribe(): Flow>> { + fun subscribe(): Flow>> { return combine( preferences.enabledLanguages().changes(), preferences.disabledAnimeSources().changes(), @@ -23,7 +24,8 @@ class GetLanguagesWithAnimeSources( .thenBy(String.CASE_INSENSITIVE_ORDER) { it.name }, ) - sortedSources.groupBy { it.lang } + sortedSources + .groupBy { it.lang } .toSortedMap( compareBy { it !in enabledLanguage }.then(LocaleHelper.comparator), ) diff --git a/app/src/main/java/eu/kanade/domain/source/manga/interactor/GetLanguagesWithMangaSources.kt b/app/src/main/java/eu/kanade/domain/source/manga/interactor/GetLanguagesWithMangaSources.kt index f76c0d886..57c62e9ca 100644 --- a/app/src/main/java/eu/kanade/domain/source/manga/interactor/GetLanguagesWithMangaSources.kt +++ b/app/src/main/java/eu/kanade/domain/source/manga/interactor/GetLanguagesWithMangaSources.kt @@ -6,13 +6,14 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import tachiyomi.domain.source.manga.model.Source import tachiyomi.domain.source.manga.repository.MangaSourceRepository +import java.util.SortedMap class GetLanguagesWithMangaSources( private val repository: MangaSourceRepository, private val preferences: SourcePreferences, ) { - fun subscribe(): Flow>> { + fun subscribe(): Flow>> { return combine( preferences.enabledLanguages().changes(), preferences.disabledMangaSources().changes(), @@ -23,7 +24,8 @@ class GetLanguagesWithMangaSources( .thenBy(String.CASE_INSENSITIVE_ORDER) { it.name }, ) - sortedSources.groupBy { it.lang } + sortedSources + .groupBy { it.lang } .toSortedMap( compareBy { it !in enabledLanguage }.then(LocaleHelper.comparator), ) diff --git a/app/src/main/java/eu/kanade/presentation/browse/anime/AnimeSourcesFilterScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/anime/AnimeSourcesFilterScreen.kt index 74436408c..6f653256f 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/anime/AnimeSourcesFilterScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/anime/AnimeSourcesFilterScreen.kt @@ -64,7 +64,7 @@ private fun AnimeSourcesFilterContent( state.items.forEach { (language, sources) -> val enabled = language in state.enabledLanguages item( - key = language.hashCode(), + key = language, contentType = "source-filter-header", ) { AnimeSourcesFilterHeader( @@ -74,18 +74,19 @@ private fun AnimeSourcesFilterContent( onClickItem = onClickLanguage, ) } - if (!enabled) return@forEach - items( - items = sources, - key = { "source-filter-${it.key()}" }, - contentType = { "source-filter-item" }, - ) { source -> - AnimeSourcesFilterItem( - modifier = Modifier.animateItemPlacement(), - source = source, - isEnabled = "${source.id}" !in state.disabledSources, - onClickItem = onClickSource, - ) + if (enabled) { + items( + items = sources, + key = { "source-filter-${it.key()}" }, + contentType = { "source-filter-item" }, + ) { source -> + AnimeSourcesFilterItem( + modifier = Modifier.animateItemPlacement(), + source = source, + isEnabled = "${source.id}" !in state.disabledSources, + onClickItem = onClickSource, + ) + } } } } 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 907156ea5..42685be87 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,6 +11,7 @@ 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 @@ -40,7 +41,10 @@ fun BrowseAnimeSourceComfortableGrid( } } - items(animeList.itemCount) { index -> + items( + count = animeList.itemCount, + key = animeList.itemKey { it.value.id }, + ) { 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 4d09066a1..423ba7463 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,6 +11,7 @@ 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 @@ -40,7 +41,10 @@ fun BrowseAnimeSourceCompactGrid( } } - items(animeList.itemCount) { index -> + items( + count = animeList.itemCount, + key = animeList.itemKey { it.value.id }, + ) { 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 662288068..5474dc956 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,6 +7,7 @@ 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 @@ -34,9 +35,11 @@ fun BrowseAnimeSourceList( } } - items(animeList) { animeflow -> - animeflow ?: return@items - val anime by animeflow.collectAsState() + items( + count = animeList.itemCount, + key = animeList.itemKey { it.value.id }, + ) { index -> + val anime by animeList[index]?.collectAsState() ?: return@items BrowseAnimeSourceListItem( anime = anime, onClick = { onAnimeClick(anime) }, diff --git a/app/src/main/java/eu/kanade/presentation/browse/manga/MangaSourcesFilterScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/manga/MangaSourcesFilterScreen.kt index 1d25f5d34..5d8934887 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/manga/MangaSourcesFilterScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/manga/MangaSourcesFilterScreen.kt @@ -64,7 +64,7 @@ private fun SourcesFilterContent( state.items.forEach { (language, sources) -> val enabled = language in state.enabledLanguages item( - key = language.hashCode(), + key = language, contentType = "source-filter-header", ) { SourcesFilterHeader( @@ -74,18 +74,19 @@ private fun SourcesFilterContent( onClickItem = onClickLanguage, ) } - if (!enabled) return@forEach - items( - items = sources, - key = { "source-filter-${it.key()}" }, - contentType = { "source-filter-item" }, - ) { source -> - SourcesFilterItem( - modifier = Modifier.animateItemPlacement(), - source = source, - enabled = "${source.id}" !in state.disabledSources, - onClickItem = onClickSource, - ) + if (enabled) { + items( + items = sources, + key = { "source-filter-${it.key()}" }, + contentType = { "source-filter-item" }, + ) { source -> + SourcesFilterItem( + modifier = Modifier.animateItemPlacement(), + source = source, + enabled = "${source.id}" !in state.disabledSources, + onClickItem = onClickSource, + ) + } } } } 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 33d738a1b..7062549f2 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,6 +11,7 @@ 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 @@ -39,7 +40,10 @@ fun BrowseMangaSourceComfortableGrid( } } - items(mangaList.itemCount) { index -> + items( + count = mangaList.itemCount, + key = mangaList.itemKey { it.value.id }, + ) { 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 9200f7f07..313b30bcc 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,6 +11,7 @@ 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 @@ -39,7 +40,10 @@ fun BrowseMangaSourceCompactGrid( } } - items(mangaList.itemCount) { index -> + items( + count = mangaList.itemCount, + key = mangaList.itemKey { it.value.id }, + ) { 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 f4bf07f41..a57825f47 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,6 +7,7 @@ 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 @@ -33,9 +34,11 @@ fun BrowseMangaSourceList( } } - items(mangaList) { mangaflow -> - mangaflow ?: return@items - val manga by mangaflow.collectAsState() + items( + count = mangaList.itemCount, + key = mangaList.itemKey { it.value.id }, + ) { index -> + val manga by mangaList[index]?.collectAsState() ?: return@items BrowseMangaSourceListItem( manga = manga, onClick = { onMangaClick(manga) }, diff --git a/app/src/main/java/eu/kanade/presentation/library/anime/AnimeLibrarySettingsDialog.kt b/app/src/main/java/eu/kanade/presentation/library/anime/AnimeLibrarySettingsDialog.kt index 76135d946..0b4075e51 100644 --- a/app/src/main/java/eu/kanade/presentation/library/anime/AnimeLibrarySettingsDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/library/anime/AnimeLibrarySettingsDialog.kt @@ -119,12 +119,13 @@ private fun ColumnScope.FilterPage( onClick = { screenModel.toggleFilter(LibraryPreferences::filterCompletedAnime) }, ) - when (screenModel.trackServices.size) { + val trackServices = remember { screenModel.trackServices } + when (trackServices.size) { 0 -> { // No trackers } 1 -> { - val service = screenModel.trackServices[0] + val service = trackServices[0] val filterTracker by screenModel.libraryPreferences.filterTrackedAnime(service.id.toInt()).collectAsState() TriStateItem( label = stringResource(R.string.action_filter_tracked), @@ -134,7 +135,7 @@ private fun ColumnScope.FilterPage( } else -> { HeadingItem(R.string.action_filter_tracked) - screenModel.trackServices.map { service -> + trackServices.map { service -> val filterTracker by screenModel.libraryPreferences.filterTrackedAnime(service.id.toInt()).collectAsState() TriStateItem( label = stringResource(service.nameRes()), diff --git a/app/src/main/java/eu/kanade/presentation/library/manga/MangaLibrarySettingsDialog.kt b/app/src/main/java/eu/kanade/presentation/library/manga/MangaLibrarySettingsDialog.kt index f3769dc86..33f8d1394 100644 --- a/app/src/main/java/eu/kanade/presentation/library/manga/MangaLibrarySettingsDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/library/manga/MangaLibrarySettingsDialog.kt @@ -119,12 +119,13 @@ private fun ColumnScope.FilterPage( onClick = { screenModel.toggleFilter(LibraryPreferences::filterCompletedManga) }, ) - when (screenModel.trackServices.size) { + val trackServices = remember { screenModel.trackServices } + when (trackServices.size) { 0 -> { // No trackers } 1 -> { - val service = screenModel.trackServices[0] + val service = trackServices[0] val filterTracker by screenModel.libraryPreferences.filterTrackedManga(service.id.toInt()).collectAsState() TriStateItem( label = stringResource(R.string.action_filter_tracked), @@ -134,7 +135,7 @@ private fun ColumnScope.FilterPage( } else -> { HeadingItem(R.string.action_filter_tracked) - screenModel.trackServices.map { service -> + trackServices.map { service -> val filterTracker by screenModel.libraryPreferences.filterTrackedManga(service.id.toInt()).collectAsState() TriStateItem( label = stringResource(service.nameRes()), diff --git a/app/src/main/java/eu/kanade/presentation/reader/ChapterNavigator.kt b/app/src/main/java/eu/kanade/presentation/reader/ChapterNavigator.kt new file mode 100644 index 000000000..ee089ce8e --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/reader/ChapterNavigator.kt @@ -0,0 +1,121 @@ +package eu.kanade.presentation.reader + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.SkipNext +import androidx.compose.material.icons.outlined.SkipPrevious +import androidx.compose.material3.FilledIconButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Slider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import eu.kanade.presentation.util.isTabletUi +import eu.kanade.tachiyomi.R + +@Composable +fun ChapterNavigator( + isRtl: Boolean, + onNextChapter: () -> Unit, + enabledNext: Boolean, + onPreviousChapter: () -> Unit, + enabledPrevious: Boolean, + currentPage: Int, + totalPages: Int, + onSliderValueChange: (Int) -> Unit, +) { + val isTabletUi = isTabletUi() + val horizontalPadding = if (isTabletUi) 24.dp else 16.dp + val layoutDirection = if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr + + val backgroundColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.9f) + val haptic = LocalHapticFeedback.current + + // We explicitly handle direction based on the reader viewer rather than the system direction + CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = horizontalPadding), + verticalAlignment = Alignment.CenterVertically, + ) { + val isLeftEnabled = if (isRtl) enabledNext else enabledPrevious + if (isLeftEnabled) { + FilledIconButton( + onClick = if (isRtl) onNextChapter else onPreviousChapter, + colors = IconButtonDefaults.filledIconButtonColors( + containerColor = backgroundColor, + ), + ) { + Icon( + imageVector = Icons.Outlined.SkipPrevious, + contentDescription = stringResource(if (isRtl) R.string.action_next_chapter else R.string.action_previous_chapter), + ) + } + } + + if (totalPages > 1) { + CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) { + Row( + modifier = Modifier + .weight(1f) + .clip(RoundedCornerShape(24.dp)) + .background(backgroundColor) + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text(text = currentPage.toString()) + + Slider( + modifier = Modifier + .weight(1f) + .padding(horizontal = 8.dp), + value = currentPage.toFloat(), + valueRange = 1f..totalPages.toFloat(), + steps = totalPages, + onValueChange = { + onSliderValueChange(it.toInt() - 1) + haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove) + }, + ) + + Text(text = totalPages.toString()) + } + } + } else { + Spacer(Modifier.weight(1f)) + } + + val isRightEnabled = if (isRtl) enabledPrevious else enabledNext + if (isRightEnabled) { + FilledIconButton( + onClick = if (isRtl) onPreviousChapter else onNextChapter, + colors = IconButtonDefaults.filledIconButtonColors( + containerColor = backgroundColor, + ), + ) { + Icon( + imageVector = Icons.Outlined.SkipNext, + contentDescription = stringResource(if (isRtl) R.string.action_previous_chapter else R.string.action_next_chapter), + ) + } + } + } + } +} diff --git a/app/src/main/java/eu/kanade/presentation/reader/PageIndicatorText.kt b/app/src/main/java/eu/kanade/presentation/reader/PageIndicatorText.kt new file mode 100644 index 000000000..c97515fb1 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/reader/PageIndicatorText.kt @@ -0,0 +1,44 @@ +package eu.kanade.presentation.reader + +import androidx.compose.foundation.layout.Box +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.text.ExperimentalTextApi +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +@OptIn(ExperimentalTextApi::class) +@Composable +fun PageIndicatorText( + currentPage: Int, + totalPages: Int, +) { + if (currentPage <= 0 || totalPages <= 0) return + + val text = "$currentPage / $totalPages" + + Box { + Text( + text = text, + color = Color(45, 45, 45), + fontSize = MaterialTheme.typography.bodySmall.fontSize, + fontWeight = FontWeight.Bold, + letterSpacing = 1.sp, + style = TextStyle.Default.copy( + drawStyle = Stroke(width = 4f), + ), + ) + + Text( + text = text, + color = Color(235, 235, 235), + fontSize = MaterialTheme.typography.bodySmall.fontSize, + fontWeight = FontWeight.Bold, + letterSpacing = 1.sp, + ) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/anime/AnimeLibraryUpdateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/anime/AnimeLibraryUpdateJob.kt index 8c6b1f01d..fbaae4285 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/anime/AnimeLibraryUpdateJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/anime/AnimeLibraryUpdateJob.kt @@ -225,7 +225,6 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa val skippedUpdates = CopyOnWriteArrayList>() val failedUpdates = CopyOnWriteArrayList>() val hasDownloads = AtomicBoolean(false) - val loggedServices by lazy { trackManager.services.filter { it.isLogged } } val restrictions = libraryPreferences.libraryUpdateItemRestriction().get() coroutineScope { @@ -290,6 +289,7 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa } if (libraryPreferences.autoUpdateTrackers().get()) { + val loggedServices = trackManager.services.filter { it.isLogged } updateTrackings(anime, loggedServices) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/manga/MangaLibraryUpdateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/manga/MangaLibraryUpdateJob.kt index e297854d9..a5effd932 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/manga/MangaLibraryUpdateJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/manga/MangaLibraryUpdateJob.kt @@ -225,7 +225,6 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa val skippedUpdates = CopyOnWriteArrayList>() val failedUpdates = CopyOnWriteArrayList>() val hasDownloads = AtomicBoolean(false) - val loggedServices by lazy { trackManager.services.filter { it.isLogged } } val restrictions = libraryPreferences.libraryUpdateItemRestriction().get() coroutineScope { @@ -290,6 +289,7 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa } if (libraryPreferences.autoUpdateTrackers().get()) { + val loggedServices = trackManager.services.filter { it.isLogged } updateTrackings(manga, loggedServices) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/source/AnimeSourcesFilterScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/source/AnimeSourcesFilterScreenModel.kt index 1de6de89c..1e5386742 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/source/AnimeSourcesFilterScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/source/AnimeSourcesFilterScreenModel.kt @@ -14,6 +14,7 @@ import kotlinx.coroutines.launch import tachiyomi.domain.source.anime.model.AnimeSource import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import java.util.SortedMap class AnimeSourcesFilterScreenModel( private val preferences: SourcePreferences = Injekt.get(), @@ -66,7 +67,7 @@ sealed class AnimeSourcesFilterState { ) : AnimeSourcesFilterState() data class Success( - val items: Map>, + val items: SortedMap>, val enabledLanguages: Set, val disabledSources: Set, ) : AnimeSourcesFilterState() { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/source/MangaSourcesFilterScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/source/MangaSourcesFilterScreenModel.kt index ca26d6cb9..28ce3221c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/source/MangaSourcesFilterScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/source/MangaSourcesFilterScreenModel.kt @@ -14,6 +14,7 @@ import kotlinx.coroutines.launch import tachiyomi.domain.source.manga.model.Source import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import java.util.SortedMap class SourcesFilterScreenModel( private val preferences: SourcePreferences = Injekt.get(), @@ -66,7 +67,7 @@ sealed class MangaSourcesFilterState { ) : MangaSourcesFilterState() data class Success( - val items: Map>, + val items: SortedMap>, val enabledLanguages: Set, val disabledSources: Set, ) : MangaSourcesFilterState() { 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 06b319d0f..94e1bbc22 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,6 +628,9 @@ class AnimeInfoScreenModel( downloadEpisodes(episodes, false, video) } if (!isFavorited && !successState.hasPromptedToAddBefore) { + updateSuccessState { successState -> + successState.copy(hasPromptedToAddBefore = true) + } coroutineScope.launch { val result = snackbarHostState.showSnackbar( message = context.getString(R.string.snack_add_to_anime_library), @@ -637,9 +640,6 @@ class AnimeInfoScreenModel( if (result == SnackbarResult.ActionPerformed && !isFavorited) { toggleFavorite() } - updateSuccessState { successState -> - successState.copy(hasPromptedToAddBefore = true) - } } } } 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 c63a50bf2..e59e78ee8 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,6 +622,9 @@ class MangaInfoScreenModel( downloadChapters(chapters) } if (!isFavorited && !successState.hasPromptedToAddBefore) { + updateSuccessState { successState -> + successState.copy(hasPromptedToAddBefore = true) + } coroutineScope.launch { val result = snackbarHostState.showSnackbar( message = context.getString(R.string.snack_add_to_manga_library), @@ -631,9 +634,6 @@ class MangaInfoScreenModel( if (result == SnackbarResult.ActionPerformed && !isFavorited) { toggleFavorite() } - updateSuccessState { successState -> - successState.copy(hasPromptedToAddBefore = true) - } } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/anime/AnimeLibrarySettingsScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/anime/AnimeLibrarySettingsScreenModel.kt index 49b934812..a6d4b37a9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/anime/AnimeLibrarySettingsScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/anime/AnimeLibrarySettingsScreenModel.kt @@ -25,10 +25,11 @@ class AnimeLibrarySettingsScreenModel( private val getCategories: GetAnimeCategories = Injekt.get(), private val setDisplayModeForCategory: SetDisplayModeForAnimeCategory = Injekt.get(), private val setSortModeForCategory: SetSortModeForAnimeCategory = Injekt.get(), - trackManager: TrackManager = Injekt.get(), + private val trackManager: TrackManager = Injekt.get(), ) : ScreenModel { - val trackServices = trackManager.services.filter { service -> service.isLogged } + val trackServices + get() = trackManager.services.filter { it.isLogged } fun togglePreference(preference: (LibraryPreferences) -> Preference) { preference(libraryPreferences).toggle() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/manga/MangaLibrarySettingsScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/manga/MangaLibrarySettingsScreenModel.kt index 8990bdc2a..cc5ec5a87 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/manga/MangaLibrarySettingsScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/manga/MangaLibrarySettingsScreenModel.kt @@ -25,10 +25,11 @@ class MangaLibrarySettingsScreenModel( private val getCategories: GetMangaCategories = Injekt.get(), private val setDisplayModeForCategory: SetDisplayModeForMangaCategory = Injekt.get(), private val setSortModeForCategory: SetSortModeForMangaCategory = Injekt.get(), - trackManager: TrackManager = Injekt.get(), + private val trackManager: TrackManager = Injekt.get(), ) : ScreenModel { - val trackServices = trackManager.services.filter { service -> service.isLogged } + val trackServices + get() = trackManager.services.filter { it.isLogged } fun togglePreference(preference: (LibraryPreferences) -> Preference) { preference(libraryPreferences).toggle() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/PageIndicatorTextView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/PageIndicatorTextView.kt deleted file mode 100644 index d0cd94f2a..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/PageIndicatorTextView.kt +++ /dev/null @@ -1,52 +0,0 @@ -package eu.kanade.tachiyomi.ui.reader - -import android.annotation.SuppressLint -import android.content.Context -import android.graphics.Color -import android.text.Spannable -import android.text.SpannableString -import android.text.style.ScaleXSpan -import android.util.AttributeSet -import androidx.appcompat.widget.AppCompatTextView -import eu.kanade.tachiyomi.widget.OutlineSpan - -/** - * Page indicator found at the bottom of the reader - */ -class PageIndicatorTextView( - context: Context, - attrs: AttributeSet? = null, -) : AppCompatTextView(context, attrs) { - - init { - setTextColor(fillColor) - } - - @SuppressLint("SetTextI18n") - override fun setText(text: CharSequence?, type: BufferType?) { - // Add spaces at the start & end of the text, otherwise the stroke is cut-off because it's - // not taken into account when measuring the text (view's padding doesn't help). - val currText = " $text " - - // Also add a bit of spacing between each character, as the stroke overlaps them - val finalText = SpannableString(currText.asIterable().joinToString("\u00A0")).apply { - // Apply text outline - setSpan(spanOutline, 1, length - 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - - for (i in 1..lastIndex step 2) { - setSpan(ScaleXSpan(0.2f), i, i + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - } - } - - super.setText(finalText, BufferType.SPANNABLE) - } -} - -private val fillColor = Color.rgb(235, 235, 235) -private val strokeColor = Color.rgb(45, 45, 45) - -// A span object with text outlining properties -private val spanOutline = OutlineSpan( - strokeColor = strokeColor, - strokeWidth = 4f, -) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt index 244e2819f..3e5c88bde 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt @@ -6,18 +6,15 @@ import android.app.ProgressDialog import android.app.assist.AssistContent import android.content.Context import android.content.Intent -import android.content.res.ColorStateList import android.graphics.Bitmap import android.graphics.Color import android.graphics.ColorMatrix import android.graphics.ColorMatrixColorFilter import android.graphics.Paint -import android.graphics.drawable.RippleDrawable import android.net.Uri import android.os.Build import android.os.Bundle import android.view.Gravity -import android.view.HapticFeedbackConstants import android.view.KeyEvent import android.view.Menu import android.view.MenuItem @@ -29,23 +26,25 @@ import android.view.animation.AnimationUtils import android.widget.FrameLayout import android.widget.Toast import androidx.activity.viewModels +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.core.graphics.ColorUtils import androidx.core.net.toUri import androidx.core.transition.doOnEnd import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat -import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import androidx.lifecycle.lifecycleScope import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.google.android.material.shape.MaterialShapeDrawable -import com.google.android.material.slider.Slider import com.google.android.material.transition.platform.MaterialContainerTransform import dev.chrisbanes.insetter.applyInsetter import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.entries.manga.model.orientationType +import eu.kanade.presentation.reader.ChapterNavigator +import eu.kanade.presentation.reader.PageIndicatorText import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.core.Constants import eu.kanade.tachiyomi.data.notification.NotificationReceiver @@ -64,20 +63,19 @@ import eu.kanade.tachiyomi.ui.reader.setting.OrientationType import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsSheet import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType -import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer import eu.kanade.tachiyomi.ui.webview.WebViewActivity import eu.kanade.tachiyomi.util.preference.toggle import eu.kanade.tachiyomi.util.system.applySystemAnimatorScale import eu.kanade.tachiyomi.util.system.createReaderThemeContext -import eu.kanade.tachiyomi.util.system.getThemeColor import eu.kanade.tachiyomi.util.system.hasDisplayCutout import eu.kanade.tachiyomi.util.system.isNightMode import eu.kanade.tachiyomi.util.system.toShareIntent import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.view.copy import eu.kanade.tachiyomi.util.view.popupMenu +import eu.kanade.tachiyomi.util.view.setComposeContent import eu.kanade.tachiyomi.util.view.setTooltip import eu.kanade.tachiyomi.widget.listener.SimpleAnimationListener import kotlinx.coroutines.flow.distinctUntilChanged @@ -90,6 +88,7 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.sample import kotlinx.coroutines.launch import logcat.LogPriority +import org.jsoup.internal.StringUtil.padding import tachiyomi.core.util.lang.launchIO import tachiyomi.core.util.lang.launchNonCancellable import tachiyomi.core.util.lang.withUIContext @@ -97,7 +96,6 @@ import tachiyomi.core.util.system.logcat import tachiyomi.domain.entries.manga.model.Manga import uy.kohesive.injekt.injectLazy import kotlin.math.abs -import kotlin.math.max class ReaderActivity : BaseActivity() { @@ -109,9 +107,6 @@ class ReaderActivity : BaseActivity() { addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) } } - - private const val ENABLED_BUTTON_IMAGE_ALPHA = 255 - private const val DISABLED_BUTTON_IMAGE_ALPHA = 64 } private val readerPreferences: ReaderPreferences by injectLazy() @@ -124,12 +119,6 @@ class ReaderActivity : BaseActivity() { val hasCutout by lazy { hasDisplayCutout() } - /** - * Viewer used to display the pages (pager, webtoon, ...). - */ - var viewer: BaseViewer? = null - private set - /** * Whether the menu is currently visible. */ @@ -251,8 +240,7 @@ class ReaderActivity : BaseActivity() { */ override fun onDestroy() { super.onDestroy() - viewer?.destroy() - viewer = null + viewModel.state.value.viewer?.destroy() config = null menuToggleToast?.cancel() readingModeToast?.cancel() @@ -362,7 +350,7 @@ class ReaderActivity : BaseActivity() { * Dispatches a key event. If the viewer doesn't handle it, call the default implementation. */ override fun dispatchKeyEvent(event: KeyEvent): Boolean { - val handled = viewer?.handleKeyEvent(event) ?: false + val handled = viewModel.state.value.viewer?.handleKeyEvent(event) ?: false return handled || super.dispatchKeyEvent(event) } @@ -371,7 +359,7 @@ class ReaderActivity : BaseActivity() { * implementation. */ override fun dispatchGenericMotionEvent(event: MotionEvent): Boolean { - val handled = viewer?.handleGenericMotionEvent(event) ?: false + val handled = viewModel.state.value.viewer?.handleGenericMotionEvent(event) ?: false return handled || super.dispatchGenericMotionEvent(event) } @@ -408,42 +396,35 @@ class ReaderActivity : BaseActivity() { } } - // Init listeners on bottom menu - binding.pageSlider.addOnSliderTouchListener( - object : Slider.OnSliderTouchListener { - override fun onStartTrackingTouch(slider: Slider) { - isScrollingThroughPages = true - } + binding.pageNumber.setComposeContent { + val state by viewModel.state.collectAsState() - override fun onStopTrackingTouch(slider: Slider) { - isScrollingThroughPages = false - } - }, - ) - binding.pageSlider.addOnChangeListener { slider, value, fromUser -> - if (viewer != null && fromUser) { - isScrollingThroughPages = true - moveToPageIndex(value.toInt()) - slider.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY) - } + PageIndicatorText( + currentPage = state.currentPage, + totalPages = state.totalPages, + ) } - binding.leftChapter.setOnClickListener { - if (viewer != null) { - if (viewer is R2LPagerViewer) { - loadNextChapter() - } else { - loadPreviousChapter() - } - } - } - binding.rightChapter.setOnClickListener { - if (viewer != null) { - if (viewer is R2LPagerViewer) { - loadPreviousChapter() - } else { - loadNextChapter() - } - } + + // Init listeners on bottom menu + binding.readerNav.setComposeContent { + val state by viewModel.state.collectAsState() + + if (state.viewer == null) return@setComposeContent + val isRtl = state.viewer is R2LPagerViewer + + ChapterNavigator( + isRtl = isRtl, + onNextChapter = ::loadNextChapter, + enabledNext = state.viewerChapters?.nextChapter != null, + onPreviousChapter = ::loadPreviousChapter, + enabledPrevious = state.viewerChapters?.prevChapter != null, + currentPage = state.currentPage, + totalPages = state.totalPages, + onSliderValueChange = { + isScrollingThroughPages = true + moveToPageIndex(it) + }, + ) } initBottomShortcuts() @@ -454,18 +435,6 @@ class ReaderActivity : BaseActivity() { } binding.toolbarBottom.background = toolbarBackground.copy(this@ReaderActivity) - binding.readerSeekbar.background = toolbarBackground.copy(this@ReaderActivity)?.apply { - setCornerSize(999F) - } - listOf(binding.leftChapter, binding.rightChapter).forEach { - it.background = binding.readerSeekbar.background.copy(this) - it.foreground = RippleDrawable( - ColorStateList.valueOf(getThemeColor(android.R.attr.colorControlHighlight)), - null, - it.background, - ) - } - val toolbarColor = ColorUtils.setAlphaComponent( toolbarBackground.resolvedTintColor, toolbarBackground.alpha, @@ -659,7 +628,7 @@ class ReaderActivity : BaseActivity() { * and the toolbar title. */ private fun setManga(manga: Manga) { - val prevViewer = viewer + val prevViewer = viewModel.state.value.viewer val viewerMode = ReadingModeType.fromPreference(viewModel.getMangaReadingMode(resolveDefault = false)) binding.actionReadingMode.setImageResource(viewerMode.iconRes) @@ -681,7 +650,7 @@ class ReaderActivity : BaseActivity() { prevViewer.destroy() binding.viewerContainer.removeAllViews() } - viewer = newViewer + viewModel.onViewerLoaded(newViewer) updateViewerInset(readerPreferences.fullscreen().get()) binding.viewerContainer.addView(newViewer.getView()) @@ -691,15 +660,6 @@ class ReaderActivity : BaseActivity() { supportActionBar?.title = manga.title - binding.pageSlider.isRTL = newViewer is R2LPagerViewer - if (newViewer is R2LPagerViewer) { - binding.leftChapter.setTooltip(R.string.action_next_chapter) - binding.rightChapter.setTooltip(R.string.action_previous_chapter) - } else { - binding.leftChapter.setTooltip(R.string.action_previous_chapter) - binding.rightChapter.setTooltip(R.string.action_next_chapter) - } - val loadingIndicatorContext = createReaderThemeContext() loadingIndicator = ReaderProgressIndicator(loadingIndicatorContext).apply { updateLayoutParams { @@ -739,26 +699,9 @@ class ReaderActivity : BaseActivity() { */ private fun setChapters(viewerChapters: ViewerChapters) { binding.readerContainer.removeView(loadingIndicator) - viewer?.setChapters(viewerChapters) + viewModel.state.value.viewer?.setChapters(viewerChapters) binding.toolbar.subtitle = viewerChapters.currChapter.chapter.name - val currentChapterPageCount = viewerChapters.currChapter.pages?.size ?: 1 - binding.readerSeekbar.isInvisible = currentChapterPageCount == 1 - - val leftChapterObject = if (viewer is R2LPagerViewer) viewerChapters.nextChapter else viewerChapters.prevChapter - val rightChapterObject = if (viewer is R2LPagerViewer) viewerChapters.prevChapter else viewerChapters.nextChapter - - if (leftChapterObject == null && rightChapterObject == null) { - binding.leftChapter.isVisible = false - binding.rightChapter.isVisible = false - } else { - binding.leftChapter.isEnabled = leftChapterObject != null - binding.leftChapter.imageAlpha = if (leftChapterObject != null) ENABLED_BUTTON_IMAGE_ALPHA else DISABLED_BUTTON_IMAGE_ALPHA - - binding.rightChapter.isEnabled = rightChapterObject != null - binding.rightChapter.imageAlpha = if (rightChapterObject != null) ENABLED_BUTTON_IMAGE_ALPHA else DISABLED_BUTTON_IMAGE_ALPHA - } - // Invalidate menu to show proper chapter bookmark state invalidateOptionsMenu() @@ -786,7 +729,7 @@ class ReaderActivity : BaseActivity() { * other cases are handled with chapter transitions on the viewers and chapter preloading. */ @Suppress("DEPRECATION") - fun setProgressDialog(show: Boolean) { + private fun setProgressDialog(show: Boolean) { progressDialog?.dismiss() progressDialog = if (show) { ProgressDialog.show(this, null, getString(R.string.loading), true) @@ -800,7 +743,7 @@ class ReaderActivity : BaseActivity() { * page is not found. */ private fun moveToPageIndex(index: Int) { - val viewer = viewer ?: return + val viewer = viewModel.state.value.viewer ?: return val currentChapter = viewModel.getCurrentChapter() ?: return val page = currentChapter.pages?.getOrNull(index) ?: return viewer.moveToPage(page) @@ -835,24 +778,6 @@ class ReaderActivity : BaseActivity() { @SuppressLint("SetTextI18n") fun onPageSelected(page: ReaderPage) { viewModel.onPageSelected(page) - val pages = page.chapter.pages ?: return - - // Set bottom page number - binding.pageNumber.text = "${page.number}/${pages.size}" - - // Set page numbers - if (viewer !is R2LPagerViewer) { - binding.leftPageText.text = "${page.number}" - binding.rightPageText.text = "${pages.size}" - } else { - binding.rightPageText.text = "${page.number}" - binding.leftPageText.text = "${pages.size}" - } - - // Set slider progress - binding.pageSlider.isEnabled = pages.size > 1 - binding.pageSlider.valueTo = max(pages.lastIndex.toFloat(), 1f) - binding.pageSlider.value = page.index.toFloat() } /** @@ -980,7 +905,7 @@ class ReaderActivity : BaseActivity() { * Updates viewer inset depending on fullscreen reader preferences. */ fun updateViewerInset(fullscreen: Boolean) { - viewer?.getView()?.applyInsetter { + viewModel.state.value.viewer?.getView()?.applyInsetter { if (!fullscreen) { type(navigationBars = true, statusBars = true) { padding() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderSlider.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderSlider.kt deleted file mode 100644 index 94f857785..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderSlider.kt +++ /dev/null @@ -1,30 +0,0 @@ -package eu.kanade.tachiyomi.ui.reader - -import android.content.Context -import android.util.AttributeSet -import com.google.android.material.slider.Slider - -/** - * Slider to show current chapter progress. - */ -class ReaderSlider @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, -) : Slider(context, attrs) { - - init { - stepSize = 1f - setLabelFormatter { value -> - (value.toInt() + 1).toString() - } - } - - /** - * Whether the slider should draw from right to left. - */ - var isRTL: Boolean - set(value) { - layoutDirection = if (value) LAYOUT_DIRECTION_RTL else LAYOUT_DIRECTION_LTR - } - get() = layoutDirection == LAYOUT_DIRECTION_RTL -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt index f0ea75d63..95808f9e3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt @@ -24,6 +24,7 @@ import eu.kanade.tachiyomi.data.saver.Location import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.online.HttpSource +import eu.kanade.tachiyomi.ui.player.viewer.AspectState.Companion.get import eu.kanade.tachiyomi.ui.reader.loader.ChapterLoader import eu.kanade.tachiyomi.ui.reader.model.InsertPage import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter @@ -33,6 +34,7 @@ import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters import eu.kanade.tachiyomi.ui.reader.setting.OrientationType import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType +import eu.kanade.tachiyomi.ui.reader.viewer.Viewer import eu.kanade.tachiyomi.util.chapter.removeDuplicates import eu.kanade.tachiyomi.util.editCover import eu.kanade.tachiyomi.util.lang.byteSize @@ -396,6 +398,14 @@ class ReaderViewModel( eventChannel.trySend(Event.ReloadViewerChapters) } + fun onViewerLoaded(viewer: Viewer?) { + mutableState.update { + it.copy( + viewer = viewer, + ) + } + } + /** * Called every time a page changes on the reader. Used to mark the flag of chapters being * read, update tracking services, enqueue downloaded chapter deletion, and updating the active chapter if this @@ -412,6 +422,11 @@ class ReaderViewModel( } // Save last page read and mark as read if needed + mutableState.update { + it.copy( + currentPage = page.index + 1, + ) + } selectedChapter.chapter.last_page_read = page.index val shouldTrack = !incognitoMode || hasTrackers if (selectedChapter.pages?.lastIndex == page.index && shouldTrack) { @@ -874,7 +889,16 @@ class ReaderViewModel( val manga: Manga? = null, val viewerChapters: ViewerChapters? = null, val isLoadingAdjacentChapter: Boolean = false, - ) + val currentPage: Int = -1, + + /** + * Viewer used to display the pages (pager, webtoon, ...). + */ + val viewer: Viewer? = null, + ) { + val totalPages: Int + get() = viewerChapters?.currChapter?.pages?.size ?: -1 + } sealed class Event { object ReloadViewerChapters : Event() 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 a9a4e91ff..c10552b4e 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,12 +1,13 @@ package eu.kanade.tachiyomi.ui.reader.loader import android.app.Application -import android.os.Build 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 java.io.File import java.io.FileInputStream -import java.util.zip.ZipInputStream /** * Loader used to load a chapter from a .zip or .cbz file. @@ -20,29 +21,21 @@ internal class ZipPageLoader(file: File) : PageLoader() { } init { - ZipInputStream(FileInputStream(file)).use { zipInputStream -> - generateSequence { zipInputStream.nextEntry } - .filterNot { it.isDirectory } - .forEach { entry -> - File(tmpDir, entry.name.substringAfterLast("/")) - .also { it.createNewFile() } - .outputStream().use { pageOutputStream -> - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - pageOutputStream.write(zipInputStream.readNBytes(entry.size.toInt())) - } else { - val buffer = ByteArray(2048) - var len: Int - while ( - zipInputStream.read(buffer, 0, buffer.size) - .also { len = it } >= 0 - ) { - pageOutputStream.write(buffer, 0, len) - } + 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() } - pageOutputStream.flush() - } - zipInputStream.closeEntry() - } + } + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderReadingModeSettings.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderReadingModeSettings.kt index e23575195..844bc5ab1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderReadingModeSettings.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderReadingModeSettings.kt @@ -34,7 +34,7 @@ class ReaderReadingModeSettings @JvmOverloads constructor(context: Context, attr initGeneralPreferences() - when ((context as ReaderActivity).viewer) { + when ((context as ReaderActivity).viewModel.state.value.viewer) { is PagerViewer -> initPagerPreferences() is WebtoonViewer -> initWebtoonPreferences() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReadingModeType.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReadingModeType.kt index 5617c7aaa..88e1a3f02 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReadingModeType.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReadingModeType.kt @@ -4,7 +4,7 @@ import androidx.annotation.DrawableRes import androidx.annotation.StringRes import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.ui.reader.ReaderActivity -import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer +import eu.kanade.tachiyomi.ui.reader.viewer.Viewer import eu.kanade.tachiyomi.ui.reader.viewer.pager.L2RPagerViewer import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer import eu.kanade.tachiyomi.ui.reader.viewer.pager.VerticalPagerViewer @@ -31,7 +31,7 @@ enum class ReadingModeType(val prefValue: Int, @StringRes val stringRes: Int, @D fun fromSpinner(position: Int?) = values().find { value -> value.prefValue == position } ?: DEFAULT - fun toViewer(preference: Int?, activity: ReaderActivity): BaseViewer { + fun toViewer(preference: Int?, activity: ReaderActivity): Viewer { return when (fromPreference(preference)) { LEFT_TO_RIGHT -> L2RPagerViewer(activity) RIGHT_TO_LEFT -> R2LPagerViewer(activity) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/BaseViewer.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/Viewer.kt similarity index 98% rename from app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/BaseViewer.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/Viewer.kt index 223cb087f..00834563c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/BaseViewer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/Viewer.kt @@ -9,7 +9,7 @@ import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters /** * Interface for implementing a viewer. */ -interface BaseViewer { +interface Viewer { /** * Returns the view this viewer uses. diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt index 07b9a2671..3c92d8ad4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt @@ -17,7 +17,7 @@ import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition import eu.kanade.tachiyomi.ui.reader.model.InsertPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters -import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer +import eu.kanade.tachiyomi.ui.reader.viewer.Viewer import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation.NavigationRegion import kotlinx.coroutines.MainScope import kotlinx.coroutines.cancel @@ -26,10 +26,10 @@ import uy.kohesive.injekt.injectLazy import kotlin.math.min /** - * Implementation of a [BaseViewer] to display pages with a [ViewPager]. + * Implementation of a [Viewer] to display pages with a [ViewPager]. */ @Suppress("LeakingThis") -abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer { +abstract class PagerViewer(val activity: ReaderActivity) : Viewer { val downloadManager: MangaDownloadManager by injectLazy() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt index 1d91cb16b..fe2dbd98d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt @@ -18,7 +18,7 @@ import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.StencilPage import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences -import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer +import eu.kanade.tachiyomi.ui.reader.viewer.Viewer import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation.NavigationRegion import kotlinx.coroutines.MainScope import kotlinx.coroutines.cancel @@ -30,9 +30,9 @@ import kotlin.math.max import kotlin.math.min /** - * Implementation of a [BaseViewer] to display pages with a [RecyclerView]. + * Implementation of a [Viewer] to display pages with a [RecyclerView]. */ -class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = true) : BaseViewer { +class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = true) : Viewer { val downloadManager: MangaDownloadManager by injectLazy() diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt index 7043544ce..ec2c62093 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt @@ -12,7 +12,6 @@ import android.graphics.drawable.Drawable import android.net.Uri import android.os.Build import android.os.PowerManager -import android.util.TypedValue import androidx.annotation.AttrRes import androidx.annotation.ColorInt import androidx.appcompat.view.ContextThemeWrapper @@ -89,19 +88,6 @@ fun Context.hasPermission(permission: String) = PermissionChecker.checkSelfPermi return color } -@ColorInt fun Context.getThemeColor(attr: Int): Int { - val tv = TypedValue() - return if (this.theme.resolveAttribute(attr, tv, true)) { - if (tv.resourceId != 0) { - getColor(tv.resourceId) - } else { - tv.data - } - } else { - 0 - } -} - val Context.powerManager: PowerManager get() = getSystemService()!! diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/LocaleHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/LocaleHelper.kt index a14450793..bb24c849b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/LocaleHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/LocaleHelper.kt @@ -10,10 +10,18 @@ import java.util.Locale */ object LocaleHelper { - val comparator = compareBy( - { getDisplayName(it) }, - { it == "all" }, - ) + /** + * Sorts by display name, except keeps the "all" (displayed as "Multi") locale at the top. + */ + val comparator = { a: String, b: String -> + if (a == "all") { + -1 + } else if (b == "all") { + 1 + } else { + getDisplayName(a).compareTo(getDisplayName(b)) + } + } /** * Returns display name of a string language code. diff --git a/app/src/main/res/layout/reader_activity.xml b/app/src/main/res/layout/reader_activity.xml index ec3dac90c..649107ca8 100644 --- a/app/src/main/res/layout/reader_activity.xml +++ b/app/src/main/res/layout/reader_activity.xml @@ -16,15 +16,11 @@ android:layout_height="match_parent" android:descendantFocusability="blocksDescendants" /> - + android:layout_gravity="bottom|center_horizontal" /> @@ -63,82 +59,12 @@ android:layout_gravity="bottom" android:orientation="vertical"> - - - - - - - - - - - - - - - - - - + android:layoutDirection="ltr" />