diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6c970060d..dde03bae5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -20,8 +20,8 @@ android { defaultConfig { applicationId = "xyz.jmir.tachiyomi.mi" - versionCode = 106 - versionName = "0.14.6" + versionCode = 108 + versionName = "0.14.7" buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"") @@ -319,12 +319,12 @@ tasks { kotlinOptions.freeCompilerArgs += listOf( "-P", "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" + - project.buildDir.absolutePath + "/compose_metrics", + project.layout.buildDirectory.dir("compose_metrics").get().asFile.absolutePath, ) kotlinOptions.freeCompilerArgs += listOf( "-P", "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" + - project.buildDir.absolutePath + "/compose_metrics", + project.layout.buildDirectory.dir("compose_metrics").get().asFile.absolutePath, ) } } diff --git a/app/src/main/java/eu/kanade/domain/DomainModule.kt b/app/src/main/java/eu/kanade/domain/DomainModule.kt index a17d68a9d..c84347e55 100644 --- a/app/src/main/java/eu/kanade/domain/DomainModule.kt +++ b/app/src/main/java/eu/kanade/domain/DomainModule.kt @@ -113,15 +113,15 @@ import tachiyomi.domain.history.manga.interactor.RemoveMangaHistory import tachiyomi.domain.history.manga.interactor.UpsertMangaHistory import tachiyomi.domain.history.manga.repository.MangaHistoryRepository import tachiyomi.domain.items.chapter.interactor.GetChapter -import tachiyomi.domain.items.chapter.interactor.GetChapterByMangaId import tachiyomi.domain.items.chapter.interactor.GetChapterByUrlAndMangaId +import tachiyomi.domain.items.chapter.interactor.GetChaptersByMangaId import tachiyomi.domain.items.chapter.interactor.SetMangaDefaultChapterFlags import tachiyomi.domain.items.chapter.interactor.ShouldUpdateDbChapter import tachiyomi.domain.items.chapter.interactor.UpdateChapter import tachiyomi.domain.items.chapter.repository.ChapterRepository import tachiyomi.domain.items.episode.interactor.GetEpisode -import tachiyomi.domain.items.episode.interactor.GetEpisodeByAnimeId import tachiyomi.domain.items.episode.interactor.GetEpisodeByUrlAndAnimeId +import tachiyomi.domain.items.episode.interactor.GetEpisodesByAnimeId import tachiyomi.domain.items.episode.interactor.SetAnimeDefaultEpisodeFlags import tachiyomi.domain.items.episode.interactor.ShouldUpdateDbEpisode import tachiyomi.domain.items.episode.interactor.UpdateEpisode @@ -250,7 +250,7 @@ class DomainModule : InjektModule { addSingletonFactory { EpisodeRepositoryImpl(get()) } addFactory { GetEpisode(get()) } - addFactory { GetEpisodeByAnimeId(get()) } + addFactory { GetEpisodesByAnimeId(get()) } addFactory { GetEpisodeByUrlAndAnimeId(get()) } addFactory { UpdateEpisode(get()) } addFactory { SetSeenStatus(get(), get(), get(), get()) } @@ -259,7 +259,7 @@ class DomainModule : InjektModule { addSingletonFactory { ChapterRepositoryImpl(get()) } addFactory { GetChapter(get()) } - addFactory { GetChapterByMangaId(get()) } + addFactory { GetChaptersByMangaId(get()) } addFactory { GetChapterByUrlAndMangaId(get()) } addFactory { UpdateChapter(get()) } addFactory { SetReadStatus(get(), get(), get(), get()) } diff --git a/app/src/main/java/eu/kanade/domain/base/BasePreferences.kt b/app/src/main/java/eu/kanade/domain/base/BasePreferences.kt index 1ba3fc288..db7a5b6fd 100644 --- a/app/src/main/java/eu/kanade/domain/base/BasePreferences.kt +++ b/app/src/main/java/eu/kanade/domain/base/BasePreferences.kt @@ -7,6 +7,7 @@ import androidx.annotation.StringRes import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.util.system.isPreviewBuildType import eu.kanade.tachiyomi.util.system.isReleaseBuildType +import tachiyomi.core.preference.Preference import tachiyomi.core.preference.PreferenceStore class BasePreferences( @@ -14,9 +15,12 @@ class BasePreferences( private val preferenceStore: PreferenceStore, ) { - fun downloadedOnly() = preferenceStore.getBoolean("pref_downloaded_only", false) + fun downloadedOnly() = preferenceStore.getBoolean( + Preference.appStateKey("pref_downloaded_only"), + false, + ) - fun incognitoMode() = preferenceStore.getBoolean("incognito_mode", false) + fun incognitoMode() = preferenceStore.getBoolean(Preference.appStateKey("incognito_mode"), false) fun extensionInstaller() = ExtensionInstallerPreference(context, preferenceStore) diff --git a/app/src/main/java/eu/kanade/domain/items/chapter/interactor/SyncChaptersWithSource.kt b/app/src/main/java/eu/kanade/domain/items/chapter/interactor/SyncChaptersWithSource.kt index d32d68e6a..928792694 100644 --- a/app/src/main/java/eu/kanade/domain/items/chapter/interactor/SyncChaptersWithSource.kt +++ b/app/src/main/java/eu/kanade/domain/items/chapter/interactor/SyncChaptersWithSource.kt @@ -11,7 +11,7 @@ import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.online.HttpSource import tachiyomi.data.items.chapter.ChapterSanitizer import tachiyomi.domain.entries.manga.model.Manga -import tachiyomi.domain.items.chapter.interactor.GetChapterByMangaId +import tachiyomi.domain.items.chapter.interactor.GetChaptersByMangaId import tachiyomi.domain.items.chapter.interactor.ShouldUpdateDbChapter import tachiyomi.domain.items.chapter.interactor.UpdateChapter import tachiyomi.domain.items.chapter.model.Chapter @@ -20,7 +20,6 @@ import tachiyomi.domain.items.chapter.model.toChapterUpdate import tachiyomi.domain.items.chapter.repository.ChapterRepository import tachiyomi.domain.items.chapter.service.ChapterRecognition import tachiyomi.source.local.entries.manga.isLocal -import uy.kohesive.injekt.api.get import java.lang.Long.max import java.time.ZonedDateTime import java.util.Date @@ -33,7 +32,7 @@ class SyncChaptersWithSource( private val shouldUpdateDbChapter: ShouldUpdateDbChapter, private val updateManga: UpdateManga, private val updateChapter: UpdateChapter, - private val getChapterByMangaId: GetChapterByMangaId, + private val getChaptersByMangaId: GetChaptersByMangaId, ) { /** @@ -67,7 +66,7 @@ class SyncChaptersWithSource( } // Chapters from db. - val dbChapters = getChapterByMangaId.await(manga.id) + val dbChapters = getChaptersByMangaId.await(manga.id) // Chapters from the source not in db. val toAdd = mutableListOf() diff --git a/app/src/main/java/eu/kanade/domain/items/chapter/model/Chapter.kt b/app/src/main/java/eu/kanade/domain/items/chapter/model/Chapter.kt index e9f36f187..fa8adf108 100644 --- a/app/src/main/java/eu/kanade/domain/items/chapter/model/Chapter.kt +++ b/app/src/main/java/eu/kanade/domain/items/chapter/model/Chapter.kt @@ -1,6 +1,5 @@ package eu.kanade.domain.items.chapter.model -import data.Chapters import eu.kanade.tachiyomi.data.database.models.manga.ChapterImpl import eu.kanade.tachiyomi.source.model.SChapter import tachiyomi.domain.items.chapter.model.Chapter @@ -27,16 +26,6 @@ fun Chapter.copyFromSChapter(sChapter: SChapter): Chapter { ) } -fun Chapter.copyFrom(other: Chapters): Chapter { - return copy( - name = other.name, - url = other.url, - dateUpload = other.date_upload, - chapterNumber = other.chapter_number, - scanlator = other.scanlator?.ifBlank { null }, - ) -} - fun Chapter.toDbChapter(): DbChapter = ChapterImpl().also { it.id = id it.manga_id = mangaId diff --git a/app/src/main/java/eu/kanade/domain/items/chapter/model/ChapterFilter.kt b/app/src/main/java/eu/kanade/domain/items/chapter/model/ChapterFilter.kt index ed15fdf0a..5e267dec3 100644 --- a/app/src/main/java/eu/kanade/domain/items/chapter/model/ChapterFilter.kt +++ b/app/src/main/java/eu/kanade/domain/items/chapter/model/ChapterFilter.kt @@ -2,7 +2,7 @@ package eu.kanade.domain.items.chapter.model import eu.kanade.domain.entries.manga.model.downloadedFilter import eu.kanade.tachiyomi.data.download.manga.MangaDownloadManager -import eu.kanade.tachiyomi.ui.entries.manga.ChapterItem +import eu.kanade.tachiyomi.ui.entries.manga.ChapterList import tachiyomi.domain.entries.applyFilter import tachiyomi.domain.entries.manga.model.Manga import tachiyomi.domain.items.chapter.model.Chapter @@ -39,7 +39,7 @@ fun List.applyFilters(manga: Manga, downloadManager: MangaDownloadManag * Applies the view filters to the list of chapters obtained from the database. * @return an observable of the list of chapters filtered and sorted. */ -fun List.applyFilters(manga: Manga): Sequence { +fun List.applyFilters(manga: Manga): Sequence { val isLocalManga = manga.isLocal() val unreadFilter = manga.unreadFilter val downloadedFilter = manga.downloadedFilter diff --git a/app/src/main/java/eu/kanade/domain/items/episode/interactor/SyncEpisodesWithSource.kt b/app/src/main/java/eu/kanade/domain/items/episode/interactor/SyncEpisodesWithSource.kt index 7d15bff11..359f95ff0 100644 --- a/app/src/main/java/eu/kanade/domain/items/episode/interactor/SyncEpisodesWithSource.kt +++ b/app/src/main/java/eu/kanade/domain/items/episode/interactor/SyncEpisodesWithSource.kt @@ -11,7 +11,7 @@ import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadManager import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadProvider import tachiyomi.data.items.episode.EpisodeSanitizer import tachiyomi.domain.entries.anime.model.Anime -import tachiyomi.domain.items.episode.interactor.GetEpisodeByAnimeId +import tachiyomi.domain.items.episode.interactor.GetEpisodesByAnimeId import tachiyomi.domain.items.episode.interactor.ShouldUpdateDbEpisode import tachiyomi.domain.items.episode.interactor.UpdateEpisode import tachiyomi.domain.items.episode.model.Episode @@ -20,7 +20,6 @@ import tachiyomi.domain.items.episode.model.toEpisodeUpdate import tachiyomi.domain.items.episode.repository.EpisodeRepository import tachiyomi.domain.items.episode.service.EpisodeRecognition import tachiyomi.source.local.entries.anime.isLocal -import uy.kohesive.injekt.api.get import java.lang.Long.max import java.time.ZonedDateTime import java.util.Date @@ -33,7 +32,7 @@ class SyncEpisodesWithSource( private val shouldUpdateDbEpisode: ShouldUpdateDbEpisode, private val updateAnime: UpdateAnime, private val updateEpisode: UpdateEpisode, - private val getEpisodeByAnimeId: GetEpisodeByAnimeId, + private val getEpisodesByAnimeId: GetEpisodesByAnimeId, ) { /** @@ -67,7 +66,7 @@ class SyncEpisodesWithSource( } // Episodes from db. - val dbEpisodes = getEpisodeByAnimeId.await(anime.id) + val dbEpisodes = getEpisodesByAnimeId.await(anime.id) // Episodes from the source not in db. val toAdd = mutableListOf() diff --git a/app/src/main/java/eu/kanade/domain/items/episode/model/Episode.kt b/app/src/main/java/eu/kanade/domain/items/episode/model/Episode.kt index 637822347..ac8f3e54d 100644 --- a/app/src/main/java/eu/kanade/domain/items/episode/model/Episode.kt +++ b/app/src/main/java/eu/kanade/domain/items/episode/model/Episode.kt @@ -1,6 +1,5 @@ package eu.kanade.domain.items.episode.model -import dataanime.Episodes import eu.kanade.tachiyomi.animesource.model.SEpisode import eu.kanade.tachiyomi.data.database.models.anime.EpisodeImpl import tachiyomi.domain.items.episode.model.Episode @@ -27,16 +26,6 @@ fun Episode.copyFromSEpisode(sEpisode: SEpisode): Episode { ) } -fun Episode.copyFrom(other: Episodes): Episode { - return copy( - name = other.name, - url = other.url, - dateUpload = other.date_upload, - episodeNumber = other.episode_number, - scanlator = other.scanlator?.ifBlank { null }, - ) -} - fun Episode.toDbEpisode(): DbEpisode = EpisodeImpl().also { it.id = id it.anime_id = animeId diff --git a/app/src/main/java/eu/kanade/domain/items/episode/model/EpisodeFilter.kt b/app/src/main/java/eu/kanade/domain/items/episode/model/EpisodeFilter.kt index f07630e28..751ccf87f 100644 --- a/app/src/main/java/eu/kanade/domain/items/episode/model/EpisodeFilter.kt +++ b/app/src/main/java/eu/kanade/domain/items/episode/model/EpisodeFilter.kt @@ -2,7 +2,7 @@ package eu.kanade.domain.items.episode.model import eu.kanade.domain.entries.anime.model.downloadedFilter import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadManager -import eu.kanade.tachiyomi.ui.entries.anime.EpisodeItem +import eu.kanade.tachiyomi.ui.entries.anime.EpisodeList import tachiyomi.domain.entries.anime.model.Anime import tachiyomi.domain.entries.applyFilter import tachiyomi.domain.items.episode.model.Episode @@ -39,7 +39,7 @@ fun List.applyFilters(anime: Anime, downloadManager: AnimeDownloadManag * Applies the view filters to the list of episodes obtained from the database. * @return an observable of the list of episodes filtered and sorted. */ -fun List.applyFilters(anime: Anime): Sequence { +fun List.applyFilters(anime: Anime): Sequence { val isLocalAnime = anime.isLocal() val unseenFilter = anime.unseenFilter val downloadedFilter = anime.downloadedFilter 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 9f8272eab..910331dd6 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 @@ -1,6 +1,7 @@ package eu.kanade.domain.source.service import eu.kanade.tachiyomi.util.system.LocaleHelper +import tachiyomi.core.preference.Preference import tachiyomi.core.preference.PreferenceStore import tachiyomi.core.preference.getEnum import tachiyomi.domain.library.model.LibraryDisplayMode @@ -35,7 +36,7 @@ class SourcePreferences( SetMigrateSorting.Direction.ASCENDING, ) - fun trustedSignatures() = preferenceStore.getStringSet("trusted_signatures", emptySet()) + fun trustedSignatures() = preferenceStore.getStringSet(Preference.appStateKey("trusted_signatures"), emptySet()) // Mixture Sources @@ -45,8 +46,14 @@ class SourcePreferences( fun pinnedAnimeSources() = preferenceStore.getStringSet("pinned_anime_catalogues", emptySet()) fun pinnedMangaSources() = preferenceStore.getStringSet("pinned_catalogues", emptySet()) - fun lastUsedAnimeSource() = preferenceStore.getLong("last_anime_catalogue_source", -1) - fun lastUsedMangaSource() = preferenceStore.getLong("last_catalogue_source", -1) + fun lastUsedAnimeSource() = preferenceStore.getLong( + Preference.appStateKey("last_anime_catalogue_source"), + -1, + ) + fun lastUsedMangaSource() = preferenceStore.getLong( + Preference.appStateKey("last_catalogue_source"), + -1, + ) fun animeExtensionUpdatesCount() = preferenceStore.getInt("animeext_updates_count", 0) fun mangaExtensionUpdatesCount() = preferenceStore.getInt("ext_updates_count", 0) diff --git a/app/src/main/java/eu/kanade/domain/track/anime/interactor/SyncEpisodeProgressWithTrack.kt b/app/src/main/java/eu/kanade/domain/track/anime/interactor/SyncEpisodeProgressWithTrack.kt index c8b9c6189..ab1087f11 100644 --- a/app/src/main/java/eu/kanade/domain/track/anime/interactor/SyncEpisodeProgressWithTrack.kt +++ b/app/src/main/java/eu/kanade/domain/track/anime/interactor/SyncEpisodeProgressWithTrack.kt @@ -5,7 +5,7 @@ import eu.kanade.tachiyomi.data.track.AnimeTracker import eu.kanade.tachiyomi.data.track.EnhancedAnimeTracker import logcat.LogPriority import tachiyomi.core.util.system.logcat -import tachiyomi.domain.items.episode.interactor.GetEpisodeByAnimeId +import tachiyomi.domain.items.episode.interactor.GetEpisodesByAnimeId import tachiyomi.domain.items.episode.interactor.UpdateEpisode import tachiyomi.domain.items.episode.model.toEpisodeUpdate import tachiyomi.domain.track.anime.interactor.InsertAnimeTrack @@ -14,7 +14,7 @@ import tachiyomi.domain.track.anime.model.AnimeTrack class SyncEpisodeProgressWithTrack( private val updateEpisode: UpdateEpisode, private val insertTrack: InsertAnimeTrack, - private val getEpisodeByAnimeId: GetEpisodeByAnimeId, + private val getEpisodesByAnimeId: GetEpisodesByAnimeId, ) { suspend fun await( @@ -26,7 +26,7 @@ class SyncEpisodeProgressWithTrack( return } - val sortedEpisodes = getEpisodeByAnimeId.await(animeId) + val sortedEpisodes = getEpisodesByAnimeId.await(animeId) .sortedBy { it.episodeNumber } .filter { it.isRecognizedNumber } diff --git a/app/src/main/java/eu/kanade/domain/track/manga/interactor/SyncChapterProgressWithTrack.kt b/app/src/main/java/eu/kanade/domain/track/manga/interactor/SyncChapterProgressWithTrack.kt index 86e8f03d7..53c92707c 100644 --- a/app/src/main/java/eu/kanade/domain/track/manga/interactor/SyncChapterProgressWithTrack.kt +++ b/app/src/main/java/eu/kanade/domain/track/manga/interactor/SyncChapterProgressWithTrack.kt @@ -5,7 +5,7 @@ import eu.kanade.tachiyomi.data.track.EnhancedMangaTracker import eu.kanade.tachiyomi.data.track.MangaTracker import logcat.LogPriority import tachiyomi.core.util.system.logcat -import tachiyomi.domain.items.chapter.interactor.GetChapterByMangaId +import tachiyomi.domain.items.chapter.interactor.GetChaptersByMangaId import tachiyomi.domain.items.chapter.interactor.UpdateChapter import tachiyomi.domain.items.chapter.model.toChapterUpdate import tachiyomi.domain.track.manga.interactor.InsertMangaTrack @@ -14,7 +14,7 @@ import tachiyomi.domain.track.manga.model.MangaTrack class SyncChapterProgressWithTrack( private val updateChapter: UpdateChapter, private val insertTrack: InsertMangaTrack, - private val getChapterByMangaId: GetChapterByMangaId, + private val getChaptersByMangaId: GetChaptersByMangaId, ) { suspend fun await( @@ -26,7 +26,7 @@ class SyncChapterProgressWithTrack( return } - val sortedChapters = getChapterByMangaId.await(mangaId) + val sortedChapters = getChaptersByMangaId.await(mangaId) .sortedBy { it.chapterNumber } .filter { it.isRecognizedNumber } 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 6a74fc643..c6e1818b9 100644 --- a/app/src/main/java/eu/kanade/presentation/components/TabbedDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/components/TabbedDialog.kt @@ -16,7 +16,6 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Tab import androidx.compose.material3.TabRow -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -33,6 +32,7 @@ import eu.kanade.tachiyomi.R import kotlinx.coroutines.launch import tachiyomi.presentation.core.components.HorizontalPager import tachiyomi.presentation.core.components.material.TabIndicator +import tachiyomi.presentation.core.components.material.TabText object TabbedDialogPaddings { val Horizontal = 24.dp @@ -71,21 +71,12 @@ fun TabbedDialog( }, divider = {}, ) { - tabTitles.fastForEachIndexed { i, tab -> - val selected = pagerState.currentPage == i + tabTitles.fastForEachIndexed { index, tab -> Tab( - selected = selected, - onClick = { scope.launch { pagerState.animateScrollToPage(i) } }, - text = { - Text( - text = tab, - color = if (selected) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, - ) - }, + selected = pagerState.currentPage == index, + onClick = { scope.launch { pagerState.animateScrollToPage(index) } }, + text = { TabText(text = tab) }, + unselectedContentColor = MaterialTheme.colorScheme.onSurface, ) } } diff --git a/app/src/main/java/eu/kanade/presentation/entries/anime/AnimeScreen.kt b/app/src/main/java/eu/kanade/presentation/entries/anime/AnimeScreen.kt index 2549320df..24813da6e 100644 --- a/app/src/main/java/eu/kanade/presentation/entries/anime/AnimeScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/entries/anime/AnimeScreen.kt @@ -5,9 +5,11 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.asPaddingValues @@ -27,7 +29,9 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text @@ -47,6 +51,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.util.fastAll import androidx.compose.ui.util.fastAny @@ -73,7 +78,7 @@ import eu.kanade.tachiyomi.data.download.anime.model.AnimeDownload import eu.kanade.tachiyomi.source.anime.getNameForAnimeInfo import eu.kanade.tachiyomi.ui.browse.anime.extension.details.SourcePreferencesScreen import eu.kanade.tachiyomi.ui.entries.anime.AnimeScreenModel -import eu.kanade.tachiyomi.ui.entries.anime.EpisodeItem +import eu.kanade.tachiyomi.ui.entries.anime.EpisodeList import eu.kanade.tachiyomi.util.lang.toRelativeString import eu.kanade.tachiyomi.util.system.copyToClipboard import kotlinx.coroutines.delay @@ -87,8 +92,10 @@ import tachiyomi.presentation.core.components.VerticalFastScroller import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton import tachiyomi.presentation.core.components.material.PullRefresh import tachiyomi.presentation.core.components.material.Scaffold +import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.util.isScrolledToEnd import tachiyomi.presentation.core.util.isScrollingUp +import tachiyomi.presentation.core.util.secondaryItemAlpha import java.text.DateFormat import java.util.Date import java.util.concurrent.TimeUnit @@ -107,7 +114,7 @@ fun AnimeScreen( alwaysUseExternalPlayer: Boolean, onBackClicked: () -> Unit, onEpisodeClicked: (episode: Episode, alt: Boolean) -> Unit, - onDownloadEpisode: ((List, EpisodeDownloadAction) -> Unit)?, + onDownloadEpisode: ((List, EpisodeDownloadAction) -> Unit)?, onAddToLibraryClicked: () -> Unit, onWebViewClicked: (() -> Unit)?, onWebViewLongClicked: (() -> Unit)?, @@ -139,10 +146,10 @@ fun AnimeScreen( onMultiDeleteClicked: (List) -> Unit, // For episode swipe - onEpisodeSwipe: (EpisodeItem, LibraryPreferences.EpisodeSwipeAction) -> Unit, + onEpisodeSwipe: (EpisodeList.Item, LibraryPreferences.EpisodeSwipeAction) -> Unit, // Episode selection - onEpisodeSelected: (EpisodeItem, Boolean, Boolean, Boolean) -> Unit, + onEpisodeSelected: (EpisodeList.Item, Boolean, Boolean, Boolean) -> Unit, onAllEpisodeSelected: (Boolean) -> Unit, onInvertSelection: () -> Unit, ) { @@ -257,7 +264,7 @@ private fun AnimeScreenSmallImpl( alwaysUseExternalPlayer: Boolean, onBackClicked: () -> Unit, onEpisodeClicked: (Episode, Boolean) -> Unit, - onDownloadEpisode: ((List, EpisodeDownloadAction) -> Unit)?, + onDownloadEpisode: ((List, EpisodeDownloadAction) -> Unit)?, onAddToLibraryClicked: () -> Unit, onWebViewClicked: (() -> Unit)?, onWebViewLongClicked: (() -> Unit)?, @@ -291,16 +298,17 @@ private fun AnimeScreenSmallImpl( onMultiDeleteClicked: (List) -> Unit, // For episode swipe - onEpisodeSwipe: (EpisodeItem, LibraryPreferences.EpisodeSwipeAction) -> Unit, + onEpisodeSwipe: (EpisodeList.Item, LibraryPreferences.EpisodeSwipeAction) -> Unit, // Episode selection - onEpisodeSelected: (EpisodeItem, Boolean, Boolean, Boolean) -> Unit, + onEpisodeSelected: (EpisodeList.Item, Boolean, Boolean, Boolean) -> Unit, onAllEpisodeSelected: (Boolean) -> Unit, onInvertSelection: () -> Unit, ) { val episodeListState = rememberLazyListState() val episodes = remember(state) { state.processedEpisodes } + val listItem = remember(state) { state.episodeListItems } val isAnySelected by remember { derivedStateOf { @@ -516,7 +524,8 @@ private fun AnimeScreenSmallImpl( sharedEpisodeItems( anime = state.anime, - episodes = episodes, + episodes = listItem, + isAnyEpisodeSelected = episodes.fastAny { it.selected }, dateRelativeTime = dateRelativeTime, dateFormat = dateFormat, episodeSwipeStartAction = episodeSwipeStartAction, @@ -546,7 +555,7 @@ fun AnimeScreenLargeImpl( alwaysUseExternalPlayer: Boolean, onBackClicked: () -> Unit, onEpisodeClicked: (Episode, Boolean) -> Unit, - onDownloadEpisode: ((List, EpisodeDownloadAction) -> Unit)?, + onDownloadEpisode: ((List, EpisodeDownloadAction) -> Unit)?, onAddToLibraryClicked: () -> Unit, onWebViewClicked: (() -> Unit)?, onWebViewLongClicked: (() -> Unit)?, @@ -580,10 +589,10 @@ fun AnimeScreenLargeImpl( onMultiDeleteClicked: (List) -> Unit, // For swipe actions - onEpisodeSwipe: (EpisodeItem, LibraryPreferences.EpisodeSwipeAction) -> Unit, + onEpisodeSwipe: (EpisodeList.Item, LibraryPreferences.EpisodeSwipeAction) -> Unit, // Episode selection - onEpisodeSelected: (EpisodeItem, Boolean, Boolean, Boolean) -> Unit, + onEpisodeSelected: (EpisodeList.Item, Boolean, Boolean, Boolean) -> Unit, onAllEpisodeSelected: (Boolean) -> Unit, onInvertSelection: () -> Unit, ) { @@ -591,6 +600,7 @@ fun AnimeScreenLargeImpl( val density = LocalDensity.current val episodes = remember(state) { state.processedEpisodes } + val listItem = remember(state) { state.episodeListItems } val isAnySelected by remember { derivedStateOf { @@ -799,7 +809,8 @@ fun AnimeScreenLargeImpl( sharedEpisodeItems( anime = state.anime, - episodes = episodes, + episodes = listItem, + isAnyEpisodeSelected = episodes.fastAny { it.selected }, dateRelativeTime = dateRelativeTime, dateFormat = dateFormat, episodeSwipeStartAction = episodeSwipeStartAction, @@ -819,13 +830,13 @@ fun AnimeScreenLargeImpl( @Composable private fun SharedAnimeBottomActionMenu( - selected: List, + selected: List, modifier: Modifier = Modifier, onEpisodeClicked: (Episode, Boolean) -> Unit, onMultiBookmarkClicked: (List, bookmarked: Boolean) -> Unit, onMultiMarkAsSeenClicked: (List, markAsSeen: Boolean) -> Unit, onMarkPreviousAsSeenClicked: (Episode) -> Unit, - onDownloadEpisode: ((List, EpisodeDownloadAction) -> Unit)?, + onDownloadEpisode: ((List, EpisodeDownloadAction) -> Unit)?, onMultiDeleteClicked: (List) -> Unit, fillFraction: Float, alwaysUseExternalPlayer: Boolean, @@ -870,100 +881,123 @@ private fun SharedAnimeBottomActionMenu( private fun LazyListScope.sharedEpisodeItems( anime: Anime, - episodes: List, + episodes: List, + isAnyEpisodeSelected: Boolean, dateRelativeTime: Boolean, dateFormat: DateFormat, episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction, episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction, onEpisodeClicked: (Episode, Boolean) -> Unit, - onDownloadEpisode: ((List, EpisodeDownloadAction) -> Unit)?, - onEpisodeSelected: (EpisodeItem, Boolean, Boolean, Boolean) -> Unit, - onEpisodeSwipe: (EpisodeItem, LibraryPreferences.EpisodeSwipeAction) -> Unit, + onDownloadEpisode: ((List, EpisodeDownloadAction) -> Unit)?, + onEpisodeSelected: (EpisodeList.Item, Boolean, Boolean, Boolean) -> Unit, + onEpisodeSwipe: (EpisodeList.Item, LibraryPreferences.EpisodeSwipeAction) -> Unit, ) { items( items = episodes, - key = { "episode-${it.episode.id}" }, + key = { item -> + when (item) { + is EpisodeList.MissingCount -> "missing-count-${item.id}" + is EpisodeList.Item -> "episode-${item.id}" + } + }, contentType = { EntryScreenItem.ITEM }, - ) { episodeItem -> + ) { item -> val haptic = LocalHapticFeedback.current val context = LocalContext.current - AnimeEpisodeListItem( - title = if (anime.displayMode == Anime.EPISODE_DISPLAY_NUMBER) { - stringResource( - R.string.display_mode_episode, - formatEpisodeNumber(episodeItem.episode.episodeNumber), - ) - } else { - episodeItem.episode.name - }, - date = episodeItem.episode.dateUpload - .takeIf { it > 0L } - ?.let { - Date(it).toRelativeString( - context, - dateRelativeTime, - dateFormat, + when (item) { + is EpisodeList.MissingCount -> { + Row( + modifier = Modifier.padding( + horizontal = MaterialTheme.padding.medium, + vertical = MaterialTheme.padding.small, + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium), + ) { + HorizontalDivider(modifier = Modifier.weight(1f)) + Text( + text = pluralStringResource( + id = R.plurals.missing_items, + count = item.count, + item.count, + ), + modifier = Modifier.secondaryItemAlpha(), ) - }, - watchProgress = episodeItem.episode.lastSecondSeen - .takeIf { !episodeItem.episode.seen && it > 0L } - ?.let { - stringResource( - R.string.episode_progress, - formatTime(it), - formatTime(episodeItem.episode.totalSeconds), - ) - }, - scanlator = episodeItem.episode.scanlator.takeIf { !it.isNullOrBlank() }, - seen = episodeItem.episode.seen, - bookmark = episodeItem.episode.bookmark, - selected = episodeItem.selected, - downloadIndicatorEnabled = episodes.fastAll { !it.selected }, - downloadStateProvider = { episodeItem.downloadState }, - downloadProgressProvider = { episodeItem.downloadProgress }, - episodeSwipeStartAction = episodeSwipeStartAction, - episodeSwipeEndAction = episodeSwipeEndAction, - onLongClick = { - onEpisodeSelected(episodeItem, !episodeItem.selected, true, true) - haptic.performHapticFeedback(HapticFeedbackType.LongPress) - }, - onClick = { - onEpisodeItemClick( - episodeItem = episodeItem, - episodes = episodes, - onToggleSelection = { - onEpisodeSelected( - episodeItem, - !episodeItem.selected, - true, - false, + HorizontalDivider(modifier = Modifier.weight(1f)) + } + } + is EpisodeList.Item -> { + AnimeEpisodeListItem( + title = if (anime.displayMode == Anime.EPISODE_DISPLAY_NUMBER) { + stringResource( + R.string.display_mode_episode, + formatEpisodeNumber(item.episode.episodeNumber), + ) + } else { + item.episode.name + }, + date = item.episode.dateUpload + .takeIf { it > 0L } + ?.let { + Date(it).toRelativeString( + context, + dateRelativeTime, + dateFormat, + ) + }, + watchProgress = item.episode.lastSecondSeen + .takeIf { !item.episode.seen && it > 0L } + ?.let { + stringResource( + R.string.episode_progress, + it + 1, + ) + }, + scanlator = item.episode.scanlator.takeIf { !it.isNullOrBlank() }, + seen = item.episode.seen, + bookmark = item.episode.bookmark, + selected = item.selected, + downloadIndicatorEnabled = !isAnyEpisodeSelected, + downloadStateProvider = { item.downloadState }, + downloadProgressProvider = { item.downloadProgress }, + episodeSwipeStartAction = episodeSwipeStartAction, + episodeSwipeEndAction = episodeSwipeEndAction, + onLongClick = { + onEpisodeSelected(item, !item.selected, true, true) + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + }, + onClick = { + onEpisodeItemClick( + episodeItem = item, + isAnyEpisodeSelected = isAnyEpisodeSelected, + onToggleSelection = { onEpisodeSelected(item, !item.selected, true, false) }, + onEpisodeClicked = onEpisodeClicked, ) }, - onEpisodeClicked = onEpisodeClicked, + onDownloadClick = if (onDownloadEpisode != null) { + { onDownloadEpisode(listOf(item), it) } + } else { + null + }, + onEpisodeSwipe = { + onEpisodeSwipe(item, it) + }, ) - }, - onDownloadClick = if (onDownloadEpisode != null) { - { onDownloadEpisode(listOf(episodeItem), it) } - } else { - null - }, - onEpisodeSwipe = { - onEpisodeSwipe(episodeItem, it) - }, - ) + } + } } } private fun onEpisodeItemClick( - episodeItem: EpisodeItem, - episodes: List, + episodeItem: EpisodeList.Item, + isAnyEpisodeSelected: Boolean, onToggleSelection: (Boolean) -> Unit, onEpisodeClicked: (Episode, Boolean) -> Unit, ) { when { episodeItem.selected -> onToggleSelection(false) - episodes.fastAny { it.selected } -> onToggleSelection(true) + isAnyEpisodeSelected -> onToggleSelection(true) else -> onEpisodeClicked(episodeItem.episode, false) } } diff --git a/app/src/main/java/eu/kanade/presentation/entries/anime/components/AnimeInfoHeader.kt b/app/src/main/java/eu/kanade/presentation/entries/anime/components/AnimeInfoHeader.kt index 255d580de..aa995715b 100644 --- a/app/src/main/java/eu/kanade/presentation/entries/anime/components/AnimeInfoHeader.kt +++ b/app/src/main/java/eu/kanade/presentation/entries/anime/components/AnimeInfoHeader.kt @@ -1,6 +1,5 @@ package eu.kanade.presentation.entries.anime.components -import android.content.Context import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.graphics.res.animatedVectorResource @@ -29,6 +28,7 @@ import androidx.compose.material.icons.filled.Brush import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.HourglassEmpty import androidx.compose.material.icons.filled.PersonOutline +import androidx.compose.material.icons.filled.Warning import androidx.compose.material.icons.outlined.AttachMoney import androidx.compose.material.icons.outlined.Block import androidx.compose.material.icons.outlined.Close @@ -43,6 +43,7 @@ import androidx.compose.material.icons.outlined.Warning import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.SuggestionChip @@ -128,7 +129,7 @@ fun AnimeInfoBox( .alpha(.2f), ) - // Manga & source info + // Anime & source info CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) { if (!isTabletUi) { AnimeAndSourceTitlesSmall( @@ -136,7 +137,6 @@ fun AnimeInfoBox( coverDataProvider = coverDataProvider, onCoverClick = onCoverClick, title = title, - context = LocalContext.current, doSearch = doSearch, author = author, artist = artist, @@ -150,7 +150,6 @@ fun AnimeInfoBox( coverDataProvider = coverDataProvider, onCoverClick = onCoverClick, title = title, - context = LocalContext.current, doSearch = doSearch, author = author, artist = artist, @@ -335,7 +334,6 @@ private fun AnimeAndSourceTitlesLarge( coverDataProvider: () -> Anime, onCoverClick: () -> Unit, title: String, - context: Context, doSearch: (query: String, global: Boolean) -> Unit, author: String?, artist: String?, @@ -356,104 +354,16 @@ private fun AnimeAndSourceTitlesLarge( onClick = onCoverClick, ) Spacer(modifier = Modifier.height(16.dp)) - Text( - text = title.ifBlank { stringResource(R.string.unknown_title) }, - style = MaterialTheme.typography.titleLarge, - modifier = Modifier.clickableNoIndication( - onLongClick = { if (title.isNotBlank()) context.copyToClipboard(title, title) }, - onClick = { if (title.isNotBlank()) doSearch(title, true) }, - ), + AnimeContentInfo( + title = title, + doSearch = doSearch, + author = author, + artist = artist, + status = status, + sourceName = sourceName, + isStubSource = isStubSource, textAlign = TextAlign.Center, ) - Spacer(modifier = Modifier.height(2.dp)) - Text( - text = author?.takeIf { it.isNotBlank() } ?: stringResource(R.string.unknown_studio), - style = MaterialTheme.typography.titleSmall, - modifier = Modifier - .secondaryItemAlpha() - .padding(top = 2.dp) - .clickableNoIndication( - onLongClick = { - if (!author.isNullOrBlank()) { - context.copyToClipboard( - author, - author, - ) - } - }, - onClick = { if (!author.isNullOrBlank()) doSearch(author, true) }, - ), - textAlign = TextAlign.Center, - ) - if (!artist.isNullOrBlank() && author != artist) { - Text( - text = artist, - style = MaterialTheme.typography.titleSmall, - modifier = Modifier - .secondaryItemAlpha() - .padding(top = 2.dp) - .clickableNoIndication( - onLongClick = { context.copyToClipboard(artist, artist) }, - onClick = { doSearch(artist, true) }, - ), - textAlign = TextAlign.Center, - ) - } - Spacer(modifier = Modifier.height(4.dp)) - Row( - modifier = Modifier.secondaryItemAlpha(), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - imageVector = when (status) { - SAnime.ONGOING.toLong() -> Icons.Outlined.Schedule - SAnime.COMPLETED.toLong() -> Icons.Outlined.DoneAll - SAnime.LICENSED.toLong() -> Icons.Outlined.AttachMoney - SAnime.PUBLISHING_FINISHED.toLong() -> Icons.Outlined.Done - SAnime.CANCELLED.toLong() -> Icons.Outlined.Close - SAnime.ON_HIATUS.toLong() -> Icons.Outlined.Pause - else -> Icons.Outlined.Block - }, - contentDescription = null, - modifier = Modifier - .padding(end = 4.dp) - .size(16.dp), - ) - ProvideTextStyle(MaterialTheme.typography.bodyMedium) { - Text( - text = when (status) { - SAnime.ONGOING.toLong() -> stringResource(R.string.ongoing) - SAnime.COMPLETED.toLong() -> stringResource(R.string.completed) - SAnime.LICENSED.toLong() -> stringResource(R.string.licensed) - SAnime.PUBLISHING_FINISHED.toLong() -> stringResource( - R.string.publishing_finished, - ) - SAnime.CANCELLED.toLong() -> stringResource(R.string.cancelled) - SAnime.ON_HIATUS.toLong() -> stringResource(R.string.on_hiatus) - else -> stringResource(R.string.unknown) - }, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - ) - DotSeparatorText() - if (isStubSource) { - Icon( - imageVector = Icons.Outlined.Warning, - contentDescription = null, - modifier = Modifier - .padding(end = 4.dp) - .size(16.dp), - tint = MaterialTheme.colorScheme.error, - ) - } - Text( - text = sourceName, - modifier = Modifier.clickableNoIndication { doSearch(sourceName, false) }, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - ) - } - } } } @@ -463,7 +373,6 @@ private fun AnimeAndSourceTitlesSmall( coverDataProvider: () -> Anime, onCoverClick: () -> Unit, title: String, - context: Context, doSearch: (query: String, global: Boolean) -> Unit, author: String?, artist: String?, @@ -489,137 +398,161 @@ private fun AnimeAndSourceTitlesSmall( Column( verticalArrangement = Arrangement.spacedBy(2.dp), ) { - Text( - text = title.ifBlank { stringResource(R.string.unknown_title) }, - style = MaterialTheme.typography.titleLarge, - modifier = Modifier.clickableNoIndication( + AnimeContentInfo( + title = title, + doSearch = doSearch, + author = author, + artist = artist, + status = status, + sourceName = sourceName, + isStubSource = isStubSource, + ) + } + } +} + +@Composable +private fun AnimeContentInfo( + title: String, + textAlign: TextAlign? = LocalTextStyle.current.textAlign, + doSearch: (query: String, global: Boolean) -> Unit, + author: String?, + artist: String?, + status: Long, + sourceName: String, + isStubSource: Boolean, +) { + val context = LocalContext.current + Text( + text = title.ifBlank { stringResource(R.string.unknown_title) }, + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.clickableNoIndication( + onLongClick = { + if (title.isNotBlank()) { + context.copyToClipboard( + title, + title, + ) + } + }, + onClick = { if (title.isNotBlank()) doSearch(title, true) }, + ), + textAlign = textAlign, + ) + + Spacer(modifier = Modifier.height(2.dp)) + + Row( + modifier = Modifier.secondaryItemAlpha(), + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = Icons.Filled.PersonOutline, + contentDescription = null, + modifier = Modifier.size(16.dp), + ) + Text( + text = author?.takeIf { it.isNotBlank() } + ?: stringResource(R.string.unknown_author), + style = MaterialTheme.typography.titleSmall, + modifier = Modifier + .clickableNoIndication( onLongClick = { - if (title.isNotBlank()) { + if (!author.isNullOrBlank()) { context.copyToClipboard( - title, - title, + author, + author, ) } }, - onClick = { if (title.isNotBlank()) doSearch(title, true) }, + onClick = { if (!author.isNullOrBlank()) doSearch(author, true) }, ), + textAlign = textAlign, + ) + } + + if (!artist.isNullOrBlank() && author != artist) { + Row( + modifier = Modifier.secondaryItemAlpha(), + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = Icons.Filled.Brush, + contentDescription = null, + modifier = Modifier.size(16.dp), ) + Text( + text = artist, + style = MaterialTheme.typography.titleSmall, + modifier = Modifier + .clickableNoIndication( + onLongClick = { context.copyToClipboard(artist, artist) }, + onClick = { doSearch(artist, true) }, + ), + textAlign = textAlign, + ) + } + } - Spacer(modifier = Modifier.height(2.dp)) + Spacer(modifier = Modifier.height(2.dp)) - Row( - modifier = Modifier.secondaryItemAlpha(), - horizontalArrangement = Arrangement.spacedBy(4.dp), - verticalAlignment = Alignment.CenterVertically, - ) { + Row( + modifier = Modifier.secondaryItemAlpha(), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = when (status) { + SAnime.ONGOING.toLong() -> Icons.Outlined.Schedule + SAnime.COMPLETED.toLong() -> Icons.Outlined.DoneAll + SAnime.LICENSED.toLong() -> Icons.Outlined.AttachMoney + SAnime.PUBLISHING_FINISHED.toLong() -> Icons.Outlined.Done + SAnime.CANCELLED.toLong() -> Icons.Outlined.Close + SAnime.ON_HIATUS.toLong() -> Icons.Outlined.Pause + else -> Icons.Outlined.Block + }, + contentDescription = null, + modifier = Modifier + .padding(end = 4.dp) + .size(16.dp), + ) + ProvideTextStyle(MaterialTheme.typography.bodyMedium) { + Text( + text = when (status) { + SAnime.ONGOING.toLong() -> stringResource(R.string.ongoing) + SAnime.COMPLETED.toLong() -> stringResource(R.string.completed) + SAnime.LICENSED.toLong() -> stringResource(R.string.licensed) + SAnime.PUBLISHING_FINISHED.toLong() -> stringResource(R.string.publishing_finished) + SAnime.CANCELLED.toLong() -> stringResource(R.string.cancelled) + SAnime.ON_HIATUS.toLong() -> stringResource(R.string.on_hiatus) + else -> stringResource(R.string.unknown) + }, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + DotSeparatorText() + if (isStubSource) { Icon( - imageVector = Icons.Filled.PersonOutline, - contentDescription = null, - modifier = Modifier.size(16.dp), - ) - Text( - text = author?.takeIf { it.isNotBlank() } - ?: stringResource(R.string.unknown_author), - style = MaterialTheme.typography.titleSmall, - modifier = Modifier - .clickableNoIndication( - onLongClick = { - if (!author.isNullOrBlank()) { - context.copyToClipboard( - author, - author, - ) - } - }, - onClick = { if (!author.isNullOrBlank()) doSearch(author, true) }, - ), - ) - } - - if (!artist.isNullOrBlank() && author != artist) { - Row( - modifier = Modifier.secondaryItemAlpha(), - horizontalArrangement = Arrangement.spacedBy(4.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - imageVector = Icons.Filled.Brush, - contentDescription = null, - modifier = Modifier.size(16.dp), - ) - Text( - text = artist, - style = MaterialTheme.typography.titleSmall, - modifier = Modifier - .clickableNoIndication( - onLongClick = { context.copyToClipboard(artist, artist) }, - onClick = { doSearch(artist, true) }, - ), - ) - } - } - - Spacer(modifier = Modifier.height(2.dp)) - - Row( - modifier = Modifier.secondaryItemAlpha(), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - imageVector = when (status) { - SAnime.ONGOING.toLong() -> Icons.Outlined.Schedule - SAnime.COMPLETED.toLong() -> Icons.Outlined.DoneAll - SAnime.LICENSED.toLong() -> Icons.Outlined.AttachMoney - SAnime.PUBLISHING_FINISHED.toLong() -> Icons.Outlined.Done - SAnime.CANCELLED.toLong() -> Icons.Outlined.Close - SAnime.ON_HIATUS.toLong() -> Icons.Outlined.Pause - else -> Icons.Outlined.Block - }, + imageVector = Icons.Filled.Warning, contentDescription = null, modifier = Modifier .padding(end = 4.dp) .size(16.dp), + tint = MaterialTheme.colorScheme.error, ) - ProvideTextStyle(MaterialTheme.typography.bodyMedium) { - Text( - text = when (status) { - SAnime.ONGOING.toLong() -> stringResource(R.string.ongoing) - SAnime.COMPLETED.toLong() -> stringResource(R.string.completed) - SAnime.LICENSED.toLong() -> stringResource(R.string.licensed) - SAnime.PUBLISHING_FINISHED.toLong() -> stringResource( - R.string.publishing_finished, - ) - SAnime.CANCELLED.toLong() -> stringResource(R.string.cancelled) - SAnime.ON_HIATUS.toLong() -> stringResource(R.string.on_hiatus) - else -> stringResource(R.string.unknown) - }, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - ) - DotSeparatorText() - if (isStubSource) { - Icon( - imageVector = Icons.Outlined.Warning, - contentDescription = null, - modifier = Modifier - .padding(end = 4.dp) - .size(16.dp), - tint = MaterialTheme.colorScheme.error, - ) - } - Text( - text = sourceName, - modifier = Modifier.clickableNoIndication { - doSearch( - sourceName, - false, - ) - }, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - ) - } } + Text( + text = sourceName, + modifier = Modifier.clickableNoIndication { + doSearch( + sourceName, + false, + ) + }, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) } } } diff --git a/app/src/main/java/eu/kanade/presentation/entries/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/presentation/entries/manga/MangaScreen.kt index a17764a88..f3faf76ab 100644 --- a/app/src/main/java/eu/kanade/presentation/entries/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/entries/manga/MangaScreen.kt @@ -5,9 +5,11 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.asPaddingValues @@ -26,7 +28,9 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text @@ -44,6 +48,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.util.fastAll import androidx.compose.ui.util.fastAny @@ -67,7 +72,7 @@ import eu.kanade.tachiyomi.data.download.manga.model.MangaDownload import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.manga.getNameForMangaInfo import eu.kanade.tachiyomi.ui.browse.manga.extension.details.MangaSourcePreferencesScreen -import eu.kanade.tachiyomi.ui.entries.manga.ChapterItem +import eu.kanade.tachiyomi.ui.entries.manga.ChapterList import eu.kanade.tachiyomi.ui.entries.manga.MangaScreenModel import eu.kanade.tachiyomi.util.lang.toRelativeString import eu.kanade.tachiyomi.util.system.copyToClipboard @@ -81,8 +86,10 @@ import tachiyomi.presentation.core.components.VerticalFastScroller import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton import tachiyomi.presentation.core.components.material.PullRefresh import tachiyomi.presentation.core.components.material.Scaffold +import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.util.isScrolledToEnd import tachiyomi.presentation.core.util.isScrollingUp +import tachiyomi.presentation.core.util.secondaryItemAlpha import java.text.DateFormat import java.util.Date @@ -98,7 +105,7 @@ fun MangaScreen( chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, onBackClicked: () -> Unit, onChapterClicked: (Chapter) -> Unit, - onDownloadChapter: ((List, ChapterDownloadAction) -> Unit)?, + onDownloadChapter: ((List, ChapterDownloadAction) -> Unit)?, onAddToLibraryClicked: () -> Unit, onWebViewClicked: (() -> Unit)?, onWebViewLongClicked: (() -> Unit)?, @@ -129,10 +136,10 @@ fun MangaScreen( onMultiDeleteClicked: (List) -> Unit, // For chapter swipe - onChapterSwipe: (ChapterItem, LibraryPreferences.ChapterSwipeAction) -> Unit, + onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit, // Chapter selection - onChapterSelected: (ChapterItem, Boolean, Boolean, Boolean) -> Unit, + onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit, onAllChapterSelected: (Boolean) -> Unit, onInvertSelection: () -> Unit, ) { @@ -238,7 +245,7 @@ private fun MangaScreenSmallImpl( chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, onBackClicked: () -> Unit, onChapterClicked: (Chapter) -> Unit, - onDownloadChapter: ((List, ChapterDownloadAction) -> Unit)?, + onDownloadChapter: ((List, ChapterDownloadAction) -> Unit)?, onAddToLibraryClicked: () -> Unit, onWebViewClicked: (() -> Unit)?, onWebViewLongClicked: (() -> Unit)?, @@ -271,16 +278,17 @@ private fun MangaScreenSmallImpl( onMultiDeleteClicked: (List) -> Unit, // For chapter swipe - onChapterSwipe: (ChapterItem, LibraryPreferences.ChapterSwipeAction) -> Unit, + onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit, // Chapter selection - onChapterSelected: (ChapterItem, Boolean, Boolean, Boolean) -> Unit, + onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit, onAllChapterSelected: (Boolean) -> Unit, onInvertSelection: () -> Unit, ) { val chapterListState = rememberLazyListState() val chapters = remember(state) { state.processedChapters } + val listItem = remember(state) { state.chapterListItems } val isAnySelected by remember { derivedStateOf { @@ -465,7 +473,8 @@ private fun MangaScreenSmallImpl( sharedChapterItems( manga = state.manga, - chapters = chapters, + chapters = listItem, + isAnyChapterSelected = chapters.fastAny { it.selected }, dateRelativeTime = dateRelativeTime, dateFormat = dateFormat, chapterSwipeStartAction = chapterSwipeStartAction, @@ -492,7 +501,7 @@ fun MangaScreenLargeImpl( chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, onBackClicked: () -> Unit, onChapterClicked: (Chapter) -> Unit, - onDownloadChapter: ((List, ChapterDownloadAction) -> Unit)?, + onDownloadChapter: ((List, ChapterDownloadAction) -> Unit)?, onAddToLibraryClicked: () -> Unit, onWebViewClicked: (() -> Unit)?, onWebViewLongClicked: (() -> Unit)?, @@ -525,10 +534,10 @@ fun MangaScreenLargeImpl( onMultiDeleteClicked: (List) -> Unit, // For swipe actions - onChapterSwipe: (ChapterItem, LibraryPreferences.ChapterSwipeAction) -> Unit, + onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit, // Chapter selection - onChapterSelected: (ChapterItem, Boolean, Boolean, Boolean) -> Unit, + onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit, onAllChapterSelected: (Boolean) -> Unit, onInvertSelection: () -> Unit, ) { @@ -536,6 +545,7 @@ fun MangaScreenLargeImpl( val density = LocalDensity.current val chapters = remember(state) { state.processedChapters } + val listItem = remember(state) { state.chapterListItems } val isAnySelected by remember { derivedStateOf { @@ -716,7 +726,8 @@ fun MangaScreenLargeImpl( sharedChapterItems( manga = state.manga, - chapters = chapters, + chapters = listItem, + isAnyChapterSelected = chapters.fastAny { it.selected }, dateRelativeTime = dateRelativeTime, dateFormat = dateFormat, chapterSwipeStartAction = chapterSwipeStartAction, @@ -736,12 +747,12 @@ fun MangaScreenLargeImpl( @Composable private fun SharedMangaBottomActionMenu( - selected: List, + selected: List, modifier: Modifier = Modifier, onMultiBookmarkClicked: (List, bookmarked: Boolean) -> Unit, onMultiMarkAsReadClicked: (List, markAsRead: Boolean) -> Unit, onMarkPreviousAsReadClicked: (Chapter) -> Unit, - onDownloadChapter: ((List, ChapterDownloadAction) -> Unit)?, + onDownloadChapter: ((List, ChapterDownloadAction) -> Unit)?, onMultiDeleteClicked: (List) -> Unit, fillFraction: Float, ) { @@ -779,99 +790,123 @@ private fun SharedMangaBottomActionMenu( private fun LazyListScope.sharedChapterItems( manga: Manga, - chapters: List, + chapters: List, + isAnyChapterSelected: Boolean, dateRelativeTime: Boolean, dateFormat: DateFormat, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, onChapterClicked: (Chapter) -> Unit, - onDownloadChapter: ((List, ChapterDownloadAction) -> Unit)?, - onChapterSelected: (ChapterItem, Boolean, Boolean, Boolean) -> Unit, - onChapterSwipe: (ChapterItem, LibraryPreferences.ChapterSwipeAction) -> Unit, + onDownloadChapter: ((List, ChapterDownloadAction) -> Unit)?, + onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit, + onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit, ) { items( items = chapters, - key = { "chapter-${it.chapter.id}" }, + key = { item -> + when (item) { + is ChapterList.MissingCount -> "missing-count-${item.id}" + is ChapterList.Item -> "chapter-${item.id}" + } + }, contentType = { EntryScreenItem.ITEM }, - ) { chapterItem -> + ) { item -> val haptic = LocalHapticFeedback.current val context = LocalContext.current - MangaChapterListItem( - title = if (manga.displayMode == Manga.CHAPTER_DISPLAY_NUMBER) { - stringResource( - R.string.display_mode_chapter, - formatChapterNumber(chapterItem.chapter.chapterNumber), - ) - } else { - chapterItem.chapter.name - }, - date = chapterItem.chapter.dateUpload - .takeIf { it > 0L } - ?.let { - Date(it).toRelativeString( - context, - dateRelativeTime, - dateFormat, + when (item) { + is ChapterList.MissingCount -> { + Row( + modifier = Modifier.padding( + horizontal = MaterialTheme.padding.medium, + vertical = MaterialTheme.padding.small, + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium), + ) { + HorizontalDivider(modifier = Modifier.weight(1f)) + Text( + text = pluralStringResource( + id = R.plurals.missing_items, + count = item.count, + item.count, + ), + modifier = Modifier.secondaryItemAlpha(), ) - }, - readProgress = chapterItem.chapter.lastPageRead - .takeIf { !chapterItem.chapter.read && it > 0L } - ?.let { - stringResource( - R.string.chapter_progress, - it + 1, - ) - }, - scanlator = chapterItem.chapter.scanlator.takeIf { !it.isNullOrBlank() }, - read = chapterItem.chapter.read, - bookmark = chapterItem.chapter.bookmark, - selected = chapterItem.selected, - downloadIndicatorEnabled = chapters.fastAll { !it.selected }, - downloadStateProvider = { chapterItem.downloadState }, - downloadProgressProvider = { chapterItem.downloadProgress }, - chapterSwipeStartAction = chapterSwipeStartAction, - chapterSwipeEndAction = chapterSwipeEndAction, - onLongClick = { - onChapterSelected(chapterItem, !chapterItem.selected, true, true) - haptic.performHapticFeedback(HapticFeedbackType.LongPress) - }, - onClick = { - onChapterItemClick( - chapterItem = chapterItem, - chapters = chapters, - onToggleSelection = { - onChapterSelected( - chapterItem, - !chapterItem.selected, - true, - false, + HorizontalDivider(modifier = Modifier.weight(1f)) + } + } + is ChapterList.Item -> { + MangaChapterListItem( + title = if (manga.displayMode == Manga.CHAPTER_DISPLAY_NUMBER) { + stringResource( + R.string.display_mode_chapter, + formatChapterNumber(item.chapter.chapterNumber), + ) + } else { + item.chapter.name + }, + date = item.chapter.dateUpload + .takeIf { it > 0L } + ?.let { + Date(it).toRelativeString( + context, + dateRelativeTime, + dateFormat, + ) + }, + readProgress = item.chapter.lastPageRead + .takeIf { !item.chapter.read && it > 0L } + ?.let { + stringResource( + R.string.chapter_progress, + it + 1, + ) + }, + scanlator = item.chapter.scanlator.takeIf { !it.isNullOrBlank() }, + read = item.chapter.read, + bookmark = item.chapter.bookmark, + selected = item.selected, + downloadIndicatorEnabled = !isAnyChapterSelected, + downloadStateProvider = { item.downloadState }, + downloadProgressProvider = { item.downloadProgress }, + chapterSwipeStartAction = chapterSwipeStartAction, + chapterSwipeEndAction = chapterSwipeEndAction, + onLongClick = { + onChapterSelected(item, !item.selected, true, true) + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + }, + onClick = { + onChapterItemClick( + chapterItem = item, + isAnyChapterSelected = isAnyChapterSelected, + onToggleSelection = { onChapterSelected(item, !item.selected, true, false) }, + onChapterClicked = onChapterClicked, ) }, - onChapterClicked = onChapterClicked, + onDownloadClick = if (onDownloadChapter != null) { + { onDownloadChapter(listOf(item), it) } + } else { + null + }, + onChapterSwipe = { + onChapterSwipe(item, it) + }, ) - }, - onDownloadClick = if (onDownloadChapter != null) { - { onDownloadChapter(listOf(chapterItem), it) } - } else { - null - }, - onChapterSwipe = { - onChapterSwipe(chapterItem, it) - }, - ) + } + } } } private fun onChapterItemClick( - chapterItem: ChapterItem, - chapters: List, + chapterItem: ChapterList.Item, + isAnyChapterSelected: Boolean, onToggleSelection: (Boolean) -> Unit, onChapterClicked: (Chapter) -> Unit, ) { when { chapterItem.selected -> onToggleSelection(false) - chapters.fastAny { it.selected } -> onToggleSelection(true) + isAnyChapterSelected -> onToggleSelection(true) else -> onChapterClicked(chapterItem.chapter) } } diff --git a/app/src/main/java/eu/kanade/presentation/entries/manga/components/MangaInfoHeader.kt b/app/src/main/java/eu/kanade/presentation/entries/manga/components/MangaInfoHeader.kt index ca83c65c9..37071c7cf 100644 --- a/app/src/main/java/eu/kanade/presentation/entries/manga/components/MangaInfoHeader.kt +++ b/app/src/main/java/eu/kanade/presentation/entries/manga/components/MangaInfoHeader.kt @@ -1,6 +1,5 @@ package eu.kanade.presentation.entries.manga.components -import android.content.Context import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.graphics.res.animatedVectorResource @@ -43,6 +42,7 @@ import androidx.compose.material.icons.outlined.Sync import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.SuggestionChip @@ -136,7 +136,6 @@ fun MangaInfoBox( coverDataProvider = coverDataProvider, onCoverClick = onCoverClick, title = title, - context = LocalContext.current, doSearch = doSearch, author = author, artist = artist, @@ -150,7 +149,6 @@ fun MangaInfoBox( coverDataProvider = coverDataProvider, onCoverClick = onCoverClick, title = title, - context = LocalContext.current, doSearch = doSearch, author = author, artist = artist, @@ -335,7 +333,6 @@ private fun MangaAndSourceTitlesLarge( coverDataProvider: () -> Manga, onCoverClick: () -> Unit, title: String, - context: Context, doSearch: (query: String, global: Boolean) -> Unit, author: String?, artist: String?, @@ -356,104 +353,16 @@ private fun MangaAndSourceTitlesLarge( onClick = onCoverClick, ) Spacer(modifier = Modifier.height(16.dp)) - Text( - text = title.ifBlank { stringResource(R.string.unknown_title) }, - style = MaterialTheme.typography.titleLarge, - modifier = Modifier.clickableNoIndication( - onLongClick = { if (title.isNotBlank()) context.copyToClipboard(title, title) }, - onClick = { if (title.isNotBlank()) doSearch(title, true) }, - ), + MangaContentInfo( + title = title, + doSearch = doSearch, + author = author, + artist = artist, + status = status, + sourceName = sourceName, + isStubSource = isStubSource, textAlign = TextAlign.Center, ) - Spacer(modifier = Modifier.height(2.dp)) - Text( - text = author?.takeIf { it.isNotBlank() } ?: stringResource(R.string.unknown_author), - style = MaterialTheme.typography.titleSmall, - modifier = Modifier - .secondaryItemAlpha() - .padding(top = 2.dp) - .clickableNoIndication( - onLongClick = { - if (!author.isNullOrBlank()) { - context.copyToClipboard( - author, - author, - ) - } - }, - onClick = { if (!author.isNullOrBlank()) doSearch(author, true) }, - ), - textAlign = TextAlign.Center, - ) - if (!artist.isNullOrBlank() && author != artist) { - Text( - text = artist, - style = MaterialTheme.typography.titleSmall, - modifier = Modifier - .secondaryItemAlpha() - .padding(top = 2.dp) - .clickableNoIndication( - onLongClick = { context.copyToClipboard(artist, artist) }, - onClick = { doSearch(artist, true) }, - ), - textAlign = TextAlign.Center, - ) - } - Spacer(modifier = Modifier.height(4.dp)) - Row( - modifier = Modifier.secondaryItemAlpha(), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - imageVector = when (status) { - SManga.ONGOING.toLong() -> Icons.Outlined.Schedule - SManga.COMPLETED.toLong() -> Icons.Outlined.DoneAll - SManga.LICENSED.toLong() -> Icons.Outlined.AttachMoney - SManga.PUBLISHING_FINISHED.toLong() -> Icons.Outlined.Done - SManga.CANCELLED.toLong() -> Icons.Outlined.Close - SManga.ON_HIATUS.toLong() -> Icons.Outlined.Pause - else -> Icons.Outlined.Block - }, - contentDescription = null, - modifier = Modifier - .padding(end = 4.dp) - .size(16.dp), - ) - ProvideTextStyle(MaterialTheme.typography.bodyMedium) { - Text( - text = when (status) { - SManga.ONGOING.toLong() -> stringResource(R.string.ongoing) - SManga.COMPLETED.toLong() -> stringResource(R.string.completed) - SManga.LICENSED.toLong() -> stringResource(R.string.licensed) - SManga.PUBLISHING_FINISHED.toLong() -> stringResource( - R.string.publishing_finished, - ) - SManga.CANCELLED.toLong() -> stringResource(R.string.cancelled) - SManga.ON_HIATUS.toLong() -> stringResource(R.string.on_hiatus) - else -> stringResource(R.string.unknown) - }, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - ) - DotSeparatorText() - if (isStubSource) { - Icon( - imageVector = Icons.Filled.Warning, - contentDescription = null, - modifier = Modifier - .padding(end = 4.dp) - .size(16.dp), - tint = MaterialTheme.colorScheme.error, - ) - } - Text( - text = sourceName, - modifier = Modifier.clickableNoIndication { doSearch(sourceName, false) }, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - ) - } - } } } @@ -463,7 +372,6 @@ private fun MangaAndSourceTitlesSmall( coverDataProvider: () -> Manga, onCoverClick: () -> Unit, title: String, - context: Context, doSearch: (query: String, global: Boolean) -> Unit, author: String?, artist: String?, @@ -489,136 +397,161 @@ private fun MangaAndSourceTitlesSmall( Column( verticalArrangement = Arrangement.spacedBy(2.dp), ) { - Text( - text = title.ifBlank { stringResource(R.string.unknown_title) }, - style = MaterialTheme.typography.titleLarge, - modifier = Modifier.clickableNoIndication( + MangaContentInfo( + title = title, + doSearch = doSearch, + author = author, + artist = artist, + status = status, + sourceName = sourceName, + isStubSource = isStubSource, + ) + } + } +} + +@Composable +private fun MangaContentInfo( + title: String, + textAlign: TextAlign? = LocalTextStyle.current.textAlign, + doSearch: (query: String, global: Boolean) -> Unit, + author: String?, + artist: String?, + status: Long, + sourceName: String, + isStubSource: Boolean, +) { + val context = LocalContext.current + Text( + text = title.ifBlank { stringResource(R.string.unknown_title) }, + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.clickableNoIndication( + onLongClick = { + if (title.isNotBlank()) { + context.copyToClipboard( + title, + title, + ) + } + }, + onClick = { if (title.isNotBlank()) doSearch(title, true) }, + ), + textAlign = textAlign, + ) + + Spacer(modifier = Modifier.height(2.dp)) + + Row( + modifier = Modifier.secondaryItemAlpha(), + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = Icons.Filled.PersonOutline, + contentDescription = null, + modifier = Modifier.size(16.dp), + ) + Text( + text = author?.takeIf { it.isNotBlank() } + ?: stringResource(R.string.unknown_author), + style = MaterialTheme.typography.titleSmall, + modifier = Modifier + .clickableNoIndication( onLongClick = { - if (title.isNotBlank()) { + if (!author.isNullOrBlank()) { context.copyToClipboard( - title, - title, + author, + author, ) } }, - onClick = { if (title.isNotBlank()) doSearch(title, true) }, + onClick = { if (!author.isNullOrBlank()) doSearch(author, true) }, ), + textAlign = textAlign, + ) + } + + if (!artist.isNullOrBlank() && author != artist) { + Row( + modifier = Modifier.secondaryItemAlpha(), + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = Icons.Filled.Brush, + contentDescription = null, + modifier = Modifier.size(16.dp), ) + Text( + text = artist, + style = MaterialTheme.typography.titleSmall, + modifier = Modifier + .clickableNoIndication( + onLongClick = { context.copyToClipboard(artist, artist) }, + onClick = { doSearch(artist, true) }, + ), + textAlign = textAlign, + ) + } + } - Spacer(modifier = Modifier.height(2.dp)) - Row( - modifier = Modifier.secondaryItemAlpha(), - horizontalArrangement = Arrangement.spacedBy(4.dp), - verticalAlignment = Alignment.CenterVertically, - ) { + Spacer(modifier = Modifier.height(2.dp)) + + Row( + modifier = Modifier.secondaryItemAlpha(), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = when (status) { + SManga.ONGOING.toLong() -> Icons.Outlined.Schedule + SManga.COMPLETED.toLong() -> Icons.Outlined.DoneAll + SManga.LICENSED.toLong() -> Icons.Outlined.AttachMoney + SManga.PUBLISHING_FINISHED.toLong() -> Icons.Outlined.Done + SManga.CANCELLED.toLong() -> Icons.Outlined.Close + SManga.ON_HIATUS.toLong() -> Icons.Outlined.Pause + else -> Icons.Outlined.Block + }, + contentDescription = null, + modifier = Modifier + .padding(end = 4.dp) + .size(16.dp), + ) + ProvideTextStyle(MaterialTheme.typography.bodyMedium) { + Text( + text = when (status) { + SManga.ONGOING.toLong() -> stringResource(R.string.ongoing) + SManga.COMPLETED.toLong() -> stringResource(R.string.completed) + SManga.LICENSED.toLong() -> stringResource(R.string.licensed) + SManga.PUBLISHING_FINISHED.toLong() -> stringResource(R.string.publishing_finished) + SManga.CANCELLED.toLong() -> stringResource(R.string.cancelled) + SManga.ON_HIATUS.toLong() -> stringResource(R.string.on_hiatus) + else -> stringResource(R.string.unknown) + }, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + DotSeparatorText() + if (isStubSource) { Icon( - imageVector = Icons.Filled.PersonOutline, - contentDescription = null, - modifier = Modifier.size(16.dp), - ) - Text( - text = author?.takeIf { it.isNotBlank() } - ?: stringResource(R.string.unknown_author), - style = MaterialTheme.typography.titleSmall, - modifier = Modifier - .clickableNoIndication( - onLongClick = { - if (!author.isNullOrBlank()) { - context.copyToClipboard( - author, - author, - ) - } - }, - onClick = { if (!author.isNullOrBlank()) doSearch(author, true) }, - ), - ) - } - - if (!artist.isNullOrBlank() && author != artist) { - Row( - modifier = Modifier.secondaryItemAlpha(), - horizontalArrangement = Arrangement.spacedBy(4.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - imageVector = Icons.Filled.Brush, - contentDescription = null, - modifier = Modifier.size(16.dp), - ) - Text( - text = artist, - style = MaterialTheme.typography.titleSmall, - modifier = Modifier - .clickableNoIndication( - onLongClick = { context.copyToClipboard(artist, artist) }, - onClick = { doSearch(artist, true) }, - ), - ) - } - } - - Spacer(modifier = Modifier.height(2.dp)) - - Row( - modifier = Modifier.secondaryItemAlpha(), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - imageVector = when (status) { - SManga.ONGOING.toLong() -> Icons.Outlined.Schedule - SManga.COMPLETED.toLong() -> Icons.Outlined.DoneAll - SManga.LICENSED.toLong() -> Icons.Outlined.AttachMoney - SManga.PUBLISHING_FINISHED.toLong() -> Icons.Outlined.Done - SManga.CANCELLED.toLong() -> Icons.Outlined.Close - SManga.ON_HIATUS.toLong() -> Icons.Outlined.Pause - else -> Icons.Outlined.Block - }, + imageVector = Icons.Filled.Warning, contentDescription = null, modifier = Modifier .padding(end = 4.dp) .size(16.dp), + tint = MaterialTheme.colorScheme.error, ) - ProvideTextStyle(MaterialTheme.typography.bodyMedium) { - Text( - text = when (status) { - SManga.ONGOING.toLong() -> stringResource(R.string.ongoing) - SManga.COMPLETED.toLong() -> stringResource(R.string.completed) - SManga.LICENSED.toLong() -> stringResource(R.string.licensed) - SManga.PUBLISHING_FINISHED.toLong() -> stringResource( - R.string.publishing_finished, - ) - SManga.CANCELLED.toLong() -> stringResource(R.string.cancelled) - SManga.ON_HIATUS.toLong() -> stringResource(R.string.on_hiatus) - else -> stringResource(R.string.unknown) - }, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - ) - DotSeparatorText() - if (isStubSource) { - Icon( - imageVector = Icons.Filled.Warning, - contentDescription = null, - modifier = Modifier - .padding(end = 4.dp) - .size(16.dp), - tint = MaterialTheme.colorScheme.error, - ) - } - Text( - text = sourceName, - modifier = Modifier.clickableNoIndication { - doSearch( - sourceName, - false, - ) - }, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - ) - } } + Text( + text = sourceName, + modifier = Modifier.clickableNoIndication { + doSearch( + sourceName, + false, + ) + }, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) } } } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt index 36c5bfc17..86d3c616e 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt @@ -4,12 +4,15 @@ import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent import android.net.Uri +import android.os.Environment +import android.text.format.Formatter import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.StringRes import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -32,6 +35,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import eu.kanade.presentation.more.settings.Preference +import eu.kanade.presentation.more.settings.widget.BasePreferenceWidget +import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding import eu.kanade.presentation.permissions.PermissionRequestHelper import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.backup.BackupConst @@ -41,6 +46,7 @@ import eu.kanade.tachiyomi.data.backup.BackupRestoreJob import eu.kanade.tachiyomi.data.backup.models.Backup import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.data.cache.EpisodeCache +import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.toast @@ -403,19 +409,22 @@ object SettingsDataScreen : SearchableSettings { val chapterCache = remember { Injekt.get() } val episodeCache = remember { Injekt.get() } - var readableSizeSema by remember { mutableIntStateOf(0) } - val readableSize = remember(readableSizeSema) { chapterCache.readableSize } - val readableAnimeSize = remember(readableSizeSema) { episodeCache.readableSize } + var cacheReadableSizeSema by remember { mutableIntStateOf(0) } + val cacheReadableMangaSize = remember(cacheReadableSizeSema) { chapterCache.readableSize } + val cacheReadableAnimeSize = remember(cacheReadableSizeSema) { episodeCache.readableSize } return Preference.PreferenceGroup( title = stringResource(R.string.label_data), preferenceItems = listOf( + getMangaStorageInfoPref(cacheReadableMangaSize), + getAnimeStorageInfoPref(cacheReadableAnimeSize), + Preference.PreferenceItem.TextPreference( title = stringResource(R.string.pref_clear_chapter_cache), subtitle = stringResource( R.string.used_cache_both, - readableAnimeSize, - readableSize, + cacheReadableAnimeSize, + cacheReadableMangaSize, ), onClick = { scope.launchNonCancellable { @@ -423,7 +432,7 @@ object SettingsDataScreen : SearchableSettings { val deletedFiles = chapterCache.clear() + episodeCache.clear() withUIContext { context.toast(context.getString(R.string.cache_deleted, deletedFiles)) - readableSizeSema++ + cacheReadableSizeSema++ } } catch (e: Throwable) { logcat(LogPriority.ERROR, e) @@ -460,6 +469,60 @@ object SettingsDataScreen : SearchableSettings { ), ) } + + @Composable + fun getMangaStorageInfoPref( + chapterCacheReadableSize: String, + ): Preference.PreferenceItem.CustomPreference { + val context = LocalContext.current + val available = remember { + Formatter.formatFileSize(context, DiskUtil.getAvailableStorageSpace(Environment.getDataDirectory())) + } + val total = remember { + Formatter.formatFileSize(context, DiskUtil.getTotalStorageSpace(Environment.getDataDirectory())) + } + + return Preference.PreferenceItem.CustomPreference( + title = stringResource(R.string.pref_manga_storage_usage), + ) { + BasePreferenceWidget( + title = stringResource(R.string.pref_manga_storage_usage), + subcomponent = { + // TODO: downloads, SD cards, bar representation?, i18n + Box(modifier = Modifier.padding(horizontal = PrefsHorizontalPadding)) { + Text(text = "Available: $available / $total (chapter cache: $chapterCacheReadableSize)") + } + }, + ) + } + } + + @Composable + fun getAnimeStorageInfoPref( + episodeCacheReadableSize: String, + ): Preference.PreferenceItem.CustomPreference { + val context = LocalContext.current + val available = remember { + Formatter.formatFileSize(context, DiskUtil.getAvailableStorageSpace(Environment.getDataDirectory())) + } + val total = remember { + Formatter.formatFileSize(context, DiskUtil.getTotalStorageSpace(Environment.getDataDirectory())) + } + + return Preference.PreferenceItem.CustomPreference( + title = stringResource(R.string.pref_anime_storage_usage), + ) { + BasePreferenceWidget( + title = stringResource(R.string.pref_anime_storage_usage), + subcomponent = { + // TODO: downloads, SD cards, bar representation?, i18n + Box(modifier = Modifier.padding(horizontal = PrefsHorizontalPadding)) { + Text(text = "Available: $available / $total (Episode cache: $episodeCacheReadableSize)") + } + }, + ) + } + } } private data class MissingRestoreComponents( diff --git a/app/src/main/java/eu/kanade/presentation/reader/ChapterTransition.kt b/app/src/main/java/eu/kanade/presentation/reader/ChapterTransition.kt index 11bb45905..3aefdbffd 100644 --- a/app/src/main/java/eu/kanade/presentation/reader/ChapterTransition.kt +++ b/app/src/main/java/eu/kanade/presentation/reader/ChapterTransition.kt @@ -12,8 +12,9 @@ import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.text.InlineTextContent import androidx.compose.foundation.text.appendInlineContent import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.outlined.CheckCircle import androidx.compose.material.icons.outlined.Info -import androidx.compose.material.icons.outlined.OfflinePin import androidx.compose.material.icons.outlined.Warning import androidx.compose.material3.CardColors import androidx.compose.material3.CardDefaults @@ -256,7 +257,7 @@ private fun ChapterText( ), ) { Icon( - imageVector = Icons.Outlined.OfflinePin, + imageVector = Icons.Filled.CheckCircle, contentDescription = stringResource(R.string.label_downloaded), ) }, diff --git a/app/src/main/java/eu/kanade/presentation/track/manga/MangaTrackerSearch.kt b/app/src/main/java/eu/kanade/presentation/track/manga/MangaTrackerSearch.kt index 066a86f1b..c0a48fa3b 100644 --- a/app/src/main/java/eu/kanade/presentation/track/manga/MangaTrackerSearch.kt +++ b/app/src/main/java/eu/kanade/presentation/track/manga/MangaTrackerSearch.kt @@ -247,7 +247,7 @@ fun SearchResultItem( ) { if (selected) { Icon( - imageVector = Icons.Default.CheckCircle, + imageVector = Icons.Filled.CheckCircle, contentDescription = null, modifier = Modifier.align(Alignment.TopEnd), tint = MaterialTheme.colorScheme.primary, diff --git a/app/src/main/java/eu/kanade/presentation/updates/anime/AnimeUpdatesScreen.kt b/app/src/main/java/eu/kanade/presentation/updates/anime/AnimeUpdatesScreen.kt index 7166f22eb..79dab931e 100644 --- a/app/src/main/java/eu/kanade/presentation/updates/anime/AnimeUpdatesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/updates/anime/AnimeUpdatesScreen.kt @@ -97,9 +97,8 @@ fun AnimeUpdateScreen( FastScrollLazyColumn( contentPadding = contentPadding, ) { - if (lastUpdated > 0L) { - animeUpdatesLastUpdatedItem(lastUpdated) - } + animeUpdatesLastUpdatedItem(lastUpdated) + animeUpdatesUiItems( uiModels = state.getUiModel(context, relativeTime), selectionMode = state.selectionMode, diff --git a/app/src/main/java/eu/kanade/presentation/updates/anime/AnimeUpdatesUiItem.kt b/app/src/main/java/eu/kanade/presentation/updates/anime/AnimeUpdatesUiItem.kt index 531026da7..e51bad177 100644 --- a/app/src/main/java/eu/kanade/presentation/updates/anime/AnimeUpdatesUiItem.kt +++ b/app/src/main/java/eu/kanade/presentation/updates/anime/AnimeUpdatesUiItem.kt @@ -1,6 +1,5 @@ package eu.kanade.presentation.updates.anime -import android.text.format.DateUtils import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Box @@ -40,6 +39,7 @@ import eu.kanade.presentation.entries.DotSeparatorText import eu.kanade.presentation.entries.ItemCover import eu.kanade.presentation.entries.anime.components.EpisodeDownloadAction import eu.kanade.presentation.entries.anime.components.EpisodeDownloadIndicator +import eu.kanade.presentation.util.relativeTimeSpanString import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.download.anime.model.AnimeDownload import eu.kanade.tachiyomi.ui.updates.anime.AnimeUpdatesItem @@ -48,24 +48,13 @@ import tachiyomi.presentation.core.components.ListGroupHeader import tachiyomi.presentation.core.components.material.ReadItemAlpha import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.util.selectedBackground -import java.util.Date import java.util.concurrent.TimeUnit -import kotlin.time.Duration.Companion.minutes @OptIn(ExperimentalFoundationApi::class) fun LazyListScope.animeUpdatesLastUpdatedItem( lastUpdated: Long, ) { item(key = "animeUpdates-lastUpdated") { - val time = remember(lastUpdated) { - val now = Date().time - if (now - lastUpdated < 1.minutes.inWholeMilliseconds) { - null - } else { - DateUtils.getRelativeTimeSpanString(lastUpdated, now, DateUtils.MINUTE_IN_MILLIS) - } - } - Box( modifier = Modifier .animateItemPlacement() @@ -75,14 +64,7 @@ fun LazyListScope.animeUpdatesLastUpdatedItem( ), ) { Text( - text = if (time.isNullOrEmpty()) { - stringResource( - R.string.updates_last_update_info, - stringResource(R.string.updates_last_update_info_just_now), - ) - } else { - stringResource(R.string.updates_last_update_info, time) - }, + text = stringResource(R.string.updates_last_update_info, relativeTimeSpanString(lastUpdated)), fontStyle = FontStyle.Italic, ) } diff --git a/app/src/main/java/eu/kanade/presentation/updates/manga/MangaUpdatesScreen.kt b/app/src/main/java/eu/kanade/presentation/updates/manga/MangaUpdatesScreen.kt index 4e0da1d97..23337df13 100644 --- a/app/src/main/java/eu/kanade/presentation/updates/manga/MangaUpdatesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/updates/manga/MangaUpdatesScreen.kt @@ -93,9 +93,7 @@ fun MangaUpdateScreen( FastScrollLazyColumn( contentPadding = contentPadding, ) { - if (lastUpdated > 0L) { - mangaUpdatesLastUpdatedItem(lastUpdated) - } + mangaUpdatesLastUpdatedItem(lastUpdated) mangaUpdatesUiItems( uiModels = state.getUiModel(context, relativeTime), diff --git a/app/src/main/java/eu/kanade/presentation/updates/manga/MangaUpdatesUiItem.kt b/app/src/main/java/eu/kanade/presentation/updates/manga/MangaUpdatesUiItem.kt index 705f13ec4..7516df9dd 100644 --- a/app/src/main/java/eu/kanade/presentation/updates/manga/MangaUpdatesUiItem.kt +++ b/app/src/main/java/eu/kanade/presentation/updates/manga/MangaUpdatesUiItem.kt @@ -1,6 +1,5 @@ package eu.kanade.presentation.updates.manga -import android.text.format.DateUtils import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Box @@ -40,6 +39,7 @@ import eu.kanade.presentation.entries.DotSeparatorText import eu.kanade.presentation.entries.ItemCover import eu.kanade.presentation.entries.manga.components.ChapterDownloadAction import eu.kanade.presentation.entries.manga.components.ChapterDownloadIndicator +import eu.kanade.presentation.util.relativeTimeSpanString import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.download.manga.model.MangaDownload import eu.kanade.tachiyomi.ui.updates.manga.MangaUpdatesItem @@ -48,23 +48,12 @@ import tachiyomi.presentation.core.components.ListGroupHeader import tachiyomi.presentation.core.components.material.ReadItemAlpha import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.util.selectedBackground -import java.util.Date -import kotlin.time.Duration.Companion.minutes @OptIn(ExperimentalFoundationApi::class) fun LazyListScope.mangaUpdatesLastUpdatedItem( lastUpdated: Long, ) { item(key = "mangaUpdates-lastUpdated") { - val time = remember(lastUpdated) { - val now = Date().time - if (now - lastUpdated < 1.minutes.inWholeMilliseconds) { - null - } else { - DateUtils.getRelativeTimeSpanString(lastUpdated, now, DateUtils.MINUTE_IN_MILLIS) - } - } - Box( modifier = Modifier .animateItemPlacement() @@ -74,14 +63,7 @@ fun LazyListScope.mangaUpdatesLastUpdatedItem( ), ) { Text( - text = if (time.isNullOrEmpty()) { - stringResource( - R.string.updates_last_update_info, - stringResource(R.string.updates_last_update_info_just_now), - ) - } else { - stringResource(R.string.updates_last_update_info, time) - }, + text = stringResource(R.string.updates_last_update_info, relativeTimeSpanString(lastUpdated)), fontStyle = FontStyle.Italic, ) } diff --git a/app/src/main/java/eu/kanade/presentation/util/DurationUtils.kt b/app/src/main/java/eu/kanade/presentation/util/TimeUtils.kt similarity index 55% rename from app/src/main/java/eu/kanade/presentation/util/DurationUtils.kt rename to app/src/main/java/eu/kanade/presentation/util/TimeUtils.kt index ce2380e5f..20ce166fa 100644 --- a/app/src/main/java/eu/kanade/presentation/util/DurationUtils.kt +++ b/app/src/main/java/eu/kanade/presentation/util/TimeUtils.kt @@ -1,8 +1,14 @@ package eu.kanade.presentation.util import android.content.Context +import android.text.format.DateUtils +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.ui.res.stringResource import eu.kanade.tachiyomi.R +import java.util.Date import kotlin.time.Duration +import kotlin.time.Duration.Companion.minutes fun Duration.toDurationString(context: Context, fallback: String): String { return toComponents { days, hours, minutes, seconds, _ -> @@ -22,3 +28,14 @@ fun Duration.toDurationString(context: Context, fallback: String): String { }.joinToString(" ").ifBlank { fallback } } } + +@Composable +@ReadOnlyComposable +fun relativeTimeSpanString(epochMillis: Long): String { + val now = Date().time + return when { + epochMillis <= 0L -> stringResource(R.string.relative_time_span_never) + now - epochMillis < 1.minutes.inWholeMilliseconds -> stringResource(R.string.updates_last_update_info_just_now) + else -> DateUtils.getRelativeTimeSpanString(epochMillis, now, DateUtils.MINUTE_IN_MILLIS).toString() + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt index a17101af3..6c9461a75 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt @@ -19,6 +19,7 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.workManager +import tachiyomi.core.preference.Preference import tachiyomi.core.preference.PreferenceStore import tachiyomi.core.preference.TriState import tachiyomi.core.preference.getAndSet @@ -503,6 +504,30 @@ object Migrations { uiPreferences.relativeTime().set(false) } } + if (oldVersion < 107) { + replacePreferences( + preferenceStore = preferenceStore, + filterPredicate = { it.key.startsWith("pref_mangasync_") || it.key.startsWith("track_token_") }, + newKey = { Preference.privateKey(it) }, + ) + } + if (oldVersion < 108) { + val prefsToReplace = listOf( + "pref_download_only", + "incognito_mode", + "last_catalogue_source", + "trusted_signatures", + "last_app_closed", + "library_update_last_timestamp", + "library_unseen_updates_count", + "last_used_category", + ) + replacePreferences( + preferenceStore = preferenceStore, + filterPredicate = { it.key in prefsToReplace }, + newKey = { Preference.appStateKey(it) }, + ) + } return true } } @@ -510,3 +535,41 @@ object Migrations { return false } } + +@Suppress("UNCHECKED_CAST") +private fun replacePreferences( + preferenceStore: PreferenceStore, + filterPredicate: (Map.Entry) -> Boolean, + newKey: (String) -> String, +) { + preferenceStore.getAll() + .filter(filterPredicate) + .forEach { (key, value) -> + when (value) { + is Int -> { + preferenceStore.getInt(newKey(key)).set(value) + preferenceStore.getInt(key).delete() + } + is Long -> { + preferenceStore.getLong(newKey(key)).set(value) + preferenceStore.getLong(key).delete() + } + is Float -> { + preferenceStore.getFloat(newKey(key)).set(value) + preferenceStore.getFloat(key).delete() + } + is String -> { + preferenceStore.getString(newKey(key)).set(value) + preferenceStore.getString(key).delete() + } + is Boolean -> { + preferenceStore.getBoolean(newKey(key)).set(value) + preferenceStore.getBoolean(key).delete() + } + is Set<*> -> (value as? Set)?.let { + preferenceStore.getStringSet(newKey(key)).set(value) + preferenceStore.getStringSet(key).delete() + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreator.kt index 1d97bbcf0..0b6db6b35 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreator.kt @@ -41,6 +41,7 @@ import logcat.LogPriority import okio.buffer import okio.gzip import okio.sink +import tachiyomi.core.preference.Preference import tachiyomi.core.util.system.logcat import tachiyomi.data.handlers.anime.AnimeDatabaseHandler import tachiyomi.data.handlers.manga.MangaDatabaseHandler @@ -444,6 +445,6 @@ class BackupCreator( } backupPreferences.add(toAdd) } - return backupPreferences + return backupPreferences.filter { !Preference.isPrivate(it.key) && !Preference.isAppState(it.key) } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt index 39e91adf3..d5fb7643a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt @@ -11,8 +11,6 @@ import dataanime.Anime_sync import dataanime.Animes import eu.kanade.domain.entries.anime.interactor.UpdateAnime import eu.kanade.domain.entries.manga.interactor.UpdateManga -import eu.kanade.domain.items.chapter.model.copyFrom -import eu.kanade.domain.items.episode.model.copyFrom import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.backup.models.BackupAnime import eu.kanade.tachiyomi.data.backup.models.BackupAnimeHistory @@ -51,7 +49,9 @@ import tachiyomi.domain.entries.manga.interactor.MangaFetchInterval import tachiyomi.domain.entries.manga.model.Manga import tachiyomi.domain.history.anime.model.AnimeHistoryUpdate import tachiyomi.domain.history.manga.model.MangaHistoryUpdate +import tachiyomi.domain.items.chapter.interactor.GetChaptersByMangaId import tachiyomi.domain.items.chapter.model.Chapter +import tachiyomi.domain.items.episode.interactor.GetEpisodesByAnimeId import tachiyomi.domain.items.episode.model.Episode import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.track.anime.model.AnimeTrack @@ -73,11 +73,13 @@ class BackupRestorer( private val mangaHandler: MangaDatabaseHandler = Injekt.get() private val updateManga: UpdateManga = Injekt.get() private val getMangaCategories: GetMangaCategories = Injekt.get() + private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get() private val mangaFetchInterval: MangaFetchInterval = Injekt.get() private val animeHandler: AnimeDatabaseHandler = Injekt.get() private val updateAnime: UpdateAnime = Injekt.get() private val getAnimeCategories: GetAnimeCategories = Injekt.get() + private val getEpisodesByAnimeId: GetEpisodesByAnimeId = Injekt.get() private val animeFetchInterval: AnimeFetchInterval = Injekt.get() private val libraryPreferences: LibraryPreferences = Injekt.get() @@ -423,33 +425,38 @@ class BackupRestorer( manga: Manga, chapters: List, ) { - val dbChapters = mangaHandler.awaitList { chaptersQueries.getChaptersByMangaId(manga.id!!) } + val dbChaptersByUrl = getChaptersByMangaId.await(manga.id) + .associateBy { it.url } val processed = chapters.map { chapter -> var updatedChapter = chapter - val dbChapter = dbChapters.find { it.url == updatedChapter.url } + + val dbChapter = dbChaptersByUrl[updatedChapter.url] if (dbChapter != null) { - updatedChapter = updatedChapter.copy(id = dbChapter._id) - updatedChapter = updatedChapter.copyFrom(dbChapter) + updatedChapter = updatedChapter + .copyFrom(dbChapter) + .copy( + id = dbChapter.id, + bookmark = updatedChapter.bookmark || dbChapter.bookmark, + ) if (dbChapter.read && !updatedChapter.read) { updatedChapter = updatedChapter.copy( read = true, - lastPageRead = dbChapter.last_page_read, + lastPageRead = dbChapter.lastPageRead, + ) + } else if (updatedChapter.lastPageRead == 0L && dbChapter.lastPageRead != 0L) { + updatedChapter = updatedChapter.copy( + lastPageRead = dbChapter.lastPageRead, ) - } else if (chapter.lastPageRead == 0L && dbChapter.last_page_read != 0L) { - updatedChapter = updatedChapter.copy(lastPageRead = dbChapter.last_page_read) - } - if (!updatedChapter.bookmark && dbChapter.bookmark) { - updatedChapter = updatedChapter.copy(bookmark = true) } } - updatedChapter.copy(mangaId = manga.id ?: -1) + updatedChapter.copy(mangaId = manga.id) } - val newChapters = processed.groupBy { it.id > 0 } - newChapters[true]?.let { updateKnownChapters(it) } - newChapters[false]?.let { insertChapters(it) } + val (existingChapters, newChapters) = processed.partition { it.id > 0 } + updateKnownChapters(existingChapters) + insertChapters(newChapters) } /** @@ -870,43 +877,46 @@ class BackupRestorer( private suspend fun restoreEpisodes( anime: Anime, - episodes: List, + episodes: List, ) { - val dbEpisodes = animeHandler.awaitList { episodesQueries.getEpisodesByAnimeId(anime.id!!) } + val dbEpisodesByUrl = getEpisodesByAnimeId.await(anime.id) + .associateBy { it.url } val processed = episodes.map { episode -> var updatedEpisode = episode - val dbEpisode = dbEpisodes.find { it.url == updatedEpisode.url } + + val dbEpisode = dbEpisodesByUrl[updatedEpisode.url] if (dbEpisode != null) { - updatedEpisode = updatedEpisode.copy(id = dbEpisode._id) - updatedEpisode = updatedEpisode.copyFrom(dbEpisode) + updatedEpisode = updatedEpisode + .copyFrom(dbEpisode) + .copy( + id = dbEpisode.id, + bookmark = updatedEpisode.bookmark || dbEpisode.bookmark, + ) if (dbEpisode.seen && !updatedEpisode.seen) { updatedEpisode = updatedEpisode.copy( seen = true, - lastSecondSeen = dbEpisode.last_second_seen, + lastSecondSeen = dbEpisode.lastSecondSeen, ) - } else if (updatedEpisode.lastSecondSeen == 0L && dbEpisode.last_second_seen != 0L) { + } else if (updatedEpisode.lastSecondSeen == 0L && dbEpisode.lastSecondSeen != 0L) { updatedEpisode = updatedEpisode.copy( - lastSecondSeen = dbEpisode.last_second_seen, + lastSecondSeen = dbEpisode.lastSecondSeen, ) } - if (!updatedEpisode.bookmark && dbEpisode.bookmark) { - updatedEpisode = updatedEpisode.copy(bookmark = true) - } } - updatedEpisode.copy(animeId = anime.id ?: -1) + updatedEpisode.copy(animeId = anime.id) } - val newEpisodes = processed.groupBy { it.id > 0 } - newEpisodes[true]?.let { updateKnownEpisodes(it) } - newEpisodes[false]?.let { insertEpisodes(it) } + val (existingEpisodes, newEpisodes) = processed.partition { it.id > 0 } + updateKnownEpisodes(existingEpisodes) + insertEpisodes(newEpisodes) } /** * Inserts list of episodes */ - private suspend fun insertEpisodes(episodes: List) { + private suspend fun insertEpisodes(episodes: List) { animeHandler.await(true) { episodes.forEach { episode -> episodesQueries.insert( @@ -931,7 +941,7 @@ class BackupRestorer( * Updates a list of episodes with known database ids */ private suspend fun updateKnownEpisodes( - episodes: List, + episodes: List, ) { animeHandler.await(true) { episodes.forEach { episode -> diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/AnimeTracker.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/AnimeTracker.kt index a2c5d7159..033165d99 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/AnimeTracker.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/AnimeTracker.kt @@ -13,7 +13,7 @@ import tachiyomi.core.util.lang.withIOContext import tachiyomi.core.util.lang.withUIContext import tachiyomi.core.util.system.logcat import tachiyomi.domain.history.anime.interactor.GetAnimeHistory -import tachiyomi.domain.items.episode.interactor.GetEpisodeByAnimeId +import tachiyomi.domain.items.episode.interactor.GetEpisodesByAnimeId import tachiyomi.domain.track.anime.interactor.InsertAnimeTrack import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -62,7 +62,7 @@ interface AnimeTracker { item.anime_id = animeId try { withIOContext { - val allEpisodes = Injekt.get().await(animeId) + val allEpisodes = Injekt.get().await(animeId) val hasSeenEpisodes = allEpisodes.any { it.seen } bind(item, hasSeenEpisodes) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/MangaTracker.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/MangaTracker.kt index 077ce37c6..a24b52f2a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/MangaTracker.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/MangaTracker.kt @@ -13,7 +13,7 @@ import tachiyomi.core.util.lang.withIOContext import tachiyomi.core.util.lang.withUIContext import tachiyomi.core.util.system.logcat import tachiyomi.domain.history.manga.interactor.GetMangaHistory -import tachiyomi.domain.items.chapter.interactor.GetChapterByMangaId +import tachiyomi.domain.items.chapter.interactor.GetChaptersByMangaId import tachiyomi.domain.track.manga.interactor.InsertMangaTrack import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -62,7 +62,7 @@ interface MangaTracker { item.manga_id = mangaId try { withIOContext { - val allChapters = Injekt.get().await(mangaId) + val allChapters = Injekt.get().await(mangaId) val hasReadChapters = allChapters.any { it.read } bind(item, hasReadChapters) 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 cf9df726e..8022ea078 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 @@ -43,7 +43,7 @@ import tachiyomi.domain.category.anime.interactor.GetAnimeCategories import tachiyomi.domain.category.anime.interactor.SetAnimeCategories import tachiyomi.domain.entries.anime.model.Anime import tachiyomi.domain.entries.anime.model.AnimeUpdate -import tachiyomi.domain.items.episode.interactor.GetEpisodeByAnimeId +import tachiyomi.domain.items.episode.interactor.GetEpisodesByAnimeId import tachiyomi.domain.items.episode.interactor.UpdateEpisode import tachiyomi.domain.items.episode.model.toEpisodeUpdate import tachiyomi.domain.source.anime.service.AnimeSourceManager @@ -150,7 +150,7 @@ internal class MigrateAnimeDialogScreenModel( private val sourceManager: AnimeSourceManager = Injekt.get(), private val downloadManager: AnimeDownloadManager = Injekt.get(), private val updateAnime: UpdateAnime = Injekt.get(), - private val getEpisodeByAnimeId: GetEpisodeByAnimeId = Injekt.get(), + private val getEpisodesByAnimeId: GetEpisodesByAnimeId = Injekt.get(), private val syncEpisodesWithSource: SyncEpisodesWithSource = Injekt.get(), private val updateEpisode: UpdateEpisode = Injekt.get(), private val getCategories: GetAnimeCategories = Injekt.get(), @@ -222,8 +222,8 @@ internal class MigrateAnimeDialogScreenModel( // Update chapters read, bookmark and dateFetch if (migrateEpisodes) { - val prevAnimeEpisodes = getEpisodeByAnimeId.await(oldAnime.id) - val animeEpisodes = getEpisodeByAnimeId.await(newAnime.id) + val prevAnimeEpisodes = getEpisodesByAnimeId.await(oldAnime.id) + val animeEpisodes = getEpisodesByAnimeId.await(newAnime.id) val maxEpisodeSeen = prevAnimeEpisodes .filter { it.seen } 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 a00fa15b1..9c3234e1d 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 @@ -43,7 +43,7 @@ import tachiyomi.domain.category.manga.interactor.GetMangaCategories import tachiyomi.domain.category.manga.interactor.SetMangaCategories import tachiyomi.domain.entries.manga.model.Manga import tachiyomi.domain.entries.manga.model.MangaUpdate -import tachiyomi.domain.items.chapter.interactor.GetChapterByMangaId +import tachiyomi.domain.items.chapter.interactor.GetChaptersByMangaId import tachiyomi.domain.items.chapter.interactor.UpdateChapter import tachiyomi.domain.items.chapter.model.toChapterUpdate import tachiyomi.domain.source.manga.service.MangaSourceManager @@ -150,7 +150,7 @@ internal class MigrateMangaDialogScreenModel( private val sourceManager: MangaSourceManager = Injekt.get(), private val downloadManager: MangaDownloadManager = Injekt.get(), private val updateManga: UpdateManga = Injekt.get(), - private val getChapterByMangaId: GetChapterByMangaId = Injekt.get(), + private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(), private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(), private val updateChapter: UpdateChapter = Injekt.get(), private val getCategories: GetMangaCategories = Injekt.get(), @@ -222,8 +222,8 @@ internal class MigrateMangaDialogScreenModel( // Update chapters read, bookmark and dateFetch if (migrateChapters) { - val prevMangaChapters = getChapterByMangaId.await(oldManga.id) - val mangaChapters = getChapterByMangaId.await(newManga.id) + val prevMangaChapters = getChaptersByMangaId.await(oldManga.id) + val mangaChapters = getChaptersByMangaId.await(newManga.id) val maxChapterRead = prevMangaChapters .filter { it.read } 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 0aaebf0b8..ee7388f9f 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 @@ -10,6 +10,7 @@ import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.screenModelScope import eu.kanade.core.preference.asState import eu.kanade.core.util.addOrRemove +import eu.kanade.core.util.insertSeparators import eu.kanade.domain.entries.anime.interactor.SetAnimeViewerFlags import eu.kanade.domain.entries.anime.interactor.UpdateAnime import eu.kanade.domain.entries.anime.model.downloadedFilter @@ -74,6 +75,7 @@ import tachiyomi.domain.items.episode.model.Episode import tachiyomi.domain.items.episode.model.EpisodeUpdate import tachiyomi.domain.items.episode.model.NoEpisodesException import tachiyomi.domain.items.episode.service.getEpisodeSort +import tachiyomi.domain.items.service.calculateEpisodeGap import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.source.anime.service.AnimeSourceManager import tachiyomi.domain.track.anime.interactor.GetAnimeTracks @@ -81,6 +83,7 @@ import tachiyomi.source.local.entries.anime.isLocal import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.util.Calendar +import kotlin.math.floor class AnimeScreenModel( val context: Context, @@ -125,7 +128,7 @@ class AnimeScreenModel( private val isFavorited: Boolean get() = anime?.favorite ?: false - private val processedEpisodes: List? + private val processedEpisodes: List? get() = successState?.processedEpisodes val episodeSwipeStartAction = libraryPreferences.swipeEpisodeEndAction().get() @@ -171,7 +174,7 @@ class AnimeScreenModel( updateSuccessState { it.copy( anime = anime, - episodes = episodes.toEpisodeItems(anime), + episodes = episodes.toEpisodeListItems(anime), ) } } @@ -182,7 +185,7 @@ class AnimeScreenModel( screenModelScope.launchIO { val anime = getAnimeAndEpisodes.awaitAnime(animeId) val episodes = getAnimeAndEpisodes.awaitEpisodes(animeId) - .toEpisodeItems(anime) + .toEpisodeListItems(anime) if (!anime.favorite) { setAnimeDefaultEpisodeFlags.await(anime) @@ -477,7 +480,7 @@ class AnimeScreenModel( private fun updateDownloadState(download: AnimeDownload) { updateSuccessState { successState -> - val modifiedIndex = successState.episodes.indexOfFirst { it.episode.id == download.episode.id } + val modifiedIndex = successState.episodes.indexOfFirst { it.id == download.episode.id } if (modifiedIndex < 0) return@updateSuccessState successState val newEpisodes = successState.episodes.toMutableList().apply { @@ -489,7 +492,7 @@ class AnimeScreenModel( } } - private fun List.toEpisodeItems(anime: Anime): List { + private fun List.toEpisodeListItems(anime: Anime): List { val isLocal = anime.isLocal() return map { episode -> val activeDownload = if (isLocal) { @@ -513,7 +516,7 @@ class AnimeScreenModel( else -> AnimeDownload.State.NOT_DOWNLOADED } - EpisodeItem( + EpisodeList.Item( episode = episode, downloadState = downloadState, downloadProgress = activeDownload?.progress ?: 0, @@ -561,7 +564,7 @@ class AnimeScreenModel( /** * @throws IllegalStateException if the swipe action is [LibraryPreferences.EpisodeSwipeAction.Disabled] */ - fun episodeSwipe(episodeItem: EpisodeItem, swipeAction: LibraryPreferences.EpisodeSwipeAction) { + fun episodeSwipe(episodeItem: EpisodeList.Item, swipeAction: LibraryPreferences.EpisodeSwipeAction) { screenModelScope.launch { executeEpisodeSwipeAction(episodeItem, swipeAction) } @@ -571,7 +574,7 @@ class AnimeScreenModel( * @throws IllegalStateException if the swipe action is [LibraryPreferences.EpisodeSwipeAction.Disabled] */ private fun executeEpisodeSwipeAction( - episodeItem: EpisodeItem, + episodeItem: EpisodeList.Item, swipeAction: LibraryPreferences.EpisodeSwipeAction, ) { val episode = episodeItem.episode @@ -654,7 +657,7 @@ class AnimeScreenModel( } fun runEpisodeDownloadActions( - items: List, + items: List, action: EpisodeDownloadAction, ) { when (action) { @@ -669,7 +672,7 @@ class AnimeScreenModel( startDownload(listOf(episode), true) } EpisodeDownloadAction.CANCEL -> { - val episodeId = items.singleOrNull()?.episode?.id ?: return + val episodeId = items.singleOrNull()?.id ?: return cancelDownload(episodeId) } EpisodeDownloadAction.DELETE -> { @@ -880,14 +883,14 @@ class AnimeScreenModel( } fun toggleSelection( - item: EpisodeItem, + item: EpisodeList.Item, selected: Boolean, userSelected: Boolean = false, fromLongPress: Boolean = false, ) { updateSuccessState { successState -> val newEpisodes = successState.processedEpisodes.toMutableList().apply { - val selectedIndex = successState.processedEpisodes.indexOfFirst { it.episode.id == item.episode.id } + val selectedIndex = successState.processedEpisodes.indexOfFirst { it.id == item.episode.id } if (selectedIndex < 0) return@apply val selectedItem = get(selectedIndex) @@ -895,7 +898,7 @@ class AnimeScreenModel( val firstSelection = none { it.selected } set(selectedIndex, selectedItem.copy(selected = selected)) - selectedEpisodeIds.addOrRemove(item.episode.id, selected) + selectedEpisodeIds.addOrRemove(item.id, selected) if (selected && userSelected && fromLongPress) { if (firstSelection) { @@ -918,7 +921,7 @@ class AnimeScreenModel( range.forEach { val inbetweenItem = get(it) if (!inbetweenItem.selected) { - selectedEpisodeIds.add(inbetweenItem.episode.id) + selectedEpisodeIds.add(inbetweenItem.id) set(it, inbetweenItem.copy(selected = true)) } } @@ -946,7 +949,7 @@ class AnimeScreenModel( fun toggleAllSelection(selected: Boolean) { updateSuccessState { successState -> val newEpisodes = successState.episodes.map { - selectedEpisodeIds.addOrRemove(it.episode.id, selected) + selectedEpisodeIds.addOrRemove(it.id, selected) it.copy(selected = selected) } selectedPositions[0] = -1 @@ -958,7 +961,7 @@ class AnimeScreenModel( fun invertSelection() { updateSuccessState { successState -> val newEpisodes = successState.episodes.map { - selectedEpisodeIds.addOrRemove(it.episode.id, !it.selected) + selectedEpisodeIds.addOrRemove(it.id, !it.selected) it.copy(selected = !it.selected) } selectedPositions[0] = -1 @@ -1060,7 +1063,7 @@ class AnimeScreenModel( val anime: Anime, val source: AnimeSource, val isFromSource: Boolean, - val episodes: List, + val episodes: List, val trackItems: List = emptyList(), val isRefreshingData: Boolean = false, val dialog: Dialog? = null, @@ -1075,6 +1078,33 @@ class AnimeScreenModel( episodes.applyFilters(anime).toList() } + val episodeListItems by lazy { + processedEpisodes.insertSeparators { before, after -> + val (lowerEpisode, higherEpisode) = if (anime.sortDescending()) { + after to before + } else { + before to after + } + if (higherEpisode == null) return@insertSeparators null + + if (lowerEpisode == null) { + floor(higherEpisode.episode.episodeNumber) + .toInt() + .minus(1) + .coerceAtLeast(0) + } else { + calculateEpisodeGap(higherEpisode.episode, lowerEpisode.episode) + } + .takeIf { it > 0 } + ?.let { missingCount -> + EpisodeList.MissingCount( + id = "${lowerEpisode?.id}-${higherEpisode.id}", + count = missingCount, + ) + } + } + } + val trackingAvailable: Boolean get() = trackItems.isNotEmpty() @@ -1093,7 +1123,7 @@ class AnimeScreenModel( * Applies the view filters to the list of episodes obtained from the database. * @return an observable of the list of episodes filtered and sorted. */ - private fun List.applyFilters(anime: Anime): Sequence { + private fun List.applyFilters(anime: Anime): Sequence { val isLocalAnime = anime.isLocal() val unseenFilter = anime.unseenFilter val downloadedFilter = anime.downloadedFilter @@ -1114,11 +1144,21 @@ class AnimeScreenModel( } @Immutable -data class EpisodeItem( - val episode: Episode, - val downloadState: AnimeDownload.State, - val downloadProgress: Int, - val selected: Boolean = false, -) { - val isDownloaded = downloadState == AnimeDownload.State.DOWNLOADED +sealed class EpisodeList { + @Immutable + data class MissingCount( + val id: String, + val count: Int, + ) : EpisodeList() + + @Immutable + data class Item( + val episode: Episode, + val downloadState: AnimeDownload.State, + val downloadProgress: Int, + val selected: Boolean = false, + ) : EpisodeList() { + val id = episode.id + val isDownloaded = downloadState == AnimeDownload.State.DOWNLOADED + } } 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 8c7e4e4ad..6f85c4ce4 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 @@ -10,6 +10,7 @@ import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.screenModelScope import eu.kanade.core.preference.asState import eu.kanade.core.util.addOrRemove +import eu.kanade.core.util.insertSeparators import eu.kanade.domain.entries.manga.interactor.UpdateManga import eu.kanade.domain.entries.manga.model.downloadedFilter import eu.kanade.domain.entries.manga.model.toSManga @@ -71,12 +72,14 @@ import tachiyomi.domain.items.chapter.model.Chapter import tachiyomi.domain.items.chapter.model.ChapterUpdate import tachiyomi.domain.items.chapter.model.NoChaptersException import tachiyomi.domain.items.chapter.service.getChapterSort +import tachiyomi.domain.items.service.calculateChapterGap import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.source.manga.service.MangaSourceManager import tachiyomi.domain.track.manga.interactor.GetMangaTracks import tachiyomi.source.local.entries.manga.isLocal import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import kotlin.math.floor class MangaScreenModel( val context: Context, @@ -120,10 +123,10 @@ class MangaScreenModel( private val isFavorited: Boolean get() = manga?.favorite ?: false - private val allChapters: List? + private val allChapters: List? get() = successState?.chapters - private val filteredChapters: List? + private val filteredChapters: List? get() = successState?.processedChapters val chapterSwipeStartAction = libraryPreferences.swipeChapterEndAction().get() @@ -166,7 +169,7 @@ class MangaScreenModel( updateSuccessState { it.copy( manga = manga, - chapters = chapters.toChapterItems(manga), + chapters = chapters.toChapterListItems(manga), ) } } @@ -177,7 +180,7 @@ class MangaScreenModel( screenModelScope.launchIO { val manga = getMangaAndChapters.awaitManga(mangaId) val chapters = getMangaAndChapters.awaitChapters(mangaId) - .toChapterItems(manga) + .toChapterListItems(manga) if (!manga.favorite) { setMangaDefaultChapterFlags.await(manga) @@ -473,7 +476,7 @@ class MangaScreenModel( private fun updateDownloadState(download: MangaDownload) { updateSuccessState { successState -> - val modifiedIndex = successState.chapters.indexOfFirst { it.chapter.id == download.chapter.id } + val modifiedIndex = successState.chapters.indexOfFirst { it.id == download.chapter.id } if (modifiedIndex < 0) return@updateSuccessState successState val newChapters = successState.chapters.toMutableList().apply { @@ -485,7 +488,7 @@ class MangaScreenModel( } } - private fun List.toChapterItems(manga: Manga): List { + private fun List.toChapterListItems(manga: Manga): List { val isLocal = manga.isLocal() return map { chapter -> val activeDownload = if (isLocal) { @@ -509,7 +512,7 @@ class MangaScreenModel( else -> MangaDownload.State.NOT_DOWNLOADED } - ChapterItem( + ChapterList.Item( chapter = chapter, downloadState = downloadState, downloadProgress = activeDownload?.progress ?: 0, @@ -557,7 +560,7 @@ class MangaScreenModel( /** * @throws IllegalStateException if the swipe action is [LibraryPreferences.ChapterSwipeAction.Disabled] */ - fun chapterSwipe(chapterItem: ChapterItem, swipeAction: LibraryPreferences.ChapterSwipeAction) { + fun chapterSwipe(chapterItem: ChapterList.Item, swipeAction: LibraryPreferences.ChapterSwipeAction) { screenModelScope.launch { executeChapterSwipeAction(chapterItem, swipeAction) } @@ -567,7 +570,7 @@ class MangaScreenModel( * @throws IllegalStateException if the swipe action is [LibraryPreferences.ChapterSwipeAction.Disabled] */ private fun executeChapterSwipeAction( - chapterItem: ChapterItem, + chapterItem: ChapterList.Item, swipeAction: LibraryPreferences.ChapterSwipeAction, ) { val chapter = chapterItem.chapter @@ -648,7 +651,7 @@ class MangaScreenModel( } fun runChapterDownloadActions( - items: List, + items: List, action: ChapterDownloadAction, ) { when (action) { @@ -663,7 +666,7 @@ class MangaScreenModel( startDownload(listOf(chapter), true) } ChapterDownloadAction.CANCEL -> { - val chapterId = items.singleOrNull()?.chapter?.id ?: return + val chapterId = items.singleOrNull()?.id ?: return cancelDownload(chapterId) } ChapterDownloadAction.DELETE -> { @@ -873,14 +876,14 @@ class MangaScreenModel( } fun toggleSelection( - item: ChapterItem, + item: ChapterList.Item, selected: Boolean, userSelected: Boolean = false, fromLongPress: Boolean = false, ) { updateSuccessState { successState -> val newChapters = successState.processedChapters.toMutableList().apply { - val selectedIndex = successState.processedChapters.indexOfFirst { it.chapter.id == item.chapter.id } + val selectedIndex = successState.processedChapters.indexOfFirst { it.id == item.chapter.id } if (selectedIndex < 0) return@apply val selectedItem = get(selectedIndex) @@ -888,7 +891,7 @@ class MangaScreenModel( val firstSelection = none { it.selected } set(selectedIndex, selectedItem.copy(selected = selected)) - selectedChapterIds.addOrRemove(item.chapter.id, selected) + selectedChapterIds.addOrRemove(item.id, selected) if (selected && userSelected && fromLongPress) { if (firstSelection) { @@ -911,7 +914,7 @@ class MangaScreenModel( range.forEach { val inbetweenItem = get(it) if (!inbetweenItem.selected) { - selectedChapterIds.add(inbetweenItem.chapter.id) + selectedChapterIds.add(inbetweenItem.id) set(it, inbetweenItem.copy(selected = true)) } } @@ -939,7 +942,7 @@ class MangaScreenModel( fun toggleAllSelection(selected: Boolean) { updateSuccessState { successState -> val newChapters = successState.chapters.map { - selectedChapterIds.addOrRemove(it.chapter.id, selected) + selectedChapterIds.addOrRemove(it.id, selected) it.copy(selected = selected) } selectedPositions[0] = -1 @@ -951,7 +954,7 @@ class MangaScreenModel( fun invertSelection() { updateSuccessState { successState -> val newChapters = successState.chapters.map { - selectedChapterIds.addOrRemove(it.chapter.id, !it.selected) + selectedChapterIds.addOrRemove(it.id, !it.selected) it.copy(selected = !it.selected) } selectedPositions[0] = -1 @@ -1033,7 +1036,7 @@ class MangaScreenModel( val manga: Manga, val source: MangaSource, val isFromSource: Boolean, - val chapters: List, + val chapters: List, val trackItems: List = emptyList(), val isRefreshingData: Boolean = false, val dialog: Dialog? = null, @@ -1044,6 +1047,33 @@ class MangaScreenModel( chapters.applyFilters(manga).toList() } + val chapterListItems by lazy { + processedChapters.insertSeparators { before, after -> + val (lowerChapter, higherChapter) = if (manga.sortDescending()) { + after to before + } else { + before to after + } + if (higherChapter == null) return@insertSeparators null + + if (lowerChapter == null) { + floor(higherChapter.chapter.chapterNumber) + .toInt() + .minus(1) + .coerceAtLeast(0) + } else { + calculateChapterGap(higherChapter.chapter, lowerChapter.chapter) + } + .takeIf { it > 0 } + ?.let { missingCount -> + ChapterList.MissingCount( + id = "${lowerChapter?.id}-${higherChapter.id}", + count = missingCount, + ) + } + } + } + val trackingAvailable: Boolean get() = trackItems.isNotEmpty() @@ -1054,7 +1084,7 @@ class MangaScreenModel( * Applies the view filters to the list of chapters obtained from the database. * @return an observable of the list of chapters filtered and sorted. */ - private fun List.applyFilters(manga: Manga): Sequence { + private fun List.applyFilters(manga: Manga): Sequence { val isLocalManga = manga.isLocal() val unreadFilter = manga.unreadFilter val downloadedFilter = manga.downloadedFilter @@ -1075,11 +1105,21 @@ class MangaScreenModel( } @Immutable -data class ChapterItem( - val chapter: Chapter, - val downloadState: MangaDownload.State, - val downloadProgress: Int, - val selected: Boolean = false, -) { - val isDownloaded = downloadState == MangaDownload.State.DOWNLOADED +sealed class ChapterList { + @Immutable + data class MissingCount( + val id: String, + val count: Int, + ) : ChapterList() + + @Immutable + data class Item( + val chapter: Chapter, + val downloadState: MangaDownload.State, + val downloadProgress: Int, + val selected: Boolean = false, + ) : ChapterList() { + val id = chapter.id + val isDownloaded = downloadState == MangaDownload.State.DOWNLOADED + } } 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 2e36a522c..9a822f61a 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 @@ -53,7 +53,7 @@ import tachiyomi.domain.entries.anime.model.Anime import tachiyomi.domain.entries.anime.model.AnimeUpdate import tachiyomi.domain.entries.applyFilter import tachiyomi.domain.history.anime.interactor.GetNextEpisodes -import tachiyomi.domain.items.episode.interactor.GetEpisodeByAnimeId +import tachiyomi.domain.items.episode.interactor.GetEpisodesByAnimeId import tachiyomi.domain.items.episode.model.Episode import tachiyomi.domain.library.anime.LibraryAnime import tachiyomi.domain.library.anime.model.AnimeLibrarySort @@ -79,7 +79,7 @@ class AnimeLibraryScreenModel( private val getCategories: GetVisibleAnimeCategories = Injekt.get(), private val getTracksPerAnime: GetTracksPerAnime = Injekt.get(), private val getNextEpisodes: GetNextEpisodes = Injekt.get(), - private val getEpisodesByAnimeId: GetEpisodeByAnimeId = Injekt.get(), + private val getEpisodesByAnimeId: GetEpisodesByAnimeId = Injekt.get(), private val setSeenStatus: SetSeenStatus = Injekt.get(), private val updateAnime: UpdateAnime = Injekt.get(), private val setAnimeCategories: SetAnimeCategories = Injekt.get(), 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 c0c613409..423abade9 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 @@ -53,7 +53,7 @@ import tachiyomi.domain.entries.manga.interactor.GetLibraryManga import tachiyomi.domain.entries.manga.model.Manga import tachiyomi.domain.entries.manga.model.MangaUpdate import tachiyomi.domain.history.manga.interactor.GetNextChapters -import tachiyomi.domain.items.chapter.interactor.GetChapterByMangaId +import tachiyomi.domain.items.chapter.interactor.GetChaptersByMangaId import tachiyomi.domain.items.chapter.model.Chapter import tachiyomi.domain.library.manga.LibraryManga import tachiyomi.domain.library.manga.model.MangaLibrarySort @@ -79,7 +79,7 @@ class MangaLibraryScreenModel( private val getCategories: GetVisibleMangaCategories = Injekt.get(), private val getTracksPerManga: GetTracksPerManga = Injekt.get(), private val getNextChapters: GetNextChapters = Injekt.get(), - private val getChaptersByMangaId: GetChapterByMangaId = Injekt.get(), + private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(), private val setReadStatus: SetReadStatus = Injekt.get(), private val updateManga: UpdateManga = Injekt.get(), private val setMangaCategories: SetMangaCategories = Injekt.get(), diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/ExternalIntents.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/ExternalIntents.kt index 7a8a133fc..b19d303cb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/ExternalIntents.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/ExternalIntents.kt @@ -31,7 +31,6 @@ import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.firstOrNull import logcat.LogPriority import tachiyomi.core.util.lang.launchIO import tachiyomi.core.util.lang.withIOContext @@ -42,7 +41,7 @@ import tachiyomi.domain.entries.anime.interactor.GetAnime import tachiyomi.domain.entries.anime.model.Anime import tachiyomi.domain.history.anime.interactor.UpsertAnimeHistory import tachiyomi.domain.history.anime.model.AnimeHistoryUpdate -import tachiyomi.domain.items.episode.interactor.GetEpisodeByAnimeId +import tachiyomi.domain.items.episode.interactor.GetEpisodesByAnimeId import tachiyomi.domain.items.episode.interactor.UpdateEpisode import tachiyomi.domain.items.episode.model.Episode import tachiyomi.domain.items.episode.model.EpisodeUpdate @@ -81,7 +80,7 @@ class ExternalIntents { ): Intent? { anime = getAnime.await(animeId!!) ?: return null source = sourceManager.get(anime.source) ?: return null - episode = getEpisodeByAnimeId.await(anime.id).find { it.id == episodeId } ?: return null + episode = getEpisodesByAnimeId.await(anime.id).find { it.id == episodeId } ?: return null val video = chosenVideo ?: EpisodeLoader.getLinks(episode, anime, source).asFlow().first().firstOrNull() @@ -392,7 +391,7 @@ class ExternalIntents { private val updateEpisode: UpdateEpisode = Injekt.get() private val getAnime: GetAnime = Injekt.get() private val sourceManager: AnimeSourceManager = Injekt.get() - private val getEpisodeByAnimeId: GetEpisodeByAnimeId = Injekt.get() + private val getEpisodesByAnimeId: GetEpisodesByAnimeId = Injekt.get() private val getTracks: GetAnimeTracks = Injekt.get() private val insertTrack: InsertAnimeTrack = Injekt.get() private val downloadManager: AnimeDownloadManager by injectLazy() @@ -469,7 +468,7 @@ class ExternalIntents { else -> throw NotImplementedError("Unknown sorting method") } - val episodes = getEpisodeByAnimeId.await(anime.id) + val episodes = getEpisodesByAnimeId.await(anime.id) .sortedWith { e1, e2 -> sortFunction(e1, e2) } val currentEpisodePosition = episodes.indexOf(episode) 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 04409104f..73002389c 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 @@ -59,7 +59,7 @@ import tachiyomi.domain.entries.anime.model.Anime import tachiyomi.domain.history.anime.interactor.GetNextEpisodes import tachiyomi.domain.history.anime.interactor.UpsertAnimeHistory import tachiyomi.domain.history.anime.model.AnimeHistoryUpdate -import tachiyomi.domain.items.episode.interactor.GetEpisodeByAnimeId +import tachiyomi.domain.items.episode.interactor.GetEpisodesByAnimeId import tachiyomi.domain.items.episode.interactor.UpdateEpisode import tachiyomi.domain.items.episode.model.EpisodeUpdate import tachiyomi.domain.items.episode.service.getEpisodeSort @@ -81,7 +81,7 @@ class PlayerViewModel @JvmOverloads constructor( private val trackEpisode: TrackEpisode = Injekt.get(), private val getAnime: GetAnime = Injekt.get(), private val getNextEpisodes: GetNextEpisodes = Injekt.get(), - private val getEpisodeByAnimeId: GetEpisodeByAnimeId = Injekt.get(), + private val getEpisodesByAnimeId: GetEpisodesByAnimeId = Injekt.get(), private val getTracks: GetAnimeTracks = Injekt.get(), private val upsertHistory: UpsertAnimeHistory = Injekt.get(), private val updateEpisode: UpdateEpisode = Injekt.get(), @@ -284,7 +284,7 @@ class PlayerViewModel @JvmOverloads constructor( ) private fun initEpisodeList(anime: Anime): List { - val episodes = runBlocking { getEpisodeByAnimeId.await(anime.id) } + val episodes = runBlocking { getEpisodesByAnimeId.await(anime.id) } return episodes .sortedWith(getEpisodeSort(anime, sortDescending = false)) 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 b668fdea8..b90d8c23f 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 @@ -66,7 +66,7 @@ import tachiyomi.domain.entries.manga.model.Manga import tachiyomi.domain.history.manga.interactor.GetNextChapters import tachiyomi.domain.history.manga.interactor.UpsertMangaHistory import tachiyomi.domain.history.manga.model.MangaHistoryUpdate -import tachiyomi.domain.items.chapter.interactor.GetChapterByMangaId +import tachiyomi.domain.items.chapter.interactor.GetChaptersByMangaId import tachiyomi.domain.items.chapter.interactor.UpdateChapter import tachiyomi.domain.items.chapter.model.ChapterUpdate import tachiyomi.domain.items.chapter.service.getChapterSort @@ -92,7 +92,7 @@ class ReaderViewModel @JvmOverloads constructor( private val trackPreferences: TrackPreferences = Injekt.get(), private val trackChapter: TrackChapter = Injekt.get(), private val getManga: GetManga = Injekt.get(), - private val getChapterByMangaId: GetChapterByMangaId = Injekt.get(), + private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(), private val getNextChapters: GetNextChapters = Injekt.get(), private val upsertHistory: UpsertMangaHistory = Injekt.get(), private val updateChapter: UpdateChapter = Injekt.get(), @@ -147,7 +147,7 @@ class ReaderViewModel @JvmOverloads constructor( */ private val chapterList by lazy { val manga = manga!! - val chapters = runBlocking { getChapterByMangaId.await(manga.id) } + val chapters = runBlocking { getChaptersByMangaId.await(manga.id) } val selectedChapter = chapters.find { it.id == chapterId } ?: error("Requested chapter of id $chapterId not found in chapter list") diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/stats/anime/AnimeStatsScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/stats/anime/AnimeStatsScreenModel.kt index 596637ff2..1f06cb468 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/stats/anime/AnimeStatsScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/stats/anime/AnimeStatsScreenModel.kt @@ -16,7 +16,7 @@ import eu.kanade.tachiyomi.data.track.TrackerManager import kotlinx.coroutines.flow.update import tachiyomi.core.util.lang.launchIO import tachiyomi.domain.entries.anime.interactor.GetLibraryAnime -import tachiyomi.domain.items.episode.interactor.GetEpisodeByAnimeId +import tachiyomi.domain.items.episode.interactor.GetEpisodesByAnimeId import tachiyomi.domain.library.anime.LibraryAnime import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_HAS_UNVIEWED @@ -31,7 +31,7 @@ import uy.kohesive.injekt.api.get class AnimeStatsScreenModel( private val downloadManager: AnimeDownloadManager = Injekt.get(), private val getAnimelibAnime: GetLibraryAnime = Injekt.get(), - private val getEpisodeByAnimeId: GetEpisodeByAnimeId = Injekt.get(), + private val getEpisodesByAnimeId: GetEpisodesByAnimeId = Injekt.get(), private val getTracks: GetAnimeTracks = Injekt.get(), private val preferences: LibraryPreferences = Injekt.get(), private val trackerManager: TrackerManager = Injekt.get(), @@ -128,7 +128,7 @@ class AnimeStatsScreenModel( private suspend fun getWatchTime(libraryAnimeList: List): Long { var watchTime = 0L libraryAnimeList.forEach { libraryAnime -> - getEpisodeByAnimeId.await(libraryAnime.anime.id).forEach { episode -> + getEpisodesByAnimeId.await(libraryAnime.anime.id).forEach { episode -> watchTime += if (episode.seen) { episode.totalSeconds } else { diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterGetNextUnread.kt b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterGetNextUnread.kt index 13f7a31ee..e55a565c6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterGetNextUnread.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterGetNextUnread.kt @@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.util.chapter import eu.kanade.domain.items.chapter.model.applyFilters import eu.kanade.tachiyomi.data.download.manga.MangaDownloadManager -import eu.kanade.tachiyomi.ui.entries.manga.ChapterItem +import eu.kanade.tachiyomi.ui.entries.manga.ChapterList import tachiyomi.domain.entries.manga.model.Manga import tachiyomi.domain.items.chapter.model.Chapter @@ -22,7 +22,7 @@ fun List.getNextUnread(manga: Manga, downloadManager: MangaDownloadMana /** * Gets next unread chapter with filters and sorting applied */ -fun List.getNextUnread(manga: Manga): Chapter? { +fun List.getNextUnread(manga: Manga): Chapter? { return applyFilters(manga).let { chapters -> if (manga.sortDescending()) { chapters.findLast { !it.chapter.read } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/episode/EpsiodeGetNextUnseen.kt b/app/src/main/java/eu/kanade/tachiyomi/util/episode/EpsiodeGetNextUnseen.kt index 17139bb62..285fbdb1d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/episode/EpsiodeGetNextUnseen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/episode/EpsiodeGetNextUnseen.kt @@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.util.episode import eu.kanade.domain.items.episode.model.applyFilters import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadManager -import eu.kanade.tachiyomi.ui.entries.anime.EpisodeItem +import eu.kanade.tachiyomi.ui.entries.anime.EpisodeList import tachiyomi.domain.entries.anime.model.Anime import tachiyomi.domain.items.episode.model.Episode @@ -22,7 +22,7 @@ fun List.getNextUnseen(anime: Anime, downloadManager: AnimeDownloadMana /** * Gets next unseen episode with filters and sorting applied */ -fun List.getNextUnseen(anime: Anime): Episode? { +fun List.getNextUnseen(anime: Anime): Episode? { return applyFilters(anime).let { episodes -> if (anime.sortDescending()) { episodes.findLast { !it.episode.seen } diff --git a/build.gradle.kts b/build.gradle.kts index 9891eb75b..6295920a1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -55,5 +55,5 @@ subprojects { } tasks.register("clean") { - delete(rootProject.buildDir) + delete(rootProject.layout.buildDirectory) } diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 006d99b54..8f125a39e 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -25,6 +25,7 @@ dependencies { api(libs.okhttp.core) api(libs.okhttp.logging) + api(libs.okhttp.brotli) api(libs.okhttp.dnsoverhttps) api(libs.okio) diff --git a/core/src/main/java/eu/kanade/tachiyomi/core/security/SecurityPreferences.kt b/core/src/main/java/eu/kanade/tachiyomi/core/security/SecurityPreferences.kt index 371bd776b..f27b1a44c 100644 --- a/core/src/main/java/eu/kanade/tachiyomi/core/security/SecurityPreferences.kt +++ b/core/src/main/java/eu/kanade/tachiyomi/core/security/SecurityPreferences.kt @@ -1,6 +1,7 @@ package eu.kanade.tachiyomi.core.security import eu.kanade.tachiyomi.core.R +import tachiyomi.core.preference.Preference import tachiyomi.core.preference.PreferenceStore import tachiyomi.core.preference.getEnum @@ -20,7 +21,10 @@ class SecurityPreferences( * For app lock. Will be set when there is a pending timed lock. * Otherwise this pref should be deleted. */ - fun lastAppClosed() = preferenceStore.getLong("last_app_closed", 0) + fun lastAppClosed() = preferenceStore.getLong( + Preference.appStateKey("last_app_closed"), + 0, + ) enum class SecureScreenMode(val titleResId: Int) { ALWAYS(R.string.lock_always), 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 6893a0495..f8f1a6395 100644 --- a/core/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt +++ b/core/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt @@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.network.interceptor.UncaughtExceptionInterceptor import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor import okhttp3.Cache import okhttp3.OkHttpClient +import okhttp3.brotli.BrotliInterceptor import okhttp3.logging.HttpLoggingInterceptor import java.io.File import java.util.concurrent.TimeUnit @@ -29,6 +30,7 @@ class NetworkHelper( maxSize = 5L * 1024 * 1024, // 5 MiB ), ) + .addInterceptor(BrotliInterceptor) .addInterceptor(UncaughtExceptionInterceptor()) .addInterceptor(UserAgentInterceptor(::defaultUserAgentProvider)) diff --git a/core/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt b/core/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt index acddaf0bd..5ea61b5a5 100644 --- a/core/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt +++ b/core/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt @@ -28,6 +28,30 @@ object DiskUtil { return size } + /** + * Gets the total space for the disk that a file path points to, in bytes. + */ + fun getTotalStorageSpace(file: File): Long { + return try { + val stat = StatFs(file.absolutePath) + stat.blockCountLong * stat.blockSizeLong + } catch (_: Exception) { + -1L + } + } + + /** + * Gets the available space for the disk that a file path points to, in bytes. + */ + fun getAvailableStorageSpace(file: File): Long { + return try { + val stat = StatFs(file.absolutePath) + stat.availableBlocksLong * stat.blockSizeLong + } catch (_: Exception) { + -1L + } + } + /** * Gets the available space for the disk that a file path points to, in bytes. */ diff --git a/core/src/main/java/tachiyomi/core/preference/Preference.kt b/core/src/main/java/tachiyomi/core/preference/Preference.kt index e76ea23d4..1cc0d3d3c 100644 --- a/core/src/main/java/tachiyomi/core/preference/Preference.kt +++ b/core/src/main/java/tachiyomi/core/preference/Preference.kt @@ -21,6 +21,32 @@ interface Preference { fun changes(): Flow fun stateIn(scope: CoroutineScope): StateFlow + + companion object { + /** + * A preference that should not be exposed in places like backups without user consent. + */ + fun isPrivate(key: String): Boolean { + return key.startsWith(PRIVATE_PREFIX) + } + fun privateKey(key: String): String { + return "${PRIVATE_PREFIX}$key" + } + + /** + * A preference used for internal app state that isn't really a user preference + * and therefore should not be in places like backups. + */ + fun isAppState(key: String): Boolean { + return key.startsWith(APP_STATE_PREFIX) + } + fun appStateKey(key: String): String { + return "${APP_STATE_PREFIX}$key" + } + + private const val APP_STATE_PREFIX = "__APP_STATE_" + private const val PRIVATE_PREFIX = "__PRIVATE_" + } } inline fun Preference.getAndSet(crossinline block: (T) -> R) = set( diff --git a/domain/src/main/java/tachiyomi/domain/entries/anime/interactor/AnimeFetchInterval.kt b/domain/src/main/java/tachiyomi/domain/entries/anime/interactor/AnimeFetchInterval.kt index 0ef833c59..ab00f6507 100644 --- a/domain/src/main/java/tachiyomi/domain/entries/anime/interactor/AnimeFetchInterval.kt +++ b/domain/src/main/java/tachiyomi/domain/entries/anime/interactor/AnimeFetchInterval.kt @@ -2,7 +2,7 @@ package tachiyomi.domain.entries.anime.interactor import tachiyomi.domain.entries.anime.model.Anime import tachiyomi.domain.entries.anime.model.AnimeUpdate -import tachiyomi.domain.items.episode.interactor.GetEpisodeByAnimeId +import tachiyomi.domain.items.episode.interactor.GetEpisodesByAnimeId import tachiyomi.domain.items.episode.model.Episode import java.time.Instant import java.time.ZoneId @@ -11,7 +11,7 @@ import java.time.temporal.ChronoUnit import kotlin.math.absoluteValue class AnimeFetchInterval( - private val getEpisodeByAnimeId: GetEpisodeByAnimeId, + private val getEpisodesByAnimeId: GetEpisodesByAnimeId, ) { suspend fun toAnimeUpdateOrNull( @@ -24,7 +24,7 @@ class AnimeFetchInterval( } else { window } - val episodes = getEpisodeByAnimeId.await(anime.id) + val episodes = getEpisodesByAnimeId.await(anime.id) val interval = anime.fetchInterval.takeIf { it < 0 } ?: calculateInterval( episodes, dateTime.zone, diff --git a/domain/src/main/java/tachiyomi/domain/entries/manga/interactor/MangaFetchInterval.kt b/domain/src/main/java/tachiyomi/domain/entries/manga/interactor/MangaFetchInterval.kt index d4044fef0..5a3f7fd68 100644 --- a/domain/src/main/java/tachiyomi/domain/entries/manga/interactor/MangaFetchInterval.kt +++ b/domain/src/main/java/tachiyomi/domain/entries/manga/interactor/MangaFetchInterval.kt @@ -2,7 +2,7 @@ package tachiyomi.domain.entries.manga.interactor import tachiyomi.domain.entries.manga.model.Manga import tachiyomi.domain.entries.manga.model.MangaUpdate -import tachiyomi.domain.items.chapter.interactor.GetChapterByMangaId +import tachiyomi.domain.items.chapter.interactor.GetChaptersByMangaId import tachiyomi.domain.items.chapter.model.Chapter import java.time.Instant import java.time.ZoneId @@ -11,7 +11,7 @@ import java.time.temporal.ChronoUnit import kotlin.math.absoluteValue class MangaFetchInterval( - private val getChapterByMangaId: GetChapterByMangaId, + private val getChaptersByMangaId: GetChaptersByMangaId, ) { suspend fun toMangaUpdateOrNull( @@ -24,7 +24,7 @@ class MangaFetchInterval( } else { window } - val chapters = getChapterByMangaId.await(manga.id) + val chapters = getChaptersByMangaId.await(manga.id) val interval = manga.fetchInterval.takeIf { it < 0 } ?: calculateInterval( chapters, dateTime.zone, diff --git a/domain/src/main/java/tachiyomi/domain/history/anime/interactor/GetNextEpisodes.kt b/domain/src/main/java/tachiyomi/domain/history/anime/interactor/GetNextEpisodes.kt index 6b7abd12e..1f12acf17 100644 --- a/domain/src/main/java/tachiyomi/domain/history/anime/interactor/GetNextEpisodes.kt +++ b/domain/src/main/java/tachiyomi/domain/history/anime/interactor/GetNextEpisodes.kt @@ -2,13 +2,13 @@ package tachiyomi.domain.history.anime.interactor import tachiyomi.domain.entries.anime.interactor.GetAnime import tachiyomi.domain.history.anime.repository.AnimeHistoryRepository -import tachiyomi.domain.items.episode.interactor.GetEpisodeByAnimeId +import tachiyomi.domain.items.episode.interactor.GetEpisodesByAnimeId import tachiyomi.domain.items.episode.model.Episode import tachiyomi.domain.items.episode.service.getEpisodeSort import kotlin.math.max class GetNextEpisodes( - private val getEpisodeByAnimeId: GetEpisodeByAnimeId, + private val getEpisodesByAnimeId: GetEpisodesByAnimeId, private val getAnime: GetAnime, private val historyRepository: AnimeHistoryRepository, ) { @@ -20,7 +20,7 @@ class GetNextEpisodes( suspend fun await(animeId: Long, onlyUnseen: Boolean = true): List { val anime = getAnime.await(animeId) ?: return emptyList() - val episodes = getEpisodeByAnimeId.await(animeId) + val episodes = getEpisodesByAnimeId.await(animeId) .sortedWith(getEpisodeSort(anime, sortDescending = false)) return if (onlyUnseen) { diff --git a/domain/src/main/java/tachiyomi/domain/history/manga/interactor/GetNextChapters.kt b/domain/src/main/java/tachiyomi/domain/history/manga/interactor/GetNextChapters.kt index 9f474a36d..a507fd4be 100644 --- a/domain/src/main/java/tachiyomi/domain/history/manga/interactor/GetNextChapters.kt +++ b/domain/src/main/java/tachiyomi/domain/history/manga/interactor/GetNextChapters.kt @@ -2,13 +2,13 @@ package tachiyomi.domain.history.manga.interactor import tachiyomi.domain.entries.manga.interactor.GetManga import tachiyomi.domain.history.manga.repository.MangaHistoryRepository -import tachiyomi.domain.items.chapter.interactor.GetChapterByMangaId +import tachiyomi.domain.items.chapter.interactor.GetChaptersByMangaId import tachiyomi.domain.items.chapter.model.Chapter import tachiyomi.domain.items.chapter.service.getChapterSort import kotlin.math.max class GetNextChapters( - private val getChapterByMangaId: GetChapterByMangaId, + private val getChaptersByMangaId: GetChaptersByMangaId, private val getManga: GetManga, private val historyRepository: MangaHistoryRepository, ) { @@ -20,7 +20,7 @@ class GetNextChapters( suspend fun await(mangaId: Long, onlyUnread: Boolean = true): List { val manga = getManga.await(mangaId) ?: return emptyList() - val chapters = getChapterByMangaId.await(mangaId) + val chapters = getChaptersByMangaId.await(mangaId) .sortedWith(getChapterSort(manga, sortDescending = false)) return if (onlyUnread) { diff --git a/domain/src/main/java/tachiyomi/domain/items/chapter/interactor/GetChapterByMangaId.kt b/domain/src/main/java/tachiyomi/domain/items/chapter/interactor/GetChaptersByMangaId.kt similarity index 95% rename from domain/src/main/java/tachiyomi/domain/items/chapter/interactor/GetChapterByMangaId.kt rename to domain/src/main/java/tachiyomi/domain/items/chapter/interactor/GetChaptersByMangaId.kt index 716bc3254..bc0b3d5c5 100644 --- a/domain/src/main/java/tachiyomi/domain/items/chapter/interactor/GetChapterByMangaId.kt +++ b/domain/src/main/java/tachiyomi/domain/items/chapter/interactor/GetChaptersByMangaId.kt @@ -5,7 +5,7 @@ import tachiyomi.core.util.system.logcat import tachiyomi.domain.items.chapter.model.Chapter import tachiyomi.domain.items.chapter.repository.ChapterRepository -class GetChapterByMangaId( +class GetChaptersByMangaId( private val chapterRepository: ChapterRepository, ) { diff --git a/domain/src/main/java/tachiyomi/domain/items/chapter/model/Chapter.kt b/domain/src/main/java/tachiyomi/domain/items/chapter/model/Chapter.kt index 7bfe8c34a..85cb86015 100644 --- a/domain/src/main/java/tachiyomi/domain/items/chapter/model/Chapter.kt +++ b/domain/src/main/java/tachiyomi/domain/items/chapter/model/Chapter.kt @@ -18,6 +18,16 @@ data class Chapter( val isRecognizedNumber: Boolean get() = chapterNumber >= 0f + fun copyFrom(other: Chapter): Chapter { + return copy( + name = other.name, + url = other.url, + dateUpload = other.dateUpload, + chapterNumber = other.chapterNumber, + scanlator = other.scanlator?.ifBlank { null }, + ) + } + companion object { fun create() = Chapter( id = -1, diff --git a/domain/src/main/java/tachiyomi/domain/items/episode/interactor/GetEpisodeByAnimeId.kt b/domain/src/main/java/tachiyomi/domain/items/episode/interactor/GetEpisodesByAnimeId.kt similarity index 95% rename from domain/src/main/java/tachiyomi/domain/items/episode/interactor/GetEpisodeByAnimeId.kt rename to domain/src/main/java/tachiyomi/domain/items/episode/interactor/GetEpisodesByAnimeId.kt index 09fe0471e..555480c88 100644 --- a/domain/src/main/java/tachiyomi/domain/items/episode/interactor/GetEpisodeByAnimeId.kt +++ b/domain/src/main/java/tachiyomi/domain/items/episode/interactor/GetEpisodesByAnimeId.kt @@ -5,7 +5,7 @@ import tachiyomi.core.util.system.logcat import tachiyomi.domain.items.episode.model.Episode import tachiyomi.domain.items.episode.repository.EpisodeRepository -class GetEpisodeByAnimeId( +class GetEpisodesByAnimeId( private val episodeRepository: EpisodeRepository, ) { diff --git a/domain/src/main/java/tachiyomi/domain/items/episode/model/Episode.kt b/domain/src/main/java/tachiyomi/domain/items/episode/model/Episode.kt index fe104a18b..01e9ae420 100644 --- a/domain/src/main/java/tachiyomi/domain/items/episode/model/Episode.kt +++ b/domain/src/main/java/tachiyomi/domain/items/episode/model/Episode.kt @@ -19,6 +19,16 @@ data class Episode( val isRecognizedNumber: Boolean get() = episodeNumber >= 0f + fun copyFrom(other: Episode): Episode { + return copy( + name = other.name, + url = other.url, + dateUpload = other.dateUpload, + episodeNumber = other.episodeNumber, + scanlator = other.scanlator?.ifBlank { null }, + ) + } + companion object { fun create() = Episode( id = -1, 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 9dc4414f0..cc8014a19 100644 --- a/domain/src/main/java/tachiyomi/domain/library/service/LibraryPreferences.kt +++ b/domain/src/main/java/tachiyomi/domain/library/service/LibraryPreferences.kt @@ -1,5 +1,6 @@ package tachiyomi.domain.library.service +import tachiyomi.core.preference.Preference import tachiyomi.core.preference.PreferenceStore import tachiyomi.core.preference.TriState import tachiyomi.core.preference.getEnum @@ -41,7 +42,7 @@ class LibraryPreferences( AnimeLibrarySort.Serializer::deserialize, ) - fun lastUpdatedTimestamp() = preferenceStore.getLong("library_update_last_timestamp", 0L) + fun lastUpdatedTimestamp() = preferenceStore.getLong(Preference.appStateKey("library_update_last_timestamp"), 0L) fun autoUpdateInterval() = preferenceStore.getInt("pref_library_update_interval_key", 0) fun autoUpdateDeviceRestrictions() = preferenceStore.getStringSet( @@ -196,8 +197,8 @@ class LibraryPreferences( fun defaultAnimeCategory() = preferenceStore.getInt("default_anime_category", -1) fun defaultMangaCategory() = preferenceStore.getInt("default_category", -1) - fun lastUsedAnimeCategory() = preferenceStore.getInt("last_used_anime_category", 0) - fun lastUsedMangaCategory() = preferenceStore.getInt("last_used_category", 0) + fun lastUsedAnimeCategory() = preferenceStore.getInt(Preference.appStateKey("last_used_anime_category"), 0) + fun lastUsedMangaCategory() = preferenceStore.getInt(Preference.appStateKey("last_used_category"), 0) fun animeUpdateCategories() = preferenceStore.getStringSet("animelib_update_categories", emptySet()) diff --git a/gradle/androidx.versions.toml b/gradle/androidx.versions.toml index 84a9cde4b..83aad11ca 100644 --- a/gradle/androidx.versions.toml +++ b/gradle/androidx.versions.toml @@ -30,7 +30,7 @@ paging-compose = { module = "androidx.paging:paging-compose", version.ref = "pag benchmark-macro = "androidx.benchmark:benchmark-macro-junit4:1.2.0" test-ext = "androidx.test.ext:junit-ktx:1.2.0-alpha01" test-espresso-core = "androidx.test.espresso:espresso-core:3.6.0-alpha01" -test-uiautomator = "androidx.test.uiautomator:uiautomator:2.3.0-alpha04" +test-uiautomator = "androidx.test.uiautomator:uiautomator:2.3.0-alpha05" [bundles] lifecycle = ["lifecycle-common", "lifecycle-process", "lifecycle-runtimektx"] \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 89425f767..beed01e0b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,6 +17,7 @@ flowreactivenetwork = "ru.beryukhov:flowreactivenetwork:1.0.4" okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp_version" } okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp_version" } +okhttp-brotli = { module = "com.squareup.okhttp3:okhttp-brotli", version.ref = "okhttp_version" } okhttp-dnsoverhttps = { module = "com.squareup.okhttp3:okhttp-dnsoverhttps", version.ref = "okhttp_version" } okio = "com.squareup.okio:okio:3.6.0" @@ -99,7 +100,7 @@ seeker = "io.github.2307vivek:seeker:1.1.1" truetypeparser = "io.github.yubyf:truetypeparser-light:2.1.4" [bundles] -okhttp = ["okhttp-core", "okhttp-logging", "okhttp-dnsoverhttps"] +okhttp = ["okhttp-core", "okhttp-logging", "okhttp-brotli", "okhttp-dnsoverhttps"] js-engine = ["quickjs-android"] sqlite = ["sqlite-framework", "sqlite-ktx", "sqlite-android"] coil = ["coil-core", "coil-gif", "coil-compose"] diff --git a/i18n/src/main/res/values/strings-aniyomi.xml b/i18n/src/main/res/values/strings-aniyomi.xml index f24e8f28c..43e792a7e 100644 --- a/i18n/src/main/res/values/strings-aniyomi.xml +++ b/i18n/src/main/res/values/strings-aniyomi.xml @@ -158,6 +158,8 @@ Show next episode\'s airing time What information to include in the backup file Clear chapter and episode cache + Anime Storage usage + Manga Storage usage Used by anime: %1$s, used by manga: %2$s Clear episode/chapter cache on app launch Clear Manga database diff --git a/i18n/src/main/res/values/strings.xml b/i18n/src/main/res/values/strings.xml index 97a73ae37..391e7d10b 100644 --- a/i18n/src/main/res/values/strings.xml +++ b/i18n/src/main/res/values/strings.xml @@ -66,6 +66,7 @@ Latest chapter Chapter fetch date Date added + Tracker score Airing time Search Search… @@ -499,6 +500,11 @@ Restoring backup failed Canceled restore You should keep copies of backups in other places as well. + Last automatically backed up: %s + Data + Used: %1$s + Cache cleared. %1$d files have been deleted + Error occurred while clearing Syncing library @@ -514,10 +520,6 @@ Reset default user agent string Requires app restart to take effect Cookies cleared - Data - Used: %1$s - Cache cleared. %1$d files have been deleted - Error occurred while clearing Invalidate downloads index Downloads index invalidated Clear database @@ -766,6 +768,7 @@ Unable to open last read chapter Library last updated: %s Just now + Never Ch. %1$s - %2$s 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 e8a0cab41..6ca213aa3 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 @@ -22,6 +22,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -69,7 +70,11 @@ fun TabText( Row( verticalAlignment = Alignment.CenterVertically, ) { - Text(text = text) + Text( + text = text, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) if (badgeCount != null) { Pill( text = "$badgeCount",