From e7b1066e3ca2a71b88031cbc81de5ca7e2dfe12c Mon Sep 17 00:00:00 2001 From: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com> Date: Sat, 11 Nov 2023 17:58:02 +0100 Subject: [PATCH] merge18 Last commit merged: https://github.com/tachiyomiorg/tachiyomi/commit/12e7ee9d0caaa56d551908d179788fa637768397 --- .../domain/entries/anime/model/Anime.kt | 18 +- .../domain/entries/manga/model/Manga.kt | 18 +- .../source/service/SourcePreferences.kt | 3 - .../browse/anime/BrowseAnimeSourceScreen.kt | 4 +- .../browse/anime/GlobalAnimeSearchScreen.kt | 102 +++++- .../browse/manga/BrowseMangaSourceScreen.kt | 4 +- .../browse/manga/GlobalMangaSearchScreen.kt | 102 +++++- .../presentation/components/SettingsItems.kt | 242 -------------- .../presentation/components/TabbedDialog.kt | 3 +- .../entries/anime/EpisodeSettingsDialog.kt | 28 +- .../entries/manga/ChapterSettingsDialog.kt | 28 +- .../anime/AnimeLibrarySettingsDialog.kt | 8 +- .../manga/MangaLibrarySettingsDialog.kt | 8 +- .../more/settings/PreferenceItem.kt | 2 +- .../settings/screen/SettingsBrowseScreen.kt | 8 - .../settings/screen/SettingsLibraryScreen.kt | 8 +- .../settings/screen/SettingsReaderScreen.kt | 24 +- .../more/storage/SelectStorageCategory.kt | 2 +- .../reader/settings/ColorFilterPage.kt | 158 +++++++++ .../reader/settings/GeneralSettingsPage.kt | 96 ++++++ .../reader/settings/ReaderSettingsDialog.kt | 66 ++++ .../reader/settings/ReadingModePage.kt | 179 ++++++++++ .../java/eu/kanade/tachiyomi/Migrations.kt | 11 +- .../anime/util/AnimeExtensionLoader.kt | 22 +- .../manga/util/MangaExtensionLoader.kt | 22 +- .../migration/search/MigrateAnimeDialog.kt | 2 + .../search/MigrateAnimeSearchScreen.kt | 1 + .../source/browse/SourceFilterAnimeDialog.kt | 22 +- .../globalsearch/AnimeSearchScreenModel.kt | 16 +- .../globalsearch/GlobalAnimeSearchScreen.kt | 4 + .../GlobalAnimeSearchScreenModel.kt | 53 ++- .../migration/search/MigrateMangaDialog.kt | 2 + .../search/MigrateMangaSearchScreen.kt | 1 + .../source/browse/SourceFilterMangaDialog.kt | 22 +- .../globalsearch/GlobalMangaSearchScreen.kt | 4 + .../GlobalMangaSearchScreenModel.kt | 53 ++- .../globalsearch/MangaSearchScreenModel.kt | 16 +- .../ui/entries/anime/AnimeScreenModel.kt | 29 +- .../anime/track/AnimeTrackInfoDialog.kt | 6 + .../ui/entries/manga/MangaScreenModel.kt | 29 +- .../manga/track/MangaTrackInfoDialog.kt | 6 + .../library/anime/AnimeLibraryScreenModel.kt | 34 +- .../anime/AnimeLibrarySettingsScreenModel.kt | 4 +- .../library/manga/MangaLibraryScreenModel.kt | 34 +- .../manga/MangaLibrarySettingsScreenModel.kt | 4 +- .../tachiyomi/ui/player/PlayerViewModel.kt | 2 + .../sheets/subtitle/SubtitleDelayPage.kt | 2 +- .../sheets/subtitle/SubtitleFontPage.kt | 2 +- .../tachiyomi/ui/reader/ReaderActivity.kt | 30 +- .../tachiyomi/ui/reader/ReaderViewModel.kt | 18 +- .../reader/setting/ReaderColorFilterDialog.kt | 164 ---------- .../reader/setting/ReaderGeneralSettings.kt | 53 --- .../ui/reader/setting/ReaderPreferences.kt | 7 +- .../setting/ReaderReadingModeSettings.kt | 145 -------- .../setting/ReaderSettingsScreenModel.kt | 16 + .../ui/reader/setting/ReaderSettingsSheet.kt | 100 ++++-- .../util/system/ContextExtensions.kt | 1 - .../util/system/DisplayExtensions.kt | 11 - .../tachiyomi/widget/MaterialSpinnerView.kt | 12 - .../widget/sheet/BaseBottomSheetDialog.kt | 55 ---- .../widget/sheet/BottomSheetViewPager.kt | 56 ---- .../res/drawable/ic_brightness_5_24dp.xml | 9 - .../main/res/layout/common_tabbed_sheet.xml | 51 --- app/src/main/res/layout/reader_activity.xml | 12 +- .../res/layout/reader_general_settings.xml | 87 ----- .../main/res/layout/reader_pager_settings.xml | 74 ----- .../layout/reader_reading_mode_settings.xml | 2 - .../res/layout/reader_webtoon_settings.xml | 61 ---- app/src/main/res/values-sw720dp/dimens.xml | 2 - app/src/main/res/values/arrays.xml | 32 -- app/src/main/res/values/dimens.xml | 2 - .../kanade/tachiyomi/network/NetworkHelper.kt | 12 +- .../tachiyomi/core/preference/TriState.kt | 16 + .../java/tachiyomi/domain/entries/TriState.kt | 9 + .../domain/entries/TriStateFilter.kt | 22 -- .../domain/entries/anime/model/Anime.kt | 18 +- .../domain/entries/manga/model/Manga.kt | 18 +- .../library/service/LibraryPreferences.kt | 46 +-- i18n/src/main/res/values/strings.xml | 10 +- presentation-core/build.gradle.kts | 2 + .../core/components/SettingsItems.kt | 309 +++++++++++++++++- .../core/components/material/Tabs.kt | 6 +- 82 files changed, 1486 insertions(+), 1496 deletions(-) delete mode 100644 app/src/main/java/eu/kanade/presentation/components/SettingsItems.kt create mode 100644 app/src/main/java/eu/kanade/presentation/reader/settings/ColorFilterPage.kt create mode 100644 app/src/main/java/eu/kanade/presentation/reader/settings/GeneralSettingsPage.kt create mode 100644 app/src/main/java/eu/kanade/presentation/reader/settings/ReaderSettingsDialog.kt create mode 100644 app/src/main/java/eu/kanade/presentation/reader/settings/ReadingModePage.kt delete 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/ReaderGeneralSettings.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderReadingModeSettings.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderSettingsScreenModel.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/widget/sheet/BaseBottomSheetDialog.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/widget/sheet/BottomSheetViewPager.kt delete mode 100644 app/src/main/res/drawable/ic_brightness_5_24dp.xml delete mode 100644 app/src/main/res/layout/common_tabbed_sheet.xml delete mode 100644 app/src/main/res/layout/reader_general_settings.xml create mode 100644 core/src/main/java/tachiyomi/core/preference/TriState.kt create mode 100644 domain/src/main/java/tachiyomi/domain/entries/TriState.kt delete mode 100644 domain/src/main/java/tachiyomi/domain/entries/TriStateFilter.kt diff --git a/app/src/main/java/eu/kanade/domain/entries/anime/model/Anime.kt b/app/src/main/java/eu/kanade/domain/entries/anime/model/Anime.kt index 52f73c269..9d2693cf3 100644 --- a/app/src/main/java/eu/kanade/domain/entries/anime/model/Anime.kt +++ b/app/src/main/java/eu/kanade/domain/entries/anime/model/Anime.kt @@ -3,24 +3,24 @@ package eu.kanade.domain.entries.anime.model import eu.kanade.domain.base.BasePreferences import eu.kanade.tachiyomi.animesource.model.SAnime import eu.kanade.tachiyomi.data.cache.AnimeCoverCache -import tachiyomi.domain.entries.TriStateFilter +import tachiyomi.core.preference.TriState import tachiyomi.domain.entries.anime.model.Anime import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -val Anime.downloadedFilter: TriStateFilter +val Anime.downloadedFilter: TriState get() { - if (forceDownloaded()) return TriStateFilter.ENABLED_IS + if (forceDownloaded()) return TriState.ENABLED_IS return when (downloadedFilterRaw) { - Anime.EPISODE_SHOW_DOWNLOADED -> TriStateFilter.ENABLED_IS - Anime.EPISODE_SHOW_NOT_DOWNLOADED -> TriStateFilter.ENABLED_NOT - else -> TriStateFilter.DISABLED + Anime.EPISODE_SHOW_DOWNLOADED -> TriState.ENABLED_IS + Anime.EPISODE_SHOW_NOT_DOWNLOADED -> TriState.ENABLED_NOT + else -> TriState.DISABLED } } fun Anime.episodesFiltered(): Boolean { - return unseenFilter != TriStateFilter.DISABLED || - downloadedFilter != TriStateFilter.DISABLED || - bookmarkedFilter != TriStateFilter.DISABLED + return unseenFilter != TriState.DISABLED || + downloadedFilter != TriState.DISABLED || + bookmarkedFilter != TriState.DISABLED } fun Anime.forceDownloaded(): Boolean { return favorite && Injekt.get().downloadedOnly().get() diff --git a/app/src/main/java/eu/kanade/domain/entries/manga/model/Manga.kt b/app/src/main/java/eu/kanade/domain/entries/manga/model/Manga.kt index 786277c69..3b5378e46 100644 --- a/app/src/main/java/eu/kanade/domain/entries/manga/model/Manga.kt +++ b/app/src/main/java/eu/kanade/domain/entries/manga/model/Manga.kt @@ -7,7 +7,7 @@ import eu.kanade.tachiyomi.ui.reader.setting.OrientationType import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType import tachiyomi.core.metadata.comicinfo.ComicInfo import tachiyomi.core.metadata.comicinfo.ComicInfoPublishingStatus -import tachiyomi.domain.entries.TriStateFilter +import tachiyomi.core.preference.TriState import tachiyomi.domain.entries.manga.model.Manga import tachiyomi.domain.items.chapter.model.Chapter import uy.kohesive.injekt.Injekt @@ -20,19 +20,19 @@ val Manga.readingModeType: Long val Manga.orientationType: Long get() = viewerFlags and OrientationType.MASK.toLong() -val Manga.downloadedFilter: TriStateFilter +val Manga.downloadedFilter: TriState get() { - if (forceDownloaded()) return TriStateFilter.ENABLED_IS + if (forceDownloaded()) return TriState.ENABLED_IS return when (downloadedFilterRaw) { - Manga.CHAPTER_SHOW_DOWNLOADED -> TriStateFilter.ENABLED_IS - Manga.CHAPTER_SHOW_NOT_DOWNLOADED -> TriStateFilter.ENABLED_NOT - else -> TriStateFilter.DISABLED + Manga.CHAPTER_SHOW_DOWNLOADED -> TriState.ENABLED_IS + Manga.CHAPTER_SHOW_NOT_DOWNLOADED -> TriState.ENABLED_NOT + else -> TriState.DISABLED } } fun Manga.chaptersFiltered(): Boolean { - return unreadFilter != TriStateFilter.DISABLED || - downloadedFilter != TriStateFilter.DISABLED || - bookmarkedFilter != TriStateFilter.DISABLED + return unreadFilter != TriState.DISABLED || + downloadedFilter != TriState.DISABLED || + bookmarkedFilter != TriState.DISABLED } fun Manga.forceDownloaded(): Boolean { return favorite && Injekt.get().downloadedOnly().get() diff --git a/app/src/main/java/eu/kanade/domain/source/service/SourcePreferences.kt b/app/src/main/java/eu/kanade/domain/source/service/SourcePreferences.kt index 22a24479b..026d15430 100644 --- a/app/src/main/java/eu/kanade/domain/source/service/SourcePreferences.kt +++ b/app/src/main/java/eu/kanade/domain/source/service/SourcePreferences.kt @@ -37,9 +37,6 @@ class SourcePreferences( fun animeExtensionUpdatesCount() = preferenceStore.getInt("animeext_updates_count", 0) fun mangaExtensionUpdatesCount() = preferenceStore.getInt("ext_updates_count", 0) - fun searchPinnedAnimeSourcesOnly() = preferenceStore.getBoolean("search_pinned_anime_sources_only", false) - fun searchPinnedMangaSourcesOnly() = preferenceStore.getBoolean("search_pinned_sources_only", false) - fun hideInAnimeLibraryItems() = preferenceStore.getBoolean("browse_hide_in_anime_library_items", false) fun hideInMangaLibraryItems() = preferenceStore.getBoolean("browse_hide_in_library_items", false) diff --git a/app/src/main/java/eu/kanade/presentation/browse/anime/BrowseAnimeSourceScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/anime/BrowseAnimeSourceScreen.kt index 025ee5d8b..0e844d434 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/anime/BrowseAnimeSourceScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/anime/BrowseAnimeSourceScreen.kt @@ -61,12 +61,12 @@ fun BrowseAnimeSourceContent( if (animeList.itemCount > 0 && errorState != null && errorState is LoadState.Error) { val result = snackbarHostState.showSnackbar( message = getErrorMessage(errorState), - actionLabel = context.getString(R.string.action_webview_refresh), + actionLabel = context.getString(R.string.action_retry), duration = SnackbarDuration.Indefinite, ) when (result) { SnackbarResult.Dismissed -> snackbarHostState.currentSnackbarData?.dismiss() - SnackbarResult.ActionPerformed -> animeList.refresh() + SnackbarResult.ActionPerformed -> animeList.retry() } } } diff --git a/app/src/main/java/eu/kanade/presentation/browse/anime/GlobalAnimeSearchScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/anime/GlobalAnimeSearchScreen.kt index 05dd2a6ba..877daf6b8 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/anime/GlobalAnimeSearchScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/anime/GlobalAnimeSearchScreen.kt @@ -1,8 +1,23 @@ package eu.kanade.presentation.browse.anime +import androidx.compose.foundation.background +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.DoneAll +import androidx.compose.material.icons.outlined.FilterList +import androidx.compose.material.icons.outlined.PushPin +import androidx.compose.material3.Divider +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -17,7 +32,8 @@ import eu.kanade.presentation.browse.anime.components.GlobalAnimeSearchCardRow import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource import eu.kanade.tachiyomi.ui.browse.anime.source.globalsearch.AnimeSearchItemResult -import eu.kanade.tachiyomi.ui.browse.anime.source.globalsearch.GlobalAnimeSearchState +import eu.kanade.tachiyomi.ui.browse.anime.source.globalsearch.AnimeSourceFilter +import eu.kanade.tachiyomi.ui.browse.anime.source.globalsearch.GlobalAnimeSearchScreenModel import eu.kanade.tachiyomi.util.system.LocaleHelper import tachiyomi.domain.entries.anime.model.Anime import tachiyomi.presentation.core.components.material.Scaffold @@ -25,10 +41,13 @@ import tachiyomi.presentation.core.components.material.padding @Composable fun GlobalAnimeSearchScreen( - state: GlobalAnimeSearchState, + state: GlobalAnimeSearchScreenModel.State, + items: Map, navigateUp: () -> Unit, onChangeSearchQuery: (String?) -> Unit, onSearch: (String) -> Unit, + onChangeSearchFilter: (AnimeSourceFilter) -> Unit, + onToggleResults: () -> Unit, getAnime: @Composable (Anime) -> State, onClickSource: (AnimeCatalogueSource) -> Unit, onClickItem: (Anime) -> Unit, @@ -36,19 +55,78 @@ fun GlobalAnimeSearchScreen( ) { Scaffold( topBar = { scrollBehavior -> - GlobalSearchToolbar( - searchQuery = state.searchQuery, - progress = state.progress, - total = state.total, - navigateUp = navigateUp, - onChangeSearchQuery = onChangeSearchQuery, - onSearch = onSearch, - scrollBehavior = scrollBehavior, - ) + Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) { + GlobalSearchToolbar( + searchQuery = state.searchQuery, + progress = state.progress, + total = state.total, + navigateUp = navigateUp, + onChangeSearchQuery = onChangeSearchQuery, + onSearch = onSearch, + scrollBehavior = scrollBehavior, + ) + + Row( + modifier = Modifier + .horizontalScroll(rememberScrollState()) + .padding(horizontal = MaterialTheme.padding.small), + horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), + ) { + // TODO: make this UX better; it only applies when triggering a new search + FilterChip( + selected = state.sourceFilter == AnimeSourceFilter.PinnedOnly, + onClick = { onChangeSearchFilter(AnimeSourceFilter.PinnedOnly) }, + leadingIcon = { + Icon( + imageVector = Icons.Outlined.PushPin, + contentDescription = null, + modifier = Modifier + .size(FilterChipDefaults.IconSize), + ) + }, + label = { + Text(text = stringResource(id = R.string.pinned_sources)) + }, + ) + FilterChip( + selected = state.sourceFilter == AnimeSourceFilter.All, + onClick = { onChangeSearchFilter(AnimeSourceFilter.All) }, + leadingIcon = { + Icon( + imageVector = Icons.Outlined.DoneAll, + contentDescription = null, + modifier = Modifier + .size(FilterChipDefaults.IconSize), + ) + }, + label = { + Text(text = stringResource(id = R.string.all)) + }, + ) + + FilterChip( + selected = state.onlyShowHasResults, + onClick = { onToggleResults() }, + leadingIcon = { + Icon( + imageVector = Icons.Outlined.FilterList, + contentDescription = null, + modifier = Modifier + .size(FilterChipDefaults.IconSize), + ) + }, + label = { + Text(text = stringResource(id = R.string.has_results)) + }, + ) + } + + Divider() + } }, ) { paddingValues -> GlobalAnimeSearchContent( - items = state.items, + items = items, contentPadding = paddingValues, getAnime = getAnime, onClickSource = onClickSource, diff --git a/app/src/main/java/eu/kanade/presentation/browse/manga/BrowseMangaSourceScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/manga/BrowseMangaSourceScreen.kt index b01d85784..5968c08dc 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/manga/BrowseMangaSourceScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/manga/BrowseMangaSourceScreen.kt @@ -61,12 +61,12 @@ fun BrowseSourceContent( if (mangaList.itemCount > 0 && errorState != null && errorState is LoadState.Error) { val result = snackbarHostState.showSnackbar( message = getErrorMessage(errorState), - actionLabel = context.getString(R.string.action_webview_refresh), + actionLabel = context.getString(R.string.action_retry), duration = SnackbarDuration.Indefinite, ) when (result) { SnackbarResult.Dismissed -> snackbarHostState.currentSnackbarData?.dismiss() - SnackbarResult.ActionPerformed -> mangaList.refresh() + SnackbarResult.ActionPerformed -> mangaList.retry() } } } diff --git a/app/src/main/java/eu/kanade/presentation/browse/manga/GlobalMangaSearchScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/manga/GlobalMangaSearchScreen.kt index a8cf3fdd8..23179557a 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/manga/GlobalMangaSearchScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/manga/GlobalMangaSearchScreen.kt @@ -1,8 +1,23 @@ package eu.kanade.presentation.browse.manga +import androidx.compose.foundation.background +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.DoneAll +import androidx.compose.material.icons.outlined.FilterList +import androidx.compose.material.icons.outlined.PushPin +import androidx.compose.material3.Divider +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -16,8 +31,9 @@ import eu.kanade.presentation.browse.GlobalSearchToolbar import eu.kanade.presentation.browse.manga.components.GlobalMangaSearchCardRow import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.source.CatalogueSource -import eu.kanade.tachiyomi.ui.browse.manga.source.globalsearch.GlobalMangaSearchState +import eu.kanade.tachiyomi.ui.browse.manga.source.globalsearch.GlobalMangaSearchScreenModel import eu.kanade.tachiyomi.ui.browse.manga.source.globalsearch.MangaSearchItemResult +import eu.kanade.tachiyomi.ui.browse.manga.source.globalsearch.MangaSourceFilter import eu.kanade.tachiyomi.util.system.LocaleHelper import tachiyomi.domain.entries.manga.model.Manga import tachiyomi.presentation.core.components.material.Scaffold @@ -25,10 +41,13 @@ import tachiyomi.presentation.core.components.material.padding @Composable fun GlobalMangaSearchScreen( - state: GlobalMangaSearchState, + state: GlobalMangaSearchScreenModel.State, + items: Map, navigateUp: () -> Unit, onChangeSearchQuery: (String?) -> Unit, onSearch: (String) -> Unit, + onChangeSearchFilter: (MangaSourceFilter) -> Unit, + onToggleResults: () -> Unit, getManga: @Composable (Manga) -> State, onClickSource: (CatalogueSource) -> Unit, onClickItem: (Manga) -> Unit, @@ -36,19 +55,78 @@ fun GlobalMangaSearchScreen( ) { Scaffold( topBar = { scrollBehavior -> - GlobalSearchToolbar( - searchQuery = state.searchQuery, - progress = state.progress, - total = state.total, - navigateUp = navigateUp, - onChangeSearchQuery = onChangeSearchQuery, - onSearch = onSearch, - scrollBehavior = scrollBehavior, - ) + Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) { + GlobalSearchToolbar( + searchQuery = state.searchQuery, + progress = state.progress, + total = state.total, + navigateUp = navigateUp, + onChangeSearchQuery = onChangeSearchQuery, + onSearch = onSearch, + scrollBehavior = scrollBehavior, + ) + + Row( + modifier = Modifier + .horizontalScroll(rememberScrollState()) + .padding(horizontal = MaterialTheme.padding.small), + horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), + ) { + // TODO: make this UX better; it only applies when triggering a new search + FilterChip( + selected = state.sourceFilter == MangaSourceFilter.PinnedOnly, + onClick = { onChangeSearchFilter(MangaSourceFilter.PinnedOnly) }, + leadingIcon = { + Icon( + imageVector = Icons.Outlined.PushPin, + contentDescription = null, + modifier = Modifier + .size(FilterChipDefaults.IconSize), + ) + }, + label = { + Text(text = stringResource(id = R.string.pinned_sources)) + }, + ) + FilterChip( + selected = state.sourceFilter == MangaSourceFilter.All, + onClick = { onChangeSearchFilter(MangaSourceFilter.All) }, + leadingIcon = { + Icon( + imageVector = Icons.Outlined.DoneAll, + contentDescription = null, + modifier = Modifier + .size(FilterChipDefaults.IconSize), + ) + }, + label = { + Text(text = stringResource(id = R.string.all)) + }, + ) + + FilterChip( + selected = state.onlyShowHasResults, + onClick = { onToggleResults() }, + leadingIcon = { + Icon( + imageVector = Icons.Outlined.FilterList, + contentDescription = null, + modifier = Modifier + .size(FilterChipDefaults.IconSize), + ) + }, + label = { + Text(text = stringResource(id = R.string.has_results)) + }, + ) + } + + Divider() + } }, ) { paddingValues -> GlobalSearchContent( - items = state.items, + items = items, contentPadding = paddingValues, getManga = getManga, onClickSource = onClickSource, diff --git a/app/src/main/java/eu/kanade/presentation/components/SettingsItems.kt b/app/src/main/java/eu/kanade/presentation/components/SettingsItems.kt deleted file mode 100644 index be52fe69e..000000000 --- a/app/src/main/java/eu/kanade/presentation/components/SettingsItems.kt +++ /dev/null @@ -1,242 +0,0 @@ -package eu.kanade.presentation.components - -import android.view.MotionEvent -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -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.widthIn -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.ContentAlpha -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.AddCircle -import androidx.compose.material.icons.outlined.RemoveCircle -import androidx.compose.material.icons.rounded.CheckBox -import androidx.compose.material.icons.rounded.CheckBoxOutlineBlank -import androidx.compose.material.icons.rounded.DisabledByDefault -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.ExposedDropdownMenuBox -import androidx.compose.material3.ExposedDropdownMenuDefaults -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.input.pointer.pointerInteropFilter -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.unit.dp -import kotlinx.coroutines.delay -import tachiyomi.domain.entries.TriStateFilter -import tachiyomi.presentation.core.components.SettingsItemsPaddings - -@Composable -fun TriStateItem( - label: String, - state: TriStateFilter, - enabled: Boolean = true, - onClick: ((TriStateFilter) -> Unit)?, -) { - Row( - modifier = Modifier - .clickable( - enabled = enabled && onClick != null, - onClick = { - when (state) { - TriStateFilter.DISABLED -> onClick?.invoke(TriStateFilter.ENABLED_IS) - TriStateFilter.ENABLED_IS -> onClick?.invoke(TriStateFilter.ENABLED_NOT) - TriStateFilter.ENABLED_NOT -> onClick?.invoke(TriStateFilter.DISABLED) - } - }, - ) - .fillMaxWidth() - .padding( - horizontal = SettingsItemsPaddings.Horizontal, - vertical = SettingsItemsPaddings.Vertical, - ), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(24.dp), - ) { - val stateAlpha = if (enabled && onClick != null) 1f else ContentAlpha.disabled - - Icon( - imageVector = when (state) { - TriStateFilter.DISABLED -> Icons.Rounded.CheckBoxOutlineBlank - TriStateFilter.ENABLED_IS -> Icons.Rounded.CheckBox - TriStateFilter.ENABLED_NOT -> Icons.Rounded.DisabledByDefault - }, - contentDescription = null, - tint = if (!enabled || state == TriStateFilter.DISABLED) { - MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = stateAlpha) - } else { - when (onClick) { - null -> MaterialTheme.colorScheme.onSurface.copy(alpha = ContentAlpha.disabled) - else -> MaterialTheme.colorScheme.primary - } - }, - ) - Text( - text = label, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = stateAlpha), - style = MaterialTheme.typography.bodyMedium, - ) - } -} - -@Composable -fun SelectItem( - label: String, - options: Array, - selectedIndex: Int, - modifier: Modifier = Modifier, - onSelect: (Int) -> Unit, - toString: (T) -> String = { it.toString() }, -) { - var expanded by remember { mutableStateOf(false) } - - ExposedDropdownMenuBox( - modifier = modifier, - expanded = expanded, - onExpandedChange = { expanded = !expanded }, - ) { - OutlinedTextField( - modifier = Modifier - .menuAnchor() - .fillMaxWidth() - .padding( - horizontal = SettingsItemsPaddings.Horizontal, - vertical = SettingsItemsPaddings.Vertical, - ), - label = { Text(text = label) }, - value = toString(options[selectedIndex]), - onValueChange = {}, - readOnly = true, - singleLine = true, - trailingIcon = { - ExposedDropdownMenuDefaults.TrailingIcon( - expanded = expanded, - ) - }, - colors = ExposedDropdownMenuDefaults.textFieldColors(), - ) - - ExposedDropdownMenu( - modifier = Modifier.exposedDropdownSize(matchTextFieldWidth = true), - expanded = expanded, - onDismissRequest = { expanded = false }, - ) { - options.forEachIndexed { index, option -> - DropdownMenuItem( - text = { Text(toString(option)) }, - onClick = { - onSelect(index) - expanded = false - }, - ) - } - } - } -} - -@Composable -fun RepeatingIconButton( - modifier: Modifier = Modifier, - onClick: () -> Unit, - enabled: Boolean = true, - interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, - maxDelayMillis: Long = 750, - minDelayMillis: Long = 5, - delayDecayFactor: Float = .25f, - content: @Composable () -> Unit, -) { - val currentClickListener by rememberUpdatedState(onClick) - var pressed by remember { mutableStateOf(false) } - - IconButton( - modifier = modifier.pointerInteropFilter { - pressed = when (it.action) { - MotionEvent.ACTION_DOWN -> true - - else -> false - } - - true - }, - onClick = {}, - enabled = enabled, - interactionSource = interactionSource, - content = content, - ) - - LaunchedEffect(pressed, enabled) { - var currentDelayMillis = maxDelayMillis - - while (enabled && pressed) { - currentClickListener() - delay(currentDelayMillis) - currentDelayMillis = - (currentDelayMillis - (currentDelayMillis * delayDecayFactor)) - .toLong().coerceAtLeast(minDelayMillis) - } - } -} - -@Composable -fun OutlinedNumericChooser( - label: String, - placeholder: String, - suffix: String, - value: Int, - step: Int, - min: Int? = null, - onValueChanged: (Int) -> Unit, -) { - var currentValue = value - - val updateValue: (Boolean) -> Unit = { - currentValue += if (it) step else -step - - if (min != null) currentValue = if (currentValue < min) min else currentValue - - onValueChanged(currentValue) - } - - Row(verticalAlignment = Alignment.CenterVertically) { - RepeatingIconButton( - onClick = { updateValue(false) }, - ) { Icon(imageVector = Icons.Outlined.RemoveCircle, contentDescription = null) } - - OutlinedTextField( - value = "%d".format(currentValue), - modifier = Modifier.widthIn(min = 140.dp), - - onValueChange = { - // Don't allow multiple decimal points, non-numeric characters, or leading zeros - currentValue = it.trim().replace(Regex("[^-\\d.]"), "").toIntOrNull() - ?: currentValue - onValueChanged(currentValue) - }, - - label = { Text(text = label) }, - placeholder = { Text(text = placeholder) }, - suffix = { Text(text = suffix) }, - - singleLine = true, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - ) - - RepeatingIconButton( - onClick = { updateValue(true) }, - ) { Icon(imageVector = Icons.Outlined.AddCircle, contentDescription = null) } - } -} diff --git a/app/src/main/java/eu/kanade/presentation/components/TabbedDialog.kt b/app/src/main/java/eu/kanade/presentation/components/TabbedDialog.kt index cdb7567f1..bf32bd8bd 100644 --- a/app/src/main/java/eu/kanade/presentation/components/TabbedDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/components/TabbedDialog.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.MoreVert @@ -46,6 +47,7 @@ fun TabbedDialog( onOverflowMenuClicked: (() -> Unit)? = null, overflowIcon: ImageVector? = null, hideSystemBars: Boolean = false, + pagerState: PagerState = rememberPagerState { tabTitles.size }, content: @Composable (Int) -> Unit, ) { AdaptiveSheet( @@ -53,7 +55,6 @@ fun TabbedDialog( onDismissRequest = onDismissRequest, ) { val scope = rememberCoroutineScope() - val pagerState = rememberPagerState { tabTitles.size } Column { Row { diff --git a/app/src/main/java/eu/kanade/presentation/entries/anime/EpisodeSettingsDialog.kt b/app/src/main/java/eu/kanade/presentation/entries/anime/EpisodeSettingsDialog.kt index cd736a7e7..beccb897f 100644 --- a/app/src/main/java/eu/kanade/presentation/entries/anime/EpisodeSettingsDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/entries/anime/EpisodeSettingsDialog.kt @@ -26,20 +26,20 @@ import eu.kanade.domain.entries.anime.model.downloadedFilter import eu.kanade.domain.entries.anime.model.forceDownloaded import eu.kanade.presentation.components.TabbedDialog import eu.kanade.presentation.components.TabbedDialogPaddings -import eu.kanade.presentation.components.TriStateItem import eu.kanade.tachiyomi.R -import tachiyomi.domain.entries.TriStateFilter +import tachiyomi.core.preference.TriState import tachiyomi.domain.entries.anime.model.Anime import tachiyomi.presentation.core.components.RadioItem import tachiyomi.presentation.core.components.SortItem +import tachiyomi.presentation.core.components.TriStateItem @Composable fun EpisodeSettingsDialog( onDismissRequest: () -> Unit, anime: Anime? = null, - onDownloadFilterChanged: (TriStateFilter) -> Unit, - onUnseenFilterChanged: (TriStateFilter) -> Unit, - onBookmarkedFilterChanged: (TriStateFilter) -> Unit, + onDownloadFilterChanged: (TriState) -> Unit, + onUnseenFilterChanged: (TriState) -> Unit, + onBookmarkedFilterChanged: (TriState) -> Unit, onSortModeChanged: (Long) -> Unit, onDisplayModeChanged: (Long) -> Unit, onSetAsDefault: (applyToExistingAnime: Boolean) -> Unit, @@ -77,11 +77,11 @@ fun EpisodeSettingsDialog( when (page) { 0 -> { FilterPage( - downloadFilter = anime?.downloadedFilter ?: TriStateFilter.DISABLED, + downloadFilter = anime?.downloadedFilter ?: TriState.DISABLED, onDownloadFilterChanged = onDownloadFilterChanged.takeUnless { anime?.forceDownloaded() == true }, - unseenFilter = anime?.unseenFilter ?: TriStateFilter.DISABLED, + unseenFilter = anime?.unseenFilter ?: TriState.DISABLED, onUnseenFilterChanged = onUnseenFilterChanged, - bookmarkedFilter = anime?.bookmarkedFilter ?: TriStateFilter.DISABLED, + bookmarkedFilter = anime?.bookmarkedFilter ?: TriState.DISABLED, onBookmarkedFilterChanged = onBookmarkedFilterChanged, ) } @@ -105,12 +105,12 @@ fun EpisodeSettingsDialog( @Composable private fun FilterPage( - downloadFilter: TriStateFilter, - onDownloadFilterChanged: ((TriStateFilter) -> Unit)?, - unseenFilter: TriStateFilter, - onUnseenFilterChanged: (TriStateFilter) -> Unit, - bookmarkedFilter: TriStateFilter, - onBookmarkedFilterChanged: (TriStateFilter) -> Unit, + downloadFilter: TriState, + onDownloadFilterChanged: ((TriState) -> Unit)?, + unseenFilter: TriState, + onUnseenFilterChanged: (TriState) -> Unit, + bookmarkedFilter: TriState, + onBookmarkedFilterChanged: (TriState) -> Unit, ) { TriStateItem( label = stringResource(R.string.label_downloaded), diff --git a/app/src/main/java/eu/kanade/presentation/entries/manga/ChapterSettingsDialog.kt b/app/src/main/java/eu/kanade/presentation/entries/manga/ChapterSettingsDialog.kt index f9b93bbed..8877c5bae 100644 --- a/app/src/main/java/eu/kanade/presentation/entries/manga/ChapterSettingsDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/entries/manga/ChapterSettingsDialog.kt @@ -26,20 +26,20 @@ import eu.kanade.domain.entries.manga.model.downloadedFilter import eu.kanade.domain.entries.manga.model.forceDownloaded import eu.kanade.presentation.components.TabbedDialog import eu.kanade.presentation.components.TabbedDialogPaddings -import eu.kanade.presentation.components.TriStateItem import eu.kanade.tachiyomi.R -import tachiyomi.domain.entries.TriStateFilter +import tachiyomi.core.preference.TriState import tachiyomi.domain.entries.manga.model.Manga import tachiyomi.presentation.core.components.RadioItem import tachiyomi.presentation.core.components.SortItem +import tachiyomi.presentation.core.components.TriStateItem @Composable fun ChapterSettingsDialog( onDismissRequest: () -> Unit, manga: Manga? = null, - onDownloadFilterChanged: (TriStateFilter) -> Unit, - onUnreadFilterChanged: (TriStateFilter) -> Unit, - onBookmarkedFilterChanged: (TriStateFilter) -> Unit, + onDownloadFilterChanged: (TriState) -> Unit, + onUnreadFilterChanged: (TriState) -> Unit, + onBookmarkedFilterChanged: (TriState) -> Unit, onSortModeChanged: (Long) -> Unit, onDisplayModeChanged: (Long) -> Unit, onSetAsDefault: (applyToExistingManga: Boolean) -> Unit, @@ -77,11 +77,11 @@ fun ChapterSettingsDialog( when (page) { 0 -> { FilterPage( - downloadFilter = manga?.downloadedFilter ?: TriStateFilter.DISABLED, + downloadFilter = manga?.downloadedFilter ?: TriState.DISABLED, onDownloadFilterChanged = onDownloadFilterChanged.takeUnless { manga?.forceDownloaded() == true }, - unreadFilter = manga?.unreadFilter ?: TriStateFilter.DISABLED, + unreadFilter = manga?.unreadFilter ?: TriState.DISABLED, onUnreadFilterChanged = onUnreadFilterChanged, - bookmarkedFilter = manga?.bookmarkedFilter ?: TriStateFilter.DISABLED, + bookmarkedFilter = manga?.bookmarkedFilter ?: TriState.DISABLED, onBookmarkedFilterChanged = onBookmarkedFilterChanged, ) } @@ -105,12 +105,12 @@ fun ChapterSettingsDialog( @Composable private fun FilterPage( - downloadFilter: TriStateFilter, - onDownloadFilterChanged: ((TriStateFilter) -> Unit)?, - unreadFilter: TriStateFilter, - onUnreadFilterChanged: (TriStateFilter) -> Unit, - bookmarkedFilter: TriStateFilter, - onBookmarkedFilterChanged: (TriStateFilter) -> Unit, + downloadFilter: TriState, + onDownloadFilterChanged: ((TriState) -> Unit)?, + unreadFilter: TriState, + onUnreadFilterChanged: (TriState) -> Unit, + bookmarkedFilter: TriState, + onBookmarkedFilterChanged: (TriState) -> Unit, ) { TriStateItem( label = stringResource(R.string.label_downloaded), 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 731727b3d..121c73b2f 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 @@ -7,21 +7,18 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll 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.Modifier import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.stringResource import eu.kanade.presentation.components.TabbedDialog import eu.kanade.presentation.components.TabbedDialogPaddings -import eu.kanade.presentation.components.TriStateItem import eu.kanade.presentation.util.collectAsState import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.ui.library.anime.AnimeLibrarySettingsScreenModel +import tachiyomi.core.preference.TriState import tachiyomi.domain.category.model.Category -import tachiyomi.domain.entries.TriStateFilter import tachiyomi.domain.library.anime.model.AnimeLibrarySort import tachiyomi.domain.library.anime.model.sort import tachiyomi.domain.library.model.LibraryDisplayMode @@ -31,6 +28,7 @@ import tachiyomi.presentation.core.components.HeadingItem import tachiyomi.presentation.core.components.RadioItem import tachiyomi.presentation.core.components.SliderItem import tachiyomi.presentation.core.components.SortItem +import tachiyomi.presentation.core.components.TriStateItem @Composable fun AnimeLibrarySettingsDialog( @@ -76,7 +74,7 @@ private fun ColumnScope.FilterPage( TriStateItem( label = stringResource(R.string.label_downloaded), state = if (downloadedOnly) { - TriStateFilter.ENABLED_IS + TriState.ENABLED_IS } else { filterDownloaded }, 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 3a436767d..f5222fc5c 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 @@ -7,21 +7,18 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll 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.Modifier import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.stringResource import eu.kanade.presentation.components.TabbedDialog import eu.kanade.presentation.components.TabbedDialogPaddings -import eu.kanade.presentation.components.TriStateItem import eu.kanade.presentation.util.collectAsState import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.ui.library.manga.MangaLibrarySettingsScreenModel +import tachiyomi.core.preference.TriState import tachiyomi.domain.category.model.Category -import tachiyomi.domain.entries.TriStateFilter import tachiyomi.domain.library.manga.model.MangaLibrarySort import tachiyomi.domain.library.manga.model.sort import tachiyomi.domain.library.model.LibraryDisplayMode @@ -31,6 +28,7 @@ import tachiyomi.presentation.core.components.HeadingItem import tachiyomi.presentation.core.components.RadioItem import tachiyomi.presentation.core.components.SliderItem import tachiyomi.presentation.core.components.SortItem +import tachiyomi.presentation.core.components.TriStateItem @Composable fun MangaLibrarySettingsDialog( @@ -76,7 +74,7 @@ private fun ColumnScope.FilterPage( TriStateItem( label = stringResource(R.string.label_downloaded), state = if (downloadedOnly) { - TriStateFilter.ENABLED_IS + TriState.ENABLED_IS } else { filterDownloaded }, 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 0c7ba6b93..42ad39dd6 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 @@ -87,7 +87,7 @@ internal fun PreferenceItem( min = item.min, max = item.max, value = item.value, - valueText = item.value.toString(), + valueText = item.subtitle.takeUnless { it.isNullOrEmpty() } ?: item.value.toString(), onChange = { scope.launch { item.onValueChanged(it) diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBrowseScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBrowseScreen.kt index 28a327490..ceefbdb32 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBrowseScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBrowseScreen.kt @@ -29,14 +29,6 @@ object SettingsBrowseScreen : SearchableSettings { Preference.PreferenceGroup( title = stringResource(R.string.label_sources), preferenceItems = listOf( - Preference.PreferenceItem.SwitchPreference( - pref = sourcePreferences.searchPinnedAnimeSourcesOnly(), - title = stringResource(R.string.pref_search_pinned_anime_sources_only), - ), - Preference.PreferenceItem.SwitchPreference( - pref = sourcePreferences.searchPinnedMangaSourcesOnly(), - title = stringResource(R.string.pref_search_pinned_manga_sources_only), - ), Preference.PreferenceItem.SwitchPreference( pref = sourcePreferences.hideInAnimeLibraryItems(), title = stringResource(R.string.pref_hide_in_anime_library_items), diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt index 237f83f71..57ba1aba7 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt @@ -394,7 +394,7 @@ object SettingsLibraryScreen : SearchableSettings { preferenceItems = listOf( Preference.PreferenceItem.ListPreference( pref = libraryPreferences.swipeChapterStartAction(), - title = stringResource(R.string.pref_chapter_swipe_end), + title = stringResource(R.string.pref_chapter_swipe_start), entries = mapOf( LibraryPreferences.ChapterSwipeAction.Disabled to stringResource(R.string.action_disable), LibraryPreferences.ChapterSwipeAction.ToggleBookmark to stringResource(R.string.action_bookmark), @@ -404,7 +404,7 @@ object SettingsLibraryScreen : SearchableSettings { ), Preference.PreferenceItem.ListPreference( pref = libraryPreferences.swipeChapterEndAction(), - title = stringResource(R.string.pref_chapter_swipe_start), + title = stringResource(R.string.pref_chapter_swipe_end), entries = mapOf( LibraryPreferences.ChapterSwipeAction.Disabled to stringResource(R.string.action_disable), LibraryPreferences.ChapterSwipeAction.ToggleBookmark to stringResource(R.string.action_bookmark), @@ -425,7 +425,7 @@ object SettingsLibraryScreen : SearchableSettings { preferenceItems = listOf( Preference.PreferenceItem.ListPreference( pref = libraryPreferences.swipeEpisodeStartAction(), - title = stringResource(R.string.pref_episode_swipe_end), + title = stringResource(R.string.pref_episode_swipe_start), entries = mapOf( LibraryPreferences.EpisodeSwipeAction.Disabled to stringResource(R.string.action_disable), LibraryPreferences.EpisodeSwipeAction.ToggleBookmark to stringResource(R.string.action_bookmark_episode), @@ -435,7 +435,7 @@ object SettingsLibraryScreen : SearchableSettings { ), Preference.PreferenceItem.ListPreference( pref = libraryPreferences.swipeEpisodeEndAction(), - title = stringResource(R.string.pref_episode_swipe_start), + title = stringResource(R.string.pref_episode_swipe_end), entries = mapOf( LibraryPreferences.EpisodeSwipeAction.Disabled to stringResource(R.string.action_disable), LibraryPreferences.EpisodeSwipeAction.ToggleBookmark to stringResource(R.string.action_bookmark_episode), diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsReaderScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsReaderScreen.kt index bd77db65d..8268ed29c 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsReaderScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsReaderScreen.kt @@ -18,6 +18,7 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType import eu.kanade.tachiyomi.util.system.isReleaseBuildType import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import java.text.NumberFormat object SettingsReaderScreen : SearchableSettings { @@ -252,11 +253,15 @@ object SettingsReaderScreen : SearchableSettings { @Composable private fun getWebtoonGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup { + val numberFormat = remember { NumberFormat.getPercentInstance() } + val navModePref = readerPreferences.navigationModeWebtoon() val dualPageSplitPref = readerPreferences.dualPageSplitWebtoon() + val webtoonSidePaddingPref = readerPreferences.webtoonSidePadding() val navMode by navModePref.collectAsState() val dualPageSplit by dualPageSplitPref.collectAsState() + val webtoonSidePadding by webtoonSidePaddingPref.collectAsState() return Preference.PreferenceGroup( title = stringResource(R.string.webtoon_viewer), @@ -279,17 +284,16 @@ object SettingsReaderScreen : SearchableSettings { ), enabled = navMode != 5, ), - Preference.PreferenceItem.ListPreference( - pref = readerPreferences.webtoonSidePadding(), + Preference.PreferenceItem.SliderPreference( + value = webtoonSidePadding, title = stringResource(R.string.pref_webtoon_side_padding), - entries = mapOf( - 0 to stringResource(R.string.webtoon_side_padding_0), - 5 to stringResource(R.string.webtoon_side_padding_5), - 10 to stringResource(R.string.webtoon_side_padding_10), - 15 to stringResource(R.string.webtoon_side_padding_15), - 20 to stringResource(R.string.webtoon_side_padding_20), - 25 to stringResource(R.string.webtoon_side_padding_25), - ), + subtitle = numberFormat.format(webtoonSidePadding / 100f), + min = ReaderPreferences.WEBTOON_PADDING_MIN, + max = ReaderPreferences.WEBTOON_PADDING_MAX, + onValueChanged = { + webtoonSidePaddingPref.set(it) + true + }, ), Preference.PreferenceItem.ListPreference( pref = readerPreferences.readerHideThreshold(), diff --git a/app/src/main/java/eu/kanade/presentation/more/storage/SelectStorageCategory.kt b/app/src/main/java/eu/kanade/presentation/more/storage/SelectStorageCategory.kt index 20dfea445..b98bdebb7 100644 --- a/app/src/main/java/eu/kanade/presentation/more/storage/SelectStorageCategory.kt +++ b/app/src/main/java/eu/kanade/presentation/more/storage/SelectStorageCategory.kt @@ -4,9 +4,9 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import eu.kanade.presentation.components.SelectItem import eu.kanade.tachiyomi.R import tachiyomi.domain.category.model.Category +import tachiyomi.presentation.core.components.SelectItem @Composable fun SelectStorageCategory( diff --git a/app/src/main/java/eu/kanade/presentation/reader/settings/ColorFilterPage.kt b/app/src/main/java/eu/kanade/presentation/reader/settings/ColorFilterPage.kt new file mode 100644 index 000000000..06bbb37b1 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/reader/settings/ColorFilterPage.kt @@ -0,0 +1,158 @@ +package eu.kanade.presentation.reader.settings + +import android.os.Build +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.res.stringResource +import androidx.core.graphics.alpha +import androidx.core.graphics.blue +import androidx.core.graphics.green +import androidx.core.graphics.red +import eu.kanade.presentation.util.collectAsState +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences +import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel +import tachiyomi.core.preference.getAndSet +import tachiyomi.presentation.core.components.CheckboxItem +import tachiyomi.presentation.core.components.SelectItem +import tachiyomi.presentation.core.components.SliderItem + +@Composable +internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel) { + 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 screenModel.preferences.customBrightness().collectAsState() + CheckboxItem( + label = stringResource(R.string.pref_custom_brightness), + checked = customBrightness, + onClick = { + screenModel.togglePreference(ReaderPreferences::customBrightness) + }, + ) + + /** + * 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. + */ + if (customBrightness) { + val customBrightnessValue by screenModel.preferences.customBrightnessValue().collectAsState() + SliderItem( + label = stringResource(R.string.pref_custom_brightness), + min = -75, + max = 100, + value = customBrightnessValue, + valueText = customBrightnessValue.toString(), + onChange = { screenModel.preferences.customBrightnessValue().set(it) }, + ) + } + + val colorFilter by screenModel.preferences.colorFilter().collectAsState() + CheckboxItem( + label = stringResource(R.string.pref_custom_color_filter), + checked = colorFilter, + onClick = { + screenModel.togglePreference(ReaderPreferences::colorFilter) + }, + ) + if (colorFilter) { + val colorFilterValue by screenModel.preferences.colorFilterValue().collectAsState() + SliderItem( + label = stringResource(R.string.color_filter_r_value), + max = 255, + value = colorFilterValue.red, + valueText = colorFilterValue.red.toString(), + onChange = { newRValue -> + screenModel.preferences.colorFilterValue().getAndSet { + getColorValue(it, newRValue, RED_MASK, 16) + } + }, + ) + SliderItem( + label = stringResource(R.string.color_filter_g_value), + max = 255, + value = colorFilterValue.green, + valueText = colorFilterValue.green.toString(), + onChange = { newGValue -> + screenModel.preferences.colorFilterValue().getAndSet { + getColorValue(it, newGValue, GREEN_MASK, 8) + } + }, + ) + SliderItem( + label = stringResource(R.string.color_filter_b_value), + max = 255, + value = colorFilterValue.blue, + valueText = colorFilterValue.blue.toString(), + onChange = { newBValue -> + screenModel.preferences.colorFilterValue().getAndSet { + getColorValue(it, newBValue, BLUE_MASK, 0) + } + }, + ) + SliderItem( + label = stringResource(R.string.color_filter_a_value), + max = 255, + value = colorFilterValue.alpha, + valueText = colorFilterValue.alpha.toString(), + onChange = { newAValue -> + screenModel.preferences.colorFilterValue().getAndSet { + getColorValue(it, newAValue, ALPHA_MASK, 24) + } + }, + ) + + val colorFilterMode by screenModel.preferences.colorFilterMode().collectAsState() + SelectItem( + label = stringResource(R.string.pref_color_filter_mode), + options = colorFilterModes.toTypedArray(), + selectedIndex = colorFilterMode, + ) { + screenModel.preferences.colorFilterMode().set(it) + } + } + + val grayscale by screenModel.preferences.grayscale().collectAsState() + CheckboxItem( + label = stringResource(R.string.pref_grayscale), + checked = grayscale, + onClick = { + screenModel.togglePreference(ReaderPreferences::grayscale) + }, + ) + val invertedColors by screenModel.preferences.invertedColors().collectAsState() + CheckboxItem( + label = stringResource(R.string.pref_inverted_colors), + checked = invertedColors, + onClick = { + screenModel.togglePreference(ReaderPreferences::invertedColors) + }, + ) +} + +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/presentation/reader/settings/GeneralSettingsPage.kt b/app/src/main/java/eu/kanade/presentation/reader/settings/GeneralSettingsPage.kt new file mode 100644 index 000000000..02738eeda --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/reader/settings/GeneralSettingsPage.kt @@ -0,0 +1,96 @@ +package eu.kanade.presentation.reader.settings + +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.res.stringResource +import eu.kanade.presentation.util.collectAsState +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences +import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel +import tachiyomi.presentation.core.components.CheckboxItem +import tachiyomi.presentation.core.components.HeadingItem +import tachiyomi.presentation.core.components.RadioItem + +@Composable +internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) { + // TODO: show this in a nicer way + HeadingItem(R.string.pref_reader_theme) + val readerTheme by screenModel.preferences.readerTheme().collectAsState() + listOf( + R.string.black_background to 1, + R.string.gray_background to 2, + R.string.white_background to 0, + R.string.automatic_background to 3, + ).map { (titleRes, theme) -> + RadioItem( + label = stringResource(titleRes), + selected = readerTheme == theme, + onClick = { screenModel.preferences.readerTheme().set(theme) }, + ) + } + + val showPageNumber by screenModel.preferences.showPageNumber().collectAsState() + CheckboxItem( + label = stringResource(R.string.pref_show_page_number), + checked = showPageNumber, + onClick = { + screenModel.togglePreference(ReaderPreferences::showPageNumber) + }, + ) + + val fullscreen by screenModel.preferences.fullscreen().collectAsState() + CheckboxItem( + label = stringResource(R.string.pref_fullscreen), + checked = fullscreen, + onClick = { + screenModel.togglePreference(ReaderPreferences::fullscreen) + }, + ) + + // TODO: hide if there's no cutout + val cutoutShort by screenModel.preferences.cutoutShort().collectAsState() + CheckboxItem( + label = stringResource(R.string.pref_cutout_short), + checked = cutoutShort, + onClick = { + screenModel.togglePreference(ReaderPreferences::cutoutShort) + }, + ) + + val keepScreenOn by screenModel.preferences.keepScreenOn().collectAsState() + CheckboxItem( + label = stringResource(R.string.pref_keep_screen_on), + checked = keepScreenOn, + onClick = { + screenModel.togglePreference(ReaderPreferences::keepScreenOn) + }, + ) + + val readWithLongTap by screenModel.preferences.readWithLongTap().collectAsState() + CheckboxItem( + label = stringResource(R.string.pref_read_with_long_tap), + checked = readWithLongTap, + onClick = { + screenModel.togglePreference(ReaderPreferences::readWithLongTap) + }, + ) + + val alwaysShowChapterTransition by screenModel.preferences.alwaysShowChapterTransition().collectAsState() + CheckboxItem( + label = stringResource(R.string.pref_always_show_chapter_transition), + checked = alwaysShowChapterTransition, + onClick = { + screenModel.togglePreference(ReaderPreferences::alwaysShowChapterTransition) + }, + ) + + val pageTransitions by screenModel.preferences.pageTransitions().collectAsState() + CheckboxItem( + label = stringResource(R.string.pref_page_transitions), + checked = pageTransitions, + onClick = { + screenModel.togglePreference(ReaderPreferences::pageTransitions) + }, + ) +} diff --git a/app/src/main/java/eu/kanade/presentation/reader/settings/ReaderSettingsDialog.kt b/app/src/main/java/eu/kanade/presentation/reader/settings/ReaderSettingsDialog.kt new file mode 100644 index 000000000..44f1bc27f --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/reader/settings/ReaderSettingsDialog.kt @@ -0,0 +1,66 @@ +package eu.kanade.presentation.reader.settings + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.window.DialogWindowProvider +import eu.kanade.presentation.components.TabbedDialog +import eu.kanade.presentation.components.TabbedDialogPaddings +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel + +@Composable +fun ReaderSettingsDialog( + onDismissRequest: () -> Unit, + onShowMenus: () -> Unit, + onHideMenus: () -> Unit, + screenModel: ReaderSettingsScreenModel, +) { + // TODO: undimming doesn't seem to work + val window = (LocalView.current.parent as? DialogWindowProvider)?.window + + val tabTitles = listOf( + stringResource(R.string.pref_category_reading_mode), + stringResource(R.string.pref_category_general), + stringResource(R.string.custom_filter), + ) + val pagerState = rememberPagerState { tabTitles.size } + + LaunchedEffect(pagerState.currentPage) { + if (pagerState.currentPage == 2) { + window?.setDimAmount(0f) + onHideMenus() + } else { + window?.setDimAmount(0.75f) + onShowMenus() + } + } + + TabbedDialog( + onDismissRequest = { + onDismissRequest() + onShowMenus() + }, + tabTitles = tabTitles, + pagerState = pagerState, + ) { page -> + Column( + modifier = Modifier + .padding(vertical = TabbedDialogPaddings.Vertical) + .verticalScroll(rememberScrollState()), + ) { + when (page) { + 0 -> ReadingModePage(screenModel) + 1 -> GeneralPage(screenModel) + 2 -> ColorFilterPage(screenModel) + } + } + } +} diff --git a/app/src/main/java/eu/kanade/presentation/reader/settings/ReadingModePage.kt b/app/src/main/java/eu/kanade/presentation/reader/settings/ReadingModePage.kt new file mode 100644 index 000000000..ff710a76d --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/reader/settings/ReadingModePage.kt @@ -0,0 +1,179 @@ +package eu.kanade.presentation.reader.settings + +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.res.stringResource +import eu.kanade.presentation.util.collectAsState +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences +import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel +import eu.kanade.tachiyomi.util.system.isReleaseBuildType +import tachiyomi.presentation.core.components.CheckboxItem +import tachiyomi.presentation.core.components.HeadingItem +import tachiyomi.presentation.core.components.SliderItem +import java.text.NumberFormat + +@Composable +internal fun ColumnScope.ReadingModePage(screenModel: ReaderSettingsScreenModel) { + HeadingItem(R.string.pref_category_for_this_series) + + // Reading mode + // Rotation type + + // if (pager) + PagerViewerSettings(screenModel) + + WebtoonViewerSettings(screenModel) +} + +@Composable +private fun ColumnScope.PagerViewerSettings(screenModel: ReaderSettingsScreenModel) { + HeadingItem(R.string.pager_viewer) + + // Tap zones + // Invert tap zones + // Scale type + // Zoom start position + + val cropBorders by screenModel.preferences.cropBorders().collectAsState() + CheckboxItem( + label = stringResource(R.string.pref_crop_borders), + checked = cropBorders, + onClick = { + screenModel.togglePreference(ReaderPreferences::cropBorders) + }, + ) + + val landscapeZoom by screenModel.preferences.landscapeZoom().collectAsState() + CheckboxItem( + label = stringResource(R.string.pref_landscape_zoom), + checked = landscapeZoom, + onClick = { + screenModel.togglePreference(ReaderPreferences::landscapeZoom) + }, + ) + + val navigateToPan by screenModel.preferences.navigateToPan().collectAsState() + CheckboxItem( + label = stringResource(R.string.pref_navigate_pan), + checked = navigateToPan, + onClick = { + screenModel.togglePreference(ReaderPreferences::navigateToPan) + }, + ) + + val dualPageSplitPaged by screenModel.preferences.dualPageSplitPaged().collectAsState() + CheckboxItem( + label = stringResource(R.string.pref_dual_page_split), + checked = dualPageSplitPaged, + onClick = { + screenModel.togglePreference(ReaderPreferences::dualPageSplitPaged) + }, + ) + + if (dualPageSplitPaged) { + val dualPageInvertPaged by screenModel.preferences.dualPageInvertPaged().collectAsState() + CheckboxItem( + label = stringResource(R.string.pref_dual_page_invert), + checked = dualPageInvertPaged, + onClick = { + screenModel.togglePreference(ReaderPreferences::dualPageInvertPaged) + }, + ) + } + + val dualPageRotateToFit by screenModel.preferences.dualPageRotateToFit().collectAsState() + CheckboxItem( + label = stringResource(R.string.pref_page_rotate), + checked = dualPageRotateToFit, + onClick = { + screenModel.togglePreference(ReaderPreferences::dualPageRotateToFit) + }, + ) + + if (dualPageRotateToFit) { + val dualPageRotateToFitInvert by screenModel.preferences.dualPageRotateToFitInvert().collectAsState() + CheckboxItem( + label = stringResource(R.string.pref_page_rotate_invert), + checked = dualPageRotateToFitInvert, + onClick = { + screenModel.togglePreference(ReaderPreferences::dualPageRotateToFitInvert) + }, + ) + } +} + +@Composable +private fun ColumnScope.WebtoonViewerSettings(screenModel: ReaderSettingsScreenModel) { + val numberFormat = remember { NumberFormat.getPercentInstance() } + + HeadingItem(R.string.webtoon_viewer) + + // TODO: Tap zones + // TODO: Invert tap zones + + val webtoonSidePadding by screenModel.preferences.webtoonSidePadding().collectAsState() + SliderItem( + label = stringResource(R.string.pref_webtoon_side_padding), + min = ReaderPreferences.WEBTOON_PADDING_MIN, + max = ReaderPreferences.WEBTOON_PADDING_MAX, + value = webtoonSidePadding, + valueText = numberFormat.format(webtoonSidePadding / 100f), + onChange = { + screenModel.preferences.webtoonSidePadding().set(it) + }, + ) + + val cropBordersWebtoon by screenModel.preferences.cropBordersWebtoon().collectAsState() + CheckboxItem( + label = stringResource(R.string.pref_crop_borders), + checked = cropBordersWebtoon, + onClick = { + screenModel.togglePreference(ReaderPreferences::cropBordersWebtoon) + }, + ) + + val dualPageSplitWebtoon by screenModel.preferences.dualPageSplitWebtoon().collectAsState() + CheckboxItem( + label = stringResource(R.string.pref_dual_page_split), + checked = dualPageSplitWebtoon, + onClick = { + screenModel.togglePreference(ReaderPreferences::dualPageSplitWebtoon) + }, + ) + + if (dualPageSplitWebtoon) { + val dualPageInvertWebtoon by screenModel.preferences.dualPageInvertWebtoon() + .collectAsState() + CheckboxItem( + label = stringResource(R.string.pref_dual_page_invert), + checked = dualPageInvertWebtoon, + onClick = { + screenModel.togglePreference(ReaderPreferences::dualPageInvertWebtoon) + }, + ) + } + + if (!isReleaseBuildType) { + val longStripSplitWebtoon by screenModel.preferences.longStripSplitWebtoon() + .collectAsState() + CheckboxItem( + label = stringResource(R.string.pref_long_strip_split), + checked = longStripSplitWebtoon, + onClick = { + screenModel.togglePreference(ReaderPreferences::longStripSplitWebtoon) + }, + ) + } + + val webtoonDoubleTapZoomEnabled by screenModel.preferences.webtoonDoubleTapZoomEnabled().collectAsState() + CheckboxItem( + label = stringResource(R.string.pref_double_tap_zoom), + checked = webtoonDoubleTapZoomEnabled, + onClick = { + screenModel.togglePreference(ReaderPreferences::webtoonDoubleTapZoomEnabled) + }, + ) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt index 9b135b451..88f88f75b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt @@ -23,12 +23,11 @@ import eu.kanade.tachiyomi.util.system.isReleaseBuildType import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.workManager import tachiyomi.core.preference.PreferenceStore +import tachiyomi.core.preference.TriState import tachiyomi.core.preference.getEnum import tachiyomi.domain.backup.service.BackupPreferences -import tachiyomi.domain.entries.TriStateFilter import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_NON_COMPLETED -import uy.kohesive.injekt.api.get import java.io.File object Migrations { @@ -434,12 +433,12 @@ object Migrations { remove(key) val newValue = when (pref.get()) { - 1 -> TriStateFilter.ENABLED_IS - 2 -> TriStateFilter.ENABLED_NOT - else -> TriStateFilter.DISABLED + 1 -> TriState.ENABLED_IS + 2 -> TriState.ENABLED_NOT + else -> TriState.DISABLED } - preferenceStore.getEnum("${key}_v2", TriStateFilter.DISABLED).set(newValue) + preferenceStore.getEnum("${key}_v2", TriState.DISABLED).set(newValue) } } } 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 5a287a049..83c81622f 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 @@ -41,7 +41,12 @@ internal object AnimeExtensionLoader { const val LIB_VERSION_MIN = 12 const val LIB_VERSION_MAX = 15 - private const val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES + private val PACKAGE_FLAGS = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNING_CERTIFICATES + } else { + @Suppress("DEPRECATION") + PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES + } // jmir1's key private const val officialSignature = "50ab1d1e3a20d204d0ad6d334c7691c632e41b98dfa132bf385695fdfa63839c" @@ -49,7 +54,7 @@ internal object AnimeExtensionLoader { /** * List of the trusted signatures. */ - var trustedSignatures = mutableSetOf() + preferences.trustedSignatures().get() + officialSignature + var trustedSignatures = mutableSetOf(officialSignature) + preferences.trustedSignatures().get() /** * Return a list of all the installed extensions initialized concurrently. @@ -59,7 +64,6 @@ internal object AnimeExtensionLoader { fun loadExtensions(context: Context): List { val pkgManager = context.packageManager - @Suppress("DEPRECATION") val installedPkgs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { pkgManager.getInstalledPackages(PackageManager.PackageInfoFlags.of(PACKAGE_FLAGS.toLong())) } else { @@ -135,7 +139,7 @@ internal object AnimeExtensionLoader { return AnimeLoadResult.Error } - val signatureHash = getSignatureHash(pkgInfo) + val signatureHash = getSignatureHash(context, pkgInfo) if (signatureHash == null) { logcat(LogPriority.WARN) { "Package $pkgName isn't signed" } @@ -221,12 +225,8 @@ internal object AnimeExtensionLoader { * * @param pkgInfo The package info of the application. */ - private fun getSignatureHash(pkgInfo: PackageInfo): String? { - val signatures = pkgInfo.signatures - return if (signatures != null && signatures.isNotEmpty()) { - Hash.sha256(signatures.first().toByteArray()) - } else { - null - } + private fun getSignatureHash(context: Context, pkgInfo: PackageInfo): String? { + val signatures = PackageInfoCompat.getSignatures(context.packageManager, pkgInfo.packageName) + return signatures.firstOrNull()?.let { Hash.sha256(it.toByteArray()) } } } 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 bf02240de..83d7d1a99 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 @@ -41,7 +41,12 @@ internal object MangaExtensionLoader { const val LIB_VERSION_MIN = 1.2 const val LIB_VERSION_MAX = 1.5 - private const val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES + private val PACKAGE_FLAGS = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNING_CERTIFICATES + } else { + @Suppress("DEPRECATION") + PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES + } // inorichi's key private const val officialSignature = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23" @@ -49,7 +54,7 @@ internal object MangaExtensionLoader { /** * List of the trusted signatures. */ - var trustedSignatures = mutableSetOf() + preferences.trustedSignatures().get() + officialSignature + var trustedSignatures = mutableSetOf(officialSignature) + preferences.trustedSignatures().get() /** * Return a list of all the installed extensions initialized concurrently. @@ -59,7 +64,6 @@ internal object MangaExtensionLoader { fun loadMangaExtensions(context: Context): List { val pkgManager = context.packageManager - @Suppress("DEPRECATION") val installedPkgs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { pkgManager.getInstalledPackages(PackageManager.PackageInfoFlags.of(PACKAGE_FLAGS.toLong())) } else { @@ -135,7 +139,7 @@ internal object MangaExtensionLoader { return MangaLoadResult.Error } - val signatureHash = getSignatureHash(pkgInfo) + val signatureHash = getSignatureHash(context, pkgInfo) if (signatureHash == null) { logcat(LogPriority.WARN) { "Package $pkgName isn't signed" } @@ -221,12 +225,8 @@ internal object MangaExtensionLoader { * * @param pkgInfo The package info of the application. */ - private fun getSignatureHash(pkgInfo: PackageInfo): String? { - val signatures = pkgInfo.signatures - return if (signatures != null && signatures.isNotEmpty()) { - Hash.sha256(signatures.first().toByteArray()) - } else { - null - } + private fun getSignatureHash(context: Context, pkgInfo: PackageInfo): String? { + val signatures = PackageInfoCompat.getSignatures(context.packageManager, pkgInfo.packageName) + return signatures.firstOrNull()?.let { Hash.sha256(it.toByteArray()) } } } 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 f688ced8d..c19ef1353 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 @@ -16,6 +16,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf @@ -313,6 +314,7 @@ internal class MigrateAnimeDialogScreenModel( ) } + @Immutable data class State( val isMigrating: Boolean = false, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/migration/search/MigrateAnimeSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/migration/search/MigrateAnimeSearchScreen.kt index f04b83a56..459efee28 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/migration/search/MigrateAnimeSearchScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/migration/search/MigrateAnimeSearchScreen.kt @@ -10,6 +10,7 @@ import eu.kanade.presentation.browse.anime.MigrateAnimeSearchScreen import eu.kanade.presentation.util.Screen import eu.kanade.tachiyomi.ui.entries.anime.AnimeScreen +// TODO: this should probably be merged with GlobalSearchScreen somehow to dedupe logic class MigrateAnimeSearchScreen(private val animeId: Long) : Screen() { @Composable diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/source/browse/SourceFilterAnimeDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/source/browse/SourceFilterAnimeDialog.kt index e4fec475b..be4e8b42b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/source/browse/SourceFilterAnimeDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/source/browse/SourceFilterAnimeDialog.kt @@ -18,17 +18,17 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import eu.kanade.presentation.components.AdaptiveSheet -import eu.kanade.presentation.components.SelectItem -import eu.kanade.presentation.components.TriStateItem import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.animesource.model.AnimeFilter import eu.kanade.tachiyomi.animesource.model.AnimeFilterList -import tachiyomi.domain.entries.TriStateFilter +import tachiyomi.core.preference.TriState import tachiyomi.presentation.core.components.CheckboxItem import tachiyomi.presentation.core.components.CollapsibleBox import tachiyomi.presentation.core.components.HeadingItem +import tachiyomi.presentation.core.components.SelectItem import tachiyomi.presentation.core.components.SortItem import tachiyomi.presentation.core.components.TextItem +import tachiyomi.presentation.core.components.TriStateItem @Composable fun SourceFilterAnimeDialog( @@ -165,19 +165,19 @@ private fun FilterItem(filter: AnimeFilter<*>, onUpdate: () -> Unit) { } } -private fun Int.toTriStateFilter(): TriStateFilter { +private fun Int.toTriStateFilter(): TriState { return when (this) { - AnimeFilter.TriState.STATE_IGNORE -> TriStateFilter.DISABLED - AnimeFilter.TriState.STATE_INCLUDE -> TriStateFilter.ENABLED_IS - AnimeFilter.TriState.STATE_EXCLUDE -> TriStateFilter.ENABLED_NOT + AnimeFilter.TriState.STATE_IGNORE -> TriState.DISABLED + AnimeFilter.TriState.STATE_INCLUDE -> TriState.ENABLED_IS + AnimeFilter.TriState.STATE_EXCLUDE -> TriState.ENABLED_NOT else -> throw IllegalStateException("Unknown TriState state: $this") } } -private fun TriStateFilter.toTriStateInt(): Int { +private fun TriState.toTriStateInt(): Int { return when (this) { - TriStateFilter.DISABLED -> AnimeFilter.TriState.STATE_IGNORE - TriStateFilter.ENABLED_IS -> AnimeFilter.TriState.STATE_INCLUDE - TriStateFilter.ENABLED_NOT -> AnimeFilter.TriState.STATE_EXCLUDE + TriState.DISABLED -> AnimeFilter.TriState.STATE_IGNORE + TriState.ENABLED_IS -> AnimeFilter.TriState.STATE_INCLUDE + TriState.ENABLED_NOT -> AnimeFilter.TriState.STATE_EXCLUDE } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/source/globalsearch/AnimeSearchScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/source/globalsearch/AnimeSearchScreenModel.kt index a69fb910f..e445b4f26 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/source/globalsearch/AnimeSearchScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/source/globalsearch/AnimeSearchScreenModel.kt @@ -68,16 +68,7 @@ abstract class AnimeSearchScreenModel( val enabledSources = getEnabledSources() if (filter.isEmpty()) { - val shouldSearchPinnedOnly = sourcePreferences.searchPinnedAnimeSourcesOnly().get() - val pinnedSources = sourcePreferences.pinnedAnimeSources().get() - - return enabledSources.filter { - if (shouldSearchPinnedOnly) { - "${it.id}" in pinnedSources - } else { - true - } - } + return enabledSources } return extensionManager.installedExtensionsFlow.value @@ -136,6 +127,11 @@ abstract class AnimeSearchScreenModel( } } +enum class AnimeSourceFilter { + All, + PinnedOnly, +} + sealed class AnimeSearchItemResult { object Loading : AnimeSearchItemResult() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/source/globalsearch/GlobalAnimeSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/source/globalsearch/GlobalAnimeSearchScreen.kt index 3f211d35e..a0b8e13b4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/source/globalsearch/GlobalAnimeSearchScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/source/globalsearch/GlobalAnimeSearchScreen.kt @@ -35,6 +35,7 @@ class GlobalAnimeSearchScreen( var showSingleLoadingScreen by remember { mutableStateOf(searchQuery.isNotEmpty() && extensionFilter.isNotEmpty() && state.total == 1) } + val filteredSources by screenModel.searchPagerFlow.collectAsState() if (showSingleLoadingScreen) { LoadingScreen() @@ -57,10 +58,13 @@ class GlobalAnimeSearchScreen( } else { GlobalAnimeSearchScreen( state = state, + items = filteredSources, navigateUp = navigator::pop, onChangeSearchQuery = screenModel::updateSearchQuery, onSearch = screenModel::search, getAnime = { screenModel.getAnime(it) }, + onChangeSearchFilter = screenModel::setSourceFilter, + onToggleResults = screenModel::toggleFilterResults, onClickSource = { if (!screenModel.incognitoMode.get()) { screenModel.lastUsedSourceId.set(it.id) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/source/globalsearch/GlobalAnimeSearchScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/source/globalsearch/GlobalAnimeSearchScreenModel.kt index 874345e5b..d1bdbef23 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/source/globalsearch/GlobalAnimeSearchScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/source/globalsearch/GlobalAnimeSearchScreenModel.kt @@ -3,7 +3,12 @@ package eu.kanade.tachiyomi.ui.browse.anime.source.globalsearch import androidx.compose.runtime.Immutable import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.source.service.SourcePreferences +import eu.kanade.presentation.util.ioCoroutineScope import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import tachiyomi.domain.source.anime.service.AnimeSourceManager import uy.kohesive.injekt.Injekt @@ -15,8 +20,8 @@ class GlobalAnimeSearchScreenModel( preferences: BasePreferences = Injekt.get(), private val sourcePreferences: SourcePreferences = Injekt.get(), private val sourceManager: AnimeSourceManager = Injekt.get(), -) : AnimeSearchScreenModel( - GlobalAnimeSearchState( +) : AnimeSearchScreenModel( + State( searchQuery = initialQuery, ), ) { @@ -24,6 +29,13 @@ class GlobalAnimeSearchScreenModel( val incognitoMode = preferences.incognitoMode() val lastUsedSourceId = sourcePreferences.lastUsedAnimeSource() + val searchPagerFlow = state.map { Pair(it.onlyShowHasResults, it.items) } + .distinctUntilChanged() + .map { (onlyShowHasResults, items) -> + items.filter { (_, result) -> result.isVisible(onlyShowHasResults) } + } + .stateIn(ioCoroutineScope, SharingStarted.Lazily, state.value.items) + init { extensionFilter = initialExtensionFilter if (initialQuery.isNotBlank() || initialExtensionFilter.isNotBlank()) { @@ -37,6 +49,7 @@ class GlobalAnimeSearchScreenModel( val pinnedSources = sourcePreferences.pinnedAnimeSources().get() return sourceManager.getCatalogueSources() + .filter { mutableState.value.sourceFilter != AnimeSourceFilter.PinnedOnly || "${it.id}" in pinnedSources } .filter { it.lang in enabledLanguages } .filterNot { "${it.id}" in disabledSources } .sortedWith(compareBy({ "${it.id}" !in pinnedSources }, { "${it.name.lowercase()} (${it.lang})" })) @@ -57,15 +70,29 @@ class GlobalAnimeSearchScreenModel( override fun getItems(): Map { return mutableState.value.items } -} - -@Immutable -data class GlobalAnimeSearchState( - val searchQuery: String? = null, - val items: Map = emptyMap(), -) { - - val progress: Int = items.count { it.value !is AnimeSearchItemResult.Loading } - - val total: Int = items.size + + fun setSourceFilter(filter: AnimeSourceFilter) { + mutableState.update { it.copy(sourceFilter = filter) } + } + + fun toggleFilterResults() { + mutableState.update { + it.copy(onlyShowHasResults = !it.onlyShowHasResults) + } + } + + private fun AnimeSearchItemResult.isVisible(onlyShowHasResults: Boolean): Boolean { + return !onlyShowHasResults || (this is AnimeSearchItemResult.Success && !this.isEmpty) + } + + @Immutable + data class State( + val searchQuery: String? = null, + val sourceFilter: AnimeSourceFilter = AnimeSourceFilter.PinnedOnly, + val onlyShowHasResults: Boolean = false, + val items: Map = emptyMap(), + ) { + val progress: Int = items.count { it.value !is AnimeSearchItemResult.Loading } + val total: Int = items.size + } } 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 89d36ef0e..3bb265c8a 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 @@ -16,6 +16,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf @@ -313,6 +314,7 @@ internal class MigrateMangaDialogScreenModel( ) } + @Immutable data class State( val isMigrating: Boolean = false, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/migration/search/MigrateMangaSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/migration/search/MigrateMangaSearchScreen.kt index 153d17dff..1d7fa6988 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/migration/search/MigrateMangaSearchScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/migration/search/MigrateMangaSearchScreen.kt @@ -10,6 +10,7 @@ import eu.kanade.presentation.browse.manga.MigrateMangaSearchScreen import eu.kanade.presentation.util.Screen import eu.kanade.tachiyomi.ui.entries.manga.MangaScreen +// TODO: this should probably be merged with GlobalSearchScreen somehow to dedupe logic class MigrateSearchScreen(private val mangaId: Long) : Screen() { @Composable diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/source/browse/SourceFilterMangaDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/source/browse/SourceFilterMangaDialog.kt index a72cbbdb7..93c4f758c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/source/browse/SourceFilterMangaDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/source/browse/SourceFilterMangaDialog.kt @@ -16,17 +16,17 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import eu.kanade.presentation.components.AdaptiveSheet -import eu.kanade.presentation.components.SelectItem -import eu.kanade.presentation.components.TriStateItem import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.FilterList -import tachiyomi.domain.entries.TriStateFilter +import tachiyomi.core.preference.TriState import tachiyomi.presentation.core.components.CheckboxItem import tachiyomi.presentation.core.components.CollapsibleBox import tachiyomi.presentation.core.components.HeadingItem +import tachiyomi.presentation.core.components.SelectItem import tachiyomi.presentation.core.components.SortItem import tachiyomi.presentation.core.components.TextItem +import tachiyomi.presentation.core.components.TriStateItem import tachiyomi.presentation.core.components.material.Button import tachiyomi.presentation.core.components.material.Divider @@ -165,19 +165,19 @@ private fun FilterItem(filter: Filter<*>, onUpdate: () -> Unit) { } } -private fun Int.toTriStateFilter(): TriStateFilter { +private fun Int.toTriStateFilter(): TriState { return when (this) { - Filter.TriState.STATE_IGNORE -> TriStateFilter.DISABLED - Filter.TriState.STATE_INCLUDE -> TriStateFilter.ENABLED_IS - Filter.TriState.STATE_EXCLUDE -> TriStateFilter.ENABLED_NOT + Filter.TriState.STATE_IGNORE -> TriState.DISABLED + Filter.TriState.STATE_INCLUDE -> TriState.ENABLED_IS + Filter.TriState.STATE_EXCLUDE -> TriState.ENABLED_NOT else -> throw IllegalStateException("Unknown TriState state: $this") } } -private fun TriStateFilter.toTriStateInt(): Int { +private fun TriState.toTriStateInt(): Int { return when (this) { - TriStateFilter.DISABLED -> Filter.TriState.STATE_IGNORE - TriStateFilter.ENABLED_IS -> Filter.TriState.STATE_INCLUDE - TriStateFilter.ENABLED_NOT -> Filter.TriState.STATE_EXCLUDE + TriState.DISABLED -> Filter.TriState.STATE_IGNORE + TriState.ENABLED_IS -> Filter.TriState.STATE_INCLUDE + TriState.ENABLED_NOT -> Filter.TriState.STATE_EXCLUDE } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/source/globalsearch/GlobalMangaSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/source/globalsearch/GlobalMangaSearchScreen.kt index 1570652a5..f0387d549 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/source/globalsearch/GlobalMangaSearchScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/source/globalsearch/GlobalMangaSearchScreen.kt @@ -35,6 +35,7 @@ class GlobalMangaSearchScreen( var showSingleLoadingScreen by remember { mutableStateOf(searchQuery.isNotEmpty() && extensionFilter.isNotEmpty() && state.total == 1) } + val filteredSources by screenModel.searchPagerFlow.collectAsState() if (showSingleLoadingScreen) { LoadingScreen() @@ -57,10 +58,13 @@ class GlobalMangaSearchScreen( } else { GlobalMangaSearchScreen( state = state, + items = filteredSources, navigateUp = navigator::pop, onChangeSearchQuery = screenModel::updateSearchQuery, onSearch = screenModel::search, getManga = { screenModel.getManga(it) }, + onChangeSearchFilter = screenModel::setSourceFilter, + onToggleResults = screenModel::toggleFilterResults, onClickSource = { if (!screenModel.incognitoMode.get()) { screenModel.lastUsedSourceId.set(it.id) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/source/globalsearch/GlobalMangaSearchScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/source/globalsearch/GlobalMangaSearchScreenModel.kt index 0f0b901b5..04e6c278f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/source/globalsearch/GlobalMangaSearchScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/source/globalsearch/GlobalMangaSearchScreenModel.kt @@ -3,7 +3,12 @@ package eu.kanade.tachiyomi.ui.browse.manga.source.globalsearch import androidx.compose.runtime.Immutable import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.source.service.SourcePreferences +import eu.kanade.presentation.util.ioCoroutineScope import eu.kanade.tachiyomi.source.CatalogueSource +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import tachiyomi.domain.source.manga.service.MangaSourceManager import uy.kohesive.injekt.Injekt @@ -15,8 +20,8 @@ class GlobalMangaSearchScreenModel( preferences: BasePreferences = Injekt.get(), private val sourcePreferences: SourcePreferences = Injekt.get(), private val sourceManager: MangaSourceManager = Injekt.get(), -) : MangaSearchScreenModel( - GlobalMangaSearchState( +) : MangaSearchScreenModel( + State( searchQuery = initialQuery, ), ) { @@ -24,6 +29,13 @@ class GlobalMangaSearchScreenModel( val incognitoMode = preferences.incognitoMode() val lastUsedSourceId = sourcePreferences.lastUsedMangaSource() + val searchPagerFlow = state.map { Pair(it.onlyShowHasResults, it.items) } + .distinctUntilChanged() + .map { (onlyShowHasResults, items) -> + items.filter { (_, result) -> result.isVisible(onlyShowHasResults) } + } + .stateIn(ioCoroutineScope, SharingStarted.Lazily, state.value.items) + init { extensionFilter = initialExtensionFilter if (initialQuery.isNotBlank() || initialExtensionFilter.isNotBlank()) { @@ -37,6 +49,7 @@ class GlobalMangaSearchScreenModel( val pinnedSources = sourcePreferences.pinnedMangaSources().get() return sourceManager.getCatalogueSources() + .filter { mutableState.value.sourceFilter != MangaSourceFilter.PinnedOnly || "${it.id}" in pinnedSources } .filter { it.lang in enabledLanguages } .filterNot { "${it.id}" in disabledSources } .sortedWith(compareBy({ "${it.id}" !in pinnedSources }, { "${it.name.lowercase()} (${it.lang})" })) @@ -57,15 +70,29 @@ class GlobalMangaSearchScreenModel( override fun getItems(): Map { return mutableState.value.items } -} - -@Immutable -data class GlobalMangaSearchState( - val searchQuery: String? = null, - val items: Map = emptyMap(), -) { - - val progress: Int = items.count { it.value !is MangaSearchItemResult.Loading } - - val total: Int = items.size + + fun setSourceFilter(filter: MangaSourceFilter) { + mutableState.update { it.copy(sourceFilter = filter) } + } + + fun toggleFilterResults() { + mutableState.update { + it.copy(onlyShowHasResults = !it.onlyShowHasResults) + } + } + + private fun MangaSearchItemResult.isVisible(onlyShowHasResults: Boolean): Boolean { + return !onlyShowHasResults || (this is MangaSearchItemResult.Success && !this.isEmpty) + } + + @Immutable + data class State( + val searchQuery: String? = null, + val sourceFilter: MangaSourceFilter = MangaSourceFilter.PinnedOnly, + val onlyShowHasResults: Boolean = false, + val items: Map = emptyMap(), + ) { + val progress: Int = items.count { it.value !is MangaSearchItemResult.Loading } + val total: Int = items.size + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/source/globalsearch/MangaSearchScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/source/globalsearch/MangaSearchScreenModel.kt index ef7373e56..f486ac2ee 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/source/globalsearch/MangaSearchScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/source/globalsearch/MangaSearchScreenModel.kt @@ -68,16 +68,7 @@ abstract class MangaSearchScreenModel( val enabledSources = getEnabledSources() if (filter.isEmpty()) { - val shouldSearchPinnedOnly = sourcePreferences.searchPinnedMangaSourcesOnly().get() - val pinnedSources = sourcePreferences.pinnedMangaSources().get() - - return enabledSources.filter { - if (shouldSearchPinnedOnly) { - "${it.id}" in pinnedSources - } else { - true - } - } + return enabledSources } return extensionManager.installedExtensionsFlow.value @@ -136,6 +127,11 @@ abstract class MangaSearchScreenModel( } } +enum class MangaSourceFilter { + All, + PinnedOnly, +} + sealed class MangaSearchItemResult { object Loading : MangaSearchItemResult() 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 cb5d77d37..f736a9998 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 @@ -50,6 +50,7 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import logcat.LogPriority import tachiyomi.core.preference.CheckboxState +import tachiyomi.core.preference.TriState import tachiyomi.core.preference.mapAsCheckboxState import tachiyomi.core.util.lang.launchIO import tachiyomi.core.util.lang.launchNonCancellable @@ -60,7 +61,6 @@ import tachiyomi.domain.category.anime.interactor.GetAnimeCategories import tachiyomi.domain.category.anime.interactor.SetAnimeCategories import tachiyomi.domain.category.model.Category import tachiyomi.domain.download.service.DownloadPreferences -import tachiyomi.domain.entries.TriStateFilter import tachiyomi.domain.entries.anime.interactor.GetAnimeWithEpisodes import tachiyomi.domain.entries.anime.interactor.GetDuplicateLibraryAnime import tachiyomi.domain.entries.anime.interactor.SetAnimeEpisodeFlags @@ -161,7 +161,8 @@ class AnimeInfoScreenModel( combine( getAnimeAndEpisodes.subscribe(animeId).distinctUntilChanged(), downloadCache.changes, - ) { animeAndEpisodes, _ -> animeAndEpisodes } + downloadManager.queueState, + ) { animeAndEpisodes, _, _ -> animeAndEpisodes } .collectLatest { (anime, episodes) -> updateSuccessState { it.copy( @@ -766,13 +767,13 @@ class AnimeInfoScreenModel( * Sets the seen filter and requests an UI update. * @param state whether to display only unseen episodes or all episodes. */ - fun setUnseenFilter(state: TriStateFilter) { + fun setUnseenFilter(state: TriState) { val anime = successState?.anime ?: return val flag = when (state) { - TriStateFilter.DISABLED -> Anime.SHOW_ALL - TriStateFilter.ENABLED_IS -> Anime.EPISODE_SHOW_UNSEEN - TriStateFilter.ENABLED_NOT -> Anime.EPISODE_SHOW_SEEN + TriState.DISABLED -> Anime.SHOW_ALL + TriState.ENABLED_IS -> Anime.EPISODE_SHOW_UNSEEN + TriState.ENABLED_NOT -> Anime.EPISODE_SHOW_SEEN } coroutineScope.launchNonCancellable { setAnimeEpisodeFlags.awaitSetUnseenFilter(anime, flag) @@ -783,13 +784,13 @@ class AnimeInfoScreenModel( * Sets the download filter and requests an UI update. * @param state whether to display only downloaded episodes or all episodes. */ - fun setDownloadedFilter(state: TriStateFilter) { + fun setDownloadedFilter(state: TriState) { val anime = successState?.anime ?: return val flag = when (state) { - TriStateFilter.DISABLED -> Anime.SHOW_ALL - TriStateFilter.ENABLED_IS -> Anime.EPISODE_SHOW_DOWNLOADED - TriStateFilter.ENABLED_NOT -> Anime.EPISODE_SHOW_NOT_DOWNLOADED + TriState.DISABLED -> Anime.SHOW_ALL + TriState.ENABLED_IS -> Anime.EPISODE_SHOW_DOWNLOADED + TriState.ENABLED_NOT -> Anime.EPISODE_SHOW_NOT_DOWNLOADED } coroutineScope.launchNonCancellable { @@ -801,13 +802,13 @@ class AnimeInfoScreenModel( * Sets the bookmark filter and requests an UI update. * @param state whether to display only bookmarked episodes or all episodes. */ - fun setBookmarkedFilter(state: TriStateFilter) { + fun setBookmarkedFilter(state: TriState) { val anime = successState?.anime ?: return val flag = when (state) { - TriStateFilter.DISABLED -> Anime.SHOW_ALL - TriStateFilter.ENABLED_IS -> Anime.EPISODE_SHOW_BOOKMARKED - TriStateFilter.ENABLED_NOT -> Anime.EPISODE_SHOW_NOT_BOOKMARKED + TriState.DISABLED -> Anime.SHOW_ALL + TriState.ENABLED_IS -> Anime.EPISODE_SHOW_BOOKMARKED + TriState.ENABLED_NOT -> Anime.EPISODE_SHOW_NOT_BOOKMARKED } coroutineScope.launchNonCancellable { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/entries/anime/track/AnimeTrackInfoDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/entries/anime/track/AnimeTrackInfoDialog.kt index ac5c713b2..7b7e10e7d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/entries/anime/track/AnimeTrackInfoDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/entries/anime/track/AnimeTrackInfoDialog.kt @@ -20,6 +20,7 @@ import androidx.compose.material3.SelectableDates import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -269,6 +270,7 @@ data class AnimeTrackInfoDialogHomeScreen( .filter { (it.service as? EnhancedAnimeTrackService)?.accept(source) ?: true } } + @Immutable data class State( val trackItems: List = emptyList(), ) @@ -318,6 +320,7 @@ private data class TrackStatusSelectorScreen( } } + @Immutable data class State( val selection: Int, ) @@ -374,6 +377,7 @@ private data class TrackEpisodeSelectorScreen( } } + @Immutable data class State( val selection: Int, ) @@ -424,6 +428,7 @@ private data class TrackScoreSelectorScreen( } } + @Immutable data class State( val selection: String, ) @@ -731,6 +736,7 @@ data class TrackServiceSearchScreen( mutableState.update { it.copy(selected = selected) } } + @Immutable data class State( val queryResult: Result>? = null, val selected: AnimeTrackSearch? = null, 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 a09cddb0a..4dfb5fecd 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 @@ -47,6 +47,7 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import logcat.LogPriority import tachiyomi.core.preference.CheckboxState +import tachiyomi.core.preference.TriState import tachiyomi.core.preference.mapAsCheckboxState import tachiyomi.core.util.lang.launchIO import tachiyomi.core.util.lang.launchNonCancellable @@ -57,7 +58,6 @@ import tachiyomi.domain.category.manga.interactor.GetMangaCategories import tachiyomi.domain.category.manga.interactor.SetMangaCategories import tachiyomi.domain.category.model.Category import tachiyomi.domain.download.service.DownloadPreferences -import tachiyomi.domain.entries.TriStateFilter import tachiyomi.domain.entries.applyFilter import tachiyomi.domain.entries.manga.interactor.GetDuplicateLibraryManga import tachiyomi.domain.entries.manga.interactor.GetMangaWithChapters @@ -156,7 +156,8 @@ class MangaInfoScreenModel( combine( getMangaAndChapters.subscribe(mangaId).distinctUntilChanged(), downloadCache.changes, - ) { mangaAndChapters, _ -> mangaAndChapters } + downloadManager.queueState, + ) { mangaAndChapters, _, _ -> mangaAndChapters } .collectLatest { (manga, chapters) -> updateSuccessState { it.copy( @@ -756,13 +757,13 @@ class MangaInfoScreenModel( * Sets the read filter and requests an UI update. * @param state whether to display only unread chapters or all chapters. */ - fun setUnreadFilter(state: TriStateFilter) { + fun setUnreadFilter(state: TriState) { val manga = successState?.manga ?: return val flag = when (state) { - TriStateFilter.DISABLED -> Manga.SHOW_ALL - TriStateFilter.ENABLED_IS -> Manga.CHAPTER_SHOW_UNREAD - TriStateFilter.ENABLED_NOT -> Manga.CHAPTER_SHOW_READ + TriState.DISABLED -> Manga.SHOW_ALL + TriState.ENABLED_IS -> Manga.CHAPTER_SHOW_UNREAD + TriState.ENABLED_NOT -> Manga.CHAPTER_SHOW_READ } coroutineScope.launchNonCancellable { setMangaChapterFlags.awaitSetUnreadFilter(manga, flag) @@ -773,13 +774,13 @@ class MangaInfoScreenModel( * Sets the download filter and requests an UI update. * @param state whether to display only downloaded chapters or all chapters. */ - fun setDownloadedFilter(state: TriStateFilter) { + fun setDownloadedFilter(state: TriState) { val manga = successState?.manga ?: return val flag = when (state) { - TriStateFilter.DISABLED -> Manga.SHOW_ALL - TriStateFilter.ENABLED_IS -> Manga.CHAPTER_SHOW_DOWNLOADED - TriStateFilter.ENABLED_NOT -> Manga.CHAPTER_SHOW_NOT_DOWNLOADED + TriState.DISABLED -> Manga.SHOW_ALL + TriState.ENABLED_IS -> Manga.CHAPTER_SHOW_DOWNLOADED + TriState.ENABLED_NOT -> Manga.CHAPTER_SHOW_NOT_DOWNLOADED } coroutineScope.launchNonCancellable { @@ -791,13 +792,13 @@ class MangaInfoScreenModel( * Sets the bookmark filter and requests an UI update. * @param state whether to display only bookmarked chapters or all chapters. */ - fun setBookmarkedFilter(state: TriStateFilter) { + fun setBookmarkedFilter(state: TriState) { val manga = successState?.manga ?: return val flag = when (state) { - TriStateFilter.DISABLED -> Manga.SHOW_ALL - TriStateFilter.ENABLED_IS -> Manga.CHAPTER_SHOW_BOOKMARKED - TriStateFilter.ENABLED_NOT -> Manga.CHAPTER_SHOW_NOT_BOOKMARKED + TriState.DISABLED -> Manga.SHOW_ALL + TriState.ENABLED_IS -> Manga.CHAPTER_SHOW_BOOKMARKED + TriState.ENABLED_NOT -> Manga.CHAPTER_SHOW_NOT_BOOKMARKED } coroutineScope.launchNonCancellable { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/entries/manga/track/MangaTrackInfoDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/entries/manga/track/MangaTrackInfoDialog.kt index 218526abd..ff73544c2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/entries/manga/track/MangaTrackInfoDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/entries/manga/track/MangaTrackInfoDialog.kt @@ -20,6 +20,7 @@ import androidx.compose.material3.SelectableDates import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -268,6 +269,7 @@ data class MangaTrackInfoDialogHomeScreen( .filter { (it.service as? EnhancedMangaTrackService)?.accept(source) ?: true } } + @Immutable data class State( val trackItems: List = emptyList(), ) @@ -317,6 +319,7 @@ private data class TrackStatusSelectorScreen( } } + @Immutable data class State( val selection: Int, ) @@ -373,6 +376,7 @@ private data class TrackChapterSelectorScreen( } } + @Immutable data class State( val selection: Int, ) @@ -423,6 +427,7 @@ private data class TrackScoreSelectorScreen( } } + @Immutable data class State( val selection: String, ) @@ -730,6 +735,7 @@ data class TrackServiceSearchScreen( mutableState.update { it.copy(selected = selected) } } + @Immutable data class State( val queryResult: Result>? = null, val selected: MangaTrackSearch? = null, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/anime/AnimeLibraryScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/anime/AnimeLibraryScreenModel.kt index 17fc9480a..73d10f9b3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/anime/AnimeLibraryScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/anime/AnimeLibraryScreenModel.kt @@ -41,13 +41,13 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import tachiyomi.core.preference.CheckboxState +import tachiyomi.core.preference.TriState import tachiyomi.core.util.lang.launchIO import tachiyomi.core.util.lang.launchNonCancellable import tachiyomi.core.util.lang.withIOContext import tachiyomi.domain.category.anime.interactor.GetVisibleAnimeCategories import tachiyomi.domain.category.anime.interactor.SetAnimeCategories import tachiyomi.domain.category.model.Category -import tachiyomi.domain.entries.TriStateFilter import tachiyomi.domain.entries.anime.interactor.GetLibraryAnime import tachiyomi.domain.entries.anime.model.Anime import tachiyomi.domain.entries.anime.model.AnimeUpdate @@ -154,7 +154,7 @@ class AnimeLibraryScreenModel( prefs.filterBookmarked, prefs.filterCompleted, ) + trackFilter.values - ).any { it != TriStateFilter.DISABLED } + ).any { it != TriState.DISABLED } } .distinctUntilChanged() .onEach { @@ -170,12 +170,12 @@ class AnimeLibraryScreenModel( */ private suspend fun AnimeLibraryMap.applyFilters( trackMap: Map>, - loggedInTrackServices: Map, + loggedInTrackServices: Map, ): AnimeLibraryMap { val prefs = getAnimelibItemPreferencesFlow().first() val downloadedOnly = prefs.globalFilterDownloaded val filterDownloaded = - if (downloadedOnly) TriStateFilter.ENABLED_IS else prefs.filterDownloaded + if (downloadedOnly) TriState.ENABLED_IS else prefs.filterDownloaded val filterUnseen = prefs.filterUnseen val filterStarted = prefs.filterStarted val filterBookmarked = prefs.filterBookmarked @@ -183,8 +183,8 @@ class AnimeLibraryScreenModel( val isNotLoggedInAnyTrack = loggedInTrackServices.isEmpty() - val excludedTracks = loggedInTrackServices.mapNotNull { if (it.value == TriStateFilter.ENABLED_NOT) it.key else null } - val includedTracks = loggedInTrackServices.mapNotNull { if (it.value == TriStateFilter.ENABLED_IS) it.key else null } + val excludedTracks = loggedInTrackServices.mapNotNull { if (it.value == TriState.ENABLED_NOT) it.key else null } + val includedTracks = loggedInTrackServices.mapNotNull { if (it.value == TriState.ENABLED_IS) it.key else null } val trackFiltersIsIgnored = includedTracks.isEmpty() && excludedTracks.isEmpty() val filterFnDownloaded: (AnimeLibraryItem) -> Boolean = { @@ -315,11 +315,11 @@ class AnimeLibraryScreenModel( localBadge = it[1] as Boolean, languageBadge = it[2] as Boolean, globalFilterDownloaded = it[3] as Boolean, - filterDownloaded = it[4] as TriStateFilter, - filterUnseen = it[5] as TriStateFilter, - filterStarted = it[6] as TriStateFilter, - filterBookmarked = it[7] as TriStateFilter, - filterCompleted = it[8] as TriStateFilter, + filterDownloaded = it[4] as TriState, + filterUnseen = it[5] as TriState, + filterStarted = it[6] as TriState, + filterBookmarked = it[7] as TriState, + filterCompleted = it[8] as TriState, ) }, ) @@ -372,7 +372,7 @@ class AnimeLibraryScreenModel( * * @return map of track id with the filter value */ - private fun getTrackingFilterFlow(): Flow> { + private fun getTrackingFilterFlow(): Flow> { val loggedServices = trackManager.services.filter { it.isLogged && it is AnimeTrackService } return if (loggedServices.isNotEmpty()) { val prefFlows = loggedServices @@ -677,11 +677,11 @@ class AnimeLibraryScreenModel( val languageBadge: Boolean, val globalFilterDownloaded: Boolean, - val filterDownloaded: TriStateFilter, - val filterUnseen: TriStateFilter, - val filterStarted: TriStateFilter, - val filterBookmarked: TriStateFilter, - val filterCompleted: TriStateFilter, + val filterDownloaded: TriState, + val filterUnseen: TriState, + val filterStarted: TriState, + val filterBookmarked: TriState, + val filterCompleted: TriState, ) @Immutable diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/anime/AnimeLibrarySettingsScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/anime/AnimeLibrarySettingsScreenModel.kt index a40060979..620c31483 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/anime/AnimeLibrarySettingsScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/anime/AnimeLibrarySettingsScreenModel.kt @@ -6,12 +6,12 @@ import eu.kanade.domain.base.BasePreferences import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.util.preference.toggle import tachiyomi.core.preference.Preference +import tachiyomi.core.preference.TriState import tachiyomi.core.preference.getAndSet import tachiyomi.core.util.lang.launchIO import tachiyomi.domain.category.anime.interactor.SetAnimeDisplayMode import tachiyomi.domain.category.anime.interactor.SetSortModeForAnimeCategory import tachiyomi.domain.category.model.Category -import tachiyomi.domain.entries.TriStateFilter import tachiyomi.domain.library.anime.model.AnimeLibrarySort import tachiyomi.domain.library.model.LibraryDisplayMode import tachiyomi.domain.library.service.LibraryPreferences @@ -33,7 +33,7 @@ class AnimeLibrarySettingsScreenModel( preference(libraryPreferences).toggle() } - fun toggleFilter(preference: (LibraryPreferences) -> Preference) { + fun toggleFilter(preference: (LibraryPreferences) -> Preference) { preference(libraryPreferences).getAndSet { it.next() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/manga/MangaLibraryScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/manga/MangaLibraryScreenModel.kt index 785035b5a..aa8f733fa 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/manga/MangaLibraryScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/manga/MangaLibraryScreenModel.kt @@ -41,13 +41,13 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import tachiyomi.core.preference.CheckboxState +import tachiyomi.core.preference.TriState import tachiyomi.core.util.lang.launchIO import tachiyomi.core.util.lang.launchNonCancellable import tachiyomi.core.util.lang.withIOContext import tachiyomi.domain.category.manga.interactor.GetVisibleMangaCategories import tachiyomi.domain.category.manga.interactor.SetMangaCategories import tachiyomi.domain.category.model.Category -import tachiyomi.domain.entries.TriStateFilter import tachiyomi.domain.entries.applyFilter import tachiyomi.domain.entries.manga.interactor.GetLibraryManga import tachiyomi.domain.entries.manga.model.Manga @@ -154,7 +154,7 @@ class MangaLibraryScreenModel( prefs.filterBookmarked, prefs.filterCompleted, ) + trackFilter.values - ).any { it != TriStateFilter.DISABLED } + ).any { it != TriState.DISABLED } } .distinctUntilChanged() .onEach { @@ -170,12 +170,12 @@ class MangaLibraryScreenModel( */ private suspend fun MangaLibraryMap.applyFilters( trackMap: Map>, - loggedInTrackServices: Map, + loggedInTrackServices: Map, ): MangaLibraryMap { val prefs = getLibraryItemPreferencesFlow().first() val downloadedOnly = prefs.globalFilterDownloaded val filterDownloaded = - if (downloadedOnly) TriStateFilter.ENABLED_IS else prefs.filterDownloaded + if (downloadedOnly) TriState.ENABLED_IS else prefs.filterDownloaded val filterUnread = prefs.filterUnread val filterStarted = prefs.filterStarted val filterBookmarked = prefs.filterBookmarked @@ -183,8 +183,8 @@ class MangaLibraryScreenModel( val isNotLoggedInAnyTrack = loggedInTrackServices.isEmpty() - val excludedTracks = loggedInTrackServices.mapNotNull { if (it.value == TriStateFilter.ENABLED_NOT) it.key else null } - val includedTracks = loggedInTrackServices.mapNotNull { if (it.value == TriStateFilter.ENABLED_IS) it.key else null } + val excludedTracks = loggedInTrackServices.mapNotNull { if (it.value == TriState.ENABLED_NOT) it.key else null } + val includedTracks = loggedInTrackServices.mapNotNull { if (it.value == TriState.ENABLED_IS) it.key else null } val trackFiltersIsIgnored = includedTracks.isEmpty() && excludedTracks.isEmpty() val filterFnDownloaded: (MangaLibraryItem) -> Boolean = { @@ -309,11 +309,11 @@ class MangaLibraryScreenModel( localBadge = it[1] as Boolean, languageBadge = it[2] as Boolean, globalFilterDownloaded = it[3] as Boolean, - filterDownloaded = it[4] as TriStateFilter, - filterUnread = it[5] as TriStateFilter, - filterStarted = it[6] as TriStateFilter, - filterBookmarked = it[7] as TriStateFilter, - filterCompleted = it[8] as TriStateFilter, + filterDownloaded = it[4] as TriState, + filterUnread = it[5] as TriState, + filterStarted = it[6] as TriState, + filterBookmarked = it[7] as TriState, + filterCompleted = it[8] as TriState, ) }, ) @@ -366,7 +366,7 @@ class MangaLibraryScreenModel( * * @return map of track id with the filter value */ - private fun getTrackingFilterFlow(): Flow> { + private fun getTrackingFilterFlow(): Flow> { val loggedServices = trackManager.services.filter { it.isLogged && it is MangaTrackService } return if (loggedServices.isNotEmpty()) { val prefFlows = loggedServices @@ -671,11 +671,11 @@ class MangaLibraryScreenModel( val languageBadge: Boolean, val globalFilterDownloaded: Boolean, - val filterDownloaded: TriStateFilter, - val filterUnread: TriStateFilter, - val filterStarted: TriStateFilter, - val filterBookmarked: TriStateFilter, - val filterCompleted: TriStateFilter, + val filterDownloaded: TriState, + val filterUnread: TriState, + val filterStarted: TriState, + val filterBookmarked: TriState, + val filterCompleted: TriState, ) @Immutable diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/manga/MangaLibrarySettingsScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/manga/MangaLibrarySettingsScreenModel.kt index e9b364c8f..9ba8969c3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/manga/MangaLibrarySettingsScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/manga/MangaLibrarySettingsScreenModel.kt @@ -6,12 +6,12 @@ import eu.kanade.domain.base.BasePreferences import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.util.preference.toggle import tachiyomi.core.preference.Preference +import tachiyomi.core.preference.TriState import tachiyomi.core.preference.getAndSet import tachiyomi.core.util.lang.launchIO import tachiyomi.domain.category.manga.interactor.SetMangaDisplayMode import tachiyomi.domain.category.manga.interactor.SetSortModeForMangaCategory import tachiyomi.domain.category.model.Category -import tachiyomi.domain.entries.TriStateFilter import tachiyomi.domain.library.manga.model.MangaLibrarySort import tachiyomi.domain.library.model.LibraryDisplayMode import tachiyomi.domain.library.service.LibraryPreferences @@ -33,7 +33,7 @@ class MangaLibrarySettingsScreenModel( preference(libraryPreferences).toggle() } - fun toggleFilter(preference: (LibraryPreferences) -> Preference) { + fun toggleFilter(preference: (LibraryPreferences) -> Preference) { preference(libraryPreferences).getAndSet { it.next() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerViewModel.kt index 0582e42d9..77d462fb9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerViewModel.kt @@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.player import android.app.Application import android.net.Uri +import androidx.compose.runtime.Immutable import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -719,6 +720,7 @@ class PlayerViewModel( mutableState.update { it.copy(dialog = null, sheet = null) } } + @Immutable data class State( val episodeList: List = emptyList(), val episode: Episode? = null, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/sheets/subtitle/SubtitleDelayPage.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/sheets/subtitle/SubtitleDelayPage.kt index 359124e67..bc0218a24 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/sheets/subtitle/SubtitleDelayPage.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/sheets/subtitle/SubtitleDelayPage.kt @@ -15,11 +15,11 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import eu.kanade.presentation.components.OutlinedNumericChooser import eu.kanade.presentation.util.collectAsState import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.ui.player.settings.PlayerSettingsScreenModel import `is`.xyz.mpv.MPVLib +import tachiyomi.presentation.core.components.OutlinedNumericChooser import tachiyomi.presentation.core.components.material.padding @Composable diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/sheets/subtitle/SubtitleFontPage.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/sheets/subtitle/SubtitleFontPage.kt index 66e99199a..80ef7bc54 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/sheets/subtitle/SubtitleFontPage.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/sheets/subtitle/SubtitleFontPage.kt @@ -32,12 +32,12 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.yubyf.truetypeparser.TTFFile import eu.kanade.presentation.components.DropdownMenu -import eu.kanade.presentation.components.OutlinedNumericChooser import eu.kanade.presentation.util.collectAsState import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences import eu.kanade.tachiyomi.ui.player.settings.PlayerSettingsScreenModel import `is`.xyz.mpv.MPVLib +import tachiyomi.presentation.core.components.OutlinedNumericChooser import tachiyomi.presentation.core.components.material.ReadItemAlpha import tachiyomi.presentation.core.components.material.padding import java.io.File 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 4622af657..08cb31f94 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 @@ -31,6 +31,7 @@ import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Text import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.unit.dp import androidx.core.graphics.ColorUtils @@ -50,6 +51,7 @@ import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.entries.manga.model.orientationType import eu.kanade.presentation.reader.ChapterNavigator import eu.kanade.presentation.reader.PageIndicatorText +import eu.kanade.presentation.reader.settings.ReaderSettingsDialog import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.core.Constants import eu.kanade.tachiyomi.data.notification.NotificationReceiver @@ -65,8 +67,8 @@ 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.ReaderSettingsScreenModel import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsSheet import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator @@ -392,6 +394,8 @@ class ReaderActivity : BaseActivity() { binding.dialogRoot.setComposeContent { val state by viewModel.state.collectAsState() + val settingsScreenModel = remember { ReaderSettingsScreenModel() } + val onDismissRequest = viewModel::closeDialog when (state.dialog) { is ReaderViewModel.Dialog.Loading -> { @@ -409,14 +413,12 @@ class ReaderActivity : BaseActivity() { }, ) } - is ReaderViewModel.Dialog.ColorFilter -> { - setMenuVisibility(false) - ReaderColorFilterDialog( - onDismissRequest = { - onDismissRequest() - setMenuVisibility(true) - }, - readerPreferences = viewModel.readerPreferences, + is ReaderViewModel.Dialog.Settings -> { + ReaderSettingsDialog( + onDismissRequest = onDismissRequest, + onShowMenus = { setMenuVisibility(true) }, + onHideMenus = { setMenuVisibility(false) }, + screenModel = settingsScreenModel, ) } is ReaderViewModel.Dialog.PageActions -> { @@ -549,7 +551,7 @@ class ReaderActivity : BaseActivity() { } // Settings sheet - with(binding.actionSettings) { + with(binding.actionSettingsLegacy) { setTooltip(R.string.action_settings) var readerSettingSheet: ReaderSettingsSheet? = null @@ -559,13 +561,11 @@ class ReaderActivity : BaseActivity() { readerSettingSheet = ReaderSettingsSheet(this@ReaderActivity).apply { show() } } } - - // Color filter sheet - with(binding.actionColorSettings) { - setTooltip(R.string.custom_filter) + with(binding.actionSettings) { + setTooltip(R.string.action_settings) setOnClickListener { - viewModel.openColorFilterDialog() + viewModel.openSettingsDialog() } } } 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 65d5e891a..f75e7f775 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 @@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.reader import android.app.Application import android.net.Uri +import androidx.compose.runtime.Immutable import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -704,8 +705,8 @@ class ReaderViewModel( mutableState.update { it.copy(dialog = Dialog.PageActions(page)) } } - fun openColorFilterDialog() { - mutableState.update { it.copy(dialog = Dialog.ColorFilter) } + fun openSettingsDialog() { + mutableState.update { it.copy(dialog = Dialog.Settings) } } fun closeDialog() { @@ -809,16 +810,12 @@ class ReaderViewModel( } } - /** - * Results of the set as cover feature. - */ enum class SetAsCoverResult { - Success, AddToLibraryFirst, Error + Success, + AddToLibraryFirst, + Error, } - /** - * Results of the save image feature. - */ sealed class SaveImageResult { class Success(val uri: Uri) : SaveImageResult() class Error(val error: Throwable) : SaveImageResult() @@ -893,6 +890,7 @@ class ReaderViewModel( } } + @Immutable data class State( val manga: Manga? = null, val viewerChapters: ViewerChapters? = null, @@ -912,7 +910,7 @@ class ReaderViewModel( sealed class Dialog { object Loading : Dialog() - object ColorFilter : Dialog() + object Settings : Dialog() data class PageActions(val page: ReaderPage) : Dialog() } 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 deleted file mode 100644 index db7a901d4..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderColorFilterDialog.kt +++ /dev/null @@ -1,164 +0,0 @@ -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/ReaderGeneralSettings.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderGeneralSettings.kt deleted file mode 100644 index 50beac220..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderGeneralSettings.kt +++ /dev/null @@ -1,53 +0,0 @@ -package eu.kanade.tachiyomi.ui.reader.setting - -import android.content.Context -import android.util.AttributeSet -import android.view.LayoutInflater -import androidx.core.view.isVisible -import androidx.core.widget.NestedScrollView -import androidx.lifecycle.lifecycleScope -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.databinding.ReaderGeneralSettingsBinding -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 uy.kohesive.injekt.injectLazy - -/** - * Sheet to show reader and viewer preferences. - */ -class ReaderGeneralSettings @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : - NestedScrollView(context, attrs) { - - private val readerPreferences: ReaderPreferences by injectLazy() - - private val binding = ReaderGeneralSettingsBinding.inflate(LayoutInflater.from(context), this, false) - - init { - addView(binding.root) - - initGeneralPreferences() - } - - /** - * Init general reader preferences. - */ - private fun initGeneralPreferences() { - binding.backgroundColor.bindToIntPreference(readerPreferences.readerTheme(), R.array.reader_themes_values) - binding.showPageNumber.bindToPreference(readerPreferences.showPageNumber()) - binding.fullscreen.bindToPreference(readerPreferences.fullscreen()) - readerPreferences.fullscreen().changes() - .onEach { - // If the preference is explicitly disabled, that means the setting was configured since there is a cutout - binding.cutoutShort.isVisible = it && ((context as ReaderActivity).hasCutout || !readerPreferences.cutoutShort().get()) - binding.cutoutShort.bindToPreference(readerPreferences.cutoutShort()) - } - .launchIn((context as ReaderActivity).lifecycleScope) - - binding.keepscreen.bindToPreference(readerPreferences.keepScreenOn()) - binding.longTap.bindToPreference(readerPreferences.readWithLongTap()) - binding.alwaysShowChapterTransition.bindToPreference(readerPreferences.alwaysShowChapterTransition()) - binding.pageTransitions.bindToPreference(readerPreferences.pageTransitions()) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderPreferences.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderPreferences.kt index b70104f64..1997e3495 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderPreferences.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderPreferences.kt @@ -54,7 +54,7 @@ class ReaderPreferences( fun cropBordersWebtoon() = preferenceStore.getBoolean("crop_borders_webtoon", false) - fun webtoonSidePadding() = preferenceStore.getInt("webtoon_side_padding", 0) + fun webtoonSidePadding() = preferenceStore.getInt("webtoon_side_padding", WEBTOON_PADDING_MIN) fun readerHideThreshold() = preferenceStore.getEnum("reader_hide_threshold", ReaderHideThreshold.LOW) @@ -137,4 +137,9 @@ class ReaderPreferences( LOW(31), LOWEST(47), } + + companion object { + const val WEBTOON_PADDING_MIN = 0 + const val WEBTOON_PADDING_MAX = 25 + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderReadingModeSettings.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderReadingModeSettings.kt deleted file mode 100644 index 844bc5ab1..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderReadingModeSettings.kt +++ /dev/null @@ -1,145 +0,0 @@ -package eu.kanade.tachiyomi.ui.reader.setting - -import android.content.Context -import android.util.AttributeSet -import android.view.LayoutInflater -import androidx.core.view.isVisible -import androidx.core.widget.NestedScrollView -import androidx.lifecycle.lifecycleScope -import eu.kanade.domain.entries.manga.model.orientationType -import eu.kanade.domain.entries.manga.model.readingModeType -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.databinding.ReaderReadingModeSettingsBinding -import eu.kanade.tachiyomi.ui.reader.ReaderActivity -import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerViewer -import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer -import eu.kanade.tachiyomi.util.preference.bindToPreference -import eu.kanade.tachiyomi.util.system.isReleaseBuildType -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import uy.kohesive.injekt.injectLazy - -/** - * Sheet to show reader and viewer preferences. - */ -class ReaderReadingModeSettings @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : - NestedScrollView(context, attrs) { - - private val readerPreferences: ReaderPreferences by injectLazy() - - private val binding = ReaderReadingModeSettingsBinding.inflate(LayoutInflater.from(context), this, false) - - init { - addView(binding.root) - - initGeneralPreferences() - - when ((context as ReaderActivity).viewModel.state.value.viewer) { - is PagerViewer -> initPagerPreferences() - is WebtoonViewer -> initWebtoonPreferences() - } - } - - /** - * Init general reader preferences. - */ - private fun initGeneralPreferences() { - binding.viewer.onItemSelectedListener = { position -> - val readingModeType = ReadingModeType.fromSpinner(position) - (context as ReaderActivity).viewModel.setMangaReadingMode(readingModeType.flagValue) - - val mangaViewer = (context as ReaderActivity).viewModel.getMangaReadingMode() - if (mangaViewer == ReadingModeType.WEBTOON.flagValue || mangaViewer == ReadingModeType.CONTINUOUS_VERTICAL.flagValue) { - initWebtoonPreferences() - } else { - initPagerPreferences() - } - } - binding.viewer.setSelection((context as ReaderActivity).viewModel.manga?.readingModeType?.let { ReadingModeType.fromPreference(it.toInt()).prefValue } ?: ReadingModeType.DEFAULT.prefValue) - - binding.rotationMode.onItemSelectedListener = { position -> - val rotationType = OrientationType.fromSpinner(position) - (context as ReaderActivity).viewModel.setMangaOrientationType(rotationType.flagValue) - } - binding.rotationMode.setSelection((context as ReaderActivity).viewModel.manga?.orientationType?.let { OrientationType.fromPreference(it.toInt()).prefValue } ?: OrientationType.DEFAULT.prefValue) - } - - /** - * Init the preferences for the pager reader. - */ - private fun initPagerPreferences() { - binding.webtoonPrefsGroup.root.isVisible = false - binding.pagerPrefsGroup.root.isVisible = true - - binding.pagerPrefsGroup.tappingInverted.bindToPreference(readerPreferences.pagerNavInverted(), ReaderPreferences.TappingInvertMode::class.java) - binding.pagerPrefsGroup.navigatePan.bindToPreference(readerPreferences.navigateToPan()) - - binding.pagerPrefsGroup.pagerNav.bindToPreference(readerPreferences.navigationModePager()) - readerPreferences.navigationModePager().changes() - .onEach { - val isTappingEnabled = it != 5 - binding.pagerPrefsGroup.tappingInverted.isVisible = isTappingEnabled - binding.pagerPrefsGroup.navigatePan.isVisible = isTappingEnabled - } - .launchIn((context as ReaderActivity).lifecycleScope) - // Makes so that landscape zoom gets hidden away when image scale type is not fit screen - binding.pagerPrefsGroup.scaleType.bindToPreference(readerPreferences.imageScaleType(), 1) - readerPreferences.imageScaleType().changes() - .onEach { binding.pagerPrefsGroup.landscapeZoom.isVisible = it == 1 } - .launchIn((context as ReaderActivity).lifecycleScope) - binding.pagerPrefsGroup.landscapeZoom.bindToPreference(readerPreferences.landscapeZoom()) - - binding.pagerPrefsGroup.zoomStart.bindToPreference(readerPreferences.zoomStart(), 1) - binding.pagerPrefsGroup.cropBorders.bindToPreference(readerPreferences.cropBorders()) - - binding.pagerPrefsGroup.dualPageSplit.bindToPreference(readerPreferences.dualPageSplitPaged()) - readerPreferences.dualPageSplitPaged().changes() - .onEach { - binding.pagerPrefsGroup.dualPageInvert.isVisible = it - if (it) { - binding.pagerPrefsGroup.dualPageRotateToFit.isChecked = false - } - } - .launchIn((context as ReaderActivity).lifecycleScope) - binding.pagerPrefsGroup.dualPageInvert.bindToPreference(readerPreferences.dualPageInvertPaged()) - - binding.pagerPrefsGroup.dualPageRotateToFit.bindToPreference(readerPreferences.dualPageRotateToFit()) - readerPreferences.dualPageRotateToFit().changes() - .onEach { - binding.pagerPrefsGroup.dualPageRotateToFitInvert.isVisible = it - if (it) { - binding.pagerPrefsGroup.dualPageSplit.isChecked = false - } - } - .launchIn((context as ReaderActivity).lifecycleScope) - binding.pagerPrefsGroup.dualPageRotateToFitInvert.bindToPreference(readerPreferences.dualPageRotateToFitInvert()) - } - - /** - * Init the preferences for the webtoon reader. - */ - private fun initWebtoonPreferences() { - binding.pagerPrefsGroup.root.isVisible = false - binding.webtoonPrefsGroup.root.isVisible = true - - binding.webtoonPrefsGroup.tappingInverted.bindToPreference(readerPreferences.webtoonNavInverted(), ReaderPreferences.TappingInvertMode::class.java) - - binding.webtoonPrefsGroup.webtoonNav.bindToPreference(readerPreferences.navigationModeWebtoon()) - readerPreferences.navigationModeWebtoon().changes() - .onEach { binding.webtoonPrefsGroup.tappingInverted.isVisible = it != 5 } - .launchIn((context as ReaderActivity).lifecycleScope) - binding.webtoonPrefsGroup.cropBordersWebtoon.bindToPreference(readerPreferences.cropBordersWebtoon()) - binding.webtoonPrefsGroup.webtoonSidePadding.bindToIntPreference(readerPreferences.webtoonSidePadding(), R.array.webtoon_side_padding_values) - - binding.webtoonPrefsGroup.dualPageSplit.bindToPreference(readerPreferences.dualPageSplitWebtoon()) - // Makes it so that dual page invert gets hidden away when dual page split is turned off - readerPreferences.dualPageSplitWebtoon().changes() - .onEach { binding.webtoonPrefsGroup.dualPageInvert.isVisible = it } - .launchIn((context as ReaderActivity).lifecycleScope) - binding.webtoonPrefsGroup.dualPageInvert.bindToPreference(readerPreferences.dualPageInvertWebtoon()) - binding.webtoonPrefsGroup.longStripSplit.isVisible = !isReleaseBuildType - binding.webtoonPrefsGroup.longStripSplit.bindToPreference(readerPreferences.longStripSplitWebtoon()) - - binding.webtoonPrefsGroup.doubleTapZoom.bindToPreference(readerPreferences.webtoonDoubleTapZoomEnabled()) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderSettingsScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderSettingsScreenModel.kt new file mode 100644 index 000000000..6a9ad2936 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderSettingsScreenModel.kt @@ -0,0 +1,16 @@ +package eu.kanade.tachiyomi.ui.reader.setting + +import cafe.adriel.voyager.core.model.ScreenModel +import eu.kanade.tachiyomi.util.preference.toggle +import tachiyomi.core.preference.Preference +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class ReaderSettingsScreenModel( + val preferences: ReaderPreferences = Injekt.get(), +) : ScreenModel { + + fun togglePreference(preference: (ReaderPreferences) -> Preference) { + preference(preferences).toggle() + } +} 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 ff432320f..dc449c2a3 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,55 +1,89 @@ package eu.kanade.tachiyomi.ui.reader.setting import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.databinding.CommonTabbedSheetBinding +import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope +import com.google.android.material.bottomsheet.BottomSheetDialog +import eu.kanade.domain.entries.manga.model.orientationType +import eu.kanade.domain.entries.manga.model.readingModeType +import eu.kanade.tachiyomi.databinding.ReaderReadingModeSettingsBinding import eu.kanade.tachiyomi.ui.reader.ReaderActivity -import eu.kanade.tachiyomi.widget.ViewPagerAdapter -import eu.kanade.tachiyomi.widget.sheet.BaseBottomSheetDialog +import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerViewer +import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import uy.kohesive.injekt.injectLazy class ReaderSettingsSheet( private val activity: ReaderActivity, -) : BaseBottomSheetDialog(activity) { +) : BottomSheetDialog(activity) { - private val tabs = listOf( - ReaderReadingModeSettings(activity) to R.string.pref_category_reading_mode, - ReaderGeneralSettings(activity) to R.string.pref_category_general, - ) + private val readerPreferences: ReaderPreferences by injectLazy() - private lateinit var binding: CommonTabbedSheetBinding - - override fun createView(inflater: LayoutInflater): View { - binding = CommonTabbedSheetBinding.inflate(activity.layoutInflater) - - val adapter = Adapter() - binding.pager.adapter = adapter - binding.tabs.setupWithViewPager(binding.pager) - - return binding.root - } + private lateinit var binding: ReaderReadingModeSettingsBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - behavior.isFitToContents = false - behavior.halfExpandedRatio = 0.25f + binding = ReaderReadingModeSettingsBinding.inflate(activity.layoutInflater) + setContentView(binding.root) + + initGeneralPreferences() + + when (activity.viewModel.state.value.viewer) { + is PagerViewer -> initPagerPreferences() + is WebtoonViewer -> initWebtoonPreferences() + } } - private inner class Adapter : ViewPagerAdapter() { + private fun initGeneralPreferences() { + binding.viewer.onItemSelectedListener = { position -> + val readingModeType = ReadingModeType.fromSpinner(position) + activity.viewModel.setMangaReadingMode(readingModeType.flagValue) - override fun createView(container: ViewGroup, position: Int): View { - return tabs[position].first + val mangaViewer = activity.viewModel.getMangaReadingMode() + if (mangaViewer == ReadingModeType.WEBTOON.flagValue || mangaViewer == ReadingModeType.CONTINUOUS_VERTICAL.flagValue) { + initWebtoonPreferences() + } else { + initPagerPreferences() + } } + binding.viewer.setSelection(activity.viewModel.manga?.readingModeType?.let { ReadingModeType.fromPreference(it.toInt()).prefValue } ?: ReadingModeType.DEFAULT.prefValue) - override fun getCount(): Int { - return tabs.size + binding.rotationMode.onItemSelectedListener = { position -> + val rotationType = OrientationType.fromSpinner(position) + activity.viewModel.setMangaOrientationType(rotationType.flagValue) } + binding.rotationMode.setSelection(activity.viewModel.manga?.orientationType?.let { OrientationType.fromPreference(it.toInt()).prefValue } ?: OrientationType.DEFAULT.prefValue) + } - override fun getPageTitle(position: Int): CharSequence { - return activity.resources!!.getString(tabs[position].second) - } + private fun initPagerPreferences() { + binding.webtoonPrefsGroup.root.isVisible = false + binding.pagerPrefsGroup.root.isVisible = true + + binding.pagerPrefsGroup.tappingInverted.bindToPreference(readerPreferences.pagerNavInverted(), ReaderPreferences.TappingInvertMode::class.java) + + binding.pagerPrefsGroup.pagerNav.bindToPreference(readerPreferences.navigationModePager()) + readerPreferences.navigationModePager().changes() + .onEach { + val isTappingEnabled = it != 5 + binding.pagerPrefsGroup.tappingInverted.isVisible = isTappingEnabled + } + .launchIn(activity.lifecycleScope) + binding.pagerPrefsGroup.scaleType.bindToPreference(readerPreferences.imageScaleType(), 1) + + binding.pagerPrefsGroup.zoomStart.bindToPreference(readerPreferences.zoomStart(), 1) + } + + private fun initWebtoonPreferences() { + binding.pagerPrefsGroup.root.isVisible = false + binding.webtoonPrefsGroup.root.isVisible = true + + binding.webtoonPrefsGroup.tappingInverted.bindToPreference(readerPreferences.webtoonNavInverted(), ReaderPreferences.TappingInvertMode::class.java) + + binding.webtoonPrefsGroup.webtoonNav.bindToPreference(readerPreferences.navigationModeWebtoon()) + readerPreferences.navigationModeWebtoon().changes() + .onEach { binding.webtoonPrefsGroup.tappingInverted.isVisible = it != 5 } + .launchIn(activity.lifecycleScope) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt index ec2c62093..f17d0bbd1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt @@ -134,7 +134,6 @@ private fun Context.defaultBrowserPackageName(): String? { val resolveInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { packageManager.resolveActivity(browserIntent, PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong())) } else { - @Suppress("DEPRECATION") packageManager.resolveActivity(browserIntent, PackageManager.MATCH_DEFAULT_ONLY) } return resolveInfo diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/DisplayExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/DisplayExtensions.kt index e93905c74..50f45ae85 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/DisplayExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/DisplayExtensions.kt @@ -5,10 +5,7 @@ import android.content.Context import android.content.res.Configuration import android.content.res.Resources import android.os.Build -import android.view.Display import android.view.View -import android.view.WindowManager -import androidx.core.content.getSystemService import eu.kanade.domain.ui.UiPreferences import eu.kanade.domain.ui.model.TabletUiMode import uy.kohesive.injekt.Injekt @@ -67,14 +64,6 @@ fun Context.isNightMode(): Boolean { return resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES } -val Context.displayCompat: Display? - get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - display - } else { - @Suppress("DEPRECATION") - getSystemService()?.defaultDisplay - } - val Resources.isLTR get() = configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/MaterialSpinnerView.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/MaterialSpinnerView.kt index b57abbba5..57289daf1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/MaterialSpinnerView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/MaterialSpinnerView.kt @@ -7,7 +7,6 @@ import android.view.Gravity import android.view.LayoutInflater import android.view.MenuItem import android.widget.FrameLayout -import androidx.annotation.ArrayRes import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.view.menu.MenuBuilder import androidx.appcompat.widget.PopupMenu @@ -99,17 +98,6 @@ class MaterialSpinnerView @JvmOverloads constructor(context: Context, attrs: Att } } - fun bindToIntPreference(pref: Preference, @ArrayRes intValuesResource: Int, block: ((Int) -> Unit)? = null) { - val intValues = resources.getStringArray(intValuesResource).map { it.toIntOrNull() } - setSelection(intValues.indexOf(pref.get())) - - popup = makeSettingsPopup(pref, intValues, block) - setOnTouchListener(popup?.dragToOpenListener) - setOnClickListener { - popup?.show() - } - } - private fun > makeSettingsPopup(preference: Preference, clazz: Class): PopupMenu { return createPopupMenu { pos -> onItemSelectedListener?.invoke(pos) diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/sheet/BaseBottomSheetDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/sheet/BaseBottomSheetDialog.kt deleted file mode 100644 index d19623525..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/sheet/BaseBottomSheetDialog.kt +++ /dev/null @@ -1,55 +0,0 @@ -package eu.kanade.tachiyomi.widget.sheet - -import android.content.Context -import android.os.Build -import android.os.Bundle -import android.util.DisplayMetrics -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import com.google.android.material.bottomsheet.BottomSheetDialog -import com.google.android.material.bottomsheet.getElevation -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.util.system.displayCompat -import eu.kanade.tachiyomi.util.system.isNightMode -import eu.kanade.tachiyomi.util.view.setNavigationBarTransparentCompat - -abstract class BaseBottomSheetDialog(context: Context) : BottomSheetDialog(context) { - - abstract fun createView(inflater: LayoutInflater): View - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - val rootView = createView(layoutInflater) - setContentView(rootView) - - // Enforce max width for tablets - val width = context.resources.getDimensionPixelSize(R.dimen.bottom_sheet_width) - if (width > 0) { - behavior.maxWidth = width - } - - // Set peek height to 50% display height - context.displayCompat?.let { - val metrics = DisplayMetrics() - it.getRealMetrics(metrics) - behavior.peekHeight = metrics.heightPixels / 2 - } - - // Set navbar color to transparent for edge-to-edge bottom sheet if we can use light navigation bar - // TODO Replace deprecated systemUiVisibility when material-components uses new API to modify status bar icons - @Suppress("DEPRECATION") - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - window?.setNavigationBarTransparentCompat(context, behavior.getElevation()) - val bottomSheet = rootView.parent as ViewGroup - var flags = bottomSheet.systemUiVisibility - flags = if (context.isNightMode()) { - flags and View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.inv() - } else { - flags or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR - } - bottomSheet.systemUiVisibility = flags - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/sheet/BottomSheetViewPager.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/sheet/BottomSheetViewPager.kt deleted file mode 100644 index 91b710757..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/sheet/BottomSheetViewPager.kt +++ /dev/null @@ -1,56 +0,0 @@ -package eu.kanade.tachiyomi.widget.sheet -import android.content.Context -import android.util.AttributeSet -import android.view.View -import androidx.viewpager.widget.ViewPager -import java.lang.reflect.Field - -/** - * From https://github.com/kafumi/android-bottomsheet-viewpager - */ -class BottomSheetViewPager @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, -) : ViewPager(context, attrs) { - - private val positionField: Field = LayoutParams::class.java.getDeclaredField("position").also { - it.isAccessible = true - } - - override fun getChildAt(index: Int): View { - val currentView = getCurrentView() ?: return super.getChildAt(index) - return if (index == 0) { - currentView - } else { - var view = super.getChildAt(index) - if (view == currentView) { - view = super.getChildAt(0) - } - return view - } - } - - private fun getCurrentView(): View? { - for (i in 0 until childCount) { - val child = super.getChildAt(i) - val lp = child.layoutParams as? LayoutParams - if (lp != null) { - val position = positionField.getInt(lp) - if (!lp.isDecor && currentItem == position) { - return child - } - } - } - return null - } - - init { - addOnPageChangeListener( - object : SimpleOnPageChangeListener() { - override fun onPageSelected(position: Int) { - requestLayout() - } - }, - ) - } -} diff --git a/app/src/main/res/drawable/ic_brightness_5_24dp.xml b/app/src/main/res/drawable/ic_brightness_5_24dp.xml deleted file mode 100644 index ae338ca19..000000000 --- a/app/src/main/res/drawable/ic_brightness_5_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/layout/common_tabbed_sheet.xml b/app/src/main/res/layout/common_tabbed_sheet.xml deleted file mode 100644 index de86274dc..000000000 --- a/app/src/main/res/layout/common_tabbed_sheet.xml +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/reader_activity.xml b/app/src/main/res/layout/reader_activity.xml index ef3efd1c1..07f01e3fa 100644 --- a/app/src/main/res/layout/reader_activity.xml +++ b/app/src/main/res/layout/reader_activity.xml @@ -120,29 +120,29 @@ app:tint="?attr/colorOnSurface" /> diff --git a/app/src/main/res/layout/reader_general_settings.xml b/app/src/main/res/layout/reader_general_settings.xml deleted file mode 100644 index 1d9fe94a3..000000000 --- a/app/src/main/res/layout/reader_general_settings.xml +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/reader_pager_settings.xml b/app/src/main/res/layout/reader_pager_settings.xml index bd430ca2f..4b9e1f877 100644 --- a/app/src/main/res/layout/reader_pager_settings.xml +++ b/app/src/main/res/layout/reader_pager_settings.xml @@ -1,7 +1,6 @@ @@ -45,77 +44,4 @@ android:entries="@array/zoom_start" app:title="@string/pref_zoom_start" /> - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/reader_reading_mode_settings.xml b/app/src/main/res/layout/reader_reading_mode_settings.xml index eeb733d47..e825c229a 100644 --- a/app/src/main/res/layout/reader_reading_mode_settings.xml +++ b/app/src/main/res/layout/reader_reading_mode_settings.xml @@ -36,7 +36,6 @@ android:entries="@array/rotation_type" app:title="@string/rotation_type" /> - - @@ -30,64 +29,4 @@ android:entries="@array/invert_tapping_mode" app:title="@string/pref_read_with_tapping_inverted" /> - - - - - - - - - - - - - - diff --git a/app/src/main/res/values-sw720dp/dimens.xml b/app/src/main/res/values-sw720dp/dimens.xml index cebce1c6a..5cb983dc4 100644 --- a/app/src/main/res/values-sw720dp/dimens.xml +++ b/app/src/main/res/values-sw720dp/dimens.xml @@ -1,5 +1,3 @@ - 480dp - 24dp diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index e0b15fedb..030c8bcc8 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -9,20 +9,6 @@ @string/vertical_plus_viewer - - @string/black_background - @string/gray_background - @string/white_background - @string/automatic_background - - - - 1 - 2 - 0 - 3 - - @string/scale_type_fit_screen @string/scale_type_stretch @@ -32,24 +18,6 @@ @string/scale_type_smart_fit - - @string/webtoon_side_padding_0 - @string/webtoon_side_padding_5 - @string/webtoon_side_padding_10 - @string/webtoon_side_padding_15 - @string/webtoon_side_padding_20 - @string/webtoon_side_padding_25 - - - - 0 - 5 - 10 - 15 - 20 - 25 - - @string/zoom_start_automatic @string/zoom_start_left diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 65275808c..f6f722017 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -1,6 +1,4 @@ - 0dp - 8dp 16dp diff --git a/core/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt b/core/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt index efbdacb9d..54d185883 100644 --- a/core/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt +++ b/core/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt @@ -44,6 +44,8 @@ class NetworkHelper( builder.addNetworkInterceptor(httpLoggingInterceptor) } + builder.addInterceptor(cloudflareInterceptor) + when (preferences.dohProvider().get()) { PREF_DOH_CLOUDFLARE -> builder.dohCloudflare() PREF_DOH_GOOGLE -> builder.dohGoogle() @@ -64,12 +66,12 @@ class NetworkHelper( val client by lazy { baseClientBuilder.cache(Cache(cacheDir, cacheSize)).build() } + /** + * @deprecated Since extension-lib 1.5 + */ + @Deprecated("The regular client handles Cloudflare by default") @Suppress("UNUSED") - val cloudflareClient by lazy { - client.newBuilder() - .addInterceptor(cloudflareInterceptor) - .build() - } + val cloudflareClient = client fun defaultUserAgentProvider() = preferences.defaultUserAgent().get().trim() } diff --git a/core/src/main/java/tachiyomi/core/preference/TriState.kt b/core/src/main/java/tachiyomi/core/preference/TriState.kt new file mode 100644 index 000000000..68b9173ce --- /dev/null +++ b/core/src/main/java/tachiyomi/core/preference/TriState.kt @@ -0,0 +1,16 @@ +package tachiyomi.core.preference + +enum class TriState { + DISABLED, // Disable filter + ENABLED_IS, // Enabled with "is" filter + ENABLED_NOT, // Enabled with "not" filter + ; + + fun next(): TriState { + return when (this) { + DISABLED -> ENABLED_IS + ENABLED_IS -> ENABLED_NOT + ENABLED_NOT -> DISABLED + } + } +} diff --git a/domain/src/main/java/tachiyomi/domain/entries/TriState.kt b/domain/src/main/java/tachiyomi/domain/entries/TriState.kt new file mode 100644 index 000000000..2e833c062 --- /dev/null +++ b/domain/src/main/java/tachiyomi/domain/entries/TriState.kt @@ -0,0 +1,9 @@ +package tachiyomi.domain.entries + +import tachiyomi.core.preference.TriState + +inline fun applyFilter(filter: TriState, predicate: () -> Boolean): Boolean = when (filter) { + TriState.DISABLED -> true + TriState.ENABLED_IS -> predicate() + TriState.ENABLED_NOT -> !predicate() +} diff --git a/domain/src/main/java/tachiyomi/domain/entries/TriStateFilter.kt b/domain/src/main/java/tachiyomi/domain/entries/TriStateFilter.kt deleted file mode 100644 index f56bfbfda..000000000 --- a/domain/src/main/java/tachiyomi/domain/entries/TriStateFilter.kt +++ /dev/null @@ -1,22 +0,0 @@ -package tachiyomi.domain.entries - -enum class TriStateFilter { - DISABLED, // Disable filter - ENABLED_IS, // Enabled with "is" filter - ENABLED_NOT, // Enabled with "not" filter - ; - - fun next(): TriStateFilter { - return when (this) { - DISABLED -> ENABLED_IS - ENABLED_IS -> ENABLED_NOT - ENABLED_NOT -> DISABLED - } - } -} - -inline fun applyFilter(filter: TriStateFilter, predicate: () -> Boolean): Boolean = when (filter) { - TriStateFilter.DISABLED -> true - TriStateFilter.ENABLED_IS -> predicate() - TriStateFilter.ENABLED_NOT -> !predicate() -} diff --git a/domain/src/main/java/tachiyomi/domain/entries/anime/model/Anime.kt b/domain/src/main/java/tachiyomi/domain/entries/anime/model/Anime.kt index 18a92d417..0f26c5a3b 100644 --- a/domain/src/main/java/tachiyomi/domain/entries/anime/model/Anime.kt +++ b/domain/src/main/java/tachiyomi/domain/entries/anime/model/Anime.kt @@ -1,7 +1,7 @@ package tachiyomi.domain.entries.anime.model import eu.kanade.tachiyomi.source.model.UpdateStrategy -import tachiyomi.domain.entries.TriStateFilter +import tachiyomi.core.preference.TriState import java.io.Serializable import kotlin.math.pow @@ -54,18 +54,18 @@ data class Anime( val nextEpisodeAiringAt: Long get() = (viewerFlags and ANIME_AIRING_TIME_MASK).removeHexZeros(zeros = 6) - val unseenFilter: TriStateFilter + val unseenFilter: TriState get() = when (unseenFilterRaw) { - EPISODE_SHOW_UNSEEN -> TriStateFilter.ENABLED_IS - EPISODE_SHOW_SEEN -> TriStateFilter.ENABLED_NOT - else -> TriStateFilter.DISABLED + EPISODE_SHOW_UNSEEN -> TriState.ENABLED_IS + EPISODE_SHOW_SEEN -> TriState.ENABLED_NOT + else -> TriState.DISABLED } - val bookmarkedFilter: TriStateFilter + val bookmarkedFilter: TriState get() = when (bookmarkedFilterRaw) { - EPISODE_SHOW_BOOKMARKED -> TriStateFilter.ENABLED_IS - EPISODE_SHOW_NOT_BOOKMARKED -> TriStateFilter.ENABLED_NOT - else -> TriStateFilter.DISABLED + EPISODE_SHOW_BOOKMARKED -> TriState.ENABLED_IS + EPISODE_SHOW_NOT_BOOKMARKED -> TriState.ENABLED_NOT + else -> TriState.DISABLED } fun sortDescending(): Boolean { diff --git a/domain/src/main/java/tachiyomi/domain/entries/manga/model/Manga.kt b/domain/src/main/java/tachiyomi/domain/entries/manga/model/Manga.kt index ff09e369b..26ee7eae8 100644 --- a/domain/src/main/java/tachiyomi/domain/entries/manga/model/Manga.kt +++ b/domain/src/main/java/tachiyomi/domain/entries/manga/model/Manga.kt @@ -1,7 +1,7 @@ package tachiyomi.domain.entries.manga.model import eu.kanade.tachiyomi.source.model.UpdateStrategy -import tachiyomi.domain.entries.TriStateFilter +import tachiyomi.core.preference.TriState import java.io.Serializable data class Manga( @@ -44,18 +44,18 @@ data class Manga( val bookmarkedFilterRaw: Long get() = chapterFlags and CHAPTER_BOOKMARKED_MASK - val unreadFilter: TriStateFilter + val unreadFilter: TriState get() = when (unreadFilterRaw) { - CHAPTER_SHOW_UNREAD -> TriStateFilter.ENABLED_IS - CHAPTER_SHOW_READ -> TriStateFilter.ENABLED_NOT - else -> TriStateFilter.DISABLED + CHAPTER_SHOW_UNREAD -> TriState.ENABLED_IS + CHAPTER_SHOW_READ -> TriState.ENABLED_NOT + else -> TriState.DISABLED } - val bookmarkedFilter: TriStateFilter + val bookmarkedFilter: TriState get() = when (bookmarkedFilterRaw) { - CHAPTER_SHOW_BOOKMARKED -> TriStateFilter.ENABLED_IS - CHAPTER_SHOW_NOT_BOOKMARKED -> TriStateFilter.ENABLED_NOT - else -> TriStateFilter.DISABLED + CHAPTER_SHOW_BOOKMARKED -> TriState.ENABLED_IS + CHAPTER_SHOW_NOT_BOOKMARKED -> TriState.ENABLED_NOT + else -> TriState.DISABLED } fun sortDescending(): Boolean { diff --git a/domain/src/main/java/tachiyomi/domain/library/service/LibraryPreferences.kt b/domain/src/main/java/tachiyomi/domain/library/service/LibraryPreferences.kt index f7410b1a4..b2f35d683 100644 --- a/domain/src/main/java/tachiyomi/domain/library/service/LibraryPreferences.kt +++ b/domain/src/main/java/tachiyomi/domain/library/service/LibraryPreferences.kt @@ -1,8 +1,8 @@ package tachiyomi.domain.library.service import tachiyomi.core.preference.PreferenceStore +import tachiyomi.core.preference.TriState import tachiyomi.core.preference.getEnum -import tachiyomi.domain.entries.TriStateFilter import tachiyomi.domain.entries.anime.model.Anime import tachiyomi.domain.entries.manga.model.Manga import tachiyomi.domain.library.anime.model.AnimeLibrarySort @@ -110,60 +110,60 @@ class LibraryPreferences( // Mixture Filter fun filterDownloadedAnime() = - preferenceStore.getEnum("pref_filter_animelib_downloaded_v2", TriStateFilter.DISABLED) + preferenceStore.getEnum("pref_filter_animelib_downloaded_v2", TriState.DISABLED) fun filterDownloadedManga() = - preferenceStore.getEnum("pref_filter_library_downloaded_v2", TriStateFilter.DISABLED) + preferenceStore.getEnum("pref_filter_library_downloaded_v2", TriState.DISABLED) fun filterUnseen() = - preferenceStore.getEnum("pref_filter_animelib_unread_v2", TriStateFilter.DISABLED) + preferenceStore.getEnum("pref_filter_animelib_unread_v2", TriState.DISABLED) fun filterUnread() = - preferenceStore.getEnum("pref_filter_library_unread_v2", TriStateFilter.DISABLED) + preferenceStore.getEnum("pref_filter_library_unread_v2", TriState.DISABLED) fun filterStartedAnime() = - preferenceStore.getEnum("pref_filter_animelib_started_v2", TriStateFilter.DISABLED) + preferenceStore.getEnum("pref_filter_animelib_started_v2", TriState.DISABLED) fun filterStartedManga() = - preferenceStore.getEnum("pref_filter_library_started_v2", TriStateFilter.DISABLED) + preferenceStore.getEnum("pref_filter_library_started_v2", TriState.DISABLED) fun filterBookmarkedAnime() = - preferenceStore.getEnum("pref_filter_animelib_bookmarked_v2", TriStateFilter.DISABLED) + preferenceStore.getEnum("pref_filter_animelib_bookmarked_v2", TriState.DISABLED) fun filterBookmarkedManga() = - preferenceStore.getEnum("pref_filter_library_bookmarked_v2", TriStateFilter.DISABLED) + preferenceStore.getEnum("pref_filter_library_bookmarked_v2", TriState.DISABLED) fun filterCompletedAnime() = - preferenceStore.getEnum("pref_filter_animelib_completed_v2", TriStateFilter.DISABLED) + preferenceStore.getEnum("pref_filter_animelib_completed_v2", TriState.DISABLED) fun filterCompletedManga() = - preferenceStore.getEnum("pref_filter_library_completed_v2", TriStateFilter.DISABLED) + preferenceStore.getEnum("pref_filter_library_completed_v2", TriState.DISABLED) - fun filterIntervalCustomAnime() = preferenceStore.getEnum("pref_filter_anime_library_interval_custom", TriStateFilter.DISABLED) + fun filterIntervalCustomAnime() = preferenceStore.getEnum("pref_filter_anime_library_interval_custom", TriState.DISABLED) - fun filterIntervalCustomManga() = preferenceStore.getEnum("pref_filter_manga_library_interval_custom", TriStateFilter.DISABLED) + fun filterIntervalCustomManga() = preferenceStore.getEnum("pref_filter_manga_library_interval_custom", TriState.DISABLED) - fun filterIntervalLongAnime() = preferenceStore.getEnum("pref_filter_anime_library_interval_long", TriStateFilter.DISABLED) + fun filterIntervalLongAnime() = preferenceStore.getEnum("pref_filter_anime_library_interval_long", TriState.DISABLED) - fun filterIntervalLongManga() = preferenceStore.getEnum("pref_filter_manga_library_interval_long", TriStateFilter.DISABLED) + fun filterIntervalLongManga() = preferenceStore.getEnum("pref_filter_manga_library_interval_long", TriState.DISABLED) - fun filterIntervalLateAnime() = preferenceStore.getEnum("pref_filter_anime_library_interval_late", TriStateFilter.DISABLED) + fun filterIntervalLateAnime() = preferenceStore.getEnum("pref_filter_anime_library_interval_late", TriState.DISABLED) - fun filterIntervalLateManga() = preferenceStore.getEnum("pref_filter_manga_library_interval_late", TriStateFilter.DISABLED) + fun filterIntervalLateManga() = preferenceStore.getEnum("pref_filter_manga_library_interval_late", TriState.DISABLED) - fun filterIntervalDroppedAnime() = preferenceStore.getEnum("pref_filter_anime_library_interval_dropped", TriStateFilter.DISABLED) + fun filterIntervalDroppedAnime() = preferenceStore.getEnum("pref_filter_anime_library_interval_dropped", TriState.DISABLED) - fun filterIntervalDroppedManga() = preferenceStore.getEnum("pref_filter_manga_library_interval_dropped", TriStateFilter.DISABLED) + fun filterIntervalDroppedManga() = preferenceStore.getEnum("pref_filter_manga_library_interval_dropped", TriState.DISABLED) - fun filterIntervalPassedAnime() = preferenceStore.getEnum("pref_filter_anime_library_interval_passed", TriStateFilter.DISABLED) + fun filterIntervalPassedAnime() = preferenceStore.getEnum("pref_filter_anime_library_interval_passed", TriState.DISABLED) - fun filterIntervalPassedManga() = preferenceStore.getEnum("pref_filter_manga_library_interval_passed", TriStateFilter.DISABLED) + fun filterIntervalPassedManga() = preferenceStore.getEnum("pref_filter_manga_library_interval_passed", TriState.DISABLED) fun filterTrackedAnime(id: Int) = - preferenceStore.getEnum("pref_filter_animelib_tracked_${id}_v2", TriStateFilter.DISABLED) + preferenceStore.getEnum("pref_filter_animelib_tracked_${id}_v2", TriState.DISABLED) fun filterTrackedManga(id: Int) = - preferenceStore.getEnum("pref_filter_library_tracked_${id}_v2", TriStateFilter.DISABLED) + preferenceStore.getEnum("pref_filter_library_tracked_${id}_v2", TriState.DISABLED) // Mixture Update Count diff --git a/i18n/src/main/res/values/strings.xml b/i18n/src/main/res/values/strings.xml index 7987c08c0..6839031f5 100644 --- a/i18n/src/main/res/values/strings.xml +++ b/i18n/src/main/res/values/strings.xml @@ -373,7 +373,7 @@ Vertical Both Actions - Show on long tap + Show on actions long tap Save pages into separate folders Creates folders according to entries\' title Background color @@ -431,12 +431,6 @@ Reading mode Reading Side padding - None - 5% - 10% - 15% - 20% - 25% Sensitivity for hiding menu on scroll Highest High @@ -479,7 +473,6 @@ Track - Only search pinned sources in global search Hide entries already in library @@ -620,6 +613,7 @@ Latest Popular Browse + Has results Local source guide You have no pinned sources Chapter not found diff --git a/presentation-core/build.gradle.kts b/presentation-core/build.gradle.kts index 53025fdaa..9c87d7465 100644 --- a/presentation-core/build.gradle.kts +++ b/presentation-core/build.gradle.kts @@ -21,6 +21,8 @@ android { } dependencies { + implementation(project(":core")) + // Compose implementation(platform(compose.bom)) implementation(compose.activity) diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/SettingsItems.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/SettingsItems.kt index fae6c8382..285c7f200 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/SettingsItems.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/SettingsItems.kt @@ -1,7 +1,9 @@ package tachiyomi.presentation.core.components +import android.view.MotionEvent import androidx.annotation.StringRes import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -10,22 +12,44 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.ContentAlpha import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowDownward import androidx.compose.material.icons.filled.ArrowUpward +import androidx.compose.material.icons.outlined.AddCircle +import androidx.compose.material.icons.outlined.RemoveCircle +import androidx.compose.material.icons.rounded.CheckBox +import androidx.compose.material.icons.rounded.CheckBoxOutlineBlank +import androidx.compose.material.icons.rounded.DisabledByDefault import androidx.compose.material3.Checkbox +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton 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.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.input.pointer.pointerInteropFilter import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp +import kotlinx.coroutines.delay +import tachiyomi.core.preference.TriState import tachiyomi.presentation.core.theme.header object SettingsItemsPaddings { @@ -174,6 +198,274 @@ fun SliderItem( } } +@Composable +fun SelectItem( + label: String, + options: Array, + selectedIndex: Int, + onSelect: (Int) -> Unit, +) { + var expanded by remember { mutableStateOf(false) } + + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = !expanded }, + ) { + OutlinedTextField( + modifier = Modifier + .menuAnchor() + .fillMaxWidth() + .padding(horizontal = SettingsItemsPaddings.Horizontal, vertical = SettingsItemsPaddings.Vertical), + label = { Text(text = label) }, + value = options[selectedIndex].toString(), + onValueChange = {}, + enabled = false, + readOnly = true, + singleLine = true, + trailingIcon = { + ExposedDropdownMenuDefaults.TrailingIcon( + expanded = expanded, + ) + }, + colors = ExposedDropdownMenuDefaults.textFieldColors( + disabledTextColor = MaterialTheme.colorScheme.onSurface, + ), + ) + + ExposedDropdownMenu( + modifier = Modifier.exposedDropdownSize(matchTextFieldWidth = true), + expanded = expanded, + onDismissRequest = { expanded = false }, + ) { + options.forEachIndexed { index, text -> + DropdownMenuItem( + text = { Text(text.toString()) }, + onClick = { + onSelect(index) + expanded = false + }, + contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding, + ) + } + } + } +} + +@Composable +fun TriStateItem( + label: String, + state: TriState, + enabled: Boolean = true, + onClick: ((TriState) -> Unit)?, +) { + Row( + modifier = Modifier + .clickable( + enabled = enabled && onClick != null, + onClick = { + when (state) { + TriState.DISABLED -> onClick?.invoke(TriState.ENABLED_IS) + TriState.ENABLED_IS -> onClick?.invoke(TriState.ENABLED_NOT) + TriState.ENABLED_NOT -> onClick?.invoke(TriState.DISABLED) + } + }, + ) + .fillMaxWidth() + .padding(horizontal = SettingsItemsPaddings.Horizontal, vertical = SettingsItemsPaddings.Vertical), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(24.dp), + ) { + val stateAlpha = if (enabled && onClick != null) 1f else ContentAlpha.disabled + + Icon( + imageVector = when (state) { + TriState.DISABLED -> Icons.Rounded.CheckBoxOutlineBlank + TriState.ENABLED_IS -> Icons.Rounded.CheckBox + TriState.ENABLED_NOT -> Icons.Rounded.DisabledByDefault + }, + contentDescription = null, + tint = if (!enabled || state == TriState.DISABLED) { + MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = stateAlpha) + } else { + when (onClick) { + null -> MaterialTheme.colorScheme.onSurface.copy(alpha = ContentAlpha.disabled) + else -> MaterialTheme.colorScheme.primary + } + }, + ) + Text( + text = label, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = stateAlpha), + style = MaterialTheme.typography.bodyMedium, + ) + } +} + +@Composable +fun SelectItem( + label: String, + options: Array, + selectedIndex: Int, + modifier: Modifier = Modifier, + onSelect: (Int) -> Unit, + toString: (T) -> String = { it.toString() }, +) { + var expanded by remember { mutableStateOf(false) } + + ExposedDropdownMenuBox( + modifier = modifier, + expanded = expanded, + onExpandedChange = { expanded = !expanded }, + ) { + OutlinedTextField( + modifier = Modifier + .menuAnchor() + .fillMaxWidth() + .padding( + horizontal = SettingsItemsPaddings.Horizontal, + vertical = SettingsItemsPaddings.Vertical, + ), + label = { Text(text = label) }, + value = toString(options[selectedIndex]), + onValueChange = {}, + readOnly = true, + singleLine = true, + trailingIcon = { + ExposedDropdownMenuDefaults.TrailingIcon( + expanded = expanded, + ) + }, + colors = ExposedDropdownMenuDefaults.textFieldColors(), + ) + + ExposedDropdownMenu( + modifier = Modifier.exposedDropdownSize(matchTextFieldWidth = true), + expanded = expanded, + onDismissRequest = { expanded = false }, + ) { + options.forEachIndexed { index, option -> + DropdownMenuItem( + text = { Text(toString(option)) }, + onClick = { + onSelect(index) + expanded = false + }, + ) + } + } + } +} + +@Composable +fun RepeatingIconButton( + modifier: Modifier = Modifier, + onClick: () -> Unit, + enabled: Boolean = true, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + maxDelayMillis: Long = 750, + minDelayMillis: Long = 5, + delayDecayFactor: Float = .25f, + content: @Composable () -> Unit, +) { + val currentClickListener by rememberUpdatedState(onClick) + var pressed by remember { mutableStateOf(false) } + + IconButton( + modifier = modifier.pointerInteropFilter { + pressed = when (it.action) { + MotionEvent.ACTION_DOWN -> true + + else -> false + } + + true + }, + onClick = {}, + enabled = enabled, + interactionSource = interactionSource, + content = content, + ) + + LaunchedEffect(pressed, enabled) { + var currentDelayMillis = maxDelayMillis + + while (enabled && pressed) { + currentClickListener() + delay(currentDelayMillis) + currentDelayMillis = + (currentDelayMillis - (currentDelayMillis * delayDecayFactor)) + .toLong().coerceAtLeast(minDelayMillis) + } + } +} + +@Composable +fun OutlinedNumericChooser( + label: String, + placeholder: String, + suffix: String, + value: Int, + step: Int, + min: Int? = null, + onValueChanged: (Int) -> Unit, +) { + var currentValue = value + + val updateValue: (Boolean) -> Unit = { + currentValue += if (it) step else -step + + if (min != null) currentValue = if (currentValue < min) min else currentValue + + onValueChanged(currentValue) + } + + Row(verticalAlignment = Alignment.CenterVertically) { + RepeatingIconButton( + onClick = { updateValue(false) }, + ) { Icon(imageVector = Icons.Outlined.RemoveCircle, contentDescription = null) } + + OutlinedTextField( + value = "%d".format(currentValue), + modifier = Modifier.widthIn(min = 140.dp), + + onValueChange = { + // Don't allow multiple decimal points, non-numeric characters, or leading zeros + currentValue = it.trim().replace(Regex("[^-\\d.]"), "").toIntOrNull() + ?: currentValue + onValueChanged(currentValue) + }, + + label = { Text(text = label) }, + placeholder = { Text(text = placeholder) }, + suffix = { Text(text = suffix) }, + + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + ) + + RepeatingIconButton( + onClick = { updateValue(true) }, + ) { Icon(imageVector = Icons.Outlined.AddCircle, contentDescription = null) } + } +} + +@Composable +fun TextItem( + label: String, + value: String, + onChange: (String) -> Unit, +) { + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = SettingsItemsPaddings.Horizontal, vertical = 4.dp), + label = { Text(text = label) }, + value = value, + onValueChange = onChange, + singleLine = true, + ) +} + @Composable private fun BaseSettingsItem( label: String, @@ -195,20 +487,3 @@ private fun BaseSettingsItem( ) } } - -@Composable -fun TextItem( - label: String, - value: String, - onChange: (String) -> Unit, -) { - OutlinedTextField( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = SettingsItemsPaddings.Horizontal, vertical = 4.dp), - label = { Text(text = label) }, - value = value, - onValueChange = onChange, - singleLine = true, - ) -} diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Tabs.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Tabs.kt index 5b029ac63..e8a0cab41 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Tabs.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Tabs.kt @@ -13,7 +13,7 @@ import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.TabPosition -import androidx.compose.material3.TabRowDefaults +import androidx.compose.material3.TabRowDefaults.SecondaryIndicator import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -51,8 +51,8 @@ fun TabIndicator( currentTabPosition: TabPosition, currentPageOffsetFraction: Float, ) { - TabRowDefaults.Indicator( - Modifier + SecondaryIndicator( + modifier = Modifier .tabIndicatorOffset(currentTabPosition, currentPageOffsetFraction) .padding(horizontal = 8.dp) .clip(RoundedCornerShape(topStart = 3.dp, topEnd = 3.dp)),