From 264b0e6127ccb9fa270c7db6a66cc38bcc9989bf Mon Sep 17 00:00:00 2001 From: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com> Date: Fri, 3 Nov 2023 22:30:37 +0100 Subject: [PATCH] merge15 Last commit merged: https://github.com/tachiyomiorg/tachiyomi/commit/cf3f2d0380e5ab70211e6aab3f45bc81da43fcf4 --- .../service/DelayedAnimeTrackingUpdateJob.kt | 15 +- .../service/DelayedMangaTrackingUpdateJob.kt | 15 +- .../presentation/entries/anime/AnimeScreen.kt | 8 +- .../anime/components/AnimeEpisodeListItem.kt | 31 +- .../presentation/entries/manga/MangaScreen.kt | 8 +- .../manga/components/MangaChapterListItem.kt | 31 +- .../anime/AnimeLibrarySettingsDialog.kt | 69 ++--- .../manga/MangaLibrarySettingsDialog.kt | 69 ++--- .../more/settings/PreferenceItem.kt | 18 ++ .../more/settings/PreferenceModel.kt | 14 + .../more/settings/screen/AboutScreen.kt | 54 +++- .../screen/OpenSourceLibraryLicenseScreen.kt | 94 ++++++ ...sScreen.kt => OpenSourceLicensesScreen.kt} | 11 +- .../settings/widget/BasePreferenceWidget.kt | 4 +- app/src/main/java/eu/kanade/tachiyomi/App.kt | 11 - .../java/eu/kanade/tachiyomi/AppModule.kt | 7 +- .../download/anime/AnimeDownloadManager.kt | 14 +- .../library/manga/MangaLibraryUpdateJob.kt | 2 +- .../anime/util/AnimeExtensionLoader.kt | 2 +- .../manga/util/MangaExtensionLoader.kt | 2 +- .../details/SourcePreferencesScreen.kt | 1 + .../anime/migration/AnimeMigrationFlags.kt | 30 +- .../migration/search/MigrateAnimeDialog.kt | 13 +- .../browse/BrowseAnimeSourceScreenModel.kt | 2 +- .../details/MangaSourcePreferencesScreen.kt | 1 + .../manga/migration/MangaMigrationFlags.kt | 31 +- .../migration/search/MigrateMangaDialog.kt | 10 + .../browse/BrowseMangaSourceScreenModel.kt | 2 +- .../ui/entries/anime/AnimeScreenModel.kt | 7 +- .../ui/entries/manga/MangaScreenModel.kt | 11 +- .../kanade/tachiyomi/ui/main/MainActivity.kt | 21 +- .../tachiyomi/ui/reader/ReaderActivity.kt | 131 ++++----- ...geDialog.kt => ReaderPageActionsDialog.kt} | 2 +- .../tachiyomi/ui/reader/ReaderViewModel.kt | 69 +++-- .../reader/setting/ReaderColorFilterDialog.kt | 164 +++++++++++ .../setting/ReaderColorFilterSettings.kt | 202 ------------- .../ui/reader/setting/ReaderSettingsSheet.kt | 45 --- .../ui/reader/viewer/pager/PagerViewer.kt | 6 +- .../ui/reader/viewer/webtoon/WebtoonFrame.kt | 17 ++ .../ui/reader/viewer/webtoon/WebtoonViewer.kt | 8 +- .../util/view/ImageViewExtensions.kt | 21 -- .../util/view/ViewGroupExtensions.kt | 15 - .../eu/kanade/tachiyomi/widget/OutlineSpan.kt | 56 ---- .../eu/kanade/tachiyomi/widget/TriState.kt | 19 -- .../listener/SimpleTabSelectedListener.kt | 14 - app/src/main/res/layout/reader_activity.xml | 27 +- .../layout/reader_color_filter_settings.xml | 269 ------------------ app/src/main/res/values-v28/arrays.xml | 15 - app/src/main/res/values/arrays.xml | 6 - app/src/main/res/xml/provider_paths.xml | 5 +- .../tachiyomi/util/system/WebViewUtil.kt | 4 +- gradle/compose.versions.toml | 4 +- gradle/kotlinx.versions.toml | 6 +- gradle/libs.versions.toml | 4 +- gradle/wrapper/gradle-wrapper.properties | 3 +- gradlew | 5 +- i18n/src/main/res/values/strings-aniyomi.xml | 2 +- i18n/src/main/res/values/strings.xml | 2 +- .../core/components/AdaptiveSheet.kt | 111 ++++---- ...tingsItemsPaddings.kt => SettingsItems.kt} | 39 +++ 60 files changed, 825 insertions(+), 1054 deletions(-) create mode 100644 app/src/main/java/eu/kanade/presentation/more/settings/screen/OpenSourceLibraryLicenseScreen.kt rename app/src/main/java/eu/kanade/presentation/more/settings/screen/{LicensesScreen.kt => OpenSourceLicensesScreen.kt} (76%) rename app/src/main/java/eu/kanade/tachiyomi/ui/reader/{ReaderPageDialog.kt => ReaderPageActionsDialog.kt} (99%) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderColorFilterDialog.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderColorFilterSettings.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/util/view/ImageViewExtensions.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/util/view/ViewGroupExtensions.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/widget/OutlineSpan.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/widget/TriState.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/widget/listener/SimpleTabSelectedListener.kt delete mode 100644 app/src/main/res/layout/reader_color_filter_settings.xml delete mode 100644 app/src/main/res/values-v28/arrays.xml rename presentation-core/src/main/java/tachiyomi/presentation/core/components/{SettingsItemsPaddings.kt => SettingsItems.kt} (80%) diff --git a/app/src/main/java/eu/kanade/domain/track/anime/service/DelayedAnimeTrackingUpdateJob.kt b/app/src/main/java/eu/kanade/domain/track/anime/service/DelayedAnimeTrackingUpdateJob.kt index 3343643dc..82d31dd60 100644 --- a/app/src/main/java/eu/kanade/domain/track/anime/service/DelayedAnimeTrackingUpdateJob.kt +++ b/app/src/main/java/eu/kanade/domain/track/anime/service/DelayedAnimeTrackingUpdateJob.kt @@ -19,19 +19,24 @@ import tachiyomi.domain.track.anime.interactor.GetAnimeTracks import tachiyomi.domain.track.anime.interactor.InsertAnimeTrack 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 DelayedAnimeTrackingUpdateJob(context: Context, workerParams: WorkerParameters) : CoroutineWorker(context, workerParams) { override suspend fun doWork(): Result { + if (runAttemptCount > 3) { + return Result.failure() + } + val getTracks = Injekt.get() val insertTrack = Injekt.get() val trackManager = Injekt.get() val delayedTrackingStore = Injekt.get() - val results = withIOContext { + withIOContext { delayedTrackingStore.getAnimeItems() .mapNotNull { val track = getTracks.awaitOne(it.trackId) @@ -40,7 +45,7 @@ class DelayedAnimeTrackingUpdateJob(context: Context, workerParams: WorkerParame } track?.copy(lastEpisodeSeen = it.lastEpisodeSeen.toDouble()) } - .mapNotNull { animeTrack -> + .forEach { animeTrack -> try { val service = trackManager.getService(animeTrack.syncId) if (service != null && service.isLogged) { @@ -57,7 +62,7 @@ class DelayedAnimeTrackingUpdateJob(context: Context, workerParams: WorkerParame } } - return if (results.isNotEmpty()) Result.failure() else Result.success() + return if (delayedTrackingStore.getAnimeItems().isEmpty()) Result.success() else Result.retry() } companion object { @@ -70,7 +75,7 @@ class DelayedAnimeTrackingUpdateJob(context: Context, workerParams: WorkerParame val request = OneTimeWorkRequestBuilder() .setConstraints(constraints) - .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 20, TimeUnit.SECONDS) + .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 5.minutes.toJavaDuration()) .addTag(TAG) .build() diff --git a/app/src/main/java/eu/kanade/domain/track/manga/service/DelayedMangaTrackingUpdateJob.kt b/app/src/main/java/eu/kanade/domain/track/manga/service/DelayedMangaTrackingUpdateJob.kt index 32fdcb8ac..e60801fa6 100644 --- a/app/src/main/java/eu/kanade/domain/track/manga/service/DelayedMangaTrackingUpdateJob.kt +++ b/app/src/main/java/eu/kanade/domain/track/manga/service/DelayedMangaTrackingUpdateJob.kt @@ -19,19 +19,24 @@ import tachiyomi.domain.track.manga.interactor.GetMangaTracks import tachiyomi.domain.track.manga.interactor.InsertMangaTrack 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 DelayedMangaTrackingUpdateJob(context: Context, workerParams: WorkerParameters) : CoroutineWorker(context, workerParams) { override suspend fun doWork(): Result { + if (runAttemptCount > 3) { + return Result.failure() + } + val getTracks = Injekt.get() val insertTrack = Injekt.get() val trackManager = Injekt.get() val delayedTrackingStore = Injekt.get() - val results = withIOContext { + withIOContext { delayedTrackingStore.getMangaItems() .mapNotNull { val track = getTracks.awaitOne(it.trackId) @@ -40,7 +45,7 @@ class DelayedMangaTrackingUpdateJob(context: Context, workerParams: WorkerParame } track?.copy(lastChapterRead = it.lastChapterRead.toDouble()) } - .mapNotNull { track -> + .forEach { track -> try { val service = trackManager.getService(track.syncId) if (service != null && service.isLogged) { @@ -57,7 +62,7 @@ class DelayedMangaTrackingUpdateJob(context: Context, workerParams: WorkerParame } } - return if (results.isNotEmpty()) Result.failure() else Result.success() + return if (delayedTrackingStore.getMangaItems().isEmpty()) Result.success() else Result.retry() } companion object { @@ -70,7 +75,7 @@ class DelayedMangaTrackingUpdateJob(context: Context, workerParams: WorkerParame val request = OneTimeWorkRequestBuilder() .setConstraints(constraints) - .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 20, TimeUnit.SECONDS) + .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 5.minutes.toJavaDuration()) .addTag(TAG) .build() diff --git a/app/src/main/java/eu/kanade/presentation/entries/anime/AnimeScreen.kt b/app/src/main/java/eu/kanade/presentation/entries/anime/AnimeScreen.kt index e98f7aa5f..979ca7417 100644 --- a/app/src/main/java/eu/kanade/presentation/entries/anime/AnimeScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/entries/anime/AnimeScreen.kt @@ -292,7 +292,7 @@ private fun AnimeScreenSmallImpl( ) { val episodeListState = rememberLazyListState() - val episodes = remember(state) { state.processedEpisodes.toList() } + val episodes = remember(state) { state.processedEpisodes } val internalOnBackPressed = { if (episodes.fastAny { it.selected }) { @@ -358,7 +358,7 @@ private fun AnimeScreenSmallImpl( ) { ExtendedFloatingActionButton( text = { - val id = if (episodes.fastAny { it.episode.seen }) { + val id = if (state.episodes.fastAny { it.episode.seen }) { R.string.action_resume } else { R.string.action_start @@ -559,7 +559,7 @@ fun AnimeScreenLargeImpl( val layoutDirection = LocalLayoutDirection.current val density = LocalDensity.current - val episodes = remember(state) { state.processedEpisodes.toList() } + val episodes = remember(state) { state.processedEpisodes } val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues() var topBarHeight by remember { mutableIntStateOf(0) } @@ -635,7 +635,7 @@ fun AnimeScreenLargeImpl( ) { ExtendedFloatingActionButton( text = { - val id = if (episodes.fastAny { it.episode.seen }) { + val id = if (state.episodes.fastAny { it.episode.seen }) { R.string.action_resume } else { R.string.action_start diff --git a/app/src/main/java/eu/kanade/presentation/entries/anime/components/AnimeEpisodeListItem.kt b/app/src/main/java/eu/kanade/presentation/entries/anime/components/AnimeEpisodeListItem.kt index 1e0a33fd5..22bf24705 100644 --- a/app/src/main/java/eu/kanade/presentation/entries/anime/components/AnimeEpisodeListItem.kt +++ b/app/src/main/java/eu/kanade/presentation/entries/anime/components/AnimeEpisodeListItem.kt @@ -18,13 +18,14 @@ import androidx.compose.material.DismissValue import androidx.compose.material.SwipeToDismiss import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Bookmark -import androidx.compose.material.icons.filled.BookmarkRemove import androidx.compose.material.icons.filled.Circle -import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.icons.filled.Download -import androidx.compose.material.icons.filled.FileDownloadOff -import androidx.compose.material.icons.filled.Visibility -import androidx.compose.material.icons.filled.VisibilityOff +import androidx.compose.material.icons.outlined.BookmarkAdd +import androidx.compose.material.icons.outlined.BookmarkRemove +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.Done +import androidx.compose.material.icons.outlined.Download +import androidx.compose.material.icons.outlined.FileDownloadOff +import androidx.compose.material.icons.outlined.RemoveDone import androidx.compose.material.rememberDismissState import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor @@ -296,32 +297,30 @@ private fun SwipeBackgroundIcon( val imageVector = when (swipeAction) { LibraryPreferences.EpisodeSwipeAction.ToggleSeen -> { if (!seen) { - Icons.Default.Visibility + Icons.Outlined.Done } else { - Icons.Default.VisibilityOff + Icons.Outlined.RemoveDone } } LibraryPreferences.EpisodeSwipeAction.ToggleBookmark -> { if (!bookmark) { - Icons.Default.Bookmark + Icons.Outlined.BookmarkAdd } else { - Icons.Default.BookmarkRemove + Icons.Outlined.BookmarkRemove } } LibraryPreferences.EpisodeSwipeAction.Download -> { when (downloadState) { AnimeDownload.State.NOT_DOWNLOADED, AnimeDownload.State.ERROR, - -> { Icons.Default.Download } + -> { Icons.Outlined.Download } AnimeDownload.State.QUEUE, AnimeDownload.State.DOWNLOADING, - -> { Icons.Default.FileDownloadOff } - AnimeDownload.State.DOWNLOADED -> { Icons.Default.Delete } + -> { Icons.Outlined.FileDownloadOff } + AnimeDownload.State.DOWNLOADED -> { Icons.Outlined.Delete } } } - LibraryPreferences.EpisodeSwipeAction.Disabled -> { - null - } + LibraryPreferences.EpisodeSwipeAction.Disabled -> null } imageVector?.let { Icon( diff --git a/app/src/main/java/eu/kanade/presentation/entries/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/presentation/entries/manga/MangaScreen.kt index 3a155fb3c..b7d44b0a3 100644 --- a/app/src/main/java/eu/kanade/presentation/entries/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/entries/manga/MangaScreen.kt @@ -272,7 +272,7 @@ private fun MangaScreenSmallImpl( ) { val chapterListState = rememberLazyListState() - val chapters = remember(state) { state.processedChapters.toList() } + val chapters = remember(state) { state.processedChapters } val internalOnBackPressed = { if (chapters.fastAny { it.selected }) { @@ -337,7 +337,7 @@ private fun MangaScreenSmallImpl( ) { ExtendedFloatingActionButton( text = { - val id = if (chapters.fastAny { it.chapter.read }) { + val id = if (state.chapters.fastAny { it.chapter.read }) { R.string.action_resume } else { R.string.action_start @@ -504,7 +504,7 @@ fun MangaScreenLargeImpl( val layoutDirection = LocalLayoutDirection.current val density = LocalDensity.current - val chapters = remember(state) { state.processedChapters.toList() } + val chapters = remember(state) { state.processedChapters } val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues() var topBarHeight by remember { mutableIntStateOf(0) } @@ -577,7 +577,7 @@ fun MangaScreenLargeImpl( ) { ExtendedFloatingActionButton( text = { - val id = if (chapters.fastAny { it.chapter.read }) { + val id = if (state.chapters.fastAny { it.chapter.read }) { R.string.action_resume } else { R.string.action_start diff --git a/app/src/main/java/eu/kanade/presentation/entries/manga/components/MangaChapterListItem.kt b/app/src/main/java/eu/kanade/presentation/entries/manga/components/MangaChapterListItem.kt index d93c495c1..00c62ad9c 100644 --- a/app/src/main/java/eu/kanade/presentation/entries/manga/components/MangaChapterListItem.kt +++ b/app/src/main/java/eu/kanade/presentation/entries/manga/components/MangaChapterListItem.kt @@ -17,13 +17,14 @@ import androidx.compose.material.DismissValue import androidx.compose.material.SwipeToDismiss import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Bookmark -import androidx.compose.material.icons.filled.BookmarkRemove import androidx.compose.material.icons.filled.Circle -import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.icons.filled.Download -import androidx.compose.material.icons.filled.FileDownloadOff -import androidx.compose.material.icons.filled.Visibility -import androidx.compose.material.icons.filled.VisibilityOff +import androidx.compose.material.icons.outlined.BookmarkAdd +import androidx.compose.material.icons.outlined.BookmarkRemove +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.Done +import androidx.compose.material.icons.outlined.Download +import androidx.compose.material.icons.outlined.FileDownloadOff +import androidx.compose.material.icons.outlined.RemoveDone import androidx.compose.material.rememberDismissState import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor @@ -302,32 +303,30 @@ private fun SwipeBackgroundIcon( val imageVector = when (swipeAction) { LibraryPreferences.ChapterSwipeAction.ToggleRead -> { if (!read) { - Icons.Default.Visibility + Icons.Outlined.Done } else { - Icons.Default.VisibilityOff + Icons.Outlined.RemoveDone } } LibraryPreferences.ChapterSwipeAction.ToggleBookmark -> { if (!bookmark) { - Icons.Default.Bookmark + Icons.Outlined.BookmarkAdd } else { - Icons.Default.BookmarkRemove + Icons.Outlined.BookmarkRemove } } LibraryPreferences.ChapterSwipeAction.Download -> { when (downloadState) { MangaDownload.State.NOT_DOWNLOADED, MangaDownload.State.ERROR, - -> { Icons.Default.Download } + -> { Icons.Outlined.Download } MangaDownload.State.QUEUE, MangaDownload.State.DOWNLOADING, - -> { Icons.Default.FileDownloadOff } - MangaDownload.State.DOWNLOADED -> { Icons.Default.Delete } + -> { Icons.Outlined.FileDownloadOff } + MangaDownload.State.DOWNLOADED -> { Icons.Outlined.Delete } } } - LibraryPreferences.ChapterSwipeAction.Disabled -> { - null - } + LibraryPreferences.ChapterSwipeAction.Disabled -> null } imageVector?.let { Icon( 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 eb3676d63..731727b3d 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 @@ -1,27 +1,19 @@ package eu.kanade.presentation.library.anime import android.content.res.Configuration -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Slider -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp import eu.kanade.presentation.components.TabbedDialog import eu.kanade.presentation.components.TabbedDialogPaddings import eu.kanade.presentation.components.TriStateItem @@ -37,7 +29,7 @@ import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.presentation.core.components.CheckboxItem import tachiyomi.presentation.core.components.HeadingItem import tachiyomi.presentation.core.components.RadioItem -import tachiyomi.presentation.core.components.SettingsItemsPaddings +import tachiyomi.presentation.core.components.SliderItem import tachiyomi.presentation.core.components.SortItem @Composable @@ -198,48 +190,27 @@ private fun ColumnScope.DisplayPage( } if (displayMode != LibraryDisplayMode.List) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding( - horizontal = SettingsItemsPaddings.Horizontal, - vertical = SettingsItemsPaddings.Vertical, - ), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(24.dp), - ) { - val configuration = LocalConfiguration.current - val columnPreference = remember { - if (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { - screenModel.libraryPreferences.animeLandscapeColumns() - } else { - screenModel.libraryPreferences.animePortraitColumns() - } + val configuration = LocalConfiguration.current + val columnPreference = remember { + if (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { + screenModel.libraryPreferences.animeLandscapeColumns() + } else { + screenModel.libraryPreferences.animePortraitColumns() } - - val columns by columnPreference.collectAsState() - Column(modifier = Modifier.weight(0.5f)) { - Text( - stringResource(id = R.string.pref_library_columns), - style = MaterialTheme.typography.bodyMedium, - ) - Text( - if (columns > 0) { - stringResource(id = R.string.pref_library_columns_per_row, columns) - } else { - stringResource(id = R.string.label_default) - }, - ) - } - - Slider( - value = columns.toFloat(), - onValueChange = { columnPreference.set(it.toInt()) }, - modifier = Modifier.weight(1.5f), - valueRange = 0f..10f, - steps = 10, - ) } + + val columns by columnPreference.collectAsState() + SliderItem( + label = stringResource(R.string.pref_library_columns), + max = 10, + value = columns, + valueText = if (columns > 0) { + stringResource(R.string.pref_library_columns_per_row, columns) + } else { + stringResource(R.string.label_default) + }, + onChange = { columnPreference.set(it) }, + ) } HeadingItem(R.string.overlay_header) 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 94b29f1d6..3a436767d 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 @@ -1,27 +1,19 @@ package eu.kanade.presentation.library.manga import android.content.res.Configuration -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Slider -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp import eu.kanade.presentation.components.TabbedDialog import eu.kanade.presentation.components.TabbedDialogPaddings import eu.kanade.presentation.components.TriStateItem @@ -37,7 +29,7 @@ import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.presentation.core.components.CheckboxItem import tachiyomi.presentation.core.components.HeadingItem import tachiyomi.presentation.core.components.RadioItem -import tachiyomi.presentation.core.components.SettingsItemsPaddings +import tachiyomi.presentation.core.components.SliderItem import tachiyomi.presentation.core.components.SortItem @Composable @@ -197,48 +189,27 @@ private fun ColumnScope.DisplayPage( } if (displayMode != LibraryDisplayMode.List) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding( - horizontal = SettingsItemsPaddings.Horizontal, - vertical = SettingsItemsPaddings.Vertical, - ), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(24.dp), - ) { - val configuration = LocalConfiguration.current - val columnPreference = remember { - if (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { - screenModel.libraryPreferences.mangaLandscapeColumns() - } else { - screenModel.libraryPreferences.mangaPortraitColumns() - } + val configuration = LocalConfiguration.current + val columnPreference = remember { + if (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { + screenModel.libraryPreferences.mangaLandscapeColumns() + } else { + screenModel.libraryPreferences.mangaPortraitColumns() } - - val columns by columnPreference.collectAsState() - Column(modifier = Modifier.weight(0.5f)) { - Text( - stringResource(id = R.string.pref_library_columns), - style = MaterialTheme.typography.bodyMedium, - ) - Text( - if (columns > 0) { - stringResource(id = R.string.pref_library_columns_per_row, columns) - } else { - stringResource(id = R.string.label_default) - }, - ) - } - - Slider( - value = columns.toFloat(), - onValueChange = { columnPreference.set(it.toInt()) }, - modifier = Modifier.weight(1.5f), - valueRange = 0f..10f, - steps = 10, - ) } + + val columns by columnPreference.collectAsState() + SliderItem( + label = stringResource(R.string.pref_library_columns), + max = 10, + value = columns, + valueText = if (columns > 0) { + stringResource(R.string.pref_library_columns_per_row, columns) + } else { + stringResource(R.string.label_default) + }, + onChange = { columnPreference.set(it) }, + ) } HeadingItem(R.string.overlay_header) diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/PreferenceItem.kt b/app/src/main/java/eu/kanade/presentation/more/settings/PreferenceItem.kt index f2d6a06f9..0c7ba6b93 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/PreferenceItem.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/PreferenceItem.kt @@ -11,6 +11,7 @@ import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.structuralEqualityPolicy +import androidx.compose.ui.unit.dp import eu.kanade.domain.track.service.TrackPreferences import eu.kanade.domain.ui.UiPreferences import eu.kanade.presentation.more.settings.widget.AppThemePreferenceWidget @@ -24,10 +25,12 @@ import eu.kanade.presentation.more.settings.widget.TrackingPreferenceWidget import eu.kanade.presentation.util.collectAsState import kotlinx.coroutines.launch import tachiyomi.core.preference.PreferenceStore +import tachiyomi.presentation.core.components.SliderItem import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get val LocalPreferenceHighlighted = compositionLocalOf(structuralEqualityPolicy()) { false } +val LocalPreferenceMinHeight = compositionLocalOf(structuralEqualityPolicy()) { 56.dp } @Composable fun StatusWrapper( @@ -77,6 +80,21 @@ internal fun PreferenceItem( }, ) } + is Preference.PreferenceItem.SliderPreference -> { + // TODO: use different composable? + SliderItem( + label = item.title, + min = item.min, + max = item.max, + value = item.value, + valueText = item.value.toString(), + onChange = { + scope.launch { + item.onValueChanged(it) + } + }, + ) + } is Preference.PreferenceItem.ListPreference<*> -> { val value by item.pref.collectAsState() ListPreferenceWidget( diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/PreferenceModel.kt b/app/src/main/java/eu/kanade/presentation/more/settings/PreferenceModel.kt index 7e192f7e3..f72d19774 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/PreferenceModel.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/PreferenceModel.kt @@ -43,6 +43,20 @@ sealed class Preference { override val onValueChanged: suspend (newValue: Boolean) -> Boolean = { true }, ) : PreferenceItem() + /** + * A [PreferenceItem] that provides a slider to select an integer number. + */ + data class SliderPreference( + val value: Int, + val min: Int = 0, + val max: Int, + override val title: String = "", + override val subtitle: String? = null, + override val icon: ImageVector? = null, + override val enabled: Boolean = true, + override val onValueChanged: suspend (newValue: Int) -> Boolean = { true }, + ) : PreferenceItem() + /** * A [PreferenceItem] that displays a list of entries as a dialog. */ diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/AboutScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/AboutScreen.kt index 90e8c9c8a..2102c2936 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/AboutScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/AboutScreen.kt @@ -1,14 +1,21 @@ package eu.kanade.presentation.more.settings.screen import android.content.Context +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Public +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler @@ -60,6 +67,7 @@ object AboutScreen : Screen() { val uriHandler = LocalUriHandler.current val handleBack = LocalBackPress.current val navigator = LocalNavigator.currentOrThrow + var isCheckingUpdates by remember { mutableStateOf(false) } Scaffold( topBar = { scrollBehavior -> @@ -92,22 +100,41 @@ object AboutScreen : Screen() { item { TextPreferenceWidget( title = stringResource(R.string.check_for_updates), + widget = { + AnimatedVisibility(visible = isCheckingUpdates) { + CircularProgressIndicator( + modifier = Modifier.size(28.dp), + strokeWidth = 3.dp, + ) + } + }, onPreferenceClick = { - scope.launch { - checkVersion(context) { result -> - val updateScreen = NewUpdateScreen( - versionName = result.release.version, - changelogInfo = result.release.info, - releaseLink = result.release.releaseLink, - downloadLink = result.release.getDownloadLink(), + if (!isCheckingUpdates) { + scope.launch { + isCheckingUpdates = true + + checkVersion( + context = context, + onAvailableUpdate = { result -> + val updateScreen = NewUpdateScreen( + versionName = result.release.version, + changelogInfo = result.release.info, + releaseLink = result.release.releaseLink, + downloadLink = result.release.getDownloadLink(), + ) + navigator.push(updateScreen) + }, + onFinish = { + isCheckingUpdates = false + }, ) - navigator.push(updateScreen) } } }, ) } } + if (!BuildConfig.DEBUG) { item { TextPreferenceWidget( @@ -127,7 +154,7 @@ object AboutScreen : Screen() { item { TextPreferenceWidget( title = stringResource(R.string.licenses), - onPreferenceClick = { navigator.push(LicensesScreen()) }, + onPreferenceClick = { navigator.push(OpenSourceLicensesScreen()) }, ) } @@ -174,10 +201,13 @@ object AboutScreen : Screen() { /** * Checks version and shows a user prompt if an update is available. */ - private suspend fun checkVersion(context: Context, onAvailableUpdate: (GetApplicationRelease.Result.NewUpdate) -> Unit) { + private suspend fun checkVersion( + context: Context, + onAvailableUpdate: (GetApplicationRelease.Result.NewUpdate) -> Unit, + onFinish: () -> Unit, + ) { val updateChecker = AppUpdateChecker() withUIContext { - context.toast(R.string.update_check_look_for_updates) try { when (val result = withIOContext { updateChecker.checkForUpdate(context, forceCheck = true) }) { is GetApplicationRelease.Result.NewUpdate -> { @@ -191,6 +221,8 @@ object AboutScreen : Screen() { } catch (e: Exception) { context.toast(e.message) logcat(LogPriority.ERROR, e) + } finally { + onFinish() } } } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/OpenSourceLibraryLicenseScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/OpenSourceLibraryLicenseScreen.kt new file mode 100644 index 000000000..2a35fb017 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/OpenSourceLibraryLicenseScreen.kt @@ -0,0 +1,94 @@ +package eu.kanade.presentation.more.settings.screen + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.Public +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.text.HtmlCompat +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow +import com.google.android.material.textview.MaterialTextView +import eu.kanade.presentation.components.AppBar +import eu.kanade.presentation.components.AppBarActions +import eu.kanade.presentation.util.Screen +import eu.kanade.tachiyomi.R +import tachiyomi.presentation.core.components.material.Scaffold + +class OpenSourceLibraryLicenseScreen( + private val name: String, + private val website: String?, + private val license: String, +) : Screen() { + + @Composable + override fun Content() { + val navigator = LocalNavigator.currentOrThrow + val uriHandler = LocalUriHandler.current + + Scaffold( + topBar = { + TopAppBar( + title = { + Text( + text = name, + maxLines = 1, + ) + }, + navigationIcon = { + IconButton(onClick = navigator::pop) { + Icon(imageVector = Icons.Default.ArrowBack, contentDescription = null) + } + }, + actions = { + if (!website.isNullOrEmpty()) { + AppBarActions( + listOf( + AppBar.Action( + title = stringResource(R.string.website), + icon = Icons.Default.Public, + onClick = { uriHandler.openUri(website) }, + ), + ), + ) + } + }, + scrollBehavior = it, + ) + }, + ) { contentPadding -> + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .padding(contentPadding) + .padding(16.dp), + ) { + HtmlLicenseText(html = license) + } + } + } + + @Composable + private fun HtmlLicenseText(html: String) { + AndroidView( + factory = { + MaterialTextView(it) + }, + update = { + it.text = HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_COMPACT) + }, + ) + } +} diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/LicensesScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/OpenSourceLicensesScreen.kt similarity index 76% rename from app/src/main/java/eu/kanade/presentation/more/settings/screen/LicensesScreen.kt rename to app/src/main/java/eu/kanade/presentation/more/settings/screen/OpenSourceLicensesScreen.kt index dc95e2fbb..96ccb4de1 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/LicensesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/OpenSourceLicensesScreen.kt @@ -9,12 +9,13 @@ import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import com.mikepenz.aboutlibraries.ui.compose.LibrariesContainer import com.mikepenz.aboutlibraries.ui.compose.LibraryDefaults +import com.mikepenz.aboutlibraries.ui.compose.util.htmlReadyLicenseContent import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.util.Screen import eu.kanade.tachiyomi.R import tachiyomi.presentation.core.components.material.Scaffold -class LicensesScreen : Screen() { +class OpenSourceLicensesScreen : Screen() { @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow @@ -37,6 +38,14 @@ class LicensesScreen : Screen() { badgeBackgroundColor = MaterialTheme.colorScheme.primary, badgeContentColor = MaterialTheme.colorScheme.onPrimary, ), + onLibraryClick = { + val libraryLicenseScreen = OpenSourceLibraryLicenseScreen( + name = it.name, + website = it.website, + license = it.licenses.firstOrNull()?.htmlReadyLicenseContent.orEmpty(), + ) + navigator.push(libraryLicenseScreen) + }, ) } } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/widget/BasePreferenceWidget.kt b/app/src/main/java/eu/kanade/presentation/more/settings/widget/BasePreferenceWidget.kt index aeb4f8570..4af36dc03 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/widget/BasePreferenceWidget.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/widget/BasePreferenceWidget.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import eu.kanade.presentation.more.settings.LocalPreferenceHighlighted +import eu.kanade.presentation.more.settings.LocalPreferenceMinHeight import kotlinx.coroutines.delay import kotlin.time.Duration.Companion.seconds @@ -44,10 +45,11 @@ internal fun BasePreferenceWidget( widget: @Composable (() -> Unit)? = null, ) { val highlighted = LocalPreferenceHighlighted.current + val minHeight = LocalPreferenceMinHeight.current Row( modifier = modifier .highlightBackground(highlighted) - .sizeIn(minHeight = 56.dp) + .sizeIn(minHeight = minHeight) .clickable(enabled = onClick != null, onClick = { onClick?.invoke() }) .fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, diff --git a/app/src/main/java/eu/kanade/tachiyomi/App.kt b/app/src/main/java/eu/kanade/tachiyomi/App.kt index 7bdacee67..2d77989f0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/App.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/App.kt @@ -29,8 +29,6 @@ import eu.kanade.domain.ui.UiPreferences import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode import eu.kanade.tachiyomi.crash.CrashActivity import eu.kanade.tachiyomi.crash.GlobalExceptionHandler -import eu.kanade.tachiyomi.data.cache.ChapterCache -import eu.kanade.tachiyomi.data.cache.EpisodeCache import eu.kanade.tachiyomi.data.coil.AnimeCoverFetcher import eu.kanade.tachiyomi.data.coil.AnimeCoverKeyer import eu.kanade.tachiyomi.data.coil.AnimeKeyer @@ -60,7 +58,6 @@ import org.acra.ktx.initAcra import org.acra.sender.HttpSender import org.conscrypt.Conscrypt import tachiyomi.core.util.system.logcat -import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.presentation.widget.entries.anime.TachiyomiAnimeWidgetManager import tachiyomi.presentation.widget.entries.manga.TachiyomiMangaWidgetManager import uy.kohesive.injekt.Injekt @@ -71,12 +68,9 @@ import java.security.Security class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory { private val basePreferences: BasePreferences by injectLazy() - private val libraryPreferences: LibraryPreferences by injectLazy() private val networkPreferences: NetworkPreferences by injectLazy() private val disableIncognitoReceiver = DisableIncognitoReceiver() - private val chapterCache: ChapterCache by injectLazy() - private val episodeCache: EpisodeCache by injectLazy() @SuppressLint("LaunchActivityFromNotification") override fun onCreate() { @@ -192,11 +186,6 @@ class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory { override fun onStop(owner: LifecycleOwner) { SecureActivityDelegate.onApplicationStopped() - - if (libraryPreferences.autoClearItemCache().get()) { - chapterCache.clear() - episodeCache.clear() - } } override fun getPackageName(): String { diff --git a/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt b/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt index fcebf23f0..6a517d60d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt @@ -45,7 +45,6 @@ import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory import kotlinx.serialization.json.Json import nl.adaptivity.xmlutil.XmlDeclMode import nl.adaptivity.xmlutil.core.XmlVersion -import nl.adaptivity.xmlutil.serialization.UnknownChildHandler import nl.adaptivity.xmlutil.serialization.XML import tachiyomi.core.preference.PreferenceStore import tachiyomi.core.provider.AndroidBackupFolderProvider @@ -167,10 +166,12 @@ class AppModule(val app: Application) : InjektModule { } addSingletonFactory { XML { - unknownChildHandler = UnknownChildHandler { _, _, _, _, _ -> emptyList() } + defaultPolicy { + ignoreUnknownChildren() + } autoPolymorphic = true xmlDeclMode = XmlDeclMode.Charset - indent = 4 + indent = 2 xmlVersion = XmlVersion.XML10 } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/AnimeDownloadManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/AnimeDownloadManager.kt index 384a89f98..3b9b42c1c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/AnimeDownloadManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/AnimeDownloadManager.kt @@ -273,13 +273,13 @@ class AnimeDownloadManager( } removeFromDownloadQueue(filteredEpisodes) - val (animeDir, episodeDirs) = provider.findEpisodeDirs( - filteredEpisodes, - anime, - source, - ) - episodeDirs.forEach { it.delete() } - cache.removeEpisodes(filteredEpisodes, anime) + val (animeDir, episodeDirs) = provider.findEpisodeDirs( + filteredEpisodes, + anime, + source, + ) + episodeDirs.forEach { it.delete() } + cache.removeEpisodes(filteredEpisodes, anime) // Delete anime directory if empty if (animeDir?.listFiles()?.isEmpty() == true) { 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 a5effd932..8ed428605 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 @@ -111,7 +111,7 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa val preferences = Injekt.get() val restrictions = preferences.libraryUpdateDeviceRestriction().get() if ((DEVICE_ONLY_ON_WIFI in restrictions) && !context.isConnectedToWifi()) { - return Result.failure() + return Result.retry() } // Find a running manual worker. If exists, try again later diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/anime/util/AnimeExtensionLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/anime/util/AnimeExtensionLoader.kt index 8bafd9cfb..5a287a049 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/anime/util/AnimeExtensionLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/anime/util/AnimeExtensionLoader.kt @@ -169,7 +169,7 @@ internal object AnimeExtensionLoader { } .flatMap { try { - when (val obj = Class.forName(it, false, classLoader).newInstance()) { + when (val obj = Class.forName(it, false, classLoader).getDeclaredConstructor().newInstance()) { is AnimeSource -> listOf(obj) is AnimeSourceFactory -> obj.createSources() else -> throw Exception("Unknown source class type! ${obj.javaClass}") diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/manga/util/MangaExtensionLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/manga/util/MangaExtensionLoader.kt index e5f1e5a0d..bf02240de 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/manga/util/MangaExtensionLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/manga/util/MangaExtensionLoader.kt @@ -169,7 +169,7 @@ internal object MangaExtensionLoader { } .flatMap { try { - when (val obj = Class.forName(it, false, classLoader).newInstance()) { + when (val obj = Class.forName(it, false, classLoader).getDeclaredConstructor().newInstance()) { is MangaSource -> listOf(obj) is SourceFactory -> obj.createSources() else -> throw Exception("Unknown source class type! ${obj.javaClass}") diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/extension/details/SourcePreferencesScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/extension/details/SourcePreferencesScreen.kt index 7168f5222..ed740f68d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/extension/details/SourcePreferencesScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/extension/details/SourcePreferencesScreen.kt @@ -152,6 +152,7 @@ class SourcePreferencesFragment : PreferenceFragmentCompat() { source.setupPreferenceScreen(sourceScreen) sourceScreen.forEach { pref -> pref.isIconSpaceReserved = false + pref.isSingleLineTitle = false if (pref is DialogPreference) { pref.dialogTitle = pref.title } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/migration/AnimeMigrationFlags.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/migration/AnimeMigrationFlags.kt index 8169fb695..26c179574 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/migration/AnimeMigrationFlags.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/migration/AnimeMigrationFlags.kt @@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.browse.anime.migration import eu.kanade.domain.entries.anime.model.hasCustomCover import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.cache.AnimeCoverCache +import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadCache import kotlinx.coroutines.runBlocking import tachiyomi.domain.entries.anime.model.Anime import tachiyomi.domain.track.anime.interactor.GetAnimeTracks @@ -12,15 +13,18 @@ import uy.kohesive.injekt.injectLazy object AnimeMigrationFlags { - private const val EPISODES = 0b0001 - private const val CATEGORIES = 0b0010 - private const val TRACK = 0b0100 - private const val CUSTOM_COVER = 0b1000 + private const val EPISODES = 0b00001 + private const val CATEGORIES = 0b00010 + private const val TRACK = 0b00100 + private const val CUSTOM_COVER = 0b01000 + private const val DELETE_DOWNLOADED = 0b10000 private val coverCache: AnimeCoverCache by injectLazy() private val getTracks: GetAnimeTracks = Injekt.get() + private val downloadCache: AnimeDownloadCache by injectLazy() - val flags get() = arrayOf(EPISODES, CATEGORIES, TRACK, CUSTOM_COVER) + val flags get() = arrayOf(EPISODES, CATEGORIES, TRACK, CUSTOM_COVER, DELETE_DOWNLOADED) + private var enableFlags = emptyList().toMutableList() fun hasEpisodes(value: Int): Boolean { return value and EPISODES != 0 @@ -38,23 +42,37 @@ object AnimeMigrationFlags { return value and CUSTOM_COVER != 0 } + fun hasDeleteDownloaded(value: Int): Boolean { + return value and DELETE_DOWNLOADED != 0 + } + fun getEnabledFlagsPositions(value: Int): List { return flags.mapIndexedNotNull { index, flag -> if (value and flag != 0) index else null } } fun getFlagsFromPositions(positions: Array): Int { - return positions.fold(0) { accumulated, position -> accumulated or (1 shl position) } + val fold = positions.fold(0) { accumulated, position -> accumulated or enableFlags[position] } + enableFlags.clear() + return fold } fun titles(anime: Anime?): Array { + enableFlags.add(EPISODES) + enableFlags.add(CATEGORIES) val titles = arrayOf(R.string.episodes, R.string.anime_categories).toMutableList() if (anime != null) { if (runBlocking { getTracks.await(anime.id) }.isNotEmpty()) { titles.add(R.string.track) + enableFlags.add(TRACK) } if (anime.hasCustomCover(coverCache)) { titles.add(R.string.custom_cover) + enableFlags.add(CUSTOM_COVER) + } + if (downloadCache.getDownloadCount(anime) > 0) { + titles.add(R.string.delete_downloaded) + enableFlags.add(DELETE_DOWNLOADED) } } return titles.toTypedArray() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/migration/search/MigrateAnimeDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/migration/search/MigrateAnimeDialog.kt index 81f24ffa0..f688ced8d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/migration/search/MigrateAnimeDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/migration/search/MigrateAnimeDialog.kt @@ -36,10 +36,10 @@ import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.animesource.AnimeSource import eu.kanade.tachiyomi.animesource.model.SEpisode import eu.kanade.tachiyomi.data.cache.AnimeCoverCache +import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadManager import eu.kanade.tachiyomi.data.track.EnhancedAnimeTrackService import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.ui.browse.anime.migration.AnimeMigrationFlags -import eu.kanade.tachiyomi.ui.browse.manga.migration.MangaMigrationFlags import kotlinx.coroutines.flow.update import tachiyomi.core.preference.Preference import tachiyomi.core.preference.PreferenceStore @@ -145,7 +145,7 @@ internal fun MigrateAnimeDialog( val selectedIndices = mutableListOf() selected.fastForEachIndexed { i, b -> if (b) selectedIndices.add(i) } val newValue = - MangaMigrationFlags.getFlagsFromPositions(selectedIndices.toTypedArray()) + AnimeMigrationFlags.getFlagsFromPositions(selectedIndices.toTypedArray()) screenModel.migrateFlags.set(newValue) screenModel.migrateAnime(oldAnime, newAnime, true) withUIContext { onPopScreen() } @@ -162,6 +162,7 @@ internal fun MigrateAnimeDialog( internal class MigrateAnimeDialogScreenModel( private val sourceManager: AnimeSourceManager = Injekt.get(), + private val downloadManager: AnimeDownloadManager = Injekt.get(), private val updateAnime: UpdateAnime = Injekt.get(), private val getEpisodeByAnimeId: GetEpisodeByAnimeId = Injekt.get(), private val syncEpisodesWithSource: SyncEpisodesWithSource = Injekt.get(), @@ -220,6 +221,7 @@ internal class MigrateAnimeDialogScreenModel( val migrateCategories = AnimeMigrationFlags.hasCategories(flags) val migrateTracks = AnimeMigrationFlags.hasTracks(flags) val migrateCustomCover = AnimeMigrationFlags.hasCustomCover(flags) + val deleteDownloaded = AnimeMigrationFlags.hasDeleteDownloaded(flags) try { syncEpisodesWithSource.await(sourceEpisodes, newAnime, newSource) @@ -284,6 +286,13 @@ internal class MigrateAnimeDialogScreenModel( insertTrack.awaitAll(tracks) } + // Delete downloaded + if (deleteDownloaded) { + if (oldSource != null) { + downloadManager.deleteAnime(oldAnime, oldSource) + } + } + if (replace) { updateAnime.await(AnimeUpdate(oldAnime.id, favorite = false, dateAdded = 0)) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/source/browse/BrowseAnimeSourceScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/source/browse/BrowseAnimeSourceScreenModel.kt index fd623b6b8..37086875b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/source/browse/BrowseAnimeSourceScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/source/browse/BrowseAnimeSourceScreenModel.kt @@ -152,7 +152,7 @@ class BrowseAnimeSourceScreenModel( } fun setListing(listing: Listing) { - mutableState.update { it.copy(listing = listing) } + mutableState.update { it.copy(listing = listing, toolbarQuery = null) } } fun setFilters(filters: AnimeFilterList) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/extension/details/MangaSourcePreferencesScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/extension/details/MangaSourcePreferencesScreen.kt index a39ef2fd6..3660b9961 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/extension/details/MangaSourcePreferencesScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/extension/details/MangaSourcePreferencesScreen.kt @@ -152,6 +152,7 @@ class MangaSourcePreferencesFragment : PreferenceFragmentCompat() { source.setupPreferenceScreen(sourceScreen) sourceScreen.forEach { pref -> pref.isIconSpaceReserved = false + pref.isSingleLineTitle = false if (pref is DialogPreference) { pref.dialogTitle = pref.title } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/migration/MangaMigrationFlags.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/migration/MangaMigrationFlags.kt index d411a35e5..5e2cc2a91 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/migration/MangaMigrationFlags.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/migration/MangaMigrationFlags.kt @@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.browse.manga.migration import eu.kanade.domain.entries.manga.model.hasCustomCover import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.cache.MangaCoverCache +import eu.kanade.tachiyomi.data.download.manga.MangaDownloadCache import kotlinx.coroutines.runBlocking import tachiyomi.domain.entries.manga.model.Manga import tachiyomi.domain.track.manga.interactor.GetMangaTracks @@ -12,15 +13,18 @@ import uy.kohesive.injekt.injectLazy object MangaMigrationFlags { - private const val CHAPTERS = 0b0001 - private const val CATEGORIES = 0b0010 - private const val TRACK = 0b0100 - private const val CUSTOM_COVER = 0b1000 + private const val CHAPTERS = 0b00001 + private const val CATEGORIES = 0b00010 + private const val TRACK = 0b00100 + private const val CUSTOM_COVER = 0b01000 + private const val DELETE_DOWNLOADED = 0b10000 private val coverCache: MangaCoverCache by injectLazy() private val getTracks: GetMangaTracks = Injekt.get() + private val downloadCache: MangaDownloadCache by injectLazy() - val flags get() = arrayOf(CHAPTERS, CATEGORIES, TRACK, CUSTOM_COVER) + val flags get() = arrayOf(CHAPTERS, CATEGORIES, TRACK, CUSTOM_COVER, DELETE_DOWNLOADED) + private var enableFlags = emptyList().toMutableList() fun hasChapters(value: Int): Boolean { return value and CHAPTERS != 0 @@ -38,23 +42,36 @@ object MangaMigrationFlags { return value and CUSTOM_COVER != 0 } + fun hasDeleteDownloaded(value: Int): Boolean { + return value and DELETE_DOWNLOADED != 0 + } + fun getEnabledFlagsPositions(value: Int): List { return flags.mapIndexedNotNull { index, flag -> if (value and flag != 0) index else null } } fun getFlagsFromPositions(positions: Array): Int { - return positions.fold(0) { accumulated, position -> accumulated or (1 shl position) } + val fold = positions.fold(0) { accumulated, position -> accumulated or enableFlags[position] } + enableFlags.clear() + return fold } fun titles(manga: Manga?): Array { + enableFlags.add(CHAPTERS) + enableFlags.add(CATEGORIES) val titles = arrayOf(R.string.chapters, R.string.manga_categories).toMutableList() if (manga != null) { if (runBlocking { getTracks.await(manga.id) }.isNotEmpty()) { titles.add(R.string.track) + enableFlags.add(TRACK) } - if (manga.hasCustomCover(coverCache)) { titles.add(R.string.custom_cover) + enableFlags.add(CUSTOM_COVER) + } + if (downloadCache.getDownloadCount(manga) > 0) { + titles.add(R.string.delete_downloaded) + enableFlags.add(DELETE_DOWNLOADED) } } return titles.toTypedArray() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/migration/search/MigrateMangaDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/migration/search/MigrateMangaDialog.kt index 241971152..89d36ef0e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/migration/search/MigrateMangaDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/migration/search/MigrateMangaDialog.kt @@ -34,6 +34,7 @@ import eu.kanade.domain.entries.manga.model.toSManga import eu.kanade.domain.items.chapter.interactor.SyncChaptersWithSource import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.cache.MangaCoverCache +import eu.kanade.tachiyomi.data.download.manga.MangaDownloadManager import eu.kanade.tachiyomi.data.track.EnhancedMangaTrackService import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.source.MangaSource @@ -161,6 +162,7 @@ internal fun MigrateMangaDialog( internal class MigrateMangaDialogScreenModel( private val sourceManager: MangaSourceManager = Injekt.get(), + private val downloadManager: MangaDownloadManager = Injekt.get(), private val updateManga: UpdateManga = Injekt.get(), private val getChapterByMangaId: GetChapterByMangaId = Injekt.get(), private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(), @@ -219,6 +221,7 @@ internal class MigrateMangaDialogScreenModel( val migrateCategories = MangaMigrationFlags.hasCategories(flags) val migrateTracks = MangaMigrationFlags.hasTracks(flags) val migrateCustomCover = MangaMigrationFlags.hasCustomCover(flags) + val deleteDownloaded = MangaMigrationFlags.hasDeleteDownloaded(flags) try { syncChaptersWithSource.await(sourceChapters, newManga, newSource) @@ -283,6 +286,13 @@ internal class MigrateMangaDialogScreenModel( insertTrack.awaitAll(tracks) } + // Delete downloaded + if (deleteDownloaded) { + if (oldSource != null) { + downloadManager.deleteManga(oldManga, oldSource) + } + } + if (replace) { updateManga.await(MangaUpdate(oldManga.id, favorite = false, dateAdded = 0)) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/source/browse/BrowseMangaSourceScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/source/browse/BrowseMangaSourceScreenModel.kt index 54f9bc45c..79e69d47a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/source/browse/BrowseMangaSourceScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/source/browse/BrowseMangaSourceScreenModel.kt @@ -152,7 +152,7 @@ class BrowseMangaSourceScreenModel( } fun setListing(listing: Listing) { - mutableState.update { it.copy(listing = listing) } + mutableState.update { it.copy(listing = listing, toolbarQuery = null) } } fun setFilters(filters: FilterList) { 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 bac811c0f..8707df72c 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 @@ -123,7 +123,7 @@ class AnimeInfoScreenModel( private val isFavorited: Boolean get() = anime?.favorite ?: false - private val processedEpisodes: Sequence? + private val processedEpisodes: List? get() = successState?.processedEpisodes val episodeSwipeEndAction = libraryPreferences.swipeEpisodeEndAction().get() @@ -1027,8 +1027,9 @@ sealed class AnimeScreenState { val nextAiringEpisode: Pair = Pair(anime.nextEpisodeToAir, anime.nextEpisodeAiringAt), ) : AnimeScreenState() { - val processedEpisodes: Sequence - get() = episodes.applyFilters(anime) + val processedEpisodes by lazy { + episodes.applyFilters(anime).toList() + } val trackingAvailable: Boolean get() = trackItems.isNotEmpty() 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 16e42b9d3..75bf39d86 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 @@ -121,7 +121,7 @@ class MangaInfoScreenModel( private val allChapters: List? get() = successState?.chapters - private val filteredChapters: Sequence? + private val filteredChapters: List? get() = successState?.processedChapters val chapterSwipeEndAction = libraryPreferences.swipeChapterEndAction().get() @@ -589,7 +589,7 @@ class MangaInfoScreenModel( } private fun getUnreadChapters(): List { - val chapterItems = if (skipFiltered) filteredChapters.orEmpty().toList() else allChapters.orEmpty() + val chapterItems = if (skipFiltered) filteredChapters.orEmpty() else allChapters.orEmpty() return chapterItems .filter { (chapter, dlStatus) -> !chapter.read && dlStatus == MangaDownload.State.NOT_DOWNLOADED } .map { it.chapter } @@ -677,7 +677,7 @@ class MangaInfoScreenModel( fun markPreviousChapterRead(pointer: Chapter) { val successState = successState ?: return - val chapters = filteredChapters.orEmpty().map { it.chapter }.toList() + val chapters = filteredChapters.orEmpty().map { it.chapter } val prevChapters = if (successState.manga.sortDescending()) chapters.asReversed() else chapters val pointerPos = prevChapters.indexOf(pointer) if (pointerPos != -1) markChaptersRead(prevChapters.take(pointerPos), true) @@ -1000,8 +1000,9 @@ sealed class MangaScreenState { val hasPromptedToAddBefore: Boolean = false, ) : MangaScreenState() { - val processedChapters: Sequence - get() = chapters.applyFilters(manga) + val processedChapters by lazy { + chapters.applyFilters(manga).toList() + } val trackingAvailable: Boolean get() = trackItems.isNotEmpty() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index ac17a5d09..125b9bbfe 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -70,6 +70,8 @@ import eu.kanade.tachiyomi.Migrations import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.core.Constants +import eu.kanade.tachiyomi.data.cache.ChapterCache +import eu.kanade.tachiyomi.data.cache.EpisodeCache import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadCache import eu.kanade.tachiyomi.data.download.manga.MangaDownloadCache import eu.kanade.tachiyomi.data.notification.NotificationReceiver @@ -121,6 +123,8 @@ class MainActivity : BaseActivity() { private val animeDownloadCache: AnimeDownloadCache by injectLazy() private val downloadCache: MangaDownloadCache by injectLazy() + private val chapterCache: ChapterCache by injectLazy() + private val episodeCache: EpisodeCache by injectLazy() // To be checked by splash screen. If true then splash screen will be removed. var ready = false @@ -128,12 +132,14 @@ class MainActivity : BaseActivity() { private var navigator: Navigator? = null override fun onCreate(savedInstanceState: Bundle?) { + val isLaunch = savedInstanceState == null + // Prevent splash screen showing up on configuration changes - val splashScreen = if (savedInstanceState == null) installSplashScreen() else null + val splashScreen = if (isLaunch) installSplashScreen() else null super.onCreate(savedInstanceState) - val didMigration = if (savedInstanceState == null) { + val didMigration = if (isLaunch) { Migrations.upgrade( context = applicationContext, basePreferences = preferences, @@ -167,7 +173,7 @@ class MainActivity : BaseActivity() { val indexing by downloadCache.isInitializing.collectAsState() val indexingAnime by animeDownloadCache.isInitializing.collectAsState() - // Set statusbar color considering the top app state banner + // Set status bar color considering the top app state banner val systemUiController = rememberSystemUiController() val isSystemInDarkTheme = isSystemInDarkTheme() val statusBarBackgroundColor = when { @@ -208,7 +214,7 @@ class MainActivity : BaseActivity() { LaunchedEffect(navigator) { this@MainActivity.navigator = navigator - if (savedInstanceState == null) { + if (isLaunch) { // Set start screen handleIntentAction(intent, navigator) @@ -287,6 +293,11 @@ class MainActivity : BaseActivity() { } setSplashScreenExitAnimation(splashScreen) + if (isLaunch && libraryPreferences.autoClearItemCache().get()) { + chapterCache.clear() + episodeCache.clear() + } + externalPlayerResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult -> if (result.resultCode == Activity.RESULT_OK) { ExternalIntents.externalIntents.onActivityResult(result.data) @@ -304,7 +315,7 @@ class MainActivity : BaseActivity() { } @Composable - fun HandleOnNewIntent(context: Context, navigator: Navigator) { + private fun HandleOnNewIntent(context: Context, navigator: Navigator) { LaunchedEffect(Unit) { callbackFlow { val componentActivity = context as ComponentActivity 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 d0a1e0fd6..3f512249f 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 @@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.ui.reader import android.annotation.SuppressLint import android.annotation.TargetApi -import android.app.ProgressDialog import android.app.assist.AssistContent import android.content.Context import android.content.Intent @@ -25,8 +24,15 @@ import android.view.animation.Animation import android.view.animation.AnimationUtils import android.widget.Toast import androidx.activity.viewModels +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Text import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.unit.dp import androidx.core.graphics.ColorUtils import androidx.core.net.toUri import androidx.core.transition.doOnEnd @@ -59,6 +65,7 @@ import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters import eu.kanade.tachiyomi.ui.reader.setting.OrientationType +import eu.kanade.tachiyomi.ui.reader.setting.ReaderColorFilterDialog import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsSheet import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType @@ -92,7 +99,9 @@ import tachiyomi.core.util.lang.launchNonCancellable import tachiyomi.core.util.lang.withUIContext import tachiyomi.core.util.system.logcat import tachiyomi.domain.entries.manga.model.Manga -import uy.kohesive.injekt.injectLazy +import tachiyomi.presentation.widget.util.stringResource +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get import kotlin.math.abs class ReaderActivity : BaseActivity() { @@ -107,8 +116,8 @@ class ReaderActivity : BaseActivity() { } } - private val readerPreferences: ReaderPreferences by injectLazy() - private val preferences: BasePreferences by injectLazy() + private val readerPreferences = Injekt.get() + private val preferences = Injekt.get() lateinit var binding: ReaderActivityBinding @@ -117,25 +126,12 @@ class ReaderActivity : BaseActivity() { val hasCutout by lazy { hasDisplayCutout() } - /** - * Whether the menu is currently visible. - */ - var menuVisible = false - private set - /** * Configuration at reader level, like background color or forced orientation. */ private var config: ReaderConfig? = null - /** - * Progress dialog used when switching chapters from the menu buttons. - */ - @Suppress("DEPRECATION") - private var progressDialog: ProgressDialog? = null - private var menuToggleToast: Toast? = null - private var readingModeToast: Toast? = null private val windowInsetsController by lazy { WindowInsetsControllerCompat(window, binding.root) } @@ -158,8 +154,8 @@ class ReaderActivity : BaseActivity() { setContentView(binding.root) if (viewModel.needsInit()) { - val manga = intent.extras!!.getLong("manga", -1) - val chapter = intent.extras!!.getLong("chapter", -1) + val manga = intent.extras?.getLong("manga", -1) ?: -1L + val chapter = intent.extras?.getLong("chapter", -1) ?: -1L if (manga == -1L || chapter == -1L) { finish() return @@ -177,10 +173,6 @@ class ReaderActivity : BaseActivity() { } } - if (savedInstanceState != null) { - menuVisible = savedInstanceState.getBoolean(::menuVisible.name) - } - config = ReaderConfig() initializeMenu() @@ -242,23 +234,6 @@ class ReaderActivity : BaseActivity() { config = null menuToggleToast?.cancel() readingModeToast?.cancel() - progressDialog?.dismiss() - progressDialog = null - } - - /** - * Called when the activity is saving instance state. Current progress is persisted if this - * activity isn't changing configurations. - */ - override fun onSaveInstanceState(outState: Bundle) { - outState.putBoolean(::menuVisible.name, menuVisible) - viewModel.onSaveInstanceState() - super.onSaveInstanceState(outState) - } - - override fun onPause() { - viewModel.saveCurrentChapterReadingProgress() - super.onPause() } /** @@ -268,7 +243,7 @@ class ReaderActivity : BaseActivity() { override fun onResume() { super.onResume() viewModel.setReadStartTime() - setMenuVisibility(menuVisible, animate = false) + setMenuVisibility(viewModel.state.value.menuVisible, animate = false) } /** @@ -278,7 +253,7 @@ class ReaderActivity : BaseActivity() { override fun onWindowFocusChanged(hasFocus: Boolean) { super.onWindowFocusChanged(hasFocus) if (hasFocus) { - setMenuVisibility(menuVisible, animate = false) + setMenuVisibility(viewModel.state.value.menuVisible, animate = false) } } @@ -413,14 +388,41 @@ class ReaderActivity : BaseActivity() { binding.dialogRoot.setComposeContent { val state by viewModel.state.collectAsState() - + val onDismissRequest = viewModel::closeDialog when (state.dialog) { - is ReaderViewModel.Dialog.Page -> ReaderPageDialog( - onDismissRequest = viewModel::closeDialog, - onSetAsCover = viewModel::setAsCover, - onShare = viewModel::shareImage, - onSave = viewModel::saveImage, - ) + is ReaderViewModel.Dialog.Loading -> { + AlertDialog( + onDismissRequest = {}, + confirmButton = {}, + text = { + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + CircularProgressIndicator() + Text(stringResource(R.string.loading)) + } + }, + ) + } + is ReaderViewModel.Dialog.ColorFilter -> { + setMenuVisibility(false) + ReaderColorFilterDialog( + onDismissRequest = { + onDismissRequest() + setMenuVisibility(true) + }, + readerPreferences = viewModel.readerPreferences, + ) + } + is ReaderViewModel.Dialog.PageActions -> { + ReaderPageActionsDialog( + onDismissRequest = onDismissRequest, + onSetAsCover = viewModel::setAsCover, + onShare = viewModel::shareImage, + onSave = viewModel::saveImage, + ) + } null -> {} } } @@ -465,7 +467,7 @@ class ReaderActivity : BaseActivity() { } // Set initial visibility - setMenuVisibility(menuVisible) + setMenuVisibility(viewModel.state.value.menuVisible) } private fun initBottomShortcuts() { @@ -552,11 +554,14 @@ class ReaderActivity : BaseActivity() { if (readerSettingSheet?.isShowing == true) return@setOnClickListener readerSettingSheet = ReaderSettingsSheet(this@ReaderActivity).apply { show() } } + } - setOnLongClickListener { - if (readerSettingSheet?.isShowing == true) return@setOnLongClickListener false - readerSettingSheet = ReaderSettingsSheet(this@ReaderActivity, showColorFilterSettings = true).apply { show() } - true + // Color filter sheet + with(binding.actionColorSettings) { + setTooltip(R.string.custom_filter) + + setOnClickListener { + viewModel.openColorFilterDialog() } } } @@ -588,7 +593,7 @@ class ReaderActivity : BaseActivity() { * [animate] the views. */ fun setMenuVisibility(visible: Boolean, animate: Boolean = true) { - menuVisible = visible + viewModel.showMenus(visible) if (visible) { windowInsetsController.show(WindowInsetsCompat.Type.systemBars()) binding.readerMenu.isVisible = true @@ -747,13 +752,11 @@ class ReaderActivity : BaseActivity() { * [show]. This is only used when the next/previous buttons on the toolbar are clicked; the * other cases are handled with chapter transitions on the viewers and chapter preloading. */ - @Suppress("DEPRECATION") private fun setProgressDialog(show: Boolean) { - progressDialog?.dismiss() - progressDialog = if (show) { - ProgressDialog.show(this, null, getString(R.string.loading), true) + if (show) { + viewModel.showLoadingDialog() } else { - null + viewModel.closeDialog() } } @@ -820,14 +823,14 @@ class ReaderActivity : BaseActivity() { * viewer because each one implements its own touch and key events. */ fun toggleMenu() { - setMenuVisibility(!menuVisible) + setMenuVisibility(!viewModel.state.value.menuVisible) } /** * Called from the viewer to show the menu. */ fun showMenu() { - if (!menuVisible) { + if (!viewModel.state.value.menuVisible) { setMenuVisibility(true) } } @@ -836,7 +839,7 @@ class ReaderActivity : BaseActivity() { * Called from the viewer to hide the menu. */ fun hideMenu() { - if (menuVisible) { + if (viewModel.state.value.menuVisible) { setMenuVisibility(false) } } @@ -1034,7 +1037,7 @@ class ReaderActivity : BaseActivity() { } // Trigger relayout - setMenuVisibility(menuVisible) + setMenuVisibility(viewModel.state.value.menuVisible) } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPageDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPageActionsDialog.kt similarity index 99% rename from app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPageDialog.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPageActionsDialog.kt index b3b07be2a..486339eba 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPageDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPageActionsDialog.kt @@ -25,7 +25,7 @@ import tachiyomi.presentation.core.components.ActionButton import tachiyomi.presentation.core.components.material.padding @Composable -fun ReaderPageDialog( +fun ReaderPageActionsDialog( onDismissRequest: () -> Unit, onSetAsCover: () -> Unit, onShare: () -> Unit, 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 6f7823bb4..9ef087a3b 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 @@ -92,9 +92,9 @@ class ReaderViewModel( private val downloadProvider: MangaDownloadProvider = Injekt.get(), private val imageSaver: ImageSaver = Injekt.get(), preferences: BasePreferences = Injekt.get(), + val readerPreferences: ReaderPreferences = Injekt.get(), private val basePreferences: BasePreferences = Injekt.get(), private val downloadPreferences: DownloadPreferences = Injekt.get(), - private val readerPreferences: ReaderPreferences = Injekt.get(), private val trackPreferences: TrackPreferences = Injekt.get(), private val delayedTrackingStore: DelayedMangaTrackingStore = Injekt.get(), private val getManga: GetManga = Injekt.get(), @@ -223,7 +223,6 @@ class ReaderViewModel( val currentChapters = state.value.viewerChapters if (currentChapters != null) { currentChapters.unref() - saveReadingProgress(currentChapters.currChapter) chapterToDownload?.let { downloadManager.addDownloadsToStartOfQueue(listOf(it)) } @@ -238,17 +237,6 @@ class ReaderViewModel( deletePendingChapters() } - /** - * Called when the activity is saved. It updates the database - * to persist the current progress of the active chapter. - */ - fun onSaveInstanceState() { - val currentChapter = getCurrentChapter() ?: return - viewModelScope.launchNonCancellable { - saveChapterProgress(currentChapter) - } - } - /** * Whether this presenter is initialized yet. */ @@ -346,7 +334,6 @@ class ReaderViewModel( */ private suspend fun loadAdjacent(chapter: ReaderChapter) { val loader = loader ?: return - saveCurrentChapterReadingProgress() logcat { "Loading adjacent ${chapter.chapter.url}" } @@ -420,16 +407,17 @@ class ReaderViewModel( * [page]'s chapter is different from the currently active. */ fun onPageSelected(page: ReaderPage) { - val currentChapters = state.value.viewerChapters ?: return - - val selectedChapter = page.chapter - // InsertPage and StencilPage doesn't change page progress if (page is InsertPage || page is StencilPage) { return } + val currentChapters = state.value.viewerChapters ?: return + val pages = page.chapter.pages ?: return + val selectedChapter = page.chapter + // Save last page read and mark as read if needed + saveReadingProgress() mutableState.update { it.copy( currentPage = page.index + 1, @@ -446,11 +434,9 @@ class ReaderViewModel( if (selectedChapter != currentChapters.currChapter) { logcat { "Setting ${selectedChapter.chapter.url} as active" } - saveReadingProgress(currentChapters.currChapter) setReadStartTime() viewModelScope.launch { loadNewChapter(selectedChapter) } } - val pages = page.chapter.pages ?: return val inDownloadRange = page.number.toDouble() / pages.size > 0.25 if (inDownloadRange) { downloadNextChapters() @@ -473,7 +459,7 @@ class ReaderViewModel( manga.title, manga.source, ) - if (!isNextChapterDownloaded) return@launchIO + if (isNextChapterDownloaded) return@launchIO val chaptersToDownload = getNextChapters.await(manga.id, nextChapter.id!!).run { if (readerPreferences.skipDupe().get()) { @@ -520,17 +506,15 @@ class ReaderViewModel( } } - fun saveCurrentChapterReadingProgress() { - getCurrentChapter()?.let { saveReadingProgress(it) } - } - /** * Called when reader chapter is changed in reader or when activity is paused. */ - private fun saveReadingProgress(readerChapter: ReaderChapter) { - viewModelScope.launchNonCancellable { - saveChapterProgress(readerChapter) - saveChapterHistory(readerChapter) + private fun saveReadingProgress() { + getCurrentChapter()?.let { + viewModelScope.launchNonCancellable { + saveChapterProgress(it) + saveChapterHistory(it) + } } } @@ -542,7 +526,7 @@ class ReaderViewModel( if (!incognitoMode) return val chapter = readerChapter.chapter - getCurrentChapter()?.requestedPage = chapter.last_page_read + readerChapter.requestedPage = chapter.last_page_read updateChapter.await( ChapterUpdate( id = chapter.id!!, @@ -718,8 +702,20 @@ class ReaderViewModel( ) + filenameSuffix } + fun showMenus(visible: Boolean) { + mutableState.update { it.copy(menuVisible = visible) } + } + + fun showLoadingDialog() { + mutableState.update { it.copy(dialog = Dialog.Loading) } + } + fun openPageDialog(page: ReaderPage) { - mutableState.update { it.copy(dialog = Dialog.Page(page)) } + mutableState.update { it.copy(dialog = Dialog.PageActions(page)) } + } + + fun openColorFilterDialog() { + mutableState.update { it.copy(dialog = Dialog.ColorFilter) } } fun closeDialog() { @@ -731,7 +727,7 @@ class ReaderViewModel( * There's also a notification to allow sharing the image somewhere else or deleting it. */ fun saveImage() { - val page = (state.value.dialog as? Dialog.Page)?.page + val page = (state.value.dialog as? Dialog.PageActions)?.page if (page?.status != Page.State.READY) return val manga = manga ?: return @@ -773,7 +769,7 @@ class ReaderViewModel( * image will be kept so it won't be taking lots of internal disk space. */ fun shareImage() { - val page = (state.value.dialog as? Dialog.Page)?.page + val page = (state.value.dialog as? Dialog.PageActions)?.page if (page?.status != Page.State.READY) return val manga = manga ?: return @@ -803,7 +799,7 @@ class ReaderViewModel( * Sets the image of this the selected page as cover and notifies the UI of the result. */ fun setAsCover() { - val page = (state.value.dialog as? Dialog.Page)?.page + val page = (state.value.dialog as? Dialog.PageActions)?.page if (page?.status != Page.State.READY) return val manga = manga ?: return val stream = page.stream ?: return @@ -918,13 +914,16 @@ class ReaderViewModel( */ val viewer: Viewer? = null, val dialog: Dialog? = null, + val menuVisible: Boolean = false, ) { val totalPages: Int get() = viewerChapters?.currChapter?.pages?.size ?: -1 } sealed class Dialog { - data class Page(val page: ReaderPage) : Dialog() + object Loading : Dialog() + object ColorFilter : Dialog() + data class PageActions(val page: ReaderPage) : Dialog() } sealed class Event { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderColorFilterDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderColorFilterDialog.kt new file mode 100644 index 000000000..db7a901d4 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderColorFilterDialog.kt @@ -0,0 +1,164 @@ +package eu.kanade.tachiyomi.ui.reader.setting + +import android.os.Build +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogWindowProvider +import androidx.core.graphics.alpha +import androidx.core.graphics.blue +import androidx.core.graphics.green +import androidx.core.graphics.red +import eu.kanade.presentation.components.AdaptiveSheet +import eu.kanade.presentation.more.settings.LocalPreferenceMinHeight +import eu.kanade.presentation.more.settings.Preference +import eu.kanade.presentation.more.settings.PreferenceScreen +import eu.kanade.presentation.util.collectAsState +import eu.kanade.tachiyomi.R +import tachiyomi.core.preference.getAndSet + +@Composable +fun ReaderColorFilterDialog( + onDismissRequest: () -> Unit, + readerPreferences: ReaderPreferences, +) { + val colorFilterModes = buildList { + addAll( + listOf( + R.string.label_default, + R.string.filter_mode_multiply, + R.string.filter_mode_screen, + ), + ) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + addAll( + listOf( + R.string.filter_mode_overlay, + R.string.filter_mode_lighten, + R.string.filter_mode_darken, + ), + ) + } + }.map { stringResource(it) } + + val customBrightness by readerPreferences.customBrightness().collectAsState() + val customBrightnessValue by readerPreferences.customBrightnessValue().collectAsState() + val colorFilter by readerPreferences.colorFilter().collectAsState() + val colorFilterValue by readerPreferences.colorFilterValue().collectAsState() + val colorFilterMode by readerPreferences.colorFilterMode().collectAsState() + + AdaptiveSheet( + onDismissRequest = onDismissRequest, + ) { + (LocalView.current.parent as? DialogWindowProvider)?.window?.setDimAmount(0f) + + CompositionLocalProvider( + LocalPreferenceMinHeight provides 48.dp, + ) { + PreferenceScreen( + items = listOfNotNull( + Preference.PreferenceItem.SwitchPreference( + pref = readerPreferences.customBrightness(), + title = stringResource(R.string.pref_custom_brightness), + ), + /** + * Sets the brightness of the screen. Range is [-75, 100]. + * From -75 to -1 a semi-transparent black view is shown at the top with the minimum brightness. + * From 1 to 100 it sets that value as brightness. + * 0 sets system brightness and hides the overlay. + */ + Preference.PreferenceItem.SliderPreference( + value = customBrightnessValue, + title = stringResource(R.string.pref_custom_brightness), + min = -75, + max = 100, + onValueChanged = { + readerPreferences.customBrightnessValue().set(it) + true + }, + ).takeIf { customBrightness }, + + Preference.PreferenceItem.SwitchPreference( + pref = readerPreferences.colorFilter(), + title = stringResource(R.string.pref_custom_color_filter), + ), + Preference.PreferenceItem.SliderPreference( + value = colorFilterValue.red, + title = stringResource(R.string.color_filter_r_value), + max = 255, + onValueChanged = { newRValue -> + readerPreferences.colorFilterValue().getAndSet { + getColorValue(it, newRValue, RED_MASK, 16) + } + true + }, + ).takeIf { colorFilter }, + Preference.PreferenceItem.SliderPreference( + value = colorFilterValue.green, + title = stringResource(R.string.color_filter_g_value), + max = 255, + onValueChanged = { newRValue -> + readerPreferences.colorFilterValue().getAndSet { + getColorValue(it, newRValue, GREEN_MASK, 8) + } + true + }, + ).takeIf { colorFilter }, + Preference.PreferenceItem.SliderPreference( + value = colorFilterValue.blue, + title = stringResource(R.string.color_filter_b_value), + max = 255, + onValueChanged = { newRValue -> + readerPreferences.colorFilterValue().getAndSet { + getColorValue(it, newRValue, BLUE_MASK, 0) + } + true + }, + ).takeIf { colorFilter }, + Preference.PreferenceItem.SliderPreference( + value = colorFilterValue.alpha, + title = stringResource(R.string.color_filter_a_value), + max = 255, + onValueChanged = { newRValue -> + readerPreferences.colorFilterValue().getAndSet { + getColorValue(it, newRValue, ALPHA_MASK, 24) + } + true + }, + ).takeIf { colorFilter }, + Preference.PreferenceItem.BasicListPreference( + value = colorFilterMode.toString(), + title = stringResource(R.string.pref_color_filter_mode), + entries = colorFilterModes + .mapIndexed { index, mode -> index.toString() to mode } + .toMap(), + onValueChanged = { newValue -> + readerPreferences.colorFilterMode().set(newValue.toInt()) + true + }, + ).takeIf { colorFilter }, + + Preference.PreferenceItem.SwitchPreference( + pref = readerPreferences.grayscale(), + title = stringResource(R.string.pref_grayscale), + ), + Preference.PreferenceItem.SwitchPreference( + pref = readerPreferences.invertedColors(), + title = stringResource(R.string.pref_inverted_colors), + ), + ), + ) + } + } +} + +private fun getColorValue(currentColor: Int, color: Int, mask: Long, bitShift: Int): Int { + return (color shl bitShift) or (currentColor and mask.inv().toInt()) +} +private const val ALPHA_MASK: Long = 0xFF000000 +private const val RED_MASK: Long = 0x00FF0000 +private const val GREEN_MASK: Long = 0x0000FF00 +private const val BLUE_MASK: Long = 0x000000FF diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderColorFilterSettings.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderColorFilterSettings.kt deleted file mode 100644 index 7f76dbb76..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderColorFilterSettings.kt +++ /dev/null @@ -1,202 +0,0 @@ -package eu.kanade.tachiyomi.ui.reader.setting - -import android.content.Context -import android.util.AttributeSet -import android.view.LayoutInflater -import androidx.annotation.ColorInt -import androidx.core.graphics.alpha -import androidx.core.graphics.blue -import androidx.core.graphics.green -import androidx.core.graphics.red -import androidx.core.widget.NestedScrollView -import androidx.lifecycle.lifecycleScope -import eu.kanade.tachiyomi.databinding.ReaderColorFilterSettingsBinding -import eu.kanade.tachiyomi.ui.reader.ReaderActivity -import eu.kanade.tachiyomi.util.preference.bindToPreference -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.sample -import tachiyomi.core.preference.getAndSet -import uy.kohesive.injekt.injectLazy - -/** - * Color filter sheet to toggle custom filter and brightness overlay. - */ -class ReaderColorFilterSettings @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : - NestedScrollView(context, attrs) { - - private val readerPreferences: ReaderPreferences by injectLazy() - - private val binding = ReaderColorFilterSettingsBinding.inflate(LayoutInflater.from(context), this, false) - - init { - addView(binding.root) - - readerPreferences.colorFilter().changes() - .onEach(::setColorFilter) - .launchIn((context as ReaderActivity).lifecycleScope) - - readerPreferences.colorFilterMode().changes() - .onEach { setColorFilter(readerPreferences.colorFilter().get()) } - .launchIn(context.lifecycleScope) - - readerPreferences.customBrightness().changes() - .onEach(::setCustomBrightness) - .launchIn(context.lifecycleScope) - - // Get color and update values - val color = readerPreferences.colorFilterValue().get() - val brightness = readerPreferences.customBrightnessValue().get() - - val argb = setValues(color) - - // Set brightness value - binding.txtBrightnessSeekbarValue.text = brightness.toString() - binding.sliderBrightness.value = brightness.toFloat() - - // Initialize seekBar progress - binding.sliderColorFilterAlpha.value = argb[0].toFloat() - binding.sliderColorFilterRed.value = argb[1].toFloat() - binding.sliderColorFilterGreen.value = argb[2].toFloat() - binding.sliderColorFilterBlue.value = argb[3].toFloat() - - // Set listeners - binding.switchColorFilter.bindToPreference(readerPreferences.colorFilter()) - binding.customBrightness.bindToPreference(readerPreferences.customBrightness()) - binding.colorFilterMode.bindToPreference(readerPreferences.colorFilterMode()) - binding.grayscale.bindToPreference(readerPreferences.grayscale()) - binding.invertedColors.bindToPreference(readerPreferences.invertedColors()) - - binding.sliderColorFilterAlpha.addOnChangeListener { _, value, fromUser -> - if (fromUser) { - setColorValue(value.toInt(), ALPHA_MASK, 24) - } - } - binding.sliderColorFilterRed.addOnChangeListener { _, value, fromUser -> - if (fromUser) { - setColorValue(value.toInt(), RED_MASK, 16) - } - } - binding.sliderColorFilterGreen.addOnChangeListener { _, value, fromUser -> - if (fromUser) { - setColorValue(value.toInt(), GREEN_MASK, 8) - } - } - binding.sliderColorFilterBlue.addOnChangeListener { _, value, fromUser -> - if (fromUser) { - setColorValue(value.toInt(), BLUE_MASK, 0) - } - } - - binding.sliderBrightness.addOnChangeListener { _, value, fromUser -> - if (fromUser) { - readerPreferences.customBrightnessValue().set(value.toInt()) - } - } - } - - /** - * Set enabled status of seekBars belonging to color filter - * @param enabled determines if seekBar gets enabled - */ - private fun setColorFilterSeekBar(enabled: Boolean) { - binding.sliderColorFilterRed.isEnabled = enabled - binding.sliderColorFilterGreen.isEnabled = enabled - binding.sliderColorFilterBlue.isEnabled = enabled - binding.sliderColorFilterAlpha.isEnabled = enabled - } - - /** - * Set enabled status of seekBars belonging to custom brightness - * @param enabled value which determines if seekBar gets enabled - */ - private fun setCustomBrightnessSeekBar(enabled: Boolean) { - binding.sliderBrightness.isEnabled = enabled - } - - /** - * Set the text value's of color filter - * @param color integer containing color information - */ - private fun setValues(color: Int): Array { - val alpha = color.alpha - val red = color.red - val green = color.green - val blue = color.blue - - // Initialize values - binding.txtColorFilterAlphaValue.text = "$alpha" - binding.txtColorFilterRedValue.text = "$red" - binding.txtColorFilterGreenValue.text = "$green" - binding.txtColorFilterBlueValue.text = "$blue" - - return arrayOf(alpha, red, green, blue) - } - - /** - * Manages the custom brightness value subscription - * @param enabled determines if the subscription get (un)subscribed - */ - private fun setCustomBrightness(enabled: Boolean) { - if (enabled) { - readerPreferences.customBrightnessValue().changes() - .sample(100) - .onEach(::setCustomBrightnessValue) - .launchIn((context as ReaderActivity).lifecycleScope) - } else { - setCustomBrightnessValue(0, true) - } - setCustomBrightnessSeekBar(enabled) - } - - /** - * Sets the brightness of the screen. Range is [-75, 100]. - * From -75 to -1 a semi-transparent black view is shown at the top with the minimum brightness. - * From 1 to 100 it sets that value as brightness. - * 0 sets system brightness and hides the overlay. - */ - private fun setCustomBrightnessValue(value: Int, isDisabled: Boolean = false) { - if (!isDisabled) { - binding.txtBrightnessSeekbarValue.text = value.toString() - } - } - - /** - * Manages the color filter value subscription - * @param enabled determines if the subscription get (un)subscribed - */ - private fun setColorFilter(enabled: Boolean) { - if (enabled) { - readerPreferences.colorFilterValue().changes() - .sample(100) - .onEach(::setColorFilterValue) - .launchIn((context as ReaderActivity).lifecycleScope) - } - setColorFilterSeekBar(enabled) - } - - /** - * Sets the color filter overlay of the screen. Determined by HEX of integer - * @param color hex of color. - */ - private fun setColorFilterValue(@ColorInt color: Int) { - setValues(color) - } - - /** - * Updates the color value in preference - * @param color value of color range [0,255] - * @param mask contains hex mask of chosen color - * @param bitShift amounts of bits that gets shifted to receive value - */ - private fun setColorValue(color: Int, mask: Long, bitShift: Int) { - readerPreferences.colorFilterValue().getAndSet { currentColor -> - (color shl bitShift) or (currentColor and mask.inv().toInt()) - } - } -} - -private const val ALPHA_MASK: Long = 0xFF000000 -private const val RED_MASK: Long = 0x00FF0000 -private const val GREEN_MASK: Long = 0x0000FF00 -private const val BLUE_MASK: Long = 0x000000FF diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderSettingsSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderSettingsSheet.kt index 0f684736a..ff432320f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderSettingsSheet.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderSettingsSheet.kt @@ -1,46 +1,30 @@ package eu.kanade.tachiyomi.ui.reader.setting -import android.animation.ValueAnimator import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import com.google.android.material.tabs.TabLayout import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.databinding.CommonTabbedSheetBinding import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.widget.ViewPagerAdapter -import eu.kanade.tachiyomi.widget.listener.SimpleTabSelectedListener import eu.kanade.tachiyomi.widget.sheet.BaseBottomSheetDialog class ReaderSettingsSheet( private val activity: ReaderActivity, - private val showColorFilterSettings: Boolean = false, ) : BaseBottomSheetDialog(activity) { private val tabs = listOf( ReaderReadingModeSettings(activity) to R.string.pref_category_reading_mode, ReaderGeneralSettings(activity) to R.string.pref_category_general, - ReaderColorFilterSettings(activity) to R.string.custom_filter, ) - private val backgroundDimAnimator by lazy { - val sheetBackgroundDim = window?.attributes?.dimAmount ?: 0.25f - ValueAnimator.ofFloat(sheetBackgroundDim, 0f).also { valueAnimator -> - valueAnimator.duration = 250 - valueAnimator.addUpdateListener { - window?.setDimAmount(it.animatedValue as Float) - } - } - } - private lateinit var binding: CommonTabbedSheetBinding override fun createView(inflater: LayoutInflater): View { binding = CommonTabbedSheetBinding.inflate(activity.layoutInflater) val adapter = Adapter() - binding.pager.offscreenPageLimit = 2 binding.pager.adapter = adapter binding.tabs.setupWithViewPager(binding.pager) @@ -52,35 +36,6 @@ class ReaderSettingsSheet( behavior.isFitToContents = false behavior.halfExpandedRatio = 0.25f - - val filterTabIndex = tabs.indexOfFirst { it.first is ReaderColorFilterSettings } - binding.tabs.addOnTabSelectedListener( - object : SimpleTabSelectedListener() { - override fun onTabSelected(tab: TabLayout.Tab?) { - val isFilterTab = tab?.position == filterTabIndex - - // Remove dimmed backdrop so color filter changes can be previewed - backgroundDimAnimator.run { - if (isFilterTab) { - if (animatedFraction < 1f) { - start() - } - } else if (animatedFraction > 0f) { - reverse() - } - } - - // Hide toolbars - if (activity.menuVisible != !isFilterTab) { - activity.setMenuVisibility(!isFilterTab) - } - } - }, - ) - - if (showColorFilterSettings) { - binding.tabs.getTabAt(filterTabIndex)?.select() - } } private inner class Adapter : ViewPagerAdapter() { 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 3c92d8ad4..bd023a36b 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 @@ -112,7 +112,7 @@ abstract class PagerViewer(val activity: ReaderActivity) : Viewer { } } pager.longTapListener = f@{ - if (activity.menuVisible || config.longTapEnabled) { + if (activity.viewModel.state.value.menuVisible || config.longTapEnabled) { val item = adapter.items.getOrNull(pager.currentItem) if (item is ReaderPage) { activity.onPageLongTap(item) @@ -374,14 +374,14 @@ abstract class PagerViewer(val activity: ReaderActivity) : Viewer { when (event.keyCode) { KeyEvent.KEYCODE_VOLUME_DOWN -> { - if (!config.volumeKeysEnabled || activity.menuVisible) { + if (!config.volumeKeysEnabled || activity.viewModel.state.value.menuVisible) { return false } else if (isUp) { if (!config.volumeKeysInverted) moveDown() else moveUp() } } KeyEvent.KEYCODE_VOLUME_UP -> { - if (!config.volumeKeysEnabled || activity.menuVisible) { + if (!config.volumeKeysEnabled || activity.viewModel.state.value.menuVisible) { return false } else if (isUp) { if (!config.volumeKeysInverted) moveUp() else moveDown() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonFrame.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonFrame.kt index cedc45ab7..572335b25 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonFrame.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonFrame.kt @@ -1,6 +1,7 @@ package eu.kanade.tachiyomi.ui.reader.viewer.webtoon import android.content.Context +import android.graphics.Rect import android.view.GestureDetector import android.view.MotionEvent import android.view.ScaleGestureDetector @@ -44,6 +45,22 @@ class WebtoonFrame(context: Context) : FrameLayout(context) { override fun dispatchTouchEvent(ev: MotionEvent): Boolean { scaleDetector.onTouchEvent(ev) flingDetector.onTouchEvent(ev) + + // Get the bounding box of the recyclerview and translate any motion events to be within it. + // Used to allow scrolling outside the recyclerview. + val recyclerRect = Rect() + recycler?.getHitRect(recyclerRect) ?: return super.dispatchTouchEvent(ev) + // Shrink the box to account for any rounding issues. + recyclerRect.inset(1, 1) + + if (recyclerRect.right < recyclerRect.left || recyclerRect.bottom < recyclerRect.top) { + return super.dispatchTouchEvent(ev) + } + + ev.setLocation( + ev.x.coerceIn(recyclerRect.left.toFloat(), recyclerRect.right.toFloat()), + ev.y.coerceIn(recyclerRect.top.toFloat(), recyclerRect.bottom.toFloat()), + ) return super.dispatchTouchEvent(ev) } 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 fe2dbd98d..c7375118d 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 @@ -91,7 +91,7 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { onScrolled() - if ((dy > threshold || dy < -threshold) && activity.menuVisible) { + if ((dy > threshold || dy < -threshold) && activity.viewModel.state.value.menuVisible) { activity.hideMenu() } @@ -120,7 +120,7 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr } } recycler.longTapListener = f@{ event -> - if (activity.menuVisible || config.longTapEnabled) { + if (activity.viewModel.state.value.menuVisible || config.longTapEnabled) { val child = recycler.findChildViewUnder(event.x, event.y) if (child != null) { val position = recycler.getChildAdapterPosition(child) @@ -310,14 +310,14 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr when (event.keyCode) { KeyEvent.KEYCODE_VOLUME_DOWN -> { - if (!config.volumeKeysEnabled || activity.menuVisible) { + if (!config.volumeKeysEnabled || activity.viewModel.state.value.menuVisible) { return false } else if (isUp) { if (!config.volumeKeysInverted) scrollDown() else scrollUp() } } KeyEvent.KEYCODE_VOLUME_UP -> { - if (!config.volumeKeysEnabled || activity.menuVisible) { + if (!config.volumeKeysEnabled || activity.viewModel.state.value.menuVisible) { return false } else if (isUp) { if (!config.volumeKeysInverted) scrollUp() else scrollDown() diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/view/ImageViewExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/view/ImageViewExtensions.kt deleted file mode 100644 index dfeb7281e..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/util/view/ImageViewExtensions.kt +++ /dev/null @@ -1,21 +0,0 @@ -package eu.kanade.tachiyomi.util.view - -import android.widget.ImageView -import androidx.annotation.AttrRes -import androidx.annotation.DrawableRes -import androidx.appcompat.content.res.AppCompatResources -import eu.kanade.tachiyomi.util.system.getResourceColor - -/** - * Set a vector on a [ImageView]. - * - * @param drawable id of drawable resource - */ -fun ImageView.setVectorCompat(@DrawableRes drawable: Int, @AttrRes tint: Int? = null) { - val vector = AppCompatResources.getDrawable(context, drawable) - if (tint != null) { - vector?.mutate() - vector?.setTint(context.getResourceColor(tint)) - } - setImageDrawable(vector) -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewGroupExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewGroupExtensions.kt deleted file mode 100644 index 64c71200a..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewGroupExtensions.kt +++ /dev/null @@ -1,15 +0,0 @@ -package eu.kanade.tachiyomi.util.view - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.annotation.LayoutRes - -/** - * Extension method to inflate a view directly from its parent. - * @param layout the layout to inflate. - * @param attachToRoot whether to attach the view to the root or not. Defaults to false. - */ -fun ViewGroup.inflate(@LayoutRes layout: Int, attachToRoot: Boolean = false): View { - return LayoutInflater.from(context).inflate(layout, this, attachToRoot) -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/OutlineSpan.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/OutlineSpan.kt deleted file mode 100644 index e5125a28c..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/OutlineSpan.kt +++ /dev/null @@ -1,56 +0,0 @@ -package eu.kanade.tachiyomi.widget - -import android.graphics.Canvas -import android.graphics.Paint -import android.text.style.ReplacementSpan -import androidx.annotation.ColorInt -import androidx.annotation.Dimension - -/** - * Source: https://github.com/santaevpavel - * - * A class that draws the outlines of a text when given a stroke color and stroke width. - */ -class OutlineSpan( - @ColorInt private val strokeColor: Int, - @Dimension private val strokeWidth: Float, -) : ReplacementSpan() { - - override fun getSize( - paint: Paint, - text: CharSequence, - start: Int, - end: Int, - fm: Paint.FontMetricsInt?, - ): Int { - return paint.measureText(text.toString().substring(start until end)).toInt() - } - - override fun draw( - canvas: Canvas, - text: CharSequence, - start: Int, - end: Int, - x: Float, - top: Int, - y: Int, - bottom: Int, - paint: Paint, - ) { - val originTextColor = paint.color - - paint.apply { - color = strokeColor - style = Paint.Style.STROKE - this.strokeWidth = this@OutlineSpan.strokeWidth - } - canvas.drawText(text, start, end, x, y.toFloat(), paint) - - paint.apply { - color = originTextColor - style = Paint.Style.FILL - } - - canvas.drawText(text, start, end, x, y.toFloat(), paint) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/TriState.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/TriState.kt deleted file mode 100644 index f30971453..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/TriState.kt +++ /dev/null @@ -1,19 +0,0 @@ -package eu.kanade.tachiyomi.widget - -import eu.kanade.tachiyomi.animesource.model.AnimeFilter -import eu.kanade.tachiyomi.source.model.Filter -import tachiyomi.domain.entries.TriStateFilter - -fun Int.toTriStateFilter(): TriStateFilter { - return when (this) { - Filter.TriState.STATE_IGNORE -> TriStateFilter.DISABLED - Filter.TriState.STATE_INCLUDE -> TriStateFilter.ENABLED_IS - Filter.TriState.STATE_EXCLUDE -> TriStateFilter.ENABLED_NOT - - AnimeFilter.TriState.STATE_IGNORE -> TriStateFilter.DISABLED - AnimeFilter.TriState.STATE_INCLUDE -> TriStateFilter.ENABLED_IS - AnimeFilter.TriState.STATE_EXCLUDE -> TriStateFilter.ENABLED_NOT - - else -> throw IllegalStateException("Unknown TriState state: $this") - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/listener/SimpleTabSelectedListener.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/listener/SimpleTabSelectedListener.kt deleted file mode 100644 index 68a226466..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/listener/SimpleTabSelectedListener.kt +++ /dev/null @@ -1,14 +0,0 @@ -package eu.kanade.tachiyomi.widget.listener - -import com.google.android.material.tabs.TabLayout - -open class SimpleTabSelectedListener : TabLayout.OnTabSelectedListener { - override fun onTabSelected(tab: TabLayout.Tab?) { - } - - override fun onTabUnselected(tab: TabLayout.Tab?) { - } - - override fun onTabReselected(tab: TabLayout.Tab?) { - } -} diff --git a/app/src/main/res/layout/reader_activity.xml b/app/src/main/res/layout/reader_activity.xml index 977922694..ef3efd1c1 100644 --- a/app/src/main/res/layout/reader_activity.xml +++ b/app/src/main/res/layout/reader_activity.xml @@ -38,6 +38,12 @@ android:focusable="false" android:visibility="gone" /> + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/values-v28/arrays.xml b/app/src/main/res/values-v28/arrays.xml deleted file mode 100644 index 9718e4511..000000000 --- a/app/src/main/res/values-v28/arrays.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - @string/label_default - @string/filter_mode_multiply - @string/filter_mode_screen - - - @string/filter_mode_overlay - @string/filter_mode_lighten - @string/filter_mode_darken - - - diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 1b7e3882c..e0b15fedb 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -67,12 +67,6 @@ @string/rotation_reverse_portrait - - @string/label_default - @string/filter_mode_multiply - @string/filter_mode_screen - - @string/tapping_inverted_none @string/tapping_inverted_horizontal diff --git a/app/src/main/res/xml/provider_paths.xml b/app/src/main/res/xml/provider_paths.xml index 9c1aaf647..22b8655eb 100644 --- a/app/src/main/res/xml/provider_paths.xml +++ b/app/src/main/res/xml/provider_paths.xml @@ -4,11 +4,14 @@ name="cache_files" path="." /> + /dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. diff --git a/i18n/src/main/res/values/strings-aniyomi.xml b/i18n/src/main/res/values/strings-aniyomi.xml index bfafaae85..a32d27535 100644 --- a/i18n/src/main/res/values/strings-aniyomi.xml +++ b/i18n/src/main/res/values/strings-aniyomi.xml @@ -159,7 +159,7 @@ What information to include in the backup file Clear chapter and episode cache Used by anime: %1$s, used by manga: %2$s - Clear episode/chapter cache on app close + Clear episode/chapter cache on app launch Clear Manga database Clear Anime database Delete history for manga that are not saved in your library diff --git a/i18n/src/main/res/values/strings.xml b/i18n/src/main/res/values/strings.xml index 3818dd8c1..837cdcc24 100644 --- a/i18n/src/main/res/values/strings.xml +++ b/i18n/src/main/res/values/strings.xml @@ -11,6 +11,7 @@ Manga Chapters Tracking + Delete downloaded History @@ -881,7 +882,6 @@ This Android version is no longer supported No new updates available - Searching for updates… Downloading… diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/AdaptiveSheet.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/AdaptiveSheet.kt index ab7ce884f..ca44001d8 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/AdaptiveSheet.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/AdaptiveSheet.kt @@ -5,8 +5,13 @@ import androidx.activity.compose.BackHandler import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.AnchoredDraggableState +import androidx.compose.foundation.gestures.DraggableAnchors import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.anchoredDraggable +import androidx.compose.foundation.gestures.animateTo import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides @@ -19,9 +24,6 @@ import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.material.SwipeableState -import androidx.compose.material.rememberSwipeableState -import androidx.compose.material.swipeable import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.Composable @@ -39,7 +41,9 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.Velocity @@ -50,8 +54,7 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch import kotlin.math.roundToInt -private const val SheetAnimationDuration = 350 -private val SheetAnimationSpec = tween(durationMillis = SheetAnimationDuration) +private val sheetAnimationSpec = tween(durationMillis = 350) @Composable fun AdaptiveSheet( @@ -61,6 +64,7 @@ fun AdaptiveSheet( onDismissRequest: () -> Unit, content: @Composable () -> Unit, ) { + val density = LocalDensity.current val scope = rememberCoroutineScope() val maxWidth = if (LocalConfiguration.current.orientation == ORIENTATION_LANDSCAPE) { 600.dp @@ -72,7 +76,7 @@ fun AdaptiveSheet( var targetAlpha by remember { mutableFloatStateOf(0f) } val alpha by animateFloatAsState( targetValue = targetAlpha, - animationSpec = SheetAnimationSpec, + animationSpec = sheetAnimationSpec, ) val internalOnDismissRequest: () -> Unit = { scope.launch { @@ -115,23 +119,36 @@ fun AdaptiveSheet( } } } else { - val swipeState = rememberSwipeableState( - initialValue = 1, - animationSpec = SheetAnimationSpec, - ) - val internalOnDismissRequest: () -> Unit = { if (swipeState.currentValue == 0) scope.launch { swipeState.animateTo(1) } } - BoxWithConstraints( + val anchoredDraggableState = remember { + AnchoredDraggableState( + initialValue = 1, + animationSpec = sheetAnimationSpec, + positionalThreshold = { with(density) { 56.dp.toPx() } }, + velocityThreshold = { with(density) { 125.dp.toPx() } }, + ) + } + val internalOnDismissRequest = { + if (anchoredDraggableState.currentValue == 0) { + scope.launch { anchoredDraggableState.animateTo(1) } + } + } + Box( modifier = Modifier .clickable( interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = internalOnDismissRequest, ) - .fillMaxSize(), + .fillMaxSize() + .onSizeChanged { + val anchors = DraggableAnchors { + 0 at 0f + 1 at it.height.toFloat() + } + anchoredDraggableState.updateAnchors(anchors) + }, contentAlignment = Alignment.BottomCenter, ) { - val fullHeight = constraints.maxHeight.toFloat() - val anchors = mapOf(0f to 0, fullHeight to 1) Surface( modifier = Modifier .widthIn(max = maxWidth) @@ -140,26 +157,27 @@ fun AdaptiveSheet( indication = null, onClick = {}, ) - .nestedScroll( - remember(enableSwipeDismiss, anchors) { - swipeState.preUpPostDownNestedScrollConnection( - enabled = enableSwipeDismiss, - anchor = anchors, + .then( + if (enableSwipeDismiss) { + Modifier.nestedScroll( + remember(anchoredDraggableState) { + anchoredDraggableState.preUpPostDownNestedScrollConnection() + }, ) + } else { + Modifier }, ) .offset { IntOffset( 0, - swipeState.offset.value.roundToInt(), + anchoredDraggableState.offset.takeIf { it.isFinite() }?.roundToInt() ?: 0, ) } - .swipeable( - enabled = enableSwipeDismiss, - state = swipeState, - anchors = anchors, + .anchoredDraggable( + state = anchoredDraggableState, orientation = Orientation.Vertical, - resistance = null, + enabled = enableSwipeDismiss, ) .windowInsetsPadding( WindowInsets.systemBars @@ -168,14 +186,14 @@ fun AdaptiveSheet( shape = MaterialTheme.shapes.extraLarge, tonalElevation = tonalElevation, content = { - BackHandler(enabled = swipeState.targetValue == 0, onBack = internalOnDismissRequest) + BackHandler(enabled = anchoredDraggableState.targetValue == 0, onBack = internalOnDismissRequest) content() }, ) - LaunchedEffect(swipeState) { - scope.launch { swipeState.animateTo(0) } - snapshotFlow { swipeState.currentValue } + LaunchedEffect(anchoredDraggableState) { + scope.launch { anchoredDraggableState.animateTo(0) } + snapshotFlow { anchoredDraggableState.currentValue } .drop(1) .filter { it == 1 } .collectLatest { @@ -186,17 +204,11 @@ fun AdaptiveSheet( } } -/** - * Yoinked from Swipeable.kt with modifications to disable - */ -private fun SwipeableState.preUpPostDownNestedScrollConnection( - enabled: Boolean = true, - anchor: Map, -) = object : NestedScrollConnection { +private fun AnchoredDraggableState.preUpPostDownNestedScrollConnection() = object : NestedScrollConnection { override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { val delta = available.toFloat() - return if (enabled && delta < 0 && source == NestedScrollSource.Drag) { - performDrag(delta).toOffset() + return if (delta < 0 && source == NestedScrollSource.Drag) { + dispatchRawDelta(delta).toOffset() } else { Offset.Zero } @@ -207,17 +219,17 @@ private fun SwipeableState.preUpPostDownNestedScrollConnection( available: Offset, source: NestedScrollSource, ): Offset { - return if (enabled && source == NestedScrollSource.Drag) { - performDrag(available.toFloat()).toOffset() + return if (source == NestedScrollSource.Drag) { + dispatchRawDelta(available.toFloat()).toOffset() } else { Offset.Zero } } override suspend fun onPreFling(available: Velocity): Velocity { - val toFling = Offset(available.x, available.y).toFloat() - return if (enabled && toFling < 0 && offset.value > anchor.keys.minOrNull()!!) { - performFling(velocity = toFling) + val toFling = available.toFloat() + return if (toFling < 0 && offset > anchors.minAnchor()) { + settle(toFling) // since we go to the anchor with tween settling, consume all for the best UX available } else { @@ -226,15 +238,14 @@ private fun SwipeableState.preUpPostDownNestedScrollConnection( } override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { - return if (enabled) { - performFling(velocity = Offset(available.x, available.y).toFloat()) - available - } else { - Velocity.Zero - } + settle(velocity = available.toFloat()) + return available } private fun Float.toOffset(): Offset = Offset(0f, this) + @JvmName("velocityToFloat") + private fun Velocity.toFloat() = this.y + private fun Offset.toFloat(): Float = this.y } diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/SettingsItemsPaddings.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/SettingsItems.kt similarity index 80% rename from presentation-core/src/main/java/tachiyomi/presentation/core/components/SettingsItemsPaddings.kt rename to presentation-core/src/main/java/tachiyomi/presentation/core/components/SettingsItems.kt index 00040026b..fae6c8382 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/SettingsItemsPaddings.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/SettingsItems.kt @@ -3,6 +3,7 @@ package tachiyomi.presentation.core.components import androidx.annotation.StringRes import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer @@ -17,6 +18,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.RadioButton +import androidx.compose.material3.Slider import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -135,6 +137,43 @@ fun RadioItem( ) } +@Composable +fun SliderItem( + label: String, + min: Int = 0, + max: Int, + value: Int, + valueText: String, + onChange: (Int) -> Unit, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding( + horizontal = SettingsItemsPaddings.Horizontal, + vertical = SettingsItemsPaddings.Vertical, + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(24.dp), + ) { + Column(modifier = Modifier.weight(0.5f)) { + Text( + text = label, + style = MaterialTheme.typography.bodyMedium, + ) + Text(valueText) + } + + Slider( + value = value.toFloat(), + onValueChange = { onChange(it.toInt()) }, + modifier = Modifier.weight(1.5f), + valueRange = min.toFloat()..max.toFloat(), + steps = max - min, + ) + } +} + @Composable private fun BaseSettingsItem( label: String,