Last commit merged: aca36f9625
This commit is contained in:
LuftVerbot 2023-11-25 17:54:17 +01:00
parent 3fba4cbc2b
commit 89777e98b0
62 changed files with 1063 additions and 866 deletions

View file

@ -20,8 +20,8 @@ android {
defaultConfig { defaultConfig {
applicationId = "xyz.jmir.tachiyomi.mi" applicationId = "xyz.jmir.tachiyomi.mi"
versionCode = 106 versionCode = 108
versionName = "0.14.6" versionName = "0.14.7"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"") buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
@ -319,12 +319,12 @@ tasks {
kotlinOptions.freeCompilerArgs += listOf( kotlinOptions.freeCompilerArgs += listOf(
"-P", "-P",
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" + "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +
project.buildDir.absolutePath + "/compose_metrics", project.layout.buildDirectory.dir("compose_metrics").get().asFile.absolutePath,
) )
kotlinOptions.freeCompilerArgs += listOf( kotlinOptions.freeCompilerArgs += listOf(
"-P", "-P",
"plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" + "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" +
project.buildDir.absolutePath + "/compose_metrics", project.layout.buildDirectory.dir("compose_metrics").get().asFile.absolutePath,
) )
} }
} }

View file

@ -113,15 +113,15 @@ import tachiyomi.domain.history.manga.interactor.RemoveMangaHistory
import tachiyomi.domain.history.manga.interactor.UpsertMangaHistory import tachiyomi.domain.history.manga.interactor.UpsertMangaHistory
import tachiyomi.domain.history.manga.repository.MangaHistoryRepository import tachiyomi.domain.history.manga.repository.MangaHistoryRepository
import tachiyomi.domain.items.chapter.interactor.GetChapter 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.GetChapterByUrlAndMangaId
import tachiyomi.domain.items.chapter.interactor.GetChaptersByMangaId
import tachiyomi.domain.items.chapter.interactor.SetMangaDefaultChapterFlags import tachiyomi.domain.items.chapter.interactor.SetMangaDefaultChapterFlags
import tachiyomi.domain.items.chapter.interactor.ShouldUpdateDbChapter import tachiyomi.domain.items.chapter.interactor.ShouldUpdateDbChapter
import tachiyomi.domain.items.chapter.interactor.UpdateChapter import tachiyomi.domain.items.chapter.interactor.UpdateChapter
import tachiyomi.domain.items.chapter.repository.ChapterRepository import tachiyomi.domain.items.chapter.repository.ChapterRepository
import tachiyomi.domain.items.episode.interactor.GetEpisode 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.GetEpisodeByUrlAndAnimeId
import tachiyomi.domain.items.episode.interactor.GetEpisodesByAnimeId
import tachiyomi.domain.items.episode.interactor.SetAnimeDefaultEpisodeFlags import tachiyomi.domain.items.episode.interactor.SetAnimeDefaultEpisodeFlags
import tachiyomi.domain.items.episode.interactor.ShouldUpdateDbEpisode import tachiyomi.domain.items.episode.interactor.ShouldUpdateDbEpisode
import tachiyomi.domain.items.episode.interactor.UpdateEpisode import tachiyomi.domain.items.episode.interactor.UpdateEpisode
@ -250,7 +250,7 @@ class DomainModule : InjektModule {
addSingletonFactory<EpisodeRepository> { EpisodeRepositoryImpl(get()) } addSingletonFactory<EpisodeRepository> { EpisodeRepositoryImpl(get()) }
addFactory { GetEpisode(get()) } addFactory { GetEpisode(get()) }
addFactory { GetEpisodeByAnimeId(get()) } addFactory { GetEpisodesByAnimeId(get()) }
addFactory { GetEpisodeByUrlAndAnimeId(get()) } addFactory { GetEpisodeByUrlAndAnimeId(get()) }
addFactory { UpdateEpisode(get()) } addFactory { UpdateEpisode(get()) }
addFactory { SetSeenStatus(get(), get(), get(), get()) } addFactory { SetSeenStatus(get(), get(), get(), get()) }
@ -259,7 +259,7 @@ class DomainModule : InjektModule {
addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) } addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) }
addFactory { GetChapter(get()) } addFactory { GetChapter(get()) }
addFactory { GetChapterByMangaId(get()) } addFactory { GetChaptersByMangaId(get()) }
addFactory { GetChapterByUrlAndMangaId(get()) } addFactory { GetChapterByUrlAndMangaId(get()) }
addFactory { UpdateChapter(get()) } addFactory { UpdateChapter(get()) }
addFactory { SetReadStatus(get(), get(), get(), get()) } addFactory { SetReadStatus(get(), get(), get(), get()) }

View file

@ -7,6 +7,7 @@ import androidx.annotation.StringRes
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.isPreviewBuildType import eu.kanade.tachiyomi.util.system.isPreviewBuildType
import eu.kanade.tachiyomi.util.system.isReleaseBuildType import eu.kanade.tachiyomi.util.system.isReleaseBuildType
import tachiyomi.core.preference.Preference
import tachiyomi.core.preference.PreferenceStore import tachiyomi.core.preference.PreferenceStore
class BasePreferences( class BasePreferences(
@ -14,9 +15,12 @@ class BasePreferences(
private val preferenceStore: PreferenceStore, 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) fun extensionInstaller() = ExtensionInstallerPreference(context, preferenceStore)

View file

@ -11,7 +11,7 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import tachiyomi.data.items.chapter.ChapterSanitizer import tachiyomi.data.items.chapter.ChapterSanitizer
import tachiyomi.domain.entries.manga.model.Manga 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.ShouldUpdateDbChapter
import tachiyomi.domain.items.chapter.interactor.UpdateChapter import tachiyomi.domain.items.chapter.interactor.UpdateChapter
import tachiyomi.domain.items.chapter.model.Chapter 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.repository.ChapterRepository
import tachiyomi.domain.items.chapter.service.ChapterRecognition import tachiyomi.domain.items.chapter.service.ChapterRecognition
import tachiyomi.source.local.entries.manga.isLocal import tachiyomi.source.local.entries.manga.isLocal
import uy.kohesive.injekt.api.get
import java.lang.Long.max import java.lang.Long.max
import java.time.ZonedDateTime import java.time.ZonedDateTime
import java.util.Date import java.util.Date
@ -33,7 +32,7 @@ class SyncChaptersWithSource(
private val shouldUpdateDbChapter: ShouldUpdateDbChapter, private val shouldUpdateDbChapter: ShouldUpdateDbChapter,
private val updateManga: UpdateManga, private val updateManga: UpdateManga,
private val updateChapter: UpdateChapter, private val updateChapter: UpdateChapter,
private val getChapterByMangaId: GetChapterByMangaId, private val getChaptersByMangaId: GetChaptersByMangaId,
) { ) {
/** /**
@ -67,7 +66,7 @@ class SyncChaptersWithSource(
} }
// Chapters from db. // Chapters from db.
val dbChapters = getChapterByMangaId.await(manga.id) val dbChapters = getChaptersByMangaId.await(manga.id)
// Chapters from the source not in db. // Chapters from the source not in db.
val toAdd = mutableListOf<Chapter>() val toAdd = mutableListOf<Chapter>()

View file

@ -1,6 +1,5 @@
package eu.kanade.domain.items.chapter.model package eu.kanade.domain.items.chapter.model
import data.Chapters
import eu.kanade.tachiyomi.data.database.models.manga.ChapterImpl import eu.kanade.tachiyomi.data.database.models.manga.ChapterImpl
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import tachiyomi.domain.items.chapter.model.Chapter 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 { fun Chapter.toDbChapter(): DbChapter = ChapterImpl().also {
it.id = id it.id = id
it.manga_id = mangaId it.manga_id = mangaId

View file

@ -2,7 +2,7 @@ package eu.kanade.domain.items.chapter.model
import eu.kanade.domain.entries.manga.model.downloadedFilter import eu.kanade.domain.entries.manga.model.downloadedFilter
import eu.kanade.tachiyomi.data.download.manga.MangaDownloadManager 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.applyFilter
import tachiyomi.domain.entries.manga.model.Manga import tachiyomi.domain.entries.manga.model.Manga
import tachiyomi.domain.items.chapter.model.Chapter import tachiyomi.domain.items.chapter.model.Chapter
@ -39,7 +39,7 @@ fun List<Chapter>.applyFilters(manga: Manga, downloadManager: MangaDownloadManag
* Applies the view filters to the list of chapters obtained from the database. * Applies the view filters to the list of chapters obtained from the database.
* @return an observable of the list of chapters filtered and sorted. * @return an observable of the list of chapters filtered and sorted.
*/ */
fun List<ChapterItem>.applyFilters(manga: Manga): Sequence<ChapterItem> { fun List<ChapterList.Item>.applyFilters(manga: Manga): Sequence<ChapterList.Item> {
val isLocalManga = manga.isLocal() val isLocalManga = manga.isLocal()
val unreadFilter = manga.unreadFilter val unreadFilter = manga.unreadFilter
val downloadedFilter = manga.downloadedFilter val downloadedFilter = manga.downloadedFilter

View file

@ -11,7 +11,7 @@ import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadManager
import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadProvider import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadProvider
import tachiyomi.data.items.episode.EpisodeSanitizer import tachiyomi.data.items.episode.EpisodeSanitizer
import tachiyomi.domain.entries.anime.model.Anime 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.ShouldUpdateDbEpisode
import tachiyomi.domain.items.episode.interactor.UpdateEpisode import tachiyomi.domain.items.episode.interactor.UpdateEpisode
import tachiyomi.domain.items.episode.model.Episode 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.repository.EpisodeRepository
import tachiyomi.domain.items.episode.service.EpisodeRecognition import tachiyomi.domain.items.episode.service.EpisodeRecognition
import tachiyomi.source.local.entries.anime.isLocal import tachiyomi.source.local.entries.anime.isLocal
import uy.kohesive.injekt.api.get
import java.lang.Long.max import java.lang.Long.max
import java.time.ZonedDateTime import java.time.ZonedDateTime
import java.util.Date import java.util.Date
@ -33,7 +32,7 @@ class SyncEpisodesWithSource(
private val shouldUpdateDbEpisode: ShouldUpdateDbEpisode, private val shouldUpdateDbEpisode: ShouldUpdateDbEpisode,
private val updateAnime: UpdateAnime, private val updateAnime: UpdateAnime,
private val updateEpisode: UpdateEpisode, private val updateEpisode: UpdateEpisode,
private val getEpisodeByAnimeId: GetEpisodeByAnimeId, private val getEpisodesByAnimeId: GetEpisodesByAnimeId,
) { ) {
/** /**
@ -67,7 +66,7 @@ class SyncEpisodesWithSource(
} }
// Episodes from db. // Episodes from db.
val dbEpisodes = getEpisodeByAnimeId.await(anime.id) val dbEpisodes = getEpisodesByAnimeId.await(anime.id)
// Episodes from the source not in db. // Episodes from the source not in db.
val toAdd = mutableListOf<Episode>() val toAdd = mutableListOf<Episode>()

View file

@ -1,6 +1,5 @@
package eu.kanade.domain.items.episode.model package eu.kanade.domain.items.episode.model
import dataanime.Episodes
import eu.kanade.tachiyomi.animesource.model.SEpisode import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.data.database.models.anime.EpisodeImpl import eu.kanade.tachiyomi.data.database.models.anime.EpisodeImpl
import tachiyomi.domain.items.episode.model.Episode 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 { fun Episode.toDbEpisode(): DbEpisode = EpisodeImpl().also {
it.id = id it.id = id
it.anime_id = animeId it.anime_id = animeId

View file

@ -2,7 +2,7 @@ package eu.kanade.domain.items.episode.model
import eu.kanade.domain.entries.anime.model.downloadedFilter import eu.kanade.domain.entries.anime.model.downloadedFilter
import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadManager 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.anime.model.Anime
import tachiyomi.domain.entries.applyFilter import tachiyomi.domain.entries.applyFilter
import tachiyomi.domain.items.episode.model.Episode import tachiyomi.domain.items.episode.model.Episode
@ -39,7 +39,7 @@ fun List<Episode>.applyFilters(anime: Anime, downloadManager: AnimeDownloadManag
* Applies the view filters to the list of episodes obtained from the database. * Applies the view filters to the list of episodes obtained from the database.
* @return an observable of the list of episodes filtered and sorted. * @return an observable of the list of episodes filtered and sorted.
*/ */
fun List<EpisodeItem>.applyFilters(anime: Anime): Sequence<EpisodeItem> { fun List<EpisodeList.Item>.applyFilters(anime: Anime): Sequence<EpisodeList.Item> {
val isLocalAnime = anime.isLocal() val isLocalAnime = anime.isLocal()
val unseenFilter = anime.unseenFilter val unseenFilter = anime.unseenFilter
val downloadedFilter = anime.downloadedFilter val downloadedFilter = anime.downloadedFilter

View file

@ -1,6 +1,7 @@
package eu.kanade.domain.source.service package eu.kanade.domain.source.service
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import tachiyomi.core.preference.Preference
import tachiyomi.core.preference.PreferenceStore import tachiyomi.core.preference.PreferenceStore
import tachiyomi.core.preference.getEnum import tachiyomi.core.preference.getEnum
import tachiyomi.domain.library.model.LibraryDisplayMode import tachiyomi.domain.library.model.LibraryDisplayMode
@ -35,7 +36,7 @@ class SourcePreferences(
SetMigrateSorting.Direction.ASCENDING, SetMigrateSorting.Direction.ASCENDING,
) )
fun trustedSignatures() = preferenceStore.getStringSet("trusted_signatures", emptySet()) fun trustedSignatures() = preferenceStore.getStringSet(Preference.appStateKey("trusted_signatures"), emptySet())
// Mixture Sources // Mixture Sources
@ -45,8 +46,14 @@ class SourcePreferences(
fun pinnedAnimeSources() = preferenceStore.getStringSet("pinned_anime_catalogues", emptySet()) fun pinnedAnimeSources() = preferenceStore.getStringSet("pinned_anime_catalogues", emptySet())
fun pinnedMangaSources() = preferenceStore.getStringSet("pinned_catalogues", emptySet()) fun pinnedMangaSources() = preferenceStore.getStringSet("pinned_catalogues", emptySet())
fun lastUsedAnimeSource() = preferenceStore.getLong("last_anime_catalogue_source", -1) fun lastUsedAnimeSource() = preferenceStore.getLong(
fun lastUsedMangaSource() = preferenceStore.getLong("last_catalogue_source", -1) 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 animeExtensionUpdatesCount() = preferenceStore.getInt("animeext_updates_count", 0)
fun mangaExtensionUpdatesCount() = preferenceStore.getInt("ext_updates_count", 0) fun mangaExtensionUpdatesCount() = preferenceStore.getInt("ext_updates_count", 0)

View file

@ -5,7 +5,7 @@ import eu.kanade.tachiyomi.data.track.AnimeTracker
import eu.kanade.tachiyomi.data.track.EnhancedAnimeTracker import eu.kanade.tachiyomi.data.track.EnhancedAnimeTracker
import logcat.LogPriority import logcat.LogPriority
import tachiyomi.core.util.system.logcat 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.interactor.UpdateEpisode
import tachiyomi.domain.items.episode.model.toEpisodeUpdate import tachiyomi.domain.items.episode.model.toEpisodeUpdate
import tachiyomi.domain.track.anime.interactor.InsertAnimeTrack import tachiyomi.domain.track.anime.interactor.InsertAnimeTrack
@ -14,7 +14,7 @@ import tachiyomi.domain.track.anime.model.AnimeTrack
class SyncEpisodeProgressWithTrack( class SyncEpisodeProgressWithTrack(
private val updateEpisode: UpdateEpisode, private val updateEpisode: UpdateEpisode,
private val insertTrack: InsertAnimeTrack, private val insertTrack: InsertAnimeTrack,
private val getEpisodeByAnimeId: GetEpisodeByAnimeId, private val getEpisodesByAnimeId: GetEpisodesByAnimeId,
) { ) {
suspend fun await( suspend fun await(
@ -26,7 +26,7 @@ class SyncEpisodeProgressWithTrack(
return return
} }
val sortedEpisodes = getEpisodeByAnimeId.await(animeId) val sortedEpisodes = getEpisodesByAnimeId.await(animeId)
.sortedBy { it.episodeNumber } .sortedBy { it.episodeNumber }
.filter { it.isRecognizedNumber } .filter { it.isRecognizedNumber }

View file

@ -5,7 +5,7 @@ import eu.kanade.tachiyomi.data.track.EnhancedMangaTracker
import eu.kanade.tachiyomi.data.track.MangaTracker import eu.kanade.tachiyomi.data.track.MangaTracker
import logcat.LogPriority import logcat.LogPriority
import tachiyomi.core.util.system.logcat 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.interactor.UpdateChapter
import tachiyomi.domain.items.chapter.model.toChapterUpdate import tachiyomi.domain.items.chapter.model.toChapterUpdate
import tachiyomi.domain.track.manga.interactor.InsertMangaTrack import tachiyomi.domain.track.manga.interactor.InsertMangaTrack
@ -14,7 +14,7 @@ import tachiyomi.domain.track.manga.model.MangaTrack
class SyncChapterProgressWithTrack( class SyncChapterProgressWithTrack(
private val updateChapter: UpdateChapter, private val updateChapter: UpdateChapter,
private val insertTrack: InsertMangaTrack, private val insertTrack: InsertMangaTrack,
private val getChapterByMangaId: GetChapterByMangaId, private val getChaptersByMangaId: GetChaptersByMangaId,
) { ) {
suspend fun await( suspend fun await(
@ -26,7 +26,7 @@ class SyncChapterProgressWithTrack(
return return
} }
val sortedChapters = getChapterByMangaId.await(mangaId) val sortedChapters = getChaptersByMangaId.await(mangaId)
.sortedBy { it.chapterNumber } .sortedBy { it.chapterNumber }
.filter { it.isRecognizedNumber } .filter { it.isRecognizedNumber }

View file

@ -16,7 +16,6 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Tab import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow import androidx.compose.material3.TabRow
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -33,6 +32,7 @@ import eu.kanade.tachiyomi.R
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import tachiyomi.presentation.core.components.HorizontalPager import tachiyomi.presentation.core.components.HorizontalPager
import tachiyomi.presentation.core.components.material.TabIndicator import tachiyomi.presentation.core.components.material.TabIndicator
import tachiyomi.presentation.core.components.material.TabText
object TabbedDialogPaddings { object TabbedDialogPaddings {
val Horizontal = 24.dp val Horizontal = 24.dp
@ -71,21 +71,12 @@ fun TabbedDialog(
}, },
divider = {}, divider = {},
) { ) {
tabTitles.fastForEachIndexed { i, tab -> tabTitles.fastForEachIndexed { index, tab ->
val selected = pagerState.currentPage == i
Tab( Tab(
selected = selected, selected = pagerState.currentPage == index,
onClick = { scope.launch { pagerState.animateScrollToPage(i) } }, onClick = { scope.launch { pagerState.animateScrollToPage(index) } },
text = { text = { TabText(text = tab) },
Text( unselectedContentColor = MaterialTheme.colorScheme.onSurface,
text = tab,
color = if (selected) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.onSurfaceVariant
},
)
},
) )
} }
} }

View file

@ -5,9 +5,11 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.asPaddingValues 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.Icons
import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text 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.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.util.fastAll import androidx.compose.ui.util.fastAll
import androidx.compose.ui.util.fastAny 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.source.anime.getNameForAnimeInfo
import eu.kanade.tachiyomi.ui.browse.anime.extension.details.SourcePreferencesScreen 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.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.lang.toRelativeString
import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.copyToClipboard
import kotlinx.coroutines.delay 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.ExtendedFloatingActionButton
import tachiyomi.presentation.core.components.material.PullRefresh import tachiyomi.presentation.core.components.material.PullRefresh
import tachiyomi.presentation.core.components.material.Scaffold 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.isScrolledToEnd
import tachiyomi.presentation.core.util.isScrollingUp import tachiyomi.presentation.core.util.isScrollingUp
import tachiyomi.presentation.core.util.secondaryItemAlpha
import java.text.DateFormat import java.text.DateFormat
import java.util.Date import java.util.Date
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -107,7 +114,7 @@ fun AnimeScreen(
alwaysUseExternalPlayer: Boolean, alwaysUseExternalPlayer: Boolean,
onBackClicked: () -> Unit, onBackClicked: () -> Unit,
onEpisodeClicked: (episode: Episode, alt: Boolean) -> Unit, onEpisodeClicked: (episode: Episode, alt: Boolean) -> Unit,
onDownloadEpisode: ((List<EpisodeItem>, EpisodeDownloadAction) -> Unit)?, onDownloadEpisode: ((List<EpisodeList.Item>, EpisodeDownloadAction) -> Unit)?,
onAddToLibraryClicked: () -> Unit, onAddToLibraryClicked: () -> Unit,
onWebViewClicked: (() -> Unit)?, onWebViewClicked: (() -> Unit)?,
onWebViewLongClicked: (() -> Unit)?, onWebViewLongClicked: (() -> Unit)?,
@ -139,10 +146,10 @@ fun AnimeScreen(
onMultiDeleteClicked: (List<Episode>) -> Unit, onMultiDeleteClicked: (List<Episode>) -> Unit,
// For episode swipe // For episode swipe
onEpisodeSwipe: (EpisodeItem, LibraryPreferences.EpisodeSwipeAction) -> Unit, onEpisodeSwipe: (EpisodeList.Item, LibraryPreferences.EpisodeSwipeAction) -> Unit,
// Episode selection // Episode selection
onEpisodeSelected: (EpisodeItem, Boolean, Boolean, Boolean) -> Unit, onEpisodeSelected: (EpisodeList.Item, Boolean, Boolean, Boolean) -> Unit,
onAllEpisodeSelected: (Boolean) -> Unit, onAllEpisodeSelected: (Boolean) -> Unit,
onInvertSelection: () -> Unit, onInvertSelection: () -> Unit,
) { ) {
@ -257,7 +264,7 @@ private fun AnimeScreenSmallImpl(
alwaysUseExternalPlayer: Boolean, alwaysUseExternalPlayer: Boolean,
onBackClicked: () -> Unit, onBackClicked: () -> Unit,
onEpisodeClicked: (Episode, Boolean) -> Unit, onEpisodeClicked: (Episode, Boolean) -> Unit,
onDownloadEpisode: ((List<EpisodeItem>, EpisodeDownloadAction) -> Unit)?, onDownloadEpisode: ((List<EpisodeList.Item>, EpisodeDownloadAction) -> Unit)?,
onAddToLibraryClicked: () -> Unit, onAddToLibraryClicked: () -> Unit,
onWebViewClicked: (() -> Unit)?, onWebViewClicked: (() -> Unit)?,
onWebViewLongClicked: (() -> Unit)?, onWebViewLongClicked: (() -> Unit)?,
@ -291,16 +298,17 @@ private fun AnimeScreenSmallImpl(
onMultiDeleteClicked: (List<Episode>) -> Unit, onMultiDeleteClicked: (List<Episode>) -> Unit,
// For episode swipe // For episode swipe
onEpisodeSwipe: (EpisodeItem, LibraryPreferences.EpisodeSwipeAction) -> Unit, onEpisodeSwipe: (EpisodeList.Item, LibraryPreferences.EpisodeSwipeAction) -> Unit,
// Episode selection // Episode selection
onEpisodeSelected: (EpisodeItem, Boolean, Boolean, Boolean) -> Unit, onEpisodeSelected: (EpisodeList.Item, Boolean, Boolean, Boolean) -> Unit,
onAllEpisodeSelected: (Boolean) -> Unit, onAllEpisodeSelected: (Boolean) -> Unit,
onInvertSelection: () -> Unit, onInvertSelection: () -> Unit,
) { ) {
val episodeListState = rememberLazyListState() val episodeListState = rememberLazyListState()
val episodes = remember(state) { state.processedEpisodes } val episodes = remember(state) { state.processedEpisodes }
val listItem = remember(state) { state.episodeListItems }
val isAnySelected by remember { val isAnySelected by remember {
derivedStateOf { derivedStateOf {
@ -516,7 +524,8 @@ private fun AnimeScreenSmallImpl(
sharedEpisodeItems( sharedEpisodeItems(
anime = state.anime, anime = state.anime,
episodes = episodes, episodes = listItem,
isAnyEpisodeSelected = episodes.fastAny { it.selected },
dateRelativeTime = dateRelativeTime, dateRelativeTime = dateRelativeTime,
dateFormat = dateFormat, dateFormat = dateFormat,
episodeSwipeStartAction = episodeSwipeStartAction, episodeSwipeStartAction = episodeSwipeStartAction,
@ -546,7 +555,7 @@ fun AnimeScreenLargeImpl(
alwaysUseExternalPlayer: Boolean, alwaysUseExternalPlayer: Boolean,
onBackClicked: () -> Unit, onBackClicked: () -> Unit,
onEpisodeClicked: (Episode, Boolean) -> Unit, onEpisodeClicked: (Episode, Boolean) -> Unit,
onDownloadEpisode: ((List<EpisodeItem>, EpisodeDownloadAction) -> Unit)?, onDownloadEpisode: ((List<EpisodeList.Item>, EpisodeDownloadAction) -> Unit)?,
onAddToLibraryClicked: () -> Unit, onAddToLibraryClicked: () -> Unit,
onWebViewClicked: (() -> Unit)?, onWebViewClicked: (() -> Unit)?,
onWebViewLongClicked: (() -> Unit)?, onWebViewLongClicked: (() -> Unit)?,
@ -580,10 +589,10 @@ fun AnimeScreenLargeImpl(
onMultiDeleteClicked: (List<Episode>) -> Unit, onMultiDeleteClicked: (List<Episode>) -> Unit,
// For swipe actions // For swipe actions
onEpisodeSwipe: (EpisodeItem, LibraryPreferences.EpisodeSwipeAction) -> Unit, onEpisodeSwipe: (EpisodeList.Item, LibraryPreferences.EpisodeSwipeAction) -> Unit,
// Episode selection // Episode selection
onEpisodeSelected: (EpisodeItem, Boolean, Boolean, Boolean) -> Unit, onEpisodeSelected: (EpisodeList.Item, Boolean, Boolean, Boolean) -> Unit,
onAllEpisodeSelected: (Boolean) -> Unit, onAllEpisodeSelected: (Boolean) -> Unit,
onInvertSelection: () -> Unit, onInvertSelection: () -> Unit,
) { ) {
@ -591,6 +600,7 @@ fun AnimeScreenLargeImpl(
val density = LocalDensity.current val density = LocalDensity.current
val episodes = remember(state) { state.processedEpisodes } val episodes = remember(state) { state.processedEpisodes }
val listItem = remember(state) { state.episodeListItems }
val isAnySelected by remember { val isAnySelected by remember {
derivedStateOf { derivedStateOf {
@ -799,7 +809,8 @@ fun AnimeScreenLargeImpl(
sharedEpisodeItems( sharedEpisodeItems(
anime = state.anime, anime = state.anime,
episodes = episodes, episodes = listItem,
isAnyEpisodeSelected = episodes.fastAny { it.selected },
dateRelativeTime = dateRelativeTime, dateRelativeTime = dateRelativeTime,
dateFormat = dateFormat, dateFormat = dateFormat,
episodeSwipeStartAction = episodeSwipeStartAction, episodeSwipeStartAction = episodeSwipeStartAction,
@ -819,13 +830,13 @@ fun AnimeScreenLargeImpl(
@Composable @Composable
private fun SharedAnimeBottomActionMenu( private fun SharedAnimeBottomActionMenu(
selected: List<EpisodeItem>, selected: List<EpisodeList.Item>,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onEpisodeClicked: (Episode, Boolean) -> Unit, onEpisodeClicked: (Episode, Boolean) -> Unit,
onMultiBookmarkClicked: (List<Episode>, bookmarked: Boolean) -> Unit, onMultiBookmarkClicked: (List<Episode>, bookmarked: Boolean) -> Unit,
onMultiMarkAsSeenClicked: (List<Episode>, markAsSeen: Boolean) -> Unit, onMultiMarkAsSeenClicked: (List<Episode>, markAsSeen: Boolean) -> Unit,
onMarkPreviousAsSeenClicked: (Episode) -> Unit, onMarkPreviousAsSeenClicked: (Episode) -> Unit,
onDownloadEpisode: ((List<EpisodeItem>, EpisodeDownloadAction) -> Unit)?, onDownloadEpisode: ((List<EpisodeList.Item>, EpisodeDownloadAction) -> Unit)?,
onMultiDeleteClicked: (List<Episode>) -> Unit, onMultiDeleteClicked: (List<Episode>) -> Unit,
fillFraction: Float, fillFraction: Float,
alwaysUseExternalPlayer: Boolean, alwaysUseExternalPlayer: Boolean,
@ -870,34 +881,63 @@ private fun SharedAnimeBottomActionMenu(
private fun LazyListScope.sharedEpisodeItems( private fun LazyListScope.sharedEpisodeItems(
anime: Anime, anime: Anime,
episodes: List<EpisodeItem>, episodes: List<EpisodeList>,
isAnyEpisodeSelected: Boolean,
dateRelativeTime: Boolean, dateRelativeTime: Boolean,
dateFormat: DateFormat, dateFormat: DateFormat,
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction, episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction, episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
onEpisodeClicked: (Episode, Boolean) -> Unit, onEpisodeClicked: (Episode, Boolean) -> Unit,
onDownloadEpisode: ((List<EpisodeItem>, EpisodeDownloadAction) -> Unit)?, onDownloadEpisode: ((List<EpisodeList.Item>, EpisodeDownloadAction) -> Unit)?,
onEpisodeSelected: (EpisodeItem, Boolean, Boolean, Boolean) -> Unit, onEpisodeSelected: (EpisodeList.Item, Boolean, Boolean, Boolean) -> Unit,
onEpisodeSwipe: (EpisodeItem, LibraryPreferences.EpisodeSwipeAction) -> Unit, onEpisodeSwipe: (EpisodeList.Item, LibraryPreferences.EpisodeSwipeAction) -> Unit,
) { ) {
items( items(
items = episodes, 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 }, contentType = { EntryScreenItem.ITEM },
) { episodeItem -> ) { item ->
val haptic = LocalHapticFeedback.current val haptic = LocalHapticFeedback.current
val context = LocalContext.current val context = LocalContext.current
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(),
)
HorizontalDivider(modifier = Modifier.weight(1f))
}
}
is EpisodeList.Item -> {
AnimeEpisodeListItem( AnimeEpisodeListItem(
title = if (anime.displayMode == Anime.EPISODE_DISPLAY_NUMBER) { title = if (anime.displayMode == Anime.EPISODE_DISPLAY_NUMBER) {
stringResource( stringResource(
R.string.display_mode_episode, R.string.display_mode_episode,
formatEpisodeNumber(episodeItem.episode.episodeNumber), formatEpisodeNumber(item.episode.episodeNumber),
) )
} else { } else {
episodeItem.episode.name item.episode.name
}, },
date = episodeItem.episode.dateUpload date = item.episode.dateUpload
.takeIf { it > 0L } .takeIf { it > 0L }
?.let { ?.let {
Date(it).toRelativeString( Date(it).toRelativeString(
@ -906,64 +946,58 @@ private fun LazyListScope.sharedEpisodeItems(
dateFormat, dateFormat,
) )
}, },
watchProgress = episodeItem.episode.lastSecondSeen watchProgress = item.episode.lastSecondSeen
.takeIf { !episodeItem.episode.seen && it > 0L } .takeIf { !item.episode.seen && it > 0L }
?.let { ?.let {
stringResource( stringResource(
R.string.episode_progress, R.string.episode_progress,
formatTime(it), it + 1,
formatTime(episodeItem.episode.totalSeconds),
) )
}, },
scanlator = episodeItem.episode.scanlator.takeIf { !it.isNullOrBlank() }, scanlator = item.episode.scanlator.takeIf { !it.isNullOrBlank() },
seen = episodeItem.episode.seen, seen = item.episode.seen,
bookmark = episodeItem.episode.bookmark, bookmark = item.episode.bookmark,
selected = episodeItem.selected, selected = item.selected,
downloadIndicatorEnabled = episodes.fastAll { !it.selected }, downloadIndicatorEnabled = !isAnyEpisodeSelected,
downloadStateProvider = { episodeItem.downloadState }, downloadStateProvider = { item.downloadState },
downloadProgressProvider = { episodeItem.downloadProgress }, downloadProgressProvider = { item.downloadProgress },
episodeSwipeStartAction = episodeSwipeStartAction, episodeSwipeStartAction = episodeSwipeStartAction,
episodeSwipeEndAction = episodeSwipeEndAction, episodeSwipeEndAction = episodeSwipeEndAction,
onLongClick = { onLongClick = {
onEpisodeSelected(episodeItem, !episodeItem.selected, true, true) onEpisodeSelected(item, !item.selected, true, true)
haptic.performHapticFeedback(HapticFeedbackType.LongPress) haptic.performHapticFeedback(HapticFeedbackType.LongPress)
}, },
onClick = { onClick = {
onEpisodeItemClick( onEpisodeItemClick(
episodeItem = episodeItem, episodeItem = item,
episodes = episodes, isAnyEpisodeSelected = isAnyEpisodeSelected,
onToggleSelection = { onToggleSelection = { onEpisodeSelected(item, !item.selected, true, false) },
onEpisodeSelected(
episodeItem,
!episodeItem.selected,
true,
false,
)
},
onEpisodeClicked = onEpisodeClicked, onEpisodeClicked = onEpisodeClicked,
) )
}, },
onDownloadClick = if (onDownloadEpisode != null) { onDownloadClick = if (onDownloadEpisode != null) {
{ onDownloadEpisode(listOf(episodeItem), it) } { onDownloadEpisode(listOf(item), it) }
} else { } else {
null null
}, },
onEpisodeSwipe = { onEpisodeSwipe = {
onEpisodeSwipe(episodeItem, it) onEpisodeSwipe(item, it)
}, },
) )
} }
} }
}
}
private fun onEpisodeItemClick( private fun onEpisodeItemClick(
episodeItem: EpisodeItem, episodeItem: EpisodeList.Item,
episodes: List<EpisodeItem>, isAnyEpisodeSelected: Boolean,
onToggleSelection: (Boolean) -> Unit, onToggleSelection: (Boolean) -> Unit,
onEpisodeClicked: (Episode, Boolean) -> Unit, onEpisodeClicked: (Episode, Boolean) -> Unit,
) { ) {
when { when {
episodeItem.selected -> onToggleSelection(false) episodeItem.selected -> onToggleSelection(false)
episodes.fastAny { it.selected } -> onToggleSelection(true) isAnyEpisodeSelected -> onToggleSelection(true)
else -> onEpisodeClicked(episodeItem.episode, false) else -> onEpisodeClicked(episodeItem.episode, false)
} }
} }

View file

@ -1,6 +1,5 @@
package eu.kanade.presentation.entries.anime.components package eu.kanade.presentation.entries.anime.components
import android.content.Context
import androidx.compose.animation.animateContentSize import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.graphics.res.animatedVectorResource 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.Favorite
import androidx.compose.material.icons.filled.HourglassEmpty import androidx.compose.material.icons.filled.HourglassEmpty
import androidx.compose.material.icons.filled.PersonOutline 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.AttachMoney
import androidx.compose.material.icons.outlined.Block import androidx.compose.material.icons.outlined.Block
import androidx.compose.material.icons.outlined.Close 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.DropdownMenuItem
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.SuggestionChip import androidx.compose.material3.SuggestionChip
@ -128,7 +129,7 @@ fun AnimeInfoBox(
.alpha(.2f), .alpha(.2f),
) )
// Manga & source info // Anime & source info
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) { CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) {
if (!isTabletUi) { if (!isTabletUi) {
AnimeAndSourceTitlesSmall( AnimeAndSourceTitlesSmall(
@ -136,7 +137,6 @@ fun AnimeInfoBox(
coverDataProvider = coverDataProvider, coverDataProvider = coverDataProvider,
onCoverClick = onCoverClick, onCoverClick = onCoverClick,
title = title, title = title,
context = LocalContext.current,
doSearch = doSearch, doSearch = doSearch,
author = author, author = author,
artist = artist, artist = artist,
@ -150,7 +150,6 @@ fun AnimeInfoBox(
coverDataProvider = coverDataProvider, coverDataProvider = coverDataProvider,
onCoverClick = onCoverClick, onCoverClick = onCoverClick,
title = title, title = title,
context = LocalContext.current,
doSearch = doSearch, doSearch = doSearch,
author = author, author = author,
artist = artist, artist = artist,
@ -335,7 +334,6 @@ private fun AnimeAndSourceTitlesLarge(
coverDataProvider: () -> Anime, coverDataProvider: () -> Anime,
onCoverClick: () -> Unit, onCoverClick: () -> Unit,
title: String, title: String,
context: Context,
doSearch: (query: String, global: Boolean) -> Unit, doSearch: (query: String, global: Boolean) -> Unit,
author: String?, author: String?,
artist: String?, artist: String?,
@ -356,104 +354,16 @@ private fun AnimeAndSourceTitlesLarge(
onClick = onCoverClick, onClick = onCoverClick,
) )
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Text( AnimeContentInfo(
text = title.ifBlank { stringResource(R.string.unknown_title) }, title = title,
style = MaterialTheme.typography.titleLarge, doSearch = doSearch,
modifier = Modifier.clickableNoIndication( author = author,
onLongClick = { if (title.isNotBlank()) context.copyToClipboard(title, title) }, artist = artist,
onClick = { if (title.isNotBlank()) doSearch(title, true) }, status = status,
), sourceName = sourceName,
isStubSource = isStubSource,
textAlign = TextAlign.Center, 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, coverDataProvider: () -> Anime,
onCoverClick: () -> Unit, onCoverClick: () -> Unit,
title: String, title: String,
context: Context,
doSearch: (query: String, global: Boolean) -> Unit, doSearch: (query: String, global: Boolean) -> Unit,
author: String?, author: String?,
artist: String?, artist: String?,
@ -489,6 +398,31 @@ private fun AnimeAndSourceTitlesSmall(
Column( Column(
verticalArrangement = Arrangement.spacedBy(2.dp), verticalArrangement = Arrangement.spacedBy(2.dp),
) { ) {
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(
text = title.ifBlank { stringResource(R.string.unknown_title) }, text = title.ifBlank { stringResource(R.string.unknown_title) },
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
@ -503,6 +437,7 @@ private fun AnimeAndSourceTitlesSmall(
}, },
onClick = { if (title.isNotBlank()) doSearch(title, true) }, onClick = { if (title.isNotBlank()) doSearch(title, true) },
), ),
textAlign = textAlign,
) )
Spacer(modifier = Modifier.height(2.dp)) Spacer(modifier = Modifier.height(2.dp))
@ -533,6 +468,7 @@ private fun AnimeAndSourceTitlesSmall(
}, },
onClick = { if (!author.isNullOrBlank()) doSearch(author, true) }, onClick = { if (!author.isNullOrBlank()) doSearch(author, true) },
), ),
textAlign = textAlign,
) )
} }
@ -555,6 +491,7 @@ private fun AnimeAndSourceTitlesSmall(
onLongClick = { context.copyToClipboard(artist, artist) }, onLongClick = { context.copyToClipboard(artist, artist) },
onClick = { doSearch(artist, true) }, onClick = { doSearch(artist, true) },
), ),
textAlign = textAlign,
) )
} }
} }
@ -586,9 +523,7 @@ private fun AnimeAndSourceTitlesSmall(
SAnime.ONGOING.toLong() -> stringResource(R.string.ongoing) SAnime.ONGOING.toLong() -> stringResource(R.string.ongoing)
SAnime.COMPLETED.toLong() -> stringResource(R.string.completed) SAnime.COMPLETED.toLong() -> stringResource(R.string.completed)
SAnime.LICENSED.toLong() -> stringResource(R.string.licensed) SAnime.LICENSED.toLong() -> stringResource(R.string.licensed)
SAnime.PUBLISHING_FINISHED.toLong() -> stringResource( SAnime.PUBLISHING_FINISHED.toLong() -> stringResource(R.string.publishing_finished)
R.string.publishing_finished,
)
SAnime.CANCELLED.toLong() -> stringResource(R.string.cancelled) SAnime.CANCELLED.toLong() -> stringResource(R.string.cancelled)
SAnime.ON_HIATUS.toLong() -> stringResource(R.string.on_hiatus) SAnime.ON_HIATUS.toLong() -> stringResource(R.string.on_hiatus)
else -> stringResource(R.string.unknown) else -> stringResource(R.string.unknown)
@ -599,7 +534,7 @@ private fun AnimeAndSourceTitlesSmall(
DotSeparatorText() DotSeparatorText()
if (isStubSource) { if (isStubSource) {
Icon( Icon(
imageVector = Icons.Outlined.Warning, imageVector = Icons.Filled.Warning,
contentDescription = null, contentDescription = null,
modifier = Modifier modifier = Modifier
.padding(end = 4.dp) .padding(end = 4.dp)
@ -621,8 +556,6 @@ private fun AnimeAndSourceTitlesSmall(
} }
} }
} }
}
}
@Composable @Composable
private fun AnimeSummary( private fun AnimeSummary(

View file

@ -5,9 +5,11 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.asPaddingValues
@ -26,7 +28,9 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text 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.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.util.fastAll import androidx.compose.ui.util.fastAll
import androidx.compose.ui.util.fastAny 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.ConfigurableSource
import eu.kanade.tachiyomi.source.manga.getNameForMangaInfo import eu.kanade.tachiyomi.source.manga.getNameForMangaInfo
import eu.kanade.tachiyomi.ui.browse.manga.extension.details.MangaSourcePreferencesScreen 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.ui.entries.manga.MangaScreenModel
import eu.kanade.tachiyomi.util.lang.toRelativeString import eu.kanade.tachiyomi.util.lang.toRelativeString
import eu.kanade.tachiyomi.util.system.copyToClipboard 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.ExtendedFloatingActionButton
import tachiyomi.presentation.core.components.material.PullRefresh import tachiyomi.presentation.core.components.material.PullRefresh
import tachiyomi.presentation.core.components.material.Scaffold 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.isScrolledToEnd
import tachiyomi.presentation.core.util.isScrollingUp import tachiyomi.presentation.core.util.isScrollingUp
import tachiyomi.presentation.core.util.secondaryItemAlpha
import java.text.DateFormat import java.text.DateFormat
import java.util.Date import java.util.Date
@ -98,7 +105,7 @@ fun MangaScreen(
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
onBackClicked: () -> Unit, onBackClicked: () -> Unit,
onChapterClicked: (Chapter) -> Unit, onChapterClicked: (Chapter) -> Unit,
onDownloadChapter: ((List<ChapterItem>, ChapterDownloadAction) -> Unit)?, onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
onAddToLibraryClicked: () -> Unit, onAddToLibraryClicked: () -> Unit,
onWebViewClicked: (() -> Unit)?, onWebViewClicked: (() -> Unit)?,
onWebViewLongClicked: (() -> Unit)?, onWebViewLongClicked: (() -> Unit)?,
@ -129,10 +136,10 @@ fun MangaScreen(
onMultiDeleteClicked: (List<Chapter>) -> Unit, onMultiDeleteClicked: (List<Chapter>) -> Unit,
// For chapter swipe // For chapter swipe
onChapterSwipe: (ChapterItem, LibraryPreferences.ChapterSwipeAction) -> Unit, onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
// Chapter selection // Chapter selection
onChapterSelected: (ChapterItem, Boolean, Boolean, Boolean) -> Unit, onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
onAllChapterSelected: (Boolean) -> Unit, onAllChapterSelected: (Boolean) -> Unit,
onInvertSelection: () -> Unit, onInvertSelection: () -> Unit,
) { ) {
@ -238,7 +245,7 @@ private fun MangaScreenSmallImpl(
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
onBackClicked: () -> Unit, onBackClicked: () -> Unit,
onChapterClicked: (Chapter) -> Unit, onChapterClicked: (Chapter) -> Unit,
onDownloadChapter: ((List<ChapterItem>, ChapterDownloadAction) -> Unit)?, onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
onAddToLibraryClicked: () -> Unit, onAddToLibraryClicked: () -> Unit,
onWebViewClicked: (() -> Unit)?, onWebViewClicked: (() -> Unit)?,
onWebViewLongClicked: (() -> Unit)?, onWebViewLongClicked: (() -> Unit)?,
@ -271,16 +278,17 @@ private fun MangaScreenSmallImpl(
onMultiDeleteClicked: (List<Chapter>) -> Unit, onMultiDeleteClicked: (List<Chapter>) -> Unit,
// For chapter swipe // For chapter swipe
onChapterSwipe: (ChapterItem, LibraryPreferences.ChapterSwipeAction) -> Unit, onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
// Chapter selection // Chapter selection
onChapterSelected: (ChapterItem, Boolean, Boolean, Boolean) -> Unit, onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
onAllChapterSelected: (Boolean) -> Unit, onAllChapterSelected: (Boolean) -> Unit,
onInvertSelection: () -> Unit, onInvertSelection: () -> Unit,
) { ) {
val chapterListState = rememberLazyListState() val chapterListState = rememberLazyListState()
val chapters = remember(state) { state.processedChapters } val chapters = remember(state) { state.processedChapters }
val listItem = remember(state) { state.chapterListItems }
val isAnySelected by remember { val isAnySelected by remember {
derivedStateOf { derivedStateOf {
@ -465,7 +473,8 @@ private fun MangaScreenSmallImpl(
sharedChapterItems( sharedChapterItems(
manga = state.manga, manga = state.manga,
chapters = chapters, chapters = listItem,
isAnyChapterSelected = chapters.fastAny { it.selected },
dateRelativeTime = dateRelativeTime, dateRelativeTime = dateRelativeTime,
dateFormat = dateFormat, dateFormat = dateFormat,
chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeStartAction = chapterSwipeStartAction,
@ -492,7 +501,7 @@ fun MangaScreenLargeImpl(
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
onBackClicked: () -> Unit, onBackClicked: () -> Unit,
onChapterClicked: (Chapter) -> Unit, onChapterClicked: (Chapter) -> Unit,
onDownloadChapter: ((List<ChapterItem>, ChapterDownloadAction) -> Unit)?, onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
onAddToLibraryClicked: () -> Unit, onAddToLibraryClicked: () -> Unit,
onWebViewClicked: (() -> Unit)?, onWebViewClicked: (() -> Unit)?,
onWebViewLongClicked: (() -> Unit)?, onWebViewLongClicked: (() -> Unit)?,
@ -525,10 +534,10 @@ fun MangaScreenLargeImpl(
onMultiDeleteClicked: (List<Chapter>) -> Unit, onMultiDeleteClicked: (List<Chapter>) -> Unit,
// For swipe actions // For swipe actions
onChapterSwipe: (ChapterItem, LibraryPreferences.ChapterSwipeAction) -> Unit, onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
// Chapter selection // Chapter selection
onChapterSelected: (ChapterItem, Boolean, Boolean, Boolean) -> Unit, onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
onAllChapterSelected: (Boolean) -> Unit, onAllChapterSelected: (Boolean) -> Unit,
onInvertSelection: () -> Unit, onInvertSelection: () -> Unit,
) { ) {
@ -536,6 +545,7 @@ fun MangaScreenLargeImpl(
val density = LocalDensity.current val density = LocalDensity.current
val chapters = remember(state) { state.processedChapters } val chapters = remember(state) { state.processedChapters }
val listItem = remember(state) { state.chapterListItems }
val isAnySelected by remember { val isAnySelected by remember {
derivedStateOf { derivedStateOf {
@ -716,7 +726,8 @@ fun MangaScreenLargeImpl(
sharedChapterItems( sharedChapterItems(
manga = state.manga, manga = state.manga,
chapters = chapters, chapters = listItem,
isAnyChapterSelected = chapters.fastAny { it.selected },
dateRelativeTime = dateRelativeTime, dateRelativeTime = dateRelativeTime,
dateFormat = dateFormat, dateFormat = dateFormat,
chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeStartAction = chapterSwipeStartAction,
@ -736,12 +747,12 @@ fun MangaScreenLargeImpl(
@Composable @Composable
private fun SharedMangaBottomActionMenu( private fun SharedMangaBottomActionMenu(
selected: List<ChapterItem>, selected: List<ChapterList.Item>,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit, onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
onMultiMarkAsReadClicked: (List<Chapter>, markAsRead: Boolean) -> Unit, onMultiMarkAsReadClicked: (List<Chapter>, markAsRead: Boolean) -> Unit,
onMarkPreviousAsReadClicked: (Chapter) -> Unit, onMarkPreviousAsReadClicked: (Chapter) -> Unit,
onDownloadChapter: ((List<ChapterItem>, ChapterDownloadAction) -> Unit)?, onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
onMultiDeleteClicked: (List<Chapter>) -> Unit, onMultiDeleteClicked: (List<Chapter>) -> Unit,
fillFraction: Float, fillFraction: Float,
) { ) {
@ -779,34 +790,63 @@ private fun SharedMangaBottomActionMenu(
private fun LazyListScope.sharedChapterItems( private fun LazyListScope.sharedChapterItems(
manga: Manga, manga: Manga,
chapters: List<ChapterItem>, chapters: List<ChapterList>,
isAnyChapterSelected: Boolean,
dateRelativeTime: Boolean, dateRelativeTime: Boolean,
dateFormat: DateFormat, dateFormat: DateFormat,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
onChapterClicked: (Chapter) -> Unit, onChapterClicked: (Chapter) -> Unit,
onDownloadChapter: ((List<ChapterItem>, ChapterDownloadAction) -> Unit)?, onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
onChapterSelected: (ChapterItem, Boolean, Boolean, Boolean) -> Unit, onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
onChapterSwipe: (ChapterItem, LibraryPreferences.ChapterSwipeAction) -> Unit, onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
) { ) {
items( items(
items = chapters, 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 }, contentType = { EntryScreenItem.ITEM },
) { chapterItem -> ) { item ->
val haptic = LocalHapticFeedback.current val haptic = LocalHapticFeedback.current
val context = LocalContext.current val context = LocalContext.current
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(),
)
HorizontalDivider(modifier = Modifier.weight(1f))
}
}
is ChapterList.Item -> {
MangaChapterListItem( MangaChapterListItem(
title = if (manga.displayMode == Manga.CHAPTER_DISPLAY_NUMBER) { title = if (manga.displayMode == Manga.CHAPTER_DISPLAY_NUMBER) {
stringResource( stringResource(
R.string.display_mode_chapter, R.string.display_mode_chapter,
formatChapterNumber(chapterItem.chapter.chapterNumber), formatChapterNumber(item.chapter.chapterNumber),
) )
} else { } else {
chapterItem.chapter.name item.chapter.name
}, },
date = chapterItem.chapter.dateUpload date = item.chapter.dateUpload
.takeIf { it > 0L } .takeIf { it > 0L }
?.let { ?.let {
Date(it).toRelativeString( Date(it).toRelativeString(
@ -815,63 +855,58 @@ private fun LazyListScope.sharedChapterItems(
dateFormat, dateFormat,
) )
}, },
readProgress = chapterItem.chapter.lastPageRead readProgress = item.chapter.lastPageRead
.takeIf { !chapterItem.chapter.read && it > 0L } .takeIf { !item.chapter.read && it > 0L }
?.let { ?.let {
stringResource( stringResource(
R.string.chapter_progress, R.string.chapter_progress,
it + 1, it + 1,
) )
}, },
scanlator = chapterItem.chapter.scanlator.takeIf { !it.isNullOrBlank() }, scanlator = item.chapter.scanlator.takeIf { !it.isNullOrBlank() },
read = chapterItem.chapter.read, read = item.chapter.read,
bookmark = chapterItem.chapter.bookmark, bookmark = item.chapter.bookmark,
selected = chapterItem.selected, selected = item.selected,
downloadIndicatorEnabled = chapters.fastAll { !it.selected }, downloadIndicatorEnabled = !isAnyChapterSelected,
downloadStateProvider = { chapterItem.downloadState }, downloadStateProvider = { item.downloadState },
downloadProgressProvider = { chapterItem.downloadProgress }, downloadProgressProvider = { item.downloadProgress },
chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeStartAction = chapterSwipeStartAction,
chapterSwipeEndAction = chapterSwipeEndAction, chapterSwipeEndAction = chapterSwipeEndAction,
onLongClick = { onLongClick = {
onChapterSelected(chapterItem, !chapterItem.selected, true, true) onChapterSelected(item, !item.selected, true, true)
haptic.performHapticFeedback(HapticFeedbackType.LongPress) haptic.performHapticFeedback(HapticFeedbackType.LongPress)
}, },
onClick = { onClick = {
onChapterItemClick( onChapterItemClick(
chapterItem = chapterItem, chapterItem = item,
chapters = chapters, isAnyChapterSelected = isAnyChapterSelected,
onToggleSelection = { onToggleSelection = { onChapterSelected(item, !item.selected, true, false) },
onChapterSelected(
chapterItem,
!chapterItem.selected,
true,
false,
)
},
onChapterClicked = onChapterClicked, onChapterClicked = onChapterClicked,
) )
}, },
onDownloadClick = if (onDownloadChapter != null) { onDownloadClick = if (onDownloadChapter != null) {
{ onDownloadChapter(listOf(chapterItem), it) } { onDownloadChapter(listOf(item), it) }
} else { } else {
null null
}, },
onChapterSwipe = { onChapterSwipe = {
onChapterSwipe(chapterItem, it) onChapterSwipe(item, it)
}, },
) )
} }
} }
}
}
private fun onChapterItemClick( private fun onChapterItemClick(
chapterItem: ChapterItem, chapterItem: ChapterList.Item,
chapters: List<ChapterItem>, isAnyChapterSelected: Boolean,
onToggleSelection: (Boolean) -> Unit, onToggleSelection: (Boolean) -> Unit,
onChapterClicked: (Chapter) -> Unit, onChapterClicked: (Chapter) -> Unit,
) { ) {
when { when {
chapterItem.selected -> onToggleSelection(false) chapterItem.selected -> onToggleSelection(false)
chapters.fastAny { it.selected } -> onToggleSelection(true) isAnyChapterSelected -> onToggleSelection(true)
else -> onChapterClicked(chapterItem.chapter) else -> onChapterClicked(chapterItem.chapter)
} }
} }

View file

@ -1,6 +1,5 @@
package eu.kanade.presentation.entries.manga.components package eu.kanade.presentation.entries.manga.components
import android.content.Context
import androidx.compose.animation.animateContentSize import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.graphics.res.animatedVectorResource 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.DropdownMenuItem
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.SuggestionChip import androidx.compose.material3.SuggestionChip
@ -136,7 +136,6 @@ fun MangaInfoBox(
coverDataProvider = coverDataProvider, coverDataProvider = coverDataProvider,
onCoverClick = onCoverClick, onCoverClick = onCoverClick,
title = title, title = title,
context = LocalContext.current,
doSearch = doSearch, doSearch = doSearch,
author = author, author = author,
artist = artist, artist = artist,
@ -150,7 +149,6 @@ fun MangaInfoBox(
coverDataProvider = coverDataProvider, coverDataProvider = coverDataProvider,
onCoverClick = onCoverClick, onCoverClick = onCoverClick,
title = title, title = title,
context = LocalContext.current,
doSearch = doSearch, doSearch = doSearch,
author = author, author = author,
artist = artist, artist = artist,
@ -335,7 +333,6 @@ private fun MangaAndSourceTitlesLarge(
coverDataProvider: () -> Manga, coverDataProvider: () -> Manga,
onCoverClick: () -> Unit, onCoverClick: () -> Unit,
title: String, title: String,
context: Context,
doSearch: (query: String, global: Boolean) -> Unit, doSearch: (query: String, global: Boolean) -> Unit,
author: String?, author: String?,
artist: String?, artist: String?,
@ -356,104 +353,16 @@ private fun MangaAndSourceTitlesLarge(
onClick = onCoverClick, onClick = onCoverClick,
) )
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Text( MangaContentInfo(
text = title.ifBlank { stringResource(R.string.unknown_title) }, title = title,
style = MaterialTheme.typography.titleLarge, doSearch = doSearch,
modifier = Modifier.clickableNoIndication( author = author,
onLongClick = { if (title.isNotBlank()) context.copyToClipboard(title, title) }, artist = artist,
onClick = { if (title.isNotBlank()) doSearch(title, true) }, status = status,
), sourceName = sourceName,
isStubSource = isStubSource,
textAlign = TextAlign.Center, 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, coverDataProvider: () -> Manga,
onCoverClick: () -> Unit, onCoverClick: () -> Unit,
title: String, title: String,
context: Context,
doSearch: (query: String, global: Boolean) -> Unit, doSearch: (query: String, global: Boolean) -> Unit,
author: String?, author: String?,
artist: String?, artist: String?,
@ -489,6 +397,31 @@ private fun MangaAndSourceTitlesSmall(
Column( Column(
verticalArrangement = Arrangement.spacedBy(2.dp), verticalArrangement = Arrangement.spacedBy(2.dp),
) { ) {
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(
text = title.ifBlank { stringResource(R.string.unknown_title) }, text = title.ifBlank { stringResource(R.string.unknown_title) },
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
@ -503,9 +436,11 @@ private fun MangaAndSourceTitlesSmall(
}, },
onClick = { if (title.isNotBlank()) doSearch(title, true) }, onClick = { if (title.isNotBlank()) doSearch(title, true) },
), ),
textAlign = textAlign,
) )
Spacer(modifier = Modifier.height(2.dp)) Spacer(modifier = Modifier.height(2.dp))
Row( Row(
modifier = Modifier.secondaryItemAlpha(), modifier = Modifier.secondaryItemAlpha(),
horizontalArrangement = Arrangement.spacedBy(4.dp), horizontalArrangement = Arrangement.spacedBy(4.dp),
@ -532,6 +467,7 @@ private fun MangaAndSourceTitlesSmall(
}, },
onClick = { if (!author.isNullOrBlank()) doSearch(author, true) }, onClick = { if (!author.isNullOrBlank()) doSearch(author, true) },
), ),
textAlign = textAlign,
) )
} }
@ -554,6 +490,7 @@ private fun MangaAndSourceTitlesSmall(
onLongClick = { context.copyToClipboard(artist, artist) }, onLongClick = { context.copyToClipboard(artist, artist) },
onClick = { doSearch(artist, true) }, onClick = { doSearch(artist, true) },
), ),
textAlign = textAlign,
) )
} }
} }
@ -585,9 +522,7 @@ private fun MangaAndSourceTitlesSmall(
SManga.ONGOING.toLong() -> stringResource(R.string.ongoing) SManga.ONGOING.toLong() -> stringResource(R.string.ongoing)
SManga.COMPLETED.toLong() -> stringResource(R.string.completed) SManga.COMPLETED.toLong() -> stringResource(R.string.completed)
SManga.LICENSED.toLong() -> stringResource(R.string.licensed) SManga.LICENSED.toLong() -> stringResource(R.string.licensed)
SManga.PUBLISHING_FINISHED.toLong() -> stringResource( SManga.PUBLISHING_FINISHED.toLong() -> stringResource(R.string.publishing_finished)
R.string.publishing_finished,
)
SManga.CANCELLED.toLong() -> stringResource(R.string.cancelled) SManga.CANCELLED.toLong() -> stringResource(R.string.cancelled)
SManga.ON_HIATUS.toLong() -> stringResource(R.string.on_hiatus) SManga.ON_HIATUS.toLong() -> stringResource(R.string.on_hiatus)
else -> stringResource(R.string.unknown) else -> stringResource(R.string.unknown)
@ -620,8 +555,6 @@ private fun MangaAndSourceTitlesSmall(
} }
} }
} }
}
}
@Composable @Composable
private fun MangaSummary( private fun MangaSummary(

View file

@ -4,12 +4,15 @@ import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Environment
import android.text.format.Formatter
import android.widget.Toast import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
@ -32,6 +35,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.more.settings.Preference 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.presentation.permissions.PermissionRequestHelper
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.BackupConst 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.backup.models.Backup
import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.cache.EpisodeCache 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.DeviceUtil
import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.copyToClipboard
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
@ -403,19 +409,22 @@ object SettingsDataScreen : SearchableSettings {
val chapterCache = remember { Injekt.get<ChapterCache>() } val chapterCache = remember { Injekt.get<ChapterCache>() }
val episodeCache = remember { Injekt.get<EpisodeCache>() } val episodeCache = remember { Injekt.get<EpisodeCache>() }
var readableSizeSema by remember { mutableIntStateOf(0) } var cacheReadableSizeSema by remember { mutableIntStateOf(0) }
val readableSize = remember(readableSizeSema) { chapterCache.readableSize } val cacheReadableMangaSize = remember(cacheReadableSizeSema) { chapterCache.readableSize }
val readableAnimeSize = remember(readableSizeSema) { episodeCache.readableSize } val cacheReadableAnimeSize = remember(cacheReadableSizeSema) { episodeCache.readableSize }
return Preference.PreferenceGroup( return Preference.PreferenceGroup(
title = stringResource(R.string.label_data), title = stringResource(R.string.label_data),
preferenceItems = listOf( preferenceItems = listOf(
getMangaStorageInfoPref(cacheReadableMangaSize),
getAnimeStorageInfoPref(cacheReadableAnimeSize),
Preference.PreferenceItem.TextPreference( Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_clear_chapter_cache), title = stringResource(R.string.pref_clear_chapter_cache),
subtitle = stringResource( subtitle = stringResource(
R.string.used_cache_both, R.string.used_cache_both,
readableAnimeSize, cacheReadableAnimeSize,
readableSize, cacheReadableMangaSize,
), ),
onClick = { onClick = {
scope.launchNonCancellable { scope.launchNonCancellable {
@ -423,7 +432,7 @@ object SettingsDataScreen : SearchableSettings {
val deletedFiles = chapterCache.clear() + episodeCache.clear() val deletedFiles = chapterCache.clear() + episodeCache.clear()
withUIContext { withUIContext {
context.toast(context.getString(R.string.cache_deleted, deletedFiles)) context.toast(context.getString(R.string.cache_deleted, deletedFiles))
readableSizeSema++ cacheReadableSizeSema++
} }
} catch (e: Throwable) { } catch (e: Throwable) {
logcat(LogPriority.ERROR, e) 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( private data class MissingRestoreComponents(

View file

@ -12,8 +12,9 @@ import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.text.InlineTextContent import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.foundation.text.appendInlineContent import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.material.icons.Icons 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.Info
import androidx.compose.material.icons.outlined.OfflinePin
import androidx.compose.material.icons.outlined.Warning import androidx.compose.material.icons.outlined.Warning
import androidx.compose.material3.CardColors import androidx.compose.material3.CardColors
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
@ -256,7 +257,7 @@ private fun ChapterText(
), ),
) { ) {
Icon( Icon(
imageVector = Icons.Outlined.OfflinePin, imageVector = Icons.Filled.CheckCircle,
contentDescription = stringResource(R.string.label_downloaded), contentDescription = stringResource(R.string.label_downloaded),
) )
}, },

View file

@ -247,7 +247,7 @@ fun SearchResultItem(
) { ) {
if (selected) { if (selected) {
Icon( Icon(
imageVector = Icons.Default.CheckCircle, imageVector = Icons.Filled.CheckCircle,
contentDescription = null, contentDescription = null,
modifier = Modifier.align(Alignment.TopEnd), modifier = Modifier.align(Alignment.TopEnd),
tint = MaterialTheme.colorScheme.primary, tint = MaterialTheme.colorScheme.primary,

View file

@ -97,9 +97,8 @@ fun AnimeUpdateScreen(
FastScrollLazyColumn( FastScrollLazyColumn(
contentPadding = contentPadding, contentPadding = contentPadding,
) { ) {
if (lastUpdated > 0L) {
animeUpdatesLastUpdatedItem(lastUpdated) animeUpdatesLastUpdatedItem(lastUpdated)
}
animeUpdatesUiItems( animeUpdatesUiItems(
uiModels = state.getUiModel(context, relativeTime), uiModels = state.getUiModel(context, relativeTime),
selectionMode = state.selectionMode, selectionMode = state.selectionMode,

View file

@ -1,6 +1,5 @@
package eu.kanade.presentation.updates.anime package eu.kanade.presentation.updates.anime
import android.text.format.DateUtils
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box 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.ItemCover
import eu.kanade.presentation.entries.anime.components.EpisodeDownloadAction import eu.kanade.presentation.entries.anime.components.EpisodeDownloadAction
import eu.kanade.presentation.entries.anime.components.EpisodeDownloadIndicator import eu.kanade.presentation.entries.anime.components.EpisodeDownloadIndicator
import eu.kanade.presentation.util.relativeTimeSpanString
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.anime.model.AnimeDownload import eu.kanade.tachiyomi.data.download.anime.model.AnimeDownload
import eu.kanade.tachiyomi.ui.updates.anime.AnimeUpdatesItem 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.ReadItemAlpha
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.util.selectedBackground import tachiyomi.presentation.core.util.selectedBackground
import java.util.Date
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.time.Duration.Companion.minutes
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
fun LazyListScope.animeUpdatesLastUpdatedItem( fun LazyListScope.animeUpdatesLastUpdatedItem(
lastUpdated: Long, lastUpdated: Long,
) { ) {
item(key = "animeUpdates-lastUpdated") { 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( Box(
modifier = Modifier modifier = Modifier
.animateItemPlacement() .animateItemPlacement()
@ -75,14 +64,7 @@ fun LazyListScope.animeUpdatesLastUpdatedItem(
), ),
) { ) {
Text( Text(
text = if (time.isNullOrEmpty()) { text = stringResource(R.string.updates_last_update_info, relativeTimeSpanString(lastUpdated)),
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)
},
fontStyle = FontStyle.Italic, fontStyle = FontStyle.Italic,
) )
} }

View file

@ -93,9 +93,7 @@ fun MangaUpdateScreen(
FastScrollLazyColumn( FastScrollLazyColumn(
contentPadding = contentPadding, contentPadding = contentPadding,
) { ) {
if (lastUpdated > 0L) {
mangaUpdatesLastUpdatedItem(lastUpdated) mangaUpdatesLastUpdatedItem(lastUpdated)
}
mangaUpdatesUiItems( mangaUpdatesUiItems(
uiModels = state.getUiModel(context, relativeTime), uiModels = state.getUiModel(context, relativeTime),

View file

@ -1,6 +1,5 @@
package eu.kanade.presentation.updates.manga package eu.kanade.presentation.updates.manga
import android.text.format.DateUtils
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box 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.ItemCover
import eu.kanade.presentation.entries.manga.components.ChapterDownloadAction import eu.kanade.presentation.entries.manga.components.ChapterDownloadAction
import eu.kanade.presentation.entries.manga.components.ChapterDownloadIndicator import eu.kanade.presentation.entries.manga.components.ChapterDownloadIndicator
import eu.kanade.presentation.util.relativeTimeSpanString
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.manga.model.MangaDownload import eu.kanade.tachiyomi.data.download.manga.model.MangaDownload
import eu.kanade.tachiyomi.ui.updates.manga.MangaUpdatesItem 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.ReadItemAlpha
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.util.selectedBackground import tachiyomi.presentation.core.util.selectedBackground
import java.util.Date
import kotlin.time.Duration.Companion.minutes
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
fun LazyListScope.mangaUpdatesLastUpdatedItem( fun LazyListScope.mangaUpdatesLastUpdatedItem(
lastUpdated: Long, lastUpdated: Long,
) { ) {
item(key = "mangaUpdates-lastUpdated") { 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( Box(
modifier = Modifier modifier = Modifier
.animateItemPlacement() .animateItemPlacement()
@ -74,14 +63,7 @@ fun LazyListScope.mangaUpdatesLastUpdatedItem(
), ),
) { ) {
Text( Text(
text = if (time.isNullOrEmpty()) { text = stringResource(R.string.updates_last_update_info, relativeTimeSpanString(lastUpdated)),
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)
},
fontStyle = FontStyle.Italic, fontStyle = FontStyle.Italic,
) )
} }

View file

@ -1,8 +1,14 @@
package eu.kanade.presentation.util package eu.kanade.presentation.util
import android.content.Context 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 eu.kanade.tachiyomi.R
import java.util.Date
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.minutes
fun Duration.toDurationString(context: Context, fallback: String): String { fun Duration.toDurationString(context: Context, fallback: String): String {
return toComponents { days, hours, minutes, seconds, _ -> return toComponents { days, hours, minutes, seconds, _ ->
@ -22,3 +28,14 @@ fun Duration.toDurationString(context: Context, fallback: String): String {
}.joinToString(" ").ifBlank { fallback } }.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()
}
}

View file

@ -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.DeviceUtil
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.system.workManager import eu.kanade.tachiyomi.util.system.workManager
import tachiyomi.core.preference.Preference
import tachiyomi.core.preference.PreferenceStore import tachiyomi.core.preference.PreferenceStore
import tachiyomi.core.preference.TriState import tachiyomi.core.preference.TriState
import tachiyomi.core.preference.getAndSet import tachiyomi.core.preference.getAndSet
@ -503,6 +504,30 @@ object Migrations {
uiPreferences.relativeTime().set(false) 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 return true
} }
} }
@ -510,3 +535,41 @@ object Migrations {
return false return false
} }
} }
@Suppress("UNCHECKED_CAST")
private fun replacePreferences(
preferenceStore: PreferenceStore,
filterPredicate: (Map.Entry<String, Any?>) -> 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<String>)?.let {
preferenceStore.getStringSet(newKey(key)).set(value)
preferenceStore.getStringSet(key).delete()
}
}
}
}

View file

@ -41,6 +41,7 @@ import logcat.LogPriority
import okio.buffer import okio.buffer
import okio.gzip import okio.gzip
import okio.sink import okio.sink
import tachiyomi.core.preference.Preference
import tachiyomi.core.util.system.logcat import tachiyomi.core.util.system.logcat
import tachiyomi.data.handlers.anime.AnimeDatabaseHandler import tachiyomi.data.handlers.anime.AnimeDatabaseHandler
import tachiyomi.data.handlers.manga.MangaDatabaseHandler import tachiyomi.data.handlers.manga.MangaDatabaseHandler
@ -444,6 +445,6 @@ class BackupCreator(
} }
backupPreferences.add(toAdd) backupPreferences.add(toAdd)
} }
return backupPreferences return backupPreferences.filter { !Preference.isPrivate(it.key) && !Preference.isAppState(it.key) }
} }
} }

View file

@ -11,8 +11,6 @@ import dataanime.Anime_sync
import dataanime.Animes import dataanime.Animes
import eu.kanade.domain.entries.anime.interactor.UpdateAnime import eu.kanade.domain.entries.anime.interactor.UpdateAnime
import eu.kanade.domain.entries.manga.interactor.UpdateManga 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.R
import eu.kanade.tachiyomi.data.backup.models.BackupAnime import eu.kanade.tachiyomi.data.backup.models.BackupAnime
import eu.kanade.tachiyomi.data.backup.models.BackupAnimeHistory 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.entries.manga.model.Manga
import tachiyomi.domain.history.anime.model.AnimeHistoryUpdate import tachiyomi.domain.history.anime.model.AnimeHistoryUpdate
import tachiyomi.domain.history.manga.model.MangaHistoryUpdate 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.chapter.model.Chapter
import tachiyomi.domain.items.episode.interactor.GetEpisodesByAnimeId
import tachiyomi.domain.items.episode.model.Episode import tachiyomi.domain.items.episode.model.Episode
import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.track.anime.model.AnimeTrack import tachiyomi.domain.track.anime.model.AnimeTrack
@ -73,11 +73,13 @@ class BackupRestorer(
private val mangaHandler: MangaDatabaseHandler = Injekt.get() private val mangaHandler: MangaDatabaseHandler = Injekt.get()
private val updateManga: UpdateManga = Injekt.get() private val updateManga: UpdateManga = Injekt.get()
private val getMangaCategories: GetMangaCategories = Injekt.get() private val getMangaCategories: GetMangaCategories = Injekt.get()
private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get()
private val mangaFetchInterval: MangaFetchInterval = Injekt.get() private val mangaFetchInterval: MangaFetchInterval = Injekt.get()
private val animeHandler: AnimeDatabaseHandler = Injekt.get() private val animeHandler: AnimeDatabaseHandler = Injekt.get()
private val updateAnime: UpdateAnime = Injekt.get() private val updateAnime: UpdateAnime = Injekt.get()
private val getAnimeCategories: GetAnimeCategories = Injekt.get() private val getAnimeCategories: GetAnimeCategories = Injekt.get()
private val getEpisodesByAnimeId: GetEpisodesByAnimeId = Injekt.get()
private val animeFetchInterval: AnimeFetchInterval = Injekt.get() private val animeFetchInterval: AnimeFetchInterval = Injekt.get()
private val libraryPreferences: LibraryPreferences = Injekt.get() private val libraryPreferences: LibraryPreferences = Injekt.get()
@ -423,33 +425,38 @@ class BackupRestorer(
manga: Manga, manga: Manga,
chapters: List<Chapter>, chapters: List<Chapter>,
) { ) {
val dbChapters = mangaHandler.awaitList { chaptersQueries.getChaptersByMangaId(manga.id!!) } val dbChaptersByUrl = getChaptersByMangaId.await(manga.id)
.associateBy { it.url }
val processed = chapters.map { chapter -> val processed = chapters.map { chapter ->
var updatedChapter = chapter var updatedChapter = chapter
val dbChapter = dbChapters.find { it.url == updatedChapter.url }
val dbChapter = dbChaptersByUrl[updatedChapter.url]
if (dbChapter != null) { if (dbChapter != null) {
updatedChapter = updatedChapter.copy(id = dbChapter._id) updatedChapter = updatedChapter
updatedChapter = updatedChapter.copyFrom(dbChapter) .copyFrom(dbChapter)
.copy(
id = dbChapter.id,
bookmark = updatedChapter.bookmark || dbChapter.bookmark,
)
if (dbChapter.read && !updatedChapter.read) { if (dbChapter.read && !updatedChapter.read) {
updatedChapter = updatedChapter.copy( updatedChapter = updatedChapter.copy(
read = true, 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 } val (existingChapters, newChapters) = processed.partition { it.id > 0 }
newChapters[true]?.let { updateKnownChapters(it) } updateKnownChapters(existingChapters)
newChapters[false]?.let { insertChapters(it) } insertChapters(newChapters)
} }
/** /**
@ -870,43 +877,46 @@ class BackupRestorer(
private suspend fun restoreEpisodes( private suspend fun restoreEpisodes(
anime: Anime, anime: Anime,
episodes: List<tachiyomi.domain.items.episode.model.Episode>, episodes: List<Episode>,
) { ) {
val dbEpisodes = animeHandler.awaitList { episodesQueries.getEpisodesByAnimeId(anime.id!!) } val dbEpisodesByUrl = getEpisodesByAnimeId.await(anime.id)
.associateBy { it.url }
val processed = episodes.map { episode -> val processed = episodes.map { episode ->
var updatedEpisode = episode var updatedEpisode = episode
val dbEpisode = dbEpisodes.find { it.url == updatedEpisode.url }
val dbEpisode = dbEpisodesByUrl[updatedEpisode.url]
if (dbEpisode != null) { if (dbEpisode != null) {
updatedEpisode = updatedEpisode.copy(id = dbEpisode._id) updatedEpisode = updatedEpisode
updatedEpisode = updatedEpisode.copyFrom(dbEpisode) .copyFrom(dbEpisode)
.copy(
id = dbEpisode.id,
bookmark = updatedEpisode.bookmark || dbEpisode.bookmark,
)
if (dbEpisode.seen && !updatedEpisode.seen) { if (dbEpisode.seen && !updatedEpisode.seen) {
updatedEpisode = updatedEpisode.copy( updatedEpisode = updatedEpisode.copy(
seen = true, 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( 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 } val (existingEpisodes, newEpisodes) = processed.partition { it.id > 0 }
newEpisodes[true]?.let { updateKnownEpisodes(it) } updateKnownEpisodes(existingEpisodes)
newEpisodes[false]?.let { insertEpisodes(it) } insertEpisodes(newEpisodes)
} }
/** /**
* Inserts list of episodes * Inserts list of episodes
*/ */
private suspend fun insertEpisodes(episodes: List<tachiyomi.domain.items.episode.model.Episode>) { private suspend fun insertEpisodes(episodes: List<Episode>) {
animeHandler.await(true) { animeHandler.await(true) {
episodes.forEach { episode -> episodes.forEach { episode ->
episodesQueries.insert( episodesQueries.insert(
@ -931,7 +941,7 @@ class BackupRestorer(
* Updates a list of episodes with known database ids * Updates a list of episodes with known database ids
*/ */
private suspend fun updateKnownEpisodes( private suspend fun updateKnownEpisodes(
episodes: List<tachiyomi.domain.items.episode.model.Episode>, episodes: List<Episode>,
) { ) {
animeHandler.await(true) { animeHandler.await(true) {
episodes.forEach { episode -> episodes.forEach { episode ->

View file

@ -13,7 +13,7 @@ import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.lang.withUIContext import tachiyomi.core.util.lang.withUIContext
import tachiyomi.core.util.system.logcat import tachiyomi.core.util.system.logcat
import tachiyomi.domain.history.anime.interactor.GetAnimeHistory 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 tachiyomi.domain.track.anime.interactor.InsertAnimeTrack
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -62,7 +62,7 @@ interface AnimeTracker {
item.anime_id = animeId item.anime_id = animeId
try { try {
withIOContext { withIOContext {
val allEpisodes = Injekt.get<GetEpisodeByAnimeId>().await(animeId) val allEpisodes = Injekt.get<GetEpisodesByAnimeId>().await(animeId)
val hasSeenEpisodes = allEpisodes.any { it.seen } val hasSeenEpisodes = allEpisodes.any { it.seen }
bind(item, hasSeenEpisodes) bind(item, hasSeenEpisodes)

View file

@ -13,7 +13,7 @@ import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.lang.withUIContext import tachiyomi.core.util.lang.withUIContext
import tachiyomi.core.util.system.logcat import tachiyomi.core.util.system.logcat
import tachiyomi.domain.history.manga.interactor.GetMangaHistory 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 tachiyomi.domain.track.manga.interactor.InsertMangaTrack
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -62,7 +62,7 @@ interface MangaTracker {
item.manga_id = mangaId item.manga_id = mangaId
try { try {
withIOContext { withIOContext {
val allChapters = Injekt.get<GetChapterByMangaId>().await(mangaId) val allChapters = Injekt.get<GetChaptersByMangaId>().await(mangaId)
val hasReadChapters = allChapters.any { it.read } val hasReadChapters = allChapters.any { it.read }
bind(item, hasReadChapters) bind(item, hasReadChapters)

View file

@ -43,7 +43,7 @@ import tachiyomi.domain.category.anime.interactor.GetAnimeCategories
import tachiyomi.domain.category.anime.interactor.SetAnimeCategories import tachiyomi.domain.category.anime.interactor.SetAnimeCategories
import tachiyomi.domain.entries.anime.model.Anime import tachiyomi.domain.entries.anime.model.Anime
import tachiyomi.domain.entries.anime.model.AnimeUpdate 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.interactor.UpdateEpisode
import tachiyomi.domain.items.episode.model.toEpisodeUpdate import tachiyomi.domain.items.episode.model.toEpisodeUpdate
import tachiyomi.domain.source.anime.service.AnimeSourceManager import tachiyomi.domain.source.anime.service.AnimeSourceManager
@ -150,7 +150,7 @@ internal class MigrateAnimeDialogScreenModel(
private val sourceManager: AnimeSourceManager = Injekt.get(), private val sourceManager: AnimeSourceManager = Injekt.get(),
private val downloadManager: AnimeDownloadManager = Injekt.get(), private val downloadManager: AnimeDownloadManager = Injekt.get(),
private val updateAnime: UpdateAnime = 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 syncEpisodesWithSource: SyncEpisodesWithSource = Injekt.get(),
private val updateEpisode: UpdateEpisode = Injekt.get(), private val updateEpisode: UpdateEpisode = Injekt.get(),
private val getCategories: GetAnimeCategories = Injekt.get(), private val getCategories: GetAnimeCategories = Injekt.get(),
@ -222,8 +222,8 @@ internal class MigrateAnimeDialogScreenModel(
// Update chapters read, bookmark and dateFetch // Update chapters read, bookmark and dateFetch
if (migrateEpisodes) { if (migrateEpisodes) {
val prevAnimeEpisodes = getEpisodeByAnimeId.await(oldAnime.id) val prevAnimeEpisodes = getEpisodesByAnimeId.await(oldAnime.id)
val animeEpisodes = getEpisodeByAnimeId.await(newAnime.id) val animeEpisodes = getEpisodesByAnimeId.await(newAnime.id)
val maxEpisodeSeen = prevAnimeEpisodes val maxEpisodeSeen = prevAnimeEpisodes
.filter { it.seen } .filter { it.seen }

View file

@ -43,7 +43,7 @@ import tachiyomi.domain.category.manga.interactor.GetMangaCategories
import tachiyomi.domain.category.manga.interactor.SetMangaCategories import tachiyomi.domain.category.manga.interactor.SetMangaCategories
import tachiyomi.domain.entries.manga.model.Manga import tachiyomi.domain.entries.manga.model.Manga
import tachiyomi.domain.entries.manga.model.MangaUpdate 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.interactor.UpdateChapter
import tachiyomi.domain.items.chapter.model.toChapterUpdate import tachiyomi.domain.items.chapter.model.toChapterUpdate
import tachiyomi.domain.source.manga.service.MangaSourceManager import tachiyomi.domain.source.manga.service.MangaSourceManager
@ -150,7 +150,7 @@ internal class MigrateMangaDialogScreenModel(
private val sourceManager: MangaSourceManager = Injekt.get(), private val sourceManager: MangaSourceManager = Injekt.get(),
private val downloadManager: MangaDownloadManager = Injekt.get(), private val downloadManager: MangaDownloadManager = Injekt.get(),
private val updateManga: UpdateManga = 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 syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(),
private val updateChapter: UpdateChapter = Injekt.get(), private val updateChapter: UpdateChapter = Injekt.get(),
private val getCategories: GetMangaCategories = Injekt.get(), private val getCategories: GetMangaCategories = Injekt.get(),
@ -222,8 +222,8 @@ internal class MigrateMangaDialogScreenModel(
// Update chapters read, bookmark and dateFetch // Update chapters read, bookmark and dateFetch
if (migrateChapters) { if (migrateChapters) {
val prevMangaChapters = getChapterByMangaId.await(oldManga.id) val prevMangaChapters = getChaptersByMangaId.await(oldManga.id)
val mangaChapters = getChapterByMangaId.await(newManga.id) val mangaChapters = getChaptersByMangaId.await(newManga.id)
val maxChapterRead = prevMangaChapters val maxChapterRead = prevMangaChapters
.filter { it.read } .filter { it.read }

View file

@ -10,6 +10,7 @@ import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.core.preference.asState import eu.kanade.core.preference.asState
import eu.kanade.core.util.addOrRemove 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.SetAnimeViewerFlags
import eu.kanade.domain.entries.anime.interactor.UpdateAnime import eu.kanade.domain.entries.anime.interactor.UpdateAnime
import eu.kanade.domain.entries.anime.model.downloadedFilter 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.EpisodeUpdate
import tachiyomi.domain.items.episode.model.NoEpisodesException import tachiyomi.domain.items.episode.model.NoEpisodesException
import tachiyomi.domain.items.episode.service.getEpisodeSort import tachiyomi.domain.items.episode.service.getEpisodeSort
import tachiyomi.domain.items.service.calculateEpisodeGap
import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.source.anime.service.AnimeSourceManager import tachiyomi.domain.source.anime.service.AnimeSourceManager
import tachiyomi.domain.track.anime.interactor.GetAnimeTracks 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.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.Calendar import java.util.Calendar
import kotlin.math.floor
class AnimeScreenModel( class AnimeScreenModel(
val context: Context, val context: Context,
@ -125,7 +128,7 @@ class AnimeScreenModel(
private val isFavorited: Boolean private val isFavorited: Boolean
get() = anime?.favorite ?: false get() = anime?.favorite ?: false
private val processedEpisodes: List<EpisodeItem>? private val processedEpisodes: List<EpisodeList.Item>?
get() = successState?.processedEpisodes get() = successState?.processedEpisodes
val episodeSwipeStartAction = libraryPreferences.swipeEpisodeEndAction().get() val episodeSwipeStartAction = libraryPreferences.swipeEpisodeEndAction().get()
@ -171,7 +174,7 @@ class AnimeScreenModel(
updateSuccessState { updateSuccessState {
it.copy( it.copy(
anime = anime, anime = anime,
episodes = episodes.toEpisodeItems(anime), episodes = episodes.toEpisodeListItems(anime),
) )
} }
} }
@ -182,7 +185,7 @@ class AnimeScreenModel(
screenModelScope.launchIO { screenModelScope.launchIO {
val anime = getAnimeAndEpisodes.awaitAnime(animeId) val anime = getAnimeAndEpisodes.awaitAnime(animeId)
val episodes = getAnimeAndEpisodes.awaitEpisodes(animeId) val episodes = getAnimeAndEpisodes.awaitEpisodes(animeId)
.toEpisodeItems(anime) .toEpisodeListItems(anime)
if (!anime.favorite) { if (!anime.favorite) {
setAnimeDefaultEpisodeFlags.await(anime) setAnimeDefaultEpisodeFlags.await(anime)
@ -477,7 +480,7 @@ class AnimeScreenModel(
private fun updateDownloadState(download: AnimeDownload) { private fun updateDownloadState(download: AnimeDownload) {
updateSuccessState { successState -> 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 if (modifiedIndex < 0) return@updateSuccessState successState
val newEpisodes = successState.episodes.toMutableList().apply { val newEpisodes = successState.episodes.toMutableList().apply {
@ -489,7 +492,7 @@ class AnimeScreenModel(
} }
} }
private fun List<Episode>.toEpisodeItems(anime: Anime): List<EpisodeItem> { private fun List<Episode>.toEpisodeListItems(anime: Anime): List<EpisodeList.Item> {
val isLocal = anime.isLocal() val isLocal = anime.isLocal()
return map { episode -> return map { episode ->
val activeDownload = if (isLocal) { val activeDownload = if (isLocal) {
@ -513,7 +516,7 @@ class AnimeScreenModel(
else -> AnimeDownload.State.NOT_DOWNLOADED else -> AnimeDownload.State.NOT_DOWNLOADED
} }
EpisodeItem( EpisodeList.Item(
episode = episode, episode = episode,
downloadState = downloadState, downloadState = downloadState,
downloadProgress = activeDownload?.progress ?: 0, downloadProgress = activeDownload?.progress ?: 0,
@ -561,7 +564,7 @@ class AnimeScreenModel(
/** /**
* @throws IllegalStateException if the swipe action is [LibraryPreferences.EpisodeSwipeAction.Disabled] * @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 { screenModelScope.launch {
executeEpisodeSwipeAction(episodeItem, swipeAction) executeEpisodeSwipeAction(episodeItem, swipeAction)
} }
@ -571,7 +574,7 @@ class AnimeScreenModel(
* @throws IllegalStateException if the swipe action is [LibraryPreferences.EpisodeSwipeAction.Disabled] * @throws IllegalStateException if the swipe action is [LibraryPreferences.EpisodeSwipeAction.Disabled]
*/ */
private fun executeEpisodeSwipeAction( private fun executeEpisodeSwipeAction(
episodeItem: EpisodeItem, episodeItem: EpisodeList.Item,
swipeAction: LibraryPreferences.EpisodeSwipeAction, swipeAction: LibraryPreferences.EpisodeSwipeAction,
) { ) {
val episode = episodeItem.episode val episode = episodeItem.episode
@ -654,7 +657,7 @@ class AnimeScreenModel(
} }
fun runEpisodeDownloadActions( fun runEpisodeDownloadActions(
items: List<EpisodeItem>, items: List<EpisodeList.Item>,
action: EpisodeDownloadAction, action: EpisodeDownloadAction,
) { ) {
when (action) { when (action) {
@ -669,7 +672,7 @@ class AnimeScreenModel(
startDownload(listOf(episode), true) startDownload(listOf(episode), true)
} }
EpisodeDownloadAction.CANCEL -> { EpisodeDownloadAction.CANCEL -> {
val episodeId = items.singleOrNull()?.episode?.id ?: return val episodeId = items.singleOrNull()?.id ?: return
cancelDownload(episodeId) cancelDownload(episodeId)
} }
EpisodeDownloadAction.DELETE -> { EpisodeDownloadAction.DELETE -> {
@ -880,14 +883,14 @@ class AnimeScreenModel(
} }
fun toggleSelection( fun toggleSelection(
item: EpisodeItem, item: EpisodeList.Item,
selected: Boolean, selected: Boolean,
userSelected: Boolean = false, userSelected: Boolean = false,
fromLongPress: Boolean = false, fromLongPress: Boolean = false,
) { ) {
updateSuccessState { successState -> updateSuccessState { successState ->
val newEpisodes = successState.processedEpisodes.toMutableList().apply { 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 if (selectedIndex < 0) return@apply
val selectedItem = get(selectedIndex) val selectedItem = get(selectedIndex)
@ -895,7 +898,7 @@ class AnimeScreenModel(
val firstSelection = none { it.selected } val firstSelection = none { it.selected }
set(selectedIndex, selectedItem.copy(selected = selected)) set(selectedIndex, selectedItem.copy(selected = selected))
selectedEpisodeIds.addOrRemove(item.episode.id, selected) selectedEpisodeIds.addOrRemove(item.id, selected)
if (selected && userSelected && fromLongPress) { if (selected && userSelected && fromLongPress) {
if (firstSelection) { if (firstSelection) {
@ -918,7 +921,7 @@ class AnimeScreenModel(
range.forEach { range.forEach {
val inbetweenItem = get(it) val inbetweenItem = get(it)
if (!inbetweenItem.selected) { if (!inbetweenItem.selected) {
selectedEpisodeIds.add(inbetweenItem.episode.id) selectedEpisodeIds.add(inbetweenItem.id)
set(it, inbetweenItem.copy(selected = true)) set(it, inbetweenItem.copy(selected = true))
} }
} }
@ -946,7 +949,7 @@ class AnimeScreenModel(
fun toggleAllSelection(selected: Boolean) { fun toggleAllSelection(selected: Boolean) {
updateSuccessState { successState -> updateSuccessState { successState ->
val newEpisodes = successState.episodes.map { val newEpisodes = successState.episodes.map {
selectedEpisodeIds.addOrRemove(it.episode.id, selected) selectedEpisodeIds.addOrRemove(it.id, selected)
it.copy(selected = selected) it.copy(selected = selected)
} }
selectedPositions[0] = -1 selectedPositions[0] = -1
@ -958,7 +961,7 @@ class AnimeScreenModel(
fun invertSelection() { fun invertSelection() {
updateSuccessState { successState -> updateSuccessState { successState ->
val newEpisodes = successState.episodes.map { val newEpisodes = successState.episodes.map {
selectedEpisodeIds.addOrRemove(it.episode.id, !it.selected) selectedEpisodeIds.addOrRemove(it.id, !it.selected)
it.copy(selected = !it.selected) it.copy(selected = !it.selected)
} }
selectedPositions[0] = -1 selectedPositions[0] = -1
@ -1060,7 +1063,7 @@ class AnimeScreenModel(
val anime: Anime, val anime: Anime,
val source: AnimeSource, val source: AnimeSource,
val isFromSource: Boolean, val isFromSource: Boolean,
val episodes: List<EpisodeItem>, val episodes: List<EpisodeList.Item>,
val trackItems: List<AnimeTrackItem> = emptyList(), val trackItems: List<AnimeTrackItem> = emptyList(),
val isRefreshingData: Boolean = false, val isRefreshingData: Boolean = false,
val dialog: Dialog? = null, val dialog: Dialog? = null,
@ -1075,6 +1078,33 @@ class AnimeScreenModel(
episodes.applyFilters(anime).toList() 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 val trackingAvailable: Boolean
get() = trackItems.isNotEmpty() get() = trackItems.isNotEmpty()
@ -1093,7 +1123,7 @@ class AnimeScreenModel(
* Applies the view filters to the list of episodes obtained from the database. * Applies the view filters to the list of episodes obtained from the database.
* @return an observable of the list of episodes filtered and sorted. * @return an observable of the list of episodes filtered and sorted.
*/ */
private fun List<EpisodeItem>.applyFilters(anime: Anime): Sequence<EpisodeItem> { private fun List<EpisodeList.Item>.applyFilters(anime: Anime): Sequence<EpisodeList.Item> {
val isLocalAnime = anime.isLocal() val isLocalAnime = anime.isLocal()
val unseenFilter = anime.unseenFilter val unseenFilter = anime.unseenFilter
val downloadedFilter = anime.downloadedFilter val downloadedFilter = anime.downloadedFilter
@ -1114,11 +1144,21 @@ class AnimeScreenModel(
} }
@Immutable @Immutable
data class EpisodeItem( sealed class EpisodeList {
@Immutable
data class MissingCount(
val id: String,
val count: Int,
) : EpisodeList()
@Immutable
data class Item(
val episode: Episode, val episode: Episode,
val downloadState: AnimeDownload.State, val downloadState: AnimeDownload.State,
val downloadProgress: Int, val downloadProgress: Int,
val selected: Boolean = false, val selected: Boolean = false,
) { ) : EpisodeList() {
val id = episode.id
val isDownloaded = downloadState == AnimeDownload.State.DOWNLOADED val isDownloaded = downloadState == AnimeDownload.State.DOWNLOADED
} }
}

View file

@ -10,6 +10,7 @@ import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.core.preference.asState import eu.kanade.core.preference.asState
import eu.kanade.core.util.addOrRemove 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.interactor.UpdateManga
import eu.kanade.domain.entries.manga.model.downloadedFilter import eu.kanade.domain.entries.manga.model.downloadedFilter
import eu.kanade.domain.entries.manga.model.toSManga 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.ChapterUpdate
import tachiyomi.domain.items.chapter.model.NoChaptersException import tachiyomi.domain.items.chapter.model.NoChaptersException
import tachiyomi.domain.items.chapter.service.getChapterSort import tachiyomi.domain.items.chapter.service.getChapterSort
import tachiyomi.domain.items.service.calculateChapterGap
import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.source.manga.service.MangaSourceManager import tachiyomi.domain.source.manga.service.MangaSourceManager
import tachiyomi.domain.track.manga.interactor.GetMangaTracks import tachiyomi.domain.track.manga.interactor.GetMangaTracks
import tachiyomi.source.local.entries.manga.isLocal import tachiyomi.source.local.entries.manga.isLocal
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import kotlin.math.floor
class MangaScreenModel( class MangaScreenModel(
val context: Context, val context: Context,
@ -120,10 +123,10 @@ class MangaScreenModel(
private val isFavorited: Boolean private val isFavorited: Boolean
get() = manga?.favorite ?: false get() = manga?.favorite ?: false
private val allChapters: List<ChapterItem>? private val allChapters: List<ChapterList.Item>?
get() = successState?.chapters get() = successState?.chapters
private val filteredChapters: List<ChapterItem>? private val filteredChapters: List<ChapterList.Item>?
get() = successState?.processedChapters get() = successState?.processedChapters
val chapterSwipeStartAction = libraryPreferences.swipeChapterEndAction().get() val chapterSwipeStartAction = libraryPreferences.swipeChapterEndAction().get()
@ -166,7 +169,7 @@ class MangaScreenModel(
updateSuccessState { updateSuccessState {
it.copy( it.copy(
manga = manga, manga = manga,
chapters = chapters.toChapterItems(manga), chapters = chapters.toChapterListItems(manga),
) )
} }
} }
@ -177,7 +180,7 @@ class MangaScreenModel(
screenModelScope.launchIO { screenModelScope.launchIO {
val manga = getMangaAndChapters.awaitManga(mangaId) val manga = getMangaAndChapters.awaitManga(mangaId)
val chapters = getMangaAndChapters.awaitChapters(mangaId) val chapters = getMangaAndChapters.awaitChapters(mangaId)
.toChapterItems(manga) .toChapterListItems(manga)
if (!manga.favorite) { if (!manga.favorite) {
setMangaDefaultChapterFlags.await(manga) setMangaDefaultChapterFlags.await(manga)
@ -473,7 +476,7 @@ class MangaScreenModel(
private fun updateDownloadState(download: MangaDownload) { private fun updateDownloadState(download: MangaDownload) {
updateSuccessState { successState -> 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 if (modifiedIndex < 0) return@updateSuccessState successState
val newChapters = successState.chapters.toMutableList().apply { val newChapters = successState.chapters.toMutableList().apply {
@ -485,7 +488,7 @@ class MangaScreenModel(
} }
} }
private fun List<Chapter>.toChapterItems(manga: Manga): List<ChapterItem> { private fun List<Chapter>.toChapterListItems(manga: Manga): List<ChapterList.Item> {
val isLocal = manga.isLocal() val isLocal = manga.isLocal()
return map { chapter -> return map { chapter ->
val activeDownload = if (isLocal) { val activeDownload = if (isLocal) {
@ -509,7 +512,7 @@ class MangaScreenModel(
else -> MangaDownload.State.NOT_DOWNLOADED else -> MangaDownload.State.NOT_DOWNLOADED
} }
ChapterItem( ChapterList.Item(
chapter = chapter, chapter = chapter,
downloadState = downloadState, downloadState = downloadState,
downloadProgress = activeDownload?.progress ?: 0, downloadProgress = activeDownload?.progress ?: 0,
@ -557,7 +560,7 @@ class MangaScreenModel(
/** /**
* @throws IllegalStateException if the swipe action is [LibraryPreferences.ChapterSwipeAction.Disabled] * @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 { screenModelScope.launch {
executeChapterSwipeAction(chapterItem, swipeAction) executeChapterSwipeAction(chapterItem, swipeAction)
} }
@ -567,7 +570,7 @@ class MangaScreenModel(
* @throws IllegalStateException if the swipe action is [LibraryPreferences.ChapterSwipeAction.Disabled] * @throws IllegalStateException if the swipe action is [LibraryPreferences.ChapterSwipeAction.Disabled]
*/ */
private fun executeChapterSwipeAction( private fun executeChapterSwipeAction(
chapterItem: ChapterItem, chapterItem: ChapterList.Item,
swipeAction: LibraryPreferences.ChapterSwipeAction, swipeAction: LibraryPreferences.ChapterSwipeAction,
) { ) {
val chapter = chapterItem.chapter val chapter = chapterItem.chapter
@ -648,7 +651,7 @@ class MangaScreenModel(
} }
fun runChapterDownloadActions( fun runChapterDownloadActions(
items: List<ChapterItem>, items: List<ChapterList.Item>,
action: ChapterDownloadAction, action: ChapterDownloadAction,
) { ) {
when (action) { when (action) {
@ -663,7 +666,7 @@ class MangaScreenModel(
startDownload(listOf(chapter), true) startDownload(listOf(chapter), true)
} }
ChapterDownloadAction.CANCEL -> { ChapterDownloadAction.CANCEL -> {
val chapterId = items.singleOrNull()?.chapter?.id ?: return val chapterId = items.singleOrNull()?.id ?: return
cancelDownload(chapterId) cancelDownload(chapterId)
} }
ChapterDownloadAction.DELETE -> { ChapterDownloadAction.DELETE -> {
@ -873,14 +876,14 @@ class MangaScreenModel(
} }
fun toggleSelection( fun toggleSelection(
item: ChapterItem, item: ChapterList.Item,
selected: Boolean, selected: Boolean,
userSelected: Boolean = false, userSelected: Boolean = false,
fromLongPress: Boolean = false, fromLongPress: Boolean = false,
) { ) {
updateSuccessState { successState -> updateSuccessState { successState ->
val newChapters = successState.processedChapters.toMutableList().apply { 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 if (selectedIndex < 0) return@apply
val selectedItem = get(selectedIndex) val selectedItem = get(selectedIndex)
@ -888,7 +891,7 @@ class MangaScreenModel(
val firstSelection = none { it.selected } val firstSelection = none { it.selected }
set(selectedIndex, selectedItem.copy(selected = selected)) set(selectedIndex, selectedItem.copy(selected = selected))
selectedChapterIds.addOrRemove(item.chapter.id, selected) selectedChapterIds.addOrRemove(item.id, selected)
if (selected && userSelected && fromLongPress) { if (selected && userSelected && fromLongPress) {
if (firstSelection) { if (firstSelection) {
@ -911,7 +914,7 @@ class MangaScreenModel(
range.forEach { range.forEach {
val inbetweenItem = get(it) val inbetweenItem = get(it)
if (!inbetweenItem.selected) { if (!inbetweenItem.selected) {
selectedChapterIds.add(inbetweenItem.chapter.id) selectedChapterIds.add(inbetweenItem.id)
set(it, inbetweenItem.copy(selected = true)) set(it, inbetweenItem.copy(selected = true))
} }
} }
@ -939,7 +942,7 @@ class MangaScreenModel(
fun toggleAllSelection(selected: Boolean) { fun toggleAllSelection(selected: Boolean) {
updateSuccessState { successState -> updateSuccessState { successState ->
val newChapters = successState.chapters.map { val newChapters = successState.chapters.map {
selectedChapterIds.addOrRemove(it.chapter.id, selected) selectedChapterIds.addOrRemove(it.id, selected)
it.copy(selected = selected) it.copy(selected = selected)
} }
selectedPositions[0] = -1 selectedPositions[0] = -1
@ -951,7 +954,7 @@ class MangaScreenModel(
fun invertSelection() { fun invertSelection() {
updateSuccessState { successState -> updateSuccessState { successState ->
val newChapters = successState.chapters.map { val newChapters = successState.chapters.map {
selectedChapterIds.addOrRemove(it.chapter.id, !it.selected) selectedChapterIds.addOrRemove(it.id, !it.selected)
it.copy(selected = !it.selected) it.copy(selected = !it.selected)
} }
selectedPositions[0] = -1 selectedPositions[0] = -1
@ -1033,7 +1036,7 @@ class MangaScreenModel(
val manga: Manga, val manga: Manga,
val source: MangaSource, val source: MangaSource,
val isFromSource: Boolean, val isFromSource: Boolean,
val chapters: List<ChapterItem>, val chapters: List<ChapterList.Item>,
val trackItems: List<MangaTrackItem> = emptyList(), val trackItems: List<MangaTrackItem> = emptyList(),
val isRefreshingData: Boolean = false, val isRefreshingData: Boolean = false,
val dialog: Dialog? = null, val dialog: Dialog? = null,
@ -1044,6 +1047,33 @@ class MangaScreenModel(
chapters.applyFilters(manga).toList() 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 val trackingAvailable: Boolean
get() = trackItems.isNotEmpty() get() = trackItems.isNotEmpty()
@ -1054,7 +1084,7 @@ class MangaScreenModel(
* Applies the view filters to the list of chapters obtained from the database. * Applies the view filters to the list of chapters obtained from the database.
* @return an observable of the list of chapters filtered and sorted. * @return an observable of the list of chapters filtered and sorted.
*/ */
private fun List<ChapterItem>.applyFilters(manga: Manga): Sequence<ChapterItem> { private fun List<ChapterList.Item>.applyFilters(manga: Manga): Sequence<ChapterList.Item> {
val isLocalManga = manga.isLocal() val isLocalManga = manga.isLocal()
val unreadFilter = manga.unreadFilter val unreadFilter = manga.unreadFilter
val downloadedFilter = manga.downloadedFilter val downloadedFilter = manga.downloadedFilter
@ -1075,11 +1105,21 @@ class MangaScreenModel(
} }
@Immutable @Immutable
data class ChapterItem( sealed class ChapterList {
@Immutable
data class MissingCount(
val id: String,
val count: Int,
) : ChapterList()
@Immutable
data class Item(
val chapter: Chapter, val chapter: Chapter,
val downloadState: MangaDownload.State, val downloadState: MangaDownload.State,
val downloadProgress: Int, val downloadProgress: Int,
val selected: Boolean = false, val selected: Boolean = false,
) { ) : ChapterList() {
val id = chapter.id
val isDownloaded = downloadState == MangaDownload.State.DOWNLOADED val isDownloaded = downloadState == MangaDownload.State.DOWNLOADED
} }
}

View file

@ -53,7 +53,7 @@ import tachiyomi.domain.entries.anime.model.Anime
import tachiyomi.domain.entries.anime.model.AnimeUpdate import tachiyomi.domain.entries.anime.model.AnimeUpdate
import tachiyomi.domain.entries.applyFilter import tachiyomi.domain.entries.applyFilter
import tachiyomi.domain.history.anime.interactor.GetNextEpisodes 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.items.episode.model.Episode
import tachiyomi.domain.library.anime.LibraryAnime import tachiyomi.domain.library.anime.LibraryAnime
import tachiyomi.domain.library.anime.model.AnimeLibrarySort import tachiyomi.domain.library.anime.model.AnimeLibrarySort
@ -79,7 +79,7 @@ class AnimeLibraryScreenModel(
private val getCategories: GetVisibleAnimeCategories = Injekt.get(), private val getCategories: GetVisibleAnimeCategories = Injekt.get(),
private val getTracksPerAnime: GetTracksPerAnime = Injekt.get(), private val getTracksPerAnime: GetTracksPerAnime = Injekt.get(),
private val getNextEpisodes: GetNextEpisodes = 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 setSeenStatus: SetSeenStatus = Injekt.get(),
private val updateAnime: UpdateAnime = Injekt.get(), private val updateAnime: UpdateAnime = Injekt.get(),
private val setAnimeCategories: SetAnimeCategories = Injekt.get(), private val setAnimeCategories: SetAnimeCategories = Injekt.get(),

View file

@ -53,7 +53,7 @@ import tachiyomi.domain.entries.manga.interactor.GetLibraryManga
import tachiyomi.domain.entries.manga.model.Manga import tachiyomi.domain.entries.manga.model.Manga
import tachiyomi.domain.entries.manga.model.MangaUpdate import tachiyomi.domain.entries.manga.model.MangaUpdate
import tachiyomi.domain.history.manga.interactor.GetNextChapters 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.items.chapter.model.Chapter
import tachiyomi.domain.library.manga.LibraryManga import tachiyomi.domain.library.manga.LibraryManga
import tachiyomi.domain.library.manga.model.MangaLibrarySort import tachiyomi.domain.library.manga.model.MangaLibrarySort
@ -79,7 +79,7 @@ class MangaLibraryScreenModel(
private val getCategories: GetVisibleMangaCategories = Injekt.get(), private val getCategories: GetVisibleMangaCategories = Injekt.get(),
private val getTracksPerManga: GetTracksPerManga = Injekt.get(), private val getTracksPerManga: GetTracksPerManga = Injekt.get(),
private val getNextChapters: GetNextChapters = 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 setReadStatus: SetReadStatus = Injekt.get(),
private val updateManga: UpdateManga = Injekt.get(), private val updateManga: UpdateManga = Injekt.get(),
private val setMangaCategories: SetMangaCategories = Injekt.get(), private val setMangaCategories: SetMangaCategories = Injekt.get(),

View file

@ -31,7 +31,6 @@ import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
import logcat.LogPriority import logcat.LogPriority
import tachiyomi.core.util.lang.launchIO import tachiyomi.core.util.lang.launchIO
import tachiyomi.core.util.lang.withIOContext 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.entries.anime.model.Anime
import tachiyomi.domain.history.anime.interactor.UpsertAnimeHistory import tachiyomi.domain.history.anime.interactor.UpsertAnimeHistory
import tachiyomi.domain.history.anime.model.AnimeHistoryUpdate 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.interactor.UpdateEpisode
import tachiyomi.domain.items.episode.model.Episode import tachiyomi.domain.items.episode.model.Episode
import tachiyomi.domain.items.episode.model.EpisodeUpdate import tachiyomi.domain.items.episode.model.EpisodeUpdate
@ -81,7 +80,7 @@ class ExternalIntents {
): Intent? { ): Intent? {
anime = getAnime.await(animeId!!) ?: return null anime = getAnime.await(animeId!!) ?: return null
source = sourceManager.get(anime.source) ?: 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 val video = chosenVideo
?: EpisodeLoader.getLinks(episode, anime, source).asFlow().first().firstOrNull() ?: EpisodeLoader.getLinks(episode, anime, source).asFlow().first().firstOrNull()
@ -392,7 +391,7 @@ class ExternalIntents {
private val updateEpisode: UpdateEpisode = Injekt.get() private val updateEpisode: UpdateEpisode = Injekt.get()
private val getAnime: GetAnime = Injekt.get() private val getAnime: GetAnime = Injekt.get()
private val sourceManager: AnimeSourceManager = 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 getTracks: GetAnimeTracks = Injekt.get()
private val insertTrack: InsertAnimeTrack = Injekt.get() private val insertTrack: InsertAnimeTrack = Injekt.get()
private val downloadManager: AnimeDownloadManager by injectLazy() private val downloadManager: AnimeDownloadManager by injectLazy()
@ -469,7 +468,7 @@ class ExternalIntents {
else -> throw NotImplementedError("Unknown sorting method") else -> throw NotImplementedError("Unknown sorting method")
} }
val episodes = getEpisodeByAnimeId.await(anime.id) val episodes = getEpisodesByAnimeId.await(anime.id)
.sortedWith { e1, e2 -> sortFunction(e1, e2) } .sortedWith { e1, e2 -> sortFunction(e1, e2) }
val currentEpisodePosition = episodes.indexOf(episode) val currentEpisodePosition = episodes.indexOf(episode)

View file

@ -59,7 +59,7 @@ import tachiyomi.domain.entries.anime.model.Anime
import tachiyomi.domain.history.anime.interactor.GetNextEpisodes import tachiyomi.domain.history.anime.interactor.GetNextEpisodes
import tachiyomi.domain.history.anime.interactor.UpsertAnimeHistory import tachiyomi.domain.history.anime.interactor.UpsertAnimeHistory
import tachiyomi.domain.history.anime.model.AnimeHistoryUpdate 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.interactor.UpdateEpisode
import tachiyomi.domain.items.episode.model.EpisodeUpdate import tachiyomi.domain.items.episode.model.EpisodeUpdate
import tachiyomi.domain.items.episode.service.getEpisodeSort import tachiyomi.domain.items.episode.service.getEpisodeSort
@ -81,7 +81,7 @@ class PlayerViewModel @JvmOverloads constructor(
private val trackEpisode: TrackEpisode = Injekt.get(), private val trackEpisode: TrackEpisode = Injekt.get(),
private val getAnime: GetAnime = Injekt.get(), private val getAnime: GetAnime = Injekt.get(),
private val getNextEpisodes: GetNextEpisodes = 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 getTracks: GetAnimeTracks = Injekt.get(),
private val upsertHistory: UpsertAnimeHistory = Injekt.get(), private val upsertHistory: UpsertAnimeHistory = Injekt.get(),
private val updateEpisode: UpdateEpisode = Injekt.get(), private val updateEpisode: UpdateEpisode = Injekt.get(),
@ -284,7 +284,7 @@ class PlayerViewModel @JvmOverloads constructor(
) )
private fun initEpisodeList(anime: Anime): List<Episode> { private fun initEpisodeList(anime: Anime): List<Episode> {
val episodes = runBlocking { getEpisodeByAnimeId.await(anime.id) } val episodes = runBlocking { getEpisodesByAnimeId.await(anime.id) }
return episodes return episodes
.sortedWith(getEpisodeSort(anime, sortDescending = false)) .sortedWith(getEpisodeSort(anime, sortDescending = false))

View file

@ -66,7 +66,7 @@ import tachiyomi.domain.entries.manga.model.Manga
import tachiyomi.domain.history.manga.interactor.GetNextChapters import tachiyomi.domain.history.manga.interactor.GetNextChapters
import tachiyomi.domain.history.manga.interactor.UpsertMangaHistory import tachiyomi.domain.history.manga.interactor.UpsertMangaHistory
import tachiyomi.domain.history.manga.model.MangaHistoryUpdate 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.interactor.UpdateChapter
import tachiyomi.domain.items.chapter.model.ChapterUpdate import tachiyomi.domain.items.chapter.model.ChapterUpdate
import tachiyomi.domain.items.chapter.service.getChapterSort import tachiyomi.domain.items.chapter.service.getChapterSort
@ -92,7 +92,7 @@ class ReaderViewModel @JvmOverloads constructor(
private val trackPreferences: TrackPreferences = Injekt.get(), private val trackPreferences: TrackPreferences = Injekt.get(),
private val trackChapter: TrackChapter = Injekt.get(), private val trackChapter: TrackChapter = Injekt.get(),
private val getManga: GetManga = 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 getNextChapters: GetNextChapters = Injekt.get(),
private val upsertHistory: UpsertMangaHistory = Injekt.get(), private val upsertHistory: UpsertMangaHistory = Injekt.get(),
private val updateChapter: UpdateChapter = Injekt.get(), private val updateChapter: UpdateChapter = Injekt.get(),
@ -147,7 +147,7 @@ class ReaderViewModel @JvmOverloads constructor(
*/ */
private val chapterList by lazy { private val chapterList by lazy {
val manga = manga!! val manga = manga!!
val chapters = runBlocking { getChapterByMangaId.await(manga.id) } val chapters = runBlocking { getChaptersByMangaId.await(manga.id) }
val selectedChapter = chapters.find { it.id == chapterId } val selectedChapter = chapters.find { it.id == chapterId }
?: error("Requested chapter of id $chapterId not found in chapter list") ?: error("Requested chapter of id $chapterId not found in chapter list")

View file

@ -16,7 +16,7 @@ import eu.kanade.tachiyomi.data.track.TrackerManager
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import tachiyomi.core.util.lang.launchIO import tachiyomi.core.util.lang.launchIO
import tachiyomi.domain.entries.anime.interactor.GetLibraryAnime 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.anime.LibraryAnime
import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_HAS_UNVIEWED import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_HAS_UNVIEWED
@ -31,7 +31,7 @@ import uy.kohesive.injekt.api.get
class AnimeStatsScreenModel( class AnimeStatsScreenModel(
private val downloadManager: AnimeDownloadManager = Injekt.get(), private val downloadManager: AnimeDownloadManager = Injekt.get(),
private val getAnimelibAnime: GetLibraryAnime = 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 getTracks: GetAnimeTracks = Injekt.get(),
private val preferences: LibraryPreferences = Injekt.get(), private val preferences: LibraryPreferences = Injekt.get(),
private val trackerManager: TrackerManager = Injekt.get(), private val trackerManager: TrackerManager = Injekt.get(),
@ -128,7 +128,7 @@ class AnimeStatsScreenModel(
private suspend fun getWatchTime(libraryAnimeList: List<LibraryAnime>): Long { private suspend fun getWatchTime(libraryAnimeList: List<LibraryAnime>): Long {
var watchTime = 0L var watchTime = 0L
libraryAnimeList.forEach { libraryAnime -> libraryAnimeList.forEach { libraryAnime ->
getEpisodeByAnimeId.await(libraryAnime.anime.id).forEach { episode -> getEpisodesByAnimeId.await(libraryAnime.anime.id).forEach { episode ->
watchTime += if (episode.seen) { watchTime += if (episode.seen) {
episode.totalSeconds episode.totalSeconds
} else { } else {

View file

@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.util.chapter
import eu.kanade.domain.items.chapter.model.applyFilters import eu.kanade.domain.items.chapter.model.applyFilters
import eu.kanade.tachiyomi.data.download.manga.MangaDownloadManager 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.entries.manga.model.Manga
import tachiyomi.domain.items.chapter.model.Chapter import tachiyomi.domain.items.chapter.model.Chapter
@ -22,7 +22,7 @@ fun List<Chapter>.getNextUnread(manga: Manga, downloadManager: MangaDownloadMana
/** /**
* Gets next unread chapter with filters and sorting applied * Gets next unread chapter with filters and sorting applied
*/ */
fun List<ChapterItem>.getNextUnread(manga: Manga): Chapter? { fun List<ChapterList.Item>.getNextUnread(manga: Manga): Chapter? {
return applyFilters(manga).let { chapters -> return applyFilters(manga).let { chapters ->
if (manga.sortDescending()) { if (manga.sortDescending()) {
chapters.findLast { !it.chapter.read } chapters.findLast { !it.chapter.read }

View file

@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.util.episode
import eu.kanade.domain.items.episode.model.applyFilters import eu.kanade.domain.items.episode.model.applyFilters
import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadManager 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.anime.model.Anime
import tachiyomi.domain.items.episode.model.Episode import tachiyomi.domain.items.episode.model.Episode
@ -22,7 +22,7 @@ fun List<Episode>.getNextUnseen(anime: Anime, downloadManager: AnimeDownloadMana
/** /**
* Gets next unseen episode with filters and sorting applied * Gets next unseen episode with filters and sorting applied
*/ */
fun List<EpisodeItem>.getNextUnseen(anime: Anime): Episode? { fun List<EpisodeList.Item>.getNextUnseen(anime: Anime): Episode? {
return applyFilters(anime).let { episodes -> return applyFilters(anime).let { episodes ->
if (anime.sortDescending()) { if (anime.sortDescending()) {
episodes.findLast { !it.episode.seen } episodes.findLast { !it.episode.seen }

View file

@ -55,5 +55,5 @@ subprojects {
} }
tasks.register<Delete>("clean") { tasks.register<Delete>("clean") {
delete(rootProject.buildDir) delete(rootProject.layout.buildDirectory)
} }

View file

@ -25,6 +25,7 @@ dependencies {
api(libs.okhttp.core) api(libs.okhttp.core)
api(libs.okhttp.logging) api(libs.okhttp.logging)
api(libs.okhttp.brotli)
api(libs.okhttp.dnsoverhttps) api(libs.okhttp.dnsoverhttps)
api(libs.okio) api(libs.okio)

View file

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.core.security package eu.kanade.tachiyomi.core.security
import eu.kanade.tachiyomi.core.R import eu.kanade.tachiyomi.core.R
import tachiyomi.core.preference.Preference
import tachiyomi.core.preference.PreferenceStore import tachiyomi.core.preference.PreferenceStore
import tachiyomi.core.preference.getEnum import tachiyomi.core.preference.getEnum
@ -20,7 +21,10 @@ class SecurityPreferences(
* For app lock. Will be set when there is a pending timed lock. * For app lock. Will be set when there is a pending timed lock.
* Otherwise this pref should be deleted. * 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) { enum class SecureScreenMode(val titleResId: Int) {
ALWAYS(R.string.lock_always), ALWAYS(R.string.lock_always),

View file

@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.network.interceptor.UncaughtExceptionInterceptor
import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor
import okhttp3.Cache import okhttp3.Cache
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.brotli.BrotliInterceptor
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
import java.io.File import java.io.File
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -29,6 +30,7 @@ class NetworkHelper(
maxSize = 5L * 1024 * 1024, // 5 MiB maxSize = 5L * 1024 * 1024, // 5 MiB
), ),
) )
.addInterceptor(BrotliInterceptor)
.addInterceptor(UncaughtExceptionInterceptor()) .addInterceptor(UncaughtExceptionInterceptor())
.addInterceptor(UserAgentInterceptor(::defaultUserAgentProvider)) .addInterceptor(UserAgentInterceptor(::defaultUserAgentProvider))

View file

@ -28,6 +28,30 @@ object DiskUtil {
return size 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. * Gets the available space for the disk that a file path points to, in bytes.
*/ */

View file

@ -21,6 +21,32 @@ interface Preference<T> {
fun changes(): Flow<T> fun changes(): Flow<T>
fun stateIn(scope: CoroutineScope): StateFlow<T> fun stateIn(scope: CoroutineScope): StateFlow<T>
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 <reified T, R : T> Preference<T>.getAndSet(crossinline block: (T) -> R) = set( inline fun <reified T, R : T> Preference<T>.getAndSet(crossinline block: (T) -> R) = set(

View file

@ -2,7 +2,7 @@ package tachiyomi.domain.entries.anime.interactor
import tachiyomi.domain.entries.anime.model.Anime import tachiyomi.domain.entries.anime.model.Anime
import tachiyomi.domain.entries.anime.model.AnimeUpdate 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 tachiyomi.domain.items.episode.model.Episode
import java.time.Instant import java.time.Instant
import java.time.ZoneId import java.time.ZoneId
@ -11,7 +11,7 @@ import java.time.temporal.ChronoUnit
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
class AnimeFetchInterval( class AnimeFetchInterval(
private val getEpisodeByAnimeId: GetEpisodeByAnimeId, private val getEpisodesByAnimeId: GetEpisodesByAnimeId,
) { ) {
suspend fun toAnimeUpdateOrNull( suspend fun toAnimeUpdateOrNull(
@ -24,7 +24,7 @@ class AnimeFetchInterval(
} else { } else {
window window
} }
val episodes = getEpisodeByAnimeId.await(anime.id) val episodes = getEpisodesByAnimeId.await(anime.id)
val interval = anime.fetchInterval.takeIf { it < 0 } ?: calculateInterval( val interval = anime.fetchInterval.takeIf { it < 0 } ?: calculateInterval(
episodes, episodes,
dateTime.zone, dateTime.zone,

View file

@ -2,7 +2,7 @@ package tachiyomi.domain.entries.manga.interactor
import tachiyomi.domain.entries.manga.model.Manga import tachiyomi.domain.entries.manga.model.Manga
import tachiyomi.domain.entries.manga.model.MangaUpdate 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 tachiyomi.domain.items.chapter.model.Chapter
import java.time.Instant import java.time.Instant
import java.time.ZoneId import java.time.ZoneId
@ -11,7 +11,7 @@ import java.time.temporal.ChronoUnit
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
class MangaFetchInterval( class MangaFetchInterval(
private val getChapterByMangaId: GetChapterByMangaId, private val getChaptersByMangaId: GetChaptersByMangaId,
) { ) {
suspend fun toMangaUpdateOrNull( suspend fun toMangaUpdateOrNull(
@ -24,7 +24,7 @@ class MangaFetchInterval(
} else { } else {
window window
} }
val chapters = getChapterByMangaId.await(manga.id) val chapters = getChaptersByMangaId.await(manga.id)
val interval = manga.fetchInterval.takeIf { it < 0 } ?: calculateInterval( val interval = manga.fetchInterval.takeIf { it < 0 } ?: calculateInterval(
chapters, chapters,
dateTime.zone, dateTime.zone,

View file

@ -2,13 +2,13 @@ package tachiyomi.domain.history.anime.interactor
import tachiyomi.domain.entries.anime.interactor.GetAnime import tachiyomi.domain.entries.anime.interactor.GetAnime
import tachiyomi.domain.history.anime.repository.AnimeHistoryRepository 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.model.Episode
import tachiyomi.domain.items.episode.service.getEpisodeSort import tachiyomi.domain.items.episode.service.getEpisodeSort
import kotlin.math.max import kotlin.math.max
class GetNextEpisodes( class GetNextEpisodes(
private val getEpisodeByAnimeId: GetEpisodeByAnimeId, private val getEpisodesByAnimeId: GetEpisodesByAnimeId,
private val getAnime: GetAnime, private val getAnime: GetAnime,
private val historyRepository: AnimeHistoryRepository, private val historyRepository: AnimeHistoryRepository,
) { ) {
@ -20,7 +20,7 @@ class GetNextEpisodes(
suspend fun await(animeId: Long, onlyUnseen: Boolean = true): List<Episode> { suspend fun await(animeId: Long, onlyUnseen: Boolean = true): List<Episode> {
val anime = getAnime.await(animeId) ?: return emptyList() val anime = getAnime.await(animeId) ?: return emptyList()
val episodes = getEpisodeByAnimeId.await(animeId) val episodes = getEpisodesByAnimeId.await(animeId)
.sortedWith(getEpisodeSort(anime, sortDescending = false)) .sortedWith(getEpisodeSort(anime, sortDescending = false))
return if (onlyUnseen) { return if (onlyUnseen) {

View file

@ -2,13 +2,13 @@ package tachiyomi.domain.history.manga.interactor
import tachiyomi.domain.entries.manga.interactor.GetManga import tachiyomi.domain.entries.manga.interactor.GetManga
import tachiyomi.domain.history.manga.repository.MangaHistoryRepository 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.model.Chapter
import tachiyomi.domain.items.chapter.service.getChapterSort import tachiyomi.domain.items.chapter.service.getChapterSort
import kotlin.math.max import kotlin.math.max
class GetNextChapters( class GetNextChapters(
private val getChapterByMangaId: GetChapterByMangaId, private val getChaptersByMangaId: GetChaptersByMangaId,
private val getManga: GetManga, private val getManga: GetManga,
private val historyRepository: MangaHistoryRepository, private val historyRepository: MangaHistoryRepository,
) { ) {
@ -20,7 +20,7 @@ class GetNextChapters(
suspend fun await(mangaId: Long, onlyUnread: Boolean = true): List<Chapter> { suspend fun await(mangaId: Long, onlyUnread: Boolean = true): List<Chapter> {
val manga = getManga.await(mangaId) ?: return emptyList() val manga = getManga.await(mangaId) ?: return emptyList()
val chapters = getChapterByMangaId.await(mangaId) val chapters = getChaptersByMangaId.await(mangaId)
.sortedWith(getChapterSort(manga, sortDescending = false)) .sortedWith(getChapterSort(manga, sortDescending = false))
return if (onlyUnread) { return if (onlyUnread) {

View file

@ -5,7 +5,7 @@ import tachiyomi.core.util.system.logcat
import tachiyomi.domain.items.chapter.model.Chapter import tachiyomi.domain.items.chapter.model.Chapter
import tachiyomi.domain.items.chapter.repository.ChapterRepository import tachiyomi.domain.items.chapter.repository.ChapterRepository
class GetChapterByMangaId( class GetChaptersByMangaId(
private val chapterRepository: ChapterRepository, private val chapterRepository: ChapterRepository,
) { ) {

View file

@ -18,6 +18,16 @@ data class Chapter(
val isRecognizedNumber: Boolean val isRecognizedNumber: Boolean
get() = chapterNumber >= 0f 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 { companion object {
fun create() = Chapter( fun create() = Chapter(
id = -1, id = -1,

View file

@ -5,7 +5,7 @@ import tachiyomi.core.util.system.logcat
import tachiyomi.domain.items.episode.model.Episode import tachiyomi.domain.items.episode.model.Episode
import tachiyomi.domain.items.episode.repository.EpisodeRepository import tachiyomi.domain.items.episode.repository.EpisodeRepository
class GetEpisodeByAnimeId( class GetEpisodesByAnimeId(
private val episodeRepository: EpisodeRepository, private val episodeRepository: EpisodeRepository,
) { ) {

View file

@ -19,6 +19,16 @@ data class Episode(
val isRecognizedNumber: Boolean val isRecognizedNumber: Boolean
get() = episodeNumber >= 0f 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 { companion object {
fun create() = Episode( fun create() = Episode(
id = -1, id = -1,

View file

@ -1,5 +1,6 @@
package tachiyomi.domain.library.service package tachiyomi.domain.library.service
import tachiyomi.core.preference.Preference
import tachiyomi.core.preference.PreferenceStore import tachiyomi.core.preference.PreferenceStore
import tachiyomi.core.preference.TriState import tachiyomi.core.preference.TriState
import tachiyomi.core.preference.getEnum import tachiyomi.core.preference.getEnum
@ -41,7 +42,7 @@ class LibraryPreferences(
AnimeLibrarySort.Serializer::deserialize, 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 autoUpdateInterval() = preferenceStore.getInt("pref_library_update_interval_key", 0)
fun autoUpdateDeviceRestrictions() = preferenceStore.getStringSet( fun autoUpdateDeviceRestrictions() = preferenceStore.getStringSet(
@ -196,8 +197,8 @@ class LibraryPreferences(
fun defaultAnimeCategory() = preferenceStore.getInt("default_anime_category", -1) fun defaultAnimeCategory() = preferenceStore.getInt("default_anime_category", -1)
fun defaultMangaCategory() = preferenceStore.getInt("default_category", -1) fun defaultMangaCategory() = preferenceStore.getInt("default_category", -1)
fun lastUsedAnimeCategory() = preferenceStore.getInt("last_used_anime_category", 0) fun lastUsedAnimeCategory() = preferenceStore.getInt(Preference.appStateKey("last_used_anime_category"), 0)
fun lastUsedMangaCategory() = preferenceStore.getInt("last_used_category", 0) fun lastUsedMangaCategory() = preferenceStore.getInt(Preference.appStateKey("last_used_category"), 0)
fun animeUpdateCategories() = fun animeUpdateCategories() =
preferenceStore.getStringSet("animelib_update_categories", emptySet()) preferenceStore.getStringSet("animelib_update_categories", emptySet())

View file

@ -30,7 +30,7 @@ paging-compose = { module = "androidx.paging:paging-compose", version.ref = "pag
benchmark-macro = "androidx.benchmark:benchmark-macro-junit4:1.2.0" benchmark-macro = "androidx.benchmark:benchmark-macro-junit4:1.2.0"
test-ext = "androidx.test.ext:junit-ktx:1.2.0-alpha01" test-ext = "androidx.test.ext:junit-ktx:1.2.0-alpha01"
test-espresso-core = "androidx.test.espresso:espresso-core:3.6.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] [bundles]
lifecycle = ["lifecycle-common", "lifecycle-process", "lifecycle-runtimektx"] lifecycle = ["lifecycle-common", "lifecycle-process", "lifecycle-runtimektx"]

View file

@ -17,6 +17,7 @@ flowreactivenetwork = "ru.beryukhov:flowreactivenetwork:1.0.4"
okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp_version" } okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp_version" }
okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", 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" } okhttp-dnsoverhttps = { module = "com.squareup.okhttp3:okhttp-dnsoverhttps", version.ref = "okhttp_version" }
okio = "com.squareup.okio:okio:3.6.0" 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" truetypeparser = "io.github.yubyf:truetypeparser-light:2.1.4"
[bundles] [bundles]
okhttp = ["okhttp-core", "okhttp-logging", "okhttp-dnsoverhttps"] okhttp = ["okhttp-core", "okhttp-logging", "okhttp-brotli", "okhttp-dnsoverhttps"]
js-engine = ["quickjs-android"] js-engine = ["quickjs-android"]
sqlite = ["sqlite-framework", "sqlite-ktx", "sqlite-android"] sqlite = ["sqlite-framework", "sqlite-ktx", "sqlite-android"]
coil = ["coil-core", "coil-gif", "coil-compose"] coil = ["coil-core", "coil-gif", "coil-compose"]

View file

@ -158,6 +158,8 @@
<string name="pref_show_next_episode_airing_time">Show next episode\'s airing time </string> <string name="pref_show_next_episode_airing_time">Show next episode\'s airing time </string>
<string name="pref_backup_flags_summary">What information to include in the backup file</string> <string name="pref_backup_flags_summary">What information to include in the backup file</string>
<string name="pref_clear_chapter_cache">Clear chapter and episode cache</string> <string name="pref_clear_chapter_cache">Clear chapter and episode cache</string>
<string name="pref_anime_storage_usage">Anime Storage usage</string>
<string name="pref_manga_storage_usage">Manga Storage usage</string>
<string name="used_cache_both">Used by anime: %1$s, used by manga: %2$s</string> <string name="used_cache_both">Used by anime: %1$s, used by manga: %2$s</string>
<string name="pref_auto_clear_chapter_cache">Clear episode/chapter cache on app launch</string> <string name="pref_auto_clear_chapter_cache">Clear episode/chapter cache on app launch</string>
<string name="pref_clear_manga_database">Clear Manga database</string> <string name="pref_clear_manga_database">Clear Manga database</string>

View file

@ -66,6 +66,7 @@
<string name="action_sort_latest_chapter">Latest chapter</string> <string name="action_sort_latest_chapter">Latest chapter</string>
<string name="action_sort_chapter_fetch_date">Chapter fetch date</string> <string name="action_sort_chapter_fetch_date">Chapter fetch date</string>
<string name="action_sort_date_added">Date added</string> <string name="action_sort_date_added">Date added</string>
<string name="action_sort_tracker_score">Tracker score</string>
<string name="action_sort_airing_time">Airing time</string> <string name="action_sort_airing_time">Airing time</string>
<string name="action_search">Search</string> <string name="action_search">Search</string>
<string name="action_search_hint">Search…</string> <string name="action_search_hint">Search…</string>
@ -499,6 +500,11 @@
<string name="restoring_backup_error">Restoring backup failed</string> <string name="restoring_backup_error">Restoring backup failed</string>
<string name="restoring_backup_canceled">Canceled restore</string> <string name="restoring_backup_canceled">Canceled restore</string>
<string name="backup_info">You should keep copies of backups in other places as well.</string> <string name="backup_info">You should keep copies of backups in other places as well.</string>
<string name="last_auto_backup_info">Last automatically backed up: %s</string>
<string name="label_data">Data</string>
<string name="used_cache">Used: %1$s</string>
<string name="cache_deleted">Cache cleared. %1$d files have been deleted</string>
<string name="cache_delete_error">Error occurred while clearing</string>
<!-- Sync section --> <!-- Sync section -->
<string name="syncing_library">Syncing library</string> <string name="syncing_library">Syncing library</string>
@ -514,10 +520,6 @@
<string name="pref_reset_user_agent_string">Reset default user agent string</string> <string name="pref_reset_user_agent_string">Reset default user agent string</string>
<string name="requires_app_restart">Requires app restart to take effect</string> <string name="requires_app_restart">Requires app restart to take effect</string>
<string name="cookies_cleared">Cookies cleared</string> <string name="cookies_cleared">Cookies cleared</string>
<string name="label_data">Data</string>
<string name="used_cache">Used: %1$s</string>
<string name="cache_deleted">Cache cleared. %1$d files have been deleted</string>
<string name="cache_delete_error">Error occurred while clearing</string>
<string name="pref_invalidate_download_cache">Invalidate downloads index</string> <string name="pref_invalidate_download_cache">Invalidate downloads index</string>
<string name="download_cache_invalidated">Downloads index invalidated</string> <string name="download_cache_invalidated">Downloads index invalidated</string>
<string name="pref_clear_database">Clear database</string> <string name="pref_clear_database">Clear database</string>
@ -766,6 +768,7 @@
<string name="cant_open_last_read_chapter">Unable to open last read chapter</string> <string name="cant_open_last_read_chapter">Unable to open last read chapter</string>
<string name="updates_last_update_info">Library last updated: %s</string> <string name="updates_last_update_info">Library last updated: %s</string>
<string name="updates_last_update_info_just_now">Just now</string> <string name="updates_last_update_info_just_now">Just now</string>
<string name="relative_time_span_never">Never</string>
<!-- History --> <!-- History -->
<string name="recent_manga_time">Ch. %1$s - %2$s</string> <string name="recent_manga_time">Ch. %1$s - %2$s</string>

View file

@ -22,6 +22,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.composed import androidx.compose.ui.composed
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@ -69,7 +70,11 @@ fun TabText(
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Text(text = text) Text(
text = text,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
if (badgeCount != null) { if (badgeCount != null) {
Pill( Pill(
text = "$badgeCount", text = "$badgeCount",