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

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.repository.MangaHistoryRepository
import tachiyomi.domain.items.chapter.interactor.GetChapter
import tachiyomi.domain.items.chapter.interactor.GetChapterByMangaId
import tachiyomi.domain.items.chapter.interactor.GetChapterByUrlAndMangaId
import tachiyomi.domain.items.chapter.interactor.GetChaptersByMangaId
import tachiyomi.domain.items.chapter.interactor.SetMangaDefaultChapterFlags
import tachiyomi.domain.items.chapter.interactor.ShouldUpdateDbChapter
import tachiyomi.domain.items.chapter.interactor.UpdateChapter
import tachiyomi.domain.items.chapter.repository.ChapterRepository
import tachiyomi.domain.items.episode.interactor.GetEpisode
import tachiyomi.domain.items.episode.interactor.GetEpisodeByAnimeId
import tachiyomi.domain.items.episode.interactor.GetEpisodeByUrlAndAnimeId
import tachiyomi.domain.items.episode.interactor.GetEpisodesByAnimeId
import tachiyomi.domain.items.episode.interactor.SetAnimeDefaultEpisodeFlags
import tachiyomi.domain.items.episode.interactor.ShouldUpdateDbEpisode
import tachiyomi.domain.items.episode.interactor.UpdateEpisode
@ -250,7 +250,7 @@ class DomainModule : InjektModule {
addSingletonFactory<EpisodeRepository> { EpisodeRepositoryImpl(get()) }
addFactory { GetEpisode(get()) }
addFactory { GetEpisodeByAnimeId(get()) }
addFactory { GetEpisodesByAnimeId(get()) }
addFactory { GetEpisodeByUrlAndAnimeId(get()) }
addFactory { UpdateEpisode(get()) }
addFactory { SetSeenStatus(get(), get(), get(), get()) }
@ -259,7 +259,7 @@ class DomainModule : InjektModule {
addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) }
addFactory { GetChapter(get()) }
addFactory { GetChapterByMangaId(get()) }
addFactory { GetChaptersByMangaId(get()) }
addFactory { GetChapterByUrlAndMangaId(get()) }
addFactory { UpdateChapter(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.util.system.isPreviewBuildType
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
import tachiyomi.core.preference.Preference
import tachiyomi.core.preference.PreferenceStore
class BasePreferences(
@ -14,9 +15,12 @@ class BasePreferences(
private val preferenceStore: PreferenceStore,
) {
fun downloadedOnly() = preferenceStore.getBoolean("pref_downloaded_only", false)
fun downloadedOnly() = preferenceStore.getBoolean(
Preference.appStateKey("pref_downloaded_only"),
false,
)
fun incognitoMode() = preferenceStore.getBoolean("incognito_mode", false)
fun incognitoMode() = preferenceStore.getBoolean(Preference.appStateKey("incognito_mode"), false)
fun extensionInstaller() = ExtensionInstallerPreference(context, preferenceStore)

View file

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

View file

@ -1,6 +1,5 @@
package eu.kanade.domain.items.chapter.model
import data.Chapters
import eu.kanade.tachiyomi.data.database.models.manga.ChapterImpl
import eu.kanade.tachiyomi.source.model.SChapter
import tachiyomi.domain.items.chapter.model.Chapter
@ -27,16 +26,6 @@ fun Chapter.copyFromSChapter(sChapter: SChapter): Chapter {
)
}
fun Chapter.copyFrom(other: Chapters): Chapter {
return copy(
name = other.name,
url = other.url,
dateUpload = other.date_upload,
chapterNumber = other.chapter_number,
scanlator = other.scanlator?.ifBlank { null },
)
}
fun Chapter.toDbChapter(): DbChapter = ChapterImpl().also {
it.id = id
it.manga_id = mangaId

View file

@ -2,7 +2,7 @@ package eu.kanade.domain.items.chapter.model
import eu.kanade.domain.entries.manga.model.downloadedFilter
import eu.kanade.tachiyomi.data.download.manga.MangaDownloadManager
import eu.kanade.tachiyomi.ui.entries.manga.ChapterItem
import eu.kanade.tachiyomi.ui.entries.manga.ChapterList
import tachiyomi.domain.entries.applyFilter
import tachiyomi.domain.entries.manga.model.Manga
import tachiyomi.domain.items.chapter.model.Chapter
@ -39,7 +39,7 @@ fun List<Chapter>.applyFilters(manga: Manga, downloadManager: MangaDownloadManag
* Applies the view filters to the list of chapters obtained from the database.
* @return an observable of the list of chapters filtered and sorted.
*/
fun List<ChapterItem>.applyFilters(manga: Manga): Sequence<ChapterItem> {
fun List<ChapterList.Item>.applyFilters(manga: Manga): Sequence<ChapterList.Item> {
val isLocalManga = manga.isLocal()
val unreadFilter = manga.unreadFilter
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 tachiyomi.data.items.episode.EpisodeSanitizer
import tachiyomi.domain.entries.anime.model.Anime
import tachiyomi.domain.items.episode.interactor.GetEpisodeByAnimeId
import tachiyomi.domain.items.episode.interactor.GetEpisodesByAnimeId
import tachiyomi.domain.items.episode.interactor.ShouldUpdateDbEpisode
import tachiyomi.domain.items.episode.interactor.UpdateEpisode
import tachiyomi.domain.items.episode.model.Episode
@ -20,7 +20,6 @@ import tachiyomi.domain.items.episode.model.toEpisodeUpdate
import tachiyomi.domain.items.episode.repository.EpisodeRepository
import tachiyomi.domain.items.episode.service.EpisodeRecognition
import tachiyomi.source.local.entries.anime.isLocal
import uy.kohesive.injekt.api.get
import java.lang.Long.max
import java.time.ZonedDateTime
import java.util.Date
@ -33,7 +32,7 @@ class SyncEpisodesWithSource(
private val shouldUpdateDbEpisode: ShouldUpdateDbEpisode,
private val updateAnime: UpdateAnime,
private val updateEpisode: UpdateEpisode,
private val getEpisodeByAnimeId: GetEpisodeByAnimeId,
private val getEpisodesByAnimeId: GetEpisodesByAnimeId,
) {
/**
@ -67,7 +66,7 @@ class SyncEpisodesWithSource(
}
// Episodes from db.
val dbEpisodes = getEpisodeByAnimeId.await(anime.id)
val dbEpisodes = getEpisodesByAnimeId.await(anime.id)
// Episodes from the source not in db.
val toAdd = mutableListOf<Episode>()

View file

@ -1,6 +1,5 @@
package eu.kanade.domain.items.episode.model
import dataanime.Episodes
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.data.database.models.anime.EpisodeImpl
import tachiyomi.domain.items.episode.model.Episode
@ -27,16 +26,6 @@ fun Episode.copyFromSEpisode(sEpisode: SEpisode): Episode {
)
}
fun Episode.copyFrom(other: Episodes): Episode {
return copy(
name = other.name,
url = other.url,
dateUpload = other.date_upload,
episodeNumber = other.episode_number,
scanlator = other.scanlator?.ifBlank { null },
)
}
fun Episode.toDbEpisode(): DbEpisode = EpisodeImpl().also {
it.id = id
it.anime_id = animeId

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,5 @@
package eu.kanade.presentation.entries.manga.components
import android.content.Context
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.graphics.res.animatedVectorResource
@ -43,6 +42,7 @@ import androidx.compose.material.icons.outlined.Sync
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.SuggestionChip
@ -136,7 +136,6 @@ fun MangaInfoBox(
coverDataProvider = coverDataProvider,
onCoverClick = onCoverClick,
title = title,
context = LocalContext.current,
doSearch = doSearch,
author = author,
artist = artist,
@ -150,7 +149,6 @@ fun MangaInfoBox(
coverDataProvider = coverDataProvider,
onCoverClick = onCoverClick,
title = title,
context = LocalContext.current,
doSearch = doSearch,
author = author,
artist = artist,
@ -335,7 +333,6 @@ private fun MangaAndSourceTitlesLarge(
coverDataProvider: () -> Manga,
onCoverClick: () -> Unit,
title: String,
context: Context,
doSearch: (query: String, global: Boolean) -> Unit,
author: String?,
artist: String?,
@ -356,104 +353,16 @@ private fun MangaAndSourceTitlesLarge(
onClick = onCoverClick,
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = title.ifBlank { stringResource(R.string.unknown_title) },
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.clickableNoIndication(
onLongClick = { if (title.isNotBlank()) context.copyToClipboard(title, title) },
onClick = { if (title.isNotBlank()) doSearch(title, true) },
),
MangaContentInfo(
title = title,
doSearch = doSearch,
author = author,
artist = artist,
status = status,
sourceName = sourceName,
isStubSource = isStubSource,
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.height(2.dp))
Text(
text = author?.takeIf { it.isNotBlank() } ?: stringResource(R.string.unknown_author),
style = MaterialTheme.typography.titleSmall,
modifier = Modifier
.secondaryItemAlpha()
.padding(top = 2.dp)
.clickableNoIndication(
onLongClick = {
if (!author.isNullOrBlank()) {
context.copyToClipboard(
author,
author,
)
}
},
onClick = { if (!author.isNullOrBlank()) doSearch(author, true) },
),
textAlign = TextAlign.Center,
)
if (!artist.isNullOrBlank() && author != artist) {
Text(
text = artist,
style = MaterialTheme.typography.titleSmall,
modifier = Modifier
.secondaryItemAlpha()
.padding(top = 2.dp)
.clickableNoIndication(
onLongClick = { context.copyToClipboard(artist, artist) },
onClick = { doSearch(artist, true) },
),
textAlign = TextAlign.Center,
)
}
Spacer(modifier = Modifier.height(4.dp))
Row(
modifier = Modifier.secondaryItemAlpha(),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = when (status) {
SManga.ONGOING.toLong() -> Icons.Outlined.Schedule
SManga.COMPLETED.toLong() -> Icons.Outlined.DoneAll
SManga.LICENSED.toLong() -> Icons.Outlined.AttachMoney
SManga.PUBLISHING_FINISHED.toLong() -> Icons.Outlined.Done
SManga.CANCELLED.toLong() -> Icons.Outlined.Close
SManga.ON_HIATUS.toLong() -> Icons.Outlined.Pause
else -> Icons.Outlined.Block
},
contentDescription = null,
modifier = Modifier
.padding(end = 4.dp)
.size(16.dp),
)
ProvideTextStyle(MaterialTheme.typography.bodyMedium) {
Text(
text = when (status) {
SManga.ONGOING.toLong() -> stringResource(R.string.ongoing)
SManga.COMPLETED.toLong() -> stringResource(R.string.completed)
SManga.LICENSED.toLong() -> stringResource(R.string.licensed)
SManga.PUBLISHING_FINISHED.toLong() -> stringResource(
R.string.publishing_finished,
)
SManga.CANCELLED.toLong() -> stringResource(R.string.cancelled)
SManga.ON_HIATUS.toLong() -> stringResource(R.string.on_hiatus)
else -> stringResource(R.string.unknown)
},
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
DotSeparatorText()
if (isStubSource) {
Icon(
imageVector = Icons.Filled.Warning,
contentDescription = null,
modifier = Modifier
.padding(end = 4.dp)
.size(16.dp),
tint = MaterialTheme.colorScheme.error,
)
}
Text(
text = sourceName,
modifier = Modifier.clickableNoIndication { doSearch(sourceName, false) },
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
}
}
}
}
@ -463,7 +372,6 @@ private fun MangaAndSourceTitlesSmall(
coverDataProvider: () -> Manga,
onCoverClick: () -> Unit,
title: String,
context: Context,
doSearch: (query: String, global: Boolean) -> Unit,
author: String?,
artist: String?,
@ -489,6 +397,31 @@ private fun MangaAndSourceTitlesSmall(
Column(
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 = title.ifBlank { stringResource(R.string.unknown_title) },
style = MaterialTheme.typography.titleLarge,
@ -503,9 +436,11 @@ private fun MangaAndSourceTitlesSmall(
},
onClick = { if (title.isNotBlank()) doSearch(title, true) },
),
textAlign = textAlign,
)
Spacer(modifier = Modifier.height(2.dp))
Row(
modifier = Modifier.secondaryItemAlpha(),
horizontalArrangement = Arrangement.spacedBy(4.dp),
@ -532,6 +467,7 @@ private fun MangaAndSourceTitlesSmall(
},
onClick = { if (!author.isNullOrBlank()) doSearch(author, true) },
),
textAlign = textAlign,
)
}
@ -554,6 +490,7 @@ private fun MangaAndSourceTitlesSmall(
onLongClick = { context.copyToClipboard(artist, artist) },
onClick = { doSearch(artist, true) },
),
textAlign = textAlign,
)
}
}
@ -585,9 +522,7 @@ private fun MangaAndSourceTitlesSmall(
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.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)
@ -620,8 +555,6 @@ private fun MangaAndSourceTitlesSmall(
}
}
}
}
}
@Composable
private fun MangaSummary(

View file

@ -4,12 +4,15 @@ import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Environment
import android.text.format.Formatter
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
@ -32,6 +35,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.more.settings.widget.BasePreferenceWidget
import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding
import eu.kanade.presentation.permissions.PermissionRequestHelper
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.BackupConst
@ -41,6 +46,7 @@ import eu.kanade.tachiyomi.data.backup.BackupRestoreJob
import eu.kanade.tachiyomi.data.backup.models.Backup
import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.cache.EpisodeCache
import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.copyToClipboard
import eu.kanade.tachiyomi.util.system.toast
@ -403,19 +409,22 @@ object SettingsDataScreen : SearchableSettings {
val chapterCache = remember { Injekt.get<ChapterCache>() }
val episodeCache = remember { Injekt.get<EpisodeCache>() }
var readableSizeSema by remember { mutableIntStateOf(0) }
val readableSize = remember(readableSizeSema) { chapterCache.readableSize }
val readableAnimeSize = remember(readableSizeSema) { episodeCache.readableSize }
var cacheReadableSizeSema by remember { mutableIntStateOf(0) }
val cacheReadableMangaSize = remember(cacheReadableSizeSema) { chapterCache.readableSize }
val cacheReadableAnimeSize = remember(cacheReadableSizeSema) { episodeCache.readableSize }
return Preference.PreferenceGroup(
title = stringResource(R.string.label_data),
preferenceItems = listOf(
getMangaStorageInfoPref(cacheReadableMangaSize),
getAnimeStorageInfoPref(cacheReadableAnimeSize),
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_clear_chapter_cache),
subtitle = stringResource(
R.string.used_cache_both,
readableAnimeSize,
readableSize,
cacheReadableAnimeSize,
cacheReadableMangaSize,
),
onClick = {
scope.launchNonCancellable {
@ -423,7 +432,7 @@ object SettingsDataScreen : SearchableSettings {
val deletedFiles = chapterCache.clear() + episodeCache.clear()
withUIContext {
context.toast(context.getString(R.string.cache_deleted, deletedFiles))
readableSizeSema++
cacheReadableSizeSema++
}
} catch (e: Throwable) {
logcat(LogPriority.ERROR, e)
@ -460,6 +469,60 @@ object SettingsDataScreen : SearchableSettings {
),
)
}
@Composable
fun getMangaStorageInfoPref(
chapterCacheReadableSize: String,
): Preference.PreferenceItem.CustomPreference {
val context = LocalContext.current
val available = remember {
Formatter.formatFileSize(context, DiskUtil.getAvailableStorageSpace(Environment.getDataDirectory()))
}
val total = remember {
Formatter.formatFileSize(context, DiskUtil.getTotalStorageSpace(Environment.getDataDirectory()))
}
return Preference.PreferenceItem.CustomPreference(
title = stringResource(R.string.pref_manga_storage_usage),
) {
BasePreferenceWidget(
title = stringResource(R.string.pref_manga_storage_usage),
subcomponent = {
// TODO: downloads, SD cards, bar representation?, i18n
Box(modifier = Modifier.padding(horizontal = PrefsHorizontalPadding)) {
Text(text = "Available: $available / $total (chapter cache: $chapterCacheReadableSize)")
}
},
)
}
}
@Composable
fun getAnimeStorageInfoPref(
episodeCacheReadableSize: String,
): Preference.PreferenceItem.CustomPreference {
val context = LocalContext.current
val available = remember {
Formatter.formatFileSize(context, DiskUtil.getAvailableStorageSpace(Environment.getDataDirectory()))
}
val total = remember {
Formatter.formatFileSize(context, DiskUtil.getTotalStorageSpace(Environment.getDataDirectory()))
}
return Preference.PreferenceItem.CustomPreference(
title = stringResource(R.string.pref_anime_storage_usage),
) {
BasePreferenceWidget(
title = stringResource(R.string.pref_anime_storage_usage),
subcomponent = {
// TODO: downloads, SD cards, bar representation?, i18n
Box(modifier = Modifier.padding(horizontal = PrefsHorizontalPadding)) {
Text(text = "Available: $available / $total (Episode cache: $episodeCacheReadableSize)")
}
},
)
}
}
}
private data class MissingRestoreComponents(

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,8 +1,14 @@
package eu.kanade.presentation.util
import android.content.Context
import android.text.format.DateUtils
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.res.stringResource
import eu.kanade.tachiyomi.R
import java.util.Date
import kotlin.time.Duration
import kotlin.time.Duration.Companion.minutes
fun Duration.toDurationString(context: Context, fallback: String): String {
return toComponents { days, hours, minutes, seconds, _ ->
@ -22,3 +28,14 @@ fun Duration.toDurationString(context: Context, fallback: String): String {
}.joinToString(" ").ifBlank { fallback }
}
}
@Composable
@ReadOnlyComposable
fun relativeTimeSpanString(epochMillis: Long): String {
val now = Date().time
return when {
epochMillis <= 0L -> stringResource(R.string.relative_time_span_never)
now - epochMillis < 1.minutes.inWholeMilliseconds -> stringResource(R.string.updates_last_update_info_just_now)
else -> DateUtils.getRelativeTimeSpanString(epochMillis, now, DateUtils.MINUTE_IN_MILLIS).toString()
}
}

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.toast
import eu.kanade.tachiyomi.util.system.workManager
import tachiyomi.core.preference.Preference
import tachiyomi.core.preference.PreferenceStore
import tachiyomi.core.preference.TriState
import tachiyomi.core.preference.getAndSet
@ -503,6 +504,30 @@ object Migrations {
uiPreferences.relativeTime().set(false)
}
}
if (oldVersion < 107) {
replacePreferences(
preferenceStore = preferenceStore,
filterPredicate = { it.key.startsWith("pref_mangasync_") || it.key.startsWith("track_token_") },
newKey = { Preference.privateKey(it) },
)
}
if (oldVersion < 108) {
val prefsToReplace = listOf(
"pref_download_only",
"incognito_mode",
"last_catalogue_source",
"trusted_signatures",
"last_app_closed",
"library_update_last_timestamp",
"library_unseen_updates_count",
"last_used_category",
)
replacePreferences(
preferenceStore = preferenceStore,
filterPredicate = { it.key in prefsToReplace },
newKey = { Preference.appStateKey(it) },
)
}
return true
}
}
@ -510,3 +535,41 @@ object Migrations {
return false
}
}
@Suppress("UNCHECKED_CAST")
private fun replacePreferences(
preferenceStore: PreferenceStore,
filterPredicate: (Map.Entry<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.gzip
import okio.sink
import tachiyomi.core.preference.Preference
import tachiyomi.core.util.system.logcat
import tachiyomi.data.handlers.anime.AnimeDatabaseHandler
import tachiyomi.data.handlers.manga.MangaDatabaseHandler
@ -444,6 +445,6 @@ class BackupCreator(
}
backupPreferences.add(toAdd)
}
return backupPreferences
return backupPreferences.filter { !Preference.isPrivate(it.key) && !Preference.isAppState(it.key) }
}
}

View file

@ -11,8 +11,6 @@ import dataanime.Anime_sync
import dataanime.Animes
import eu.kanade.domain.entries.anime.interactor.UpdateAnime
import eu.kanade.domain.entries.manga.interactor.UpdateManga
import eu.kanade.domain.items.chapter.model.copyFrom
import eu.kanade.domain.items.episode.model.copyFrom
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.models.BackupAnime
import eu.kanade.tachiyomi.data.backup.models.BackupAnimeHistory
@ -51,7 +49,9 @@ import tachiyomi.domain.entries.manga.interactor.MangaFetchInterval
import tachiyomi.domain.entries.manga.model.Manga
import tachiyomi.domain.history.anime.model.AnimeHistoryUpdate
import tachiyomi.domain.history.manga.model.MangaHistoryUpdate
import tachiyomi.domain.items.chapter.interactor.GetChaptersByMangaId
import tachiyomi.domain.items.chapter.model.Chapter
import tachiyomi.domain.items.episode.interactor.GetEpisodesByAnimeId
import tachiyomi.domain.items.episode.model.Episode
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.track.anime.model.AnimeTrack
@ -73,11 +73,13 @@ class BackupRestorer(
private val mangaHandler: MangaDatabaseHandler = Injekt.get()
private val updateManga: UpdateManga = Injekt.get()
private val getMangaCategories: GetMangaCategories = Injekt.get()
private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get()
private val mangaFetchInterval: MangaFetchInterval = Injekt.get()
private val animeHandler: AnimeDatabaseHandler = Injekt.get()
private val updateAnime: UpdateAnime = Injekt.get()
private val getAnimeCategories: GetAnimeCategories = Injekt.get()
private val getEpisodesByAnimeId: GetEpisodesByAnimeId = Injekt.get()
private val animeFetchInterval: AnimeFetchInterval = Injekt.get()
private val libraryPreferences: LibraryPreferences = Injekt.get()
@ -423,33 +425,38 @@ class BackupRestorer(
manga: Manga,
chapters: List<Chapter>,
) {
val dbChapters = mangaHandler.awaitList { chaptersQueries.getChaptersByMangaId(manga.id!!) }
val dbChaptersByUrl = getChaptersByMangaId.await(manga.id)
.associateBy { it.url }
val processed = chapters.map { chapter ->
var updatedChapter = chapter
val dbChapter = dbChapters.find { it.url == updatedChapter.url }
val dbChapter = dbChaptersByUrl[updatedChapter.url]
if (dbChapter != null) {
updatedChapter = updatedChapter.copy(id = dbChapter._id)
updatedChapter = updatedChapter.copyFrom(dbChapter)
updatedChapter = updatedChapter
.copyFrom(dbChapter)
.copy(
id = dbChapter.id,
bookmark = updatedChapter.bookmark || dbChapter.bookmark,
)
if (dbChapter.read && !updatedChapter.read) {
updatedChapter = updatedChapter.copy(
read = true,
lastPageRead = dbChapter.last_page_read,
lastPageRead = dbChapter.lastPageRead,
)
} else if (updatedChapter.lastPageRead == 0L && dbChapter.lastPageRead != 0L) {
updatedChapter = updatedChapter.copy(
lastPageRead = dbChapter.lastPageRead,
)
} else if (chapter.lastPageRead == 0L && dbChapter.last_page_read != 0L) {
updatedChapter = updatedChapter.copy(lastPageRead = dbChapter.last_page_read)
}
if (!updatedChapter.bookmark && dbChapter.bookmark) {
updatedChapter = updatedChapter.copy(bookmark = true)
}
}
updatedChapter.copy(mangaId = manga.id ?: -1)
updatedChapter.copy(mangaId = manga.id)
}
val newChapters = processed.groupBy { it.id > 0 }
newChapters[true]?.let { updateKnownChapters(it) }
newChapters[false]?.let { insertChapters(it) }
val (existingChapters, newChapters) = processed.partition { it.id > 0 }
updateKnownChapters(existingChapters)
insertChapters(newChapters)
}
/**
@ -870,43 +877,46 @@ class BackupRestorer(
private suspend fun restoreEpisodes(
anime: Anime,
episodes: List<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 ->
var updatedEpisode = episode
val dbEpisode = dbEpisodes.find { it.url == updatedEpisode.url }
val dbEpisode = dbEpisodesByUrl[updatedEpisode.url]
if (dbEpisode != null) {
updatedEpisode = updatedEpisode.copy(id = dbEpisode._id)
updatedEpisode = updatedEpisode.copyFrom(dbEpisode)
updatedEpisode = updatedEpisode
.copyFrom(dbEpisode)
.copy(
id = dbEpisode.id,
bookmark = updatedEpisode.bookmark || dbEpisode.bookmark,
)
if (dbEpisode.seen && !updatedEpisode.seen) {
updatedEpisode = updatedEpisode.copy(
seen = true,
lastSecondSeen = dbEpisode.last_second_seen,
lastSecondSeen = dbEpisode.lastSecondSeen,
)
} else if (updatedEpisode.lastSecondSeen == 0L && dbEpisode.last_second_seen != 0L) {
} else if (updatedEpisode.lastSecondSeen == 0L && dbEpisode.lastSecondSeen != 0L) {
updatedEpisode = updatedEpisode.copy(
lastSecondSeen = dbEpisode.last_second_seen,
lastSecondSeen = dbEpisode.lastSecondSeen,
)
}
if (!updatedEpisode.bookmark && dbEpisode.bookmark) {
updatedEpisode = updatedEpisode.copy(bookmark = true)
}
}
updatedEpisode.copy(animeId = anime.id ?: -1)
updatedEpisode.copy(animeId = anime.id)
}
val newEpisodes = processed.groupBy { it.id > 0 }
newEpisodes[true]?.let { updateKnownEpisodes(it) }
newEpisodes[false]?.let { insertEpisodes(it) }
val (existingEpisodes, newEpisodes) = processed.partition { it.id > 0 }
updateKnownEpisodes(existingEpisodes)
insertEpisodes(newEpisodes)
}
/**
* Inserts list of episodes
*/
private suspend fun insertEpisodes(episodes: List<tachiyomi.domain.items.episode.model.Episode>) {
private suspend fun insertEpisodes(episodes: List<Episode>) {
animeHandler.await(true) {
episodes.forEach { episode ->
episodesQueries.insert(
@ -931,7 +941,7 @@ class BackupRestorer(
* Updates a list of episodes with known database ids
*/
private suspend fun updateKnownEpisodes(
episodes: List<tachiyomi.domain.items.episode.model.Episode>,
episodes: List<Episode>,
) {
animeHandler.await(true) {
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.system.logcat
import tachiyomi.domain.history.anime.interactor.GetAnimeHistory
import tachiyomi.domain.items.episode.interactor.GetEpisodeByAnimeId
import tachiyomi.domain.items.episode.interactor.GetEpisodesByAnimeId
import tachiyomi.domain.track.anime.interactor.InsertAnimeTrack
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -62,7 +62,7 @@ interface AnimeTracker {
item.anime_id = animeId
try {
withIOContext {
val allEpisodes = Injekt.get<GetEpisodeByAnimeId>().await(animeId)
val allEpisodes = Injekt.get<GetEpisodesByAnimeId>().await(animeId)
val hasSeenEpisodes = allEpisodes.any { it.seen }
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.system.logcat
import tachiyomi.domain.history.manga.interactor.GetMangaHistory
import tachiyomi.domain.items.chapter.interactor.GetChapterByMangaId
import tachiyomi.domain.items.chapter.interactor.GetChaptersByMangaId
import tachiyomi.domain.track.manga.interactor.InsertMangaTrack
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -62,7 +62,7 @@ interface MangaTracker {
item.manga_id = mangaId
try {
withIOContext {
val allChapters = Injekt.get<GetChapterByMangaId>().await(mangaId)
val allChapters = Injekt.get<GetChaptersByMangaId>().await(mangaId)
val hasReadChapters = allChapters.any { it.read }
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.entries.anime.model.Anime
import tachiyomi.domain.entries.anime.model.AnimeUpdate
import tachiyomi.domain.items.episode.interactor.GetEpisodeByAnimeId
import tachiyomi.domain.items.episode.interactor.GetEpisodesByAnimeId
import tachiyomi.domain.items.episode.interactor.UpdateEpisode
import tachiyomi.domain.items.episode.model.toEpisodeUpdate
import tachiyomi.domain.source.anime.service.AnimeSourceManager
@ -150,7 +150,7 @@ internal class MigrateAnimeDialogScreenModel(
private val sourceManager: AnimeSourceManager = Injekt.get(),
private val downloadManager: AnimeDownloadManager = Injekt.get(),
private val updateAnime: UpdateAnime = Injekt.get(),
private val getEpisodeByAnimeId: GetEpisodeByAnimeId = Injekt.get(),
private val getEpisodesByAnimeId: GetEpisodesByAnimeId = Injekt.get(),
private val syncEpisodesWithSource: SyncEpisodesWithSource = Injekt.get(),
private val updateEpisode: UpdateEpisode = Injekt.get(),
private val getCategories: GetAnimeCategories = Injekt.get(),
@ -222,8 +222,8 @@ internal class MigrateAnimeDialogScreenModel(
// Update chapters read, bookmark and dateFetch
if (migrateEpisodes) {
val prevAnimeEpisodes = getEpisodeByAnimeId.await(oldAnime.id)
val animeEpisodes = getEpisodeByAnimeId.await(newAnime.id)
val prevAnimeEpisodes = getEpisodesByAnimeId.await(oldAnime.id)
val animeEpisodes = getEpisodesByAnimeId.await(newAnime.id)
val maxEpisodeSeen = prevAnimeEpisodes
.filter { it.seen }

View file

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

View file

@ -10,6 +10,7 @@ import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.core.preference.asState
import eu.kanade.core.util.addOrRemove
import eu.kanade.core.util.insertSeparators
import eu.kanade.domain.entries.anime.interactor.SetAnimeViewerFlags
import eu.kanade.domain.entries.anime.interactor.UpdateAnime
import eu.kanade.domain.entries.anime.model.downloadedFilter
@ -74,6 +75,7 @@ import tachiyomi.domain.items.episode.model.Episode
import tachiyomi.domain.items.episode.model.EpisodeUpdate
import tachiyomi.domain.items.episode.model.NoEpisodesException
import tachiyomi.domain.items.episode.service.getEpisodeSort
import tachiyomi.domain.items.service.calculateEpisodeGap
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.source.anime.service.AnimeSourceManager
import tachiyomi.domain.track.anime.interactor.GetAnimeTracks
@ -81,6 +83,7 @@ import tachiyomi.source.local.entries.anime.isLocal
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Calendar
import kotlin.math.floor
class AnimeScreenModel(
val context: Context,
@ -125,7 +128,7 @@ class AnimeScreenModel(
private val isFavorited: Boolean
get() = anime?.favorite ?: false
private val processedEpisodes: List<EpisodeItem>?
private val processedEpisodes: List<EpisodeList.Item>?
get() = successState?.processedEpisodes
val episodeSwipeStartAction = libraryPreferences.swipeEpisodeEndAction().get()
@ -171,7 +174,7 @@ class AnimeScreenModel(
updateSuccessState {
it.copy(
anime = anime,
episodes = episodes.toEpisodeItems(anime),
episodes = episodes.toEpisodeListItems(anime),
)
}
}
@ -182,7 +185,7 @@ class AnimeScreenModel(
screenModelScope.launchIO {
val anime = getAnimeAndEpisodes.awaitAnime(animeId)
val episodes = getAnimeAndEpisodes.awaitEpisodes(animeId)
.toEpisodeItems(anime)
.toEpisodeListItems(anime)
if (!anime.favorite) {
setAnimeDefaultEpisodeFlags.await(anime)
@ -477,7 +480,7 @@ class AnimeScreenModel(
private fun updateDownloadState(download: AnimeDownload) {
updateSuccessState { successState ->
val modifiedIndex = successState.episodes.indexOfFirst { it.episode.id == download.episode.id }
val modifiedIndex = successState.episodes.indexOfFirst { it.id == download.episode.id }
if (modifiedIndex < 0) return@updateSuccessState successState
val newEpisodes = successState.episodes.toMutableList().apply {
@ -489,7 +492,7 @@ class AnimeScreenModel(
}
}
private fun List<Episode>.toEpisodeItems(anime: Anime): List<EpisodeItem> {
private fun List<Episode>.toEpisodeListItems(anime: Anime): List<EpisodeList.Item> {
val isLocal = anime.isLocal()
return map { episode ->
val activeDownload = if (isLocal) {
@ -513,7 +516,7 @@ class AnimeScreenModel(
else -> AnimeDownload.State.NOT_DOWNLOADED
}
EpisodeItem(
EpisodeList.Item(
episode = episode,
downloadState = downloadState,
downloadProgress = activeDownload?.progress ?: 0,
@ -561,7 +564,7 @@ class AnimeScreenModel(
/**
* @throws IllegalStateException if the swipe action is [LibraryPreferences.EpisodeSwipeAction.Disabled]
*/
fun episodeSwipe(episodeItem: EpisodeItem, swipeAction: LibraryPreferences.EpisodeSwipeAction) {
fun episodeSwipe(episodeItem: EpisodeList.Item, swipeAction: LibraryPreferences.EpisodeSwipeAction) {
screenModelScope.launch {
executeEpisodeSwipeAction(episodeItem, swipeAction)
}
@ -571,7 +574,7 @@ class AnimeScreenModel(
* @throws IllegalStateException if the swipe action is [LibraryPreferences.EpisodeSwipeAction.Disabled]
*/
private fun executeEpisodeSwipeAction(
episodeItem: EpisodeItem,
episodeItem: EpisodeList.Item,
swipeAction: LibraryPreferences.EpisodeSwipeAction,
) {
val episode = episodeItem.episode
@ -654,7 +657,7 @@ class AnimeScreenModel(
}
fun runEpisodeDownloadActions(
items: List<EpisodeItem>,
items: List<EpisodeList.Item>,
action: EpisodeDownloadAction,
) {
when (action) {
@ -669,7 +672,7 @@ class AnimeScreenModel(
startDownload(listOf(episode), true)
}
EpisodeDownloadAction.CANCEL -> {
val episodeId = items.singleOrNull()?.episode?.id ?: return
val episodeId = items.singleOrNull()?.id ?: return
cancelDownload(episodeId)
}
EpisodeDownloadAction.DELETE -> {
@ -880,14 +883,14 @@ class AnimeScreenModel(
}
fun toggleSelection(
item: EpisodeItem,
item: EpisodeList.Item,
selected: Boolean,
userSelected: Boolean = false,
fromLongPress: Boolean = false,
) {
updateSuccessState { successState ->
val newEpisodes = successState.processedEpisodes.toMutableList().apply {
val selectedIndex = successState.processedEpisodes.indexOfFirst { it.episode.id == item.episode.id }
val selectedIndex = successState.processedEpisodes.indexOfFirst { it.id == item.episode.id }
if (selectedIndex < 0) return@apply
val selectedItem = get(selectedIndex)
@ -895,7 +898,7 @@ class AnimeScreenModel(
val firstSelection = none { it.selected }
set(selectedIndex, selectedItem.copy(selected = selected))
selectedEpisodeIds.addOrRemove(item.episode.id, selected)
selectedEpisodeIds.addOrRemove(item.id, selected)
if (selected && userSelected && fromLongPress) {
if (firstSelection) {
@ -918,7 +921,7 @@ class AnimeScreenModel(
range.forEach {
val inbetweenItem = get(it)
if (!inbetweenItem.selected) {
selectedEpisodeIds.add(inbetweenItem.episode.id)
selectedEpisodeIds.add(inbetweenItem.id)
set(it, inbetweenItem.copy(selected = true))
}
}
@ -946,7 +949,7 @@ class AnimeScreenModel(
fun toggleAllSelection(selected: Boolean) {
updateSuccessState { successState ->
val newEpisodes = successState.episodes.map {
selectedEpisodeIds.addOrRemove(it.episode.id, selected)
selectedEpisodeIds.addOrRemove(it.id, selected)
it.copy(selected = selected)
}
selectedPositions[0] = -1
@ -958,7 +961,7 @@ class AnimeScreenModel(
fun invertSelection() {
updateSuccessState { successState ->
val newEpisodes = successState.episodes.map {
selectedEpisodeIds.addOrRemove(it.episode.id, !it.selected)
selectedEpisodeIds.addOrRemove(it.id, !it.selected)
it.copy(selected = !it.selected)
}
selectedPositions[0] = -1
@ -1060,7 +1063,7 @@ class AnimeScreenModel(
val anime: Anime,
val source: AnimeSource,
val isFromSource: Boolean,
val episodes: List<EpisodeItem>,
val episodes: List<EpisodeList.Item>,
val trackItems: List<AnimeTrackItem> = emptyList(),
val isRefreshingData: Boolean = false,
val dialog: Dialog? = null,
@ -1075,6 +1078,33 @@ class AnimeScreenModel(
episodes.applyFilters(anime).toList()
}
val episodeListItems by lazy {
processedEpisodes.insertSeparators { before, after ->
val (lowerEpisode, higherEpisode) = if (anime.sortDescending()) {
after to before
} else {
before to after
}
if (higherEpisode == null) return@insertSeparators null
if (lowerEpisode == null) {
floor(higherEpisode.episode.episodeNumber)
.toInt()
.minus(1)
.coerceAtLeast(0)
} else {
calculateEpisodeGap(higherEpisode.episode, lowerEpisode.episode)
}
.takeIf { it > 0 }
?.let { missingCount ->
EpisodeList.MissingCount(
id = "${lowerEpisode?.id}-${higherEpisode.id}",
count = missingCount,
)
}
}
}
val trackingAvailable: Boolean
get() = trackItems.isNotEmpty()
@ -1093,7 +1123,7 @@ class AnimeScreenModel(
* Applies the view filters to the list of episodes obtained from the database.
* @return an observable of the list of episodes filtered and sorted.
*/
private fun List<EpisodeItem>.applyFilters(anime: Anime): Sequence<EpisodeItem> {
private fun List<EpisodeList.Item>.applyFilters(anime: Anime): Sequence<EpisodeList.Item> {
val isLocalAnime = anime.isLocal()
val unseenFilter = anime.unseenFilter
val downloadedFilter = anime.downloadedFilter
@ -1114,11 +1144,21 @@ class AnimeScreenModel(
}
@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 downloadState: AnimeDownload.State,
val downloadProgress: Int,
val selected: Boolean = false,
) {
) : EpisodeList() {
val id = episode.id
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 eu.kanade.core.preference.asState
import eu.kanade.core.util.addOrRemove
import eu.kanade.core.util.insertSeparators
import eu.kanade.domain.entries.manga.interactor.UpdateManga
import eu.kanade.domain.entries.manga.model.downloadedFilter
import eu.kanade.domain.entries.manga.model.toSManga
@ -71,12 +72,14 @@ import tachiyomi.domain.items.chapter.model.Chapter
import tachiyomi.domain.items.chapter.model.ChapterUpdate
import tachiyomi.domain.items.chapter.model.NoChaptersException
import tachiyomi.domain.items.chapter.service.getChapterSort
import tachiyomi.domain.items.service.calculateChapterGap
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.source.manga.service.MangaSourceManager
import tachiyomi.domain.track.manga.interactor.GetMangaTracks
import tachiyomi.source.local.entries.manga.isLocal
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import kotlin.math.floor
class MangaScreenModel(
val context: Context,
@ -120,10 +123,10 @@ class MangaScreenModel(
private val isFavorited: Boolean
get() = manga?.favorite ?: false
private val allChapters: List<ChapterItem>?
private val allChapters: List<ChapterList.Item>?
get() = successState?.chapters
private val filteredChapters: List<ChapterItem>?
private val filteredChapters: List<ChapterList.Item>?
get() = successState?.processedChapters
val chapterSwipeStartAction = libraryPreferences.swipeChapterEndAction().get()
@ -166,7 +169,7 @@ class MangaScreenModel(
updateSuccessState {
it.copy(
manga = manga,
chapters = chapters.toChapterItems(manga),
chapters = chapters.toChapterListItems(manga),
)
}
}
@ -177,7 +180,7 @@ class MangaScreenModel(
screenModelScope.launchIO {
val manga = getMangaAndChapters.awaitManga(mangaId)
val chapters = getMangaAndChapters.awaitChapters(mangaId)
.toChapterItems(manga)
.toChapterListItems(manga)
if (!manga.favorite) {
setMangaDefaultChapterFlags.await(manga)
@ -473,7 +476,7 @@ class MangaScreenModel(
private fun updateDownloadState(download: MangaDownload) {
updateSuccessState { successState ->
val modifiedIndex = successState.chapters.indexOfFirst { it.chapter.id == download.chapter.id }
val modifiedIndex = successState.chapters.indexOfFirst { it.id == download.chapter.id }
if (modifiedIndex < 0) return@updateSuccessState successState
val newChapters = successState.chapters.toMutableList().apply {
@ -485,7 +488,7 @@ class MangaScreenModel(
}
}
private fun List<Chapter>.toChapterItems(manga: Manga): List<ChapterItem> {
private fun List<Chapter>.toChapterListItems(manga: Manga): List<ChapterList.Item> {
val isLocal = manga.isLocal()
return map { chapter ->
val activeDownload = if (isLocal) {
@ -509,7 +512,7 @@ class MangaScreenModel(
else -> MangaDownload.State.NOT_DOWNLOADED
}
ChapterItem(
ChapterList.Item(
chapter = chapter,
downloadState = downloadState,
downloadProgress = activeDownload?.progress ?: 0,
@ -557,7 +560,7 @@ class MangaScreenModel(
/**
* @throws IllegalStateException if the swipe action is [LibraryPreferences.ChapterSwipeAction.Disabled]
*/
fun chapterSwipe(chapterItem: ChapterItem, swipeAction: LibraryPreferences.ChapterSwipeAction) {
fun chapterSwipe(chapterItem: ChapterList.Item, swipeAction: LibraryPreferences.ChapterSwipeAction) {
screenModelScope.launch {
executeChapterSwipeAction(chapterItem, swipeAction)
}
@ -567,7 +570,7 @@ class MangaScreenModel(
* @throws IllegalStateException if the swipe action is [LibraryPreferences.ChapterSwipeAction.Disabled]
*/
private fun executeChapterSwipeAction(
chapterItem: ChapterItem,
chapterItem: ChapterList.Item,
swipeAction: LibraryPreferences.ChapterSwipeAction,
) {
val chapter = chapterItem.chapter
@ -648,7 +651,7 @@ class MangaScreenModel(
}
fun runChapterDownloadActions(
items: List<ChapterItem>,
items: List<ChapterList.Item>,
action: ChapterDownloadAction,
) {
when (action) {
@ -663,7 +666,7 @@ class MangaScreenModel(
startDownload(listOf(chapter), true)
}
ChapterDownloadAction.CANCEL -> {
val chapterId = items.singleOrNull()?.chapter?.id ?: return
val chapterId = items.singleOrNull()?.id ?: return
cancelDownload(chapterId)
}
ChapterDownloadAction.DELETE -> {
@ -873,14 +876,14 @@ class MangaScreenModel(
}
fun toggleSelection(
item: ChapterItem,
item: ChapterList.Item,
selected: Boolean,
userSelected: Boolean = false,
fromLongPress: Boolean = false,
) {
updateSuccessState { successState ->
val newChapters = successState.processedChapters.toMutableList().apply {
val selectedIndex = successState.processedChapters.indexOfFirst { it.chapter.id == item.chapter.id }
val selectedIndex = successState.processedChapters.indexOfFirst { it.id == item.chapter.id }
if (selectedIndex < 0) return@apply
val selectedItem = get(selectedIndex)
@ -888,7 +891,7 @@ class MangaScreenModel(
val firstSelection = none { it.selected }
set(selectedIndex, selectedItem.copy(selected = selected))
selectedChapterIds.addOrRemove(item.chapter.id, selected)
selectedChapterIds.addOrRemove(item.id, selected)
if (selected && userSelected && fromLongPress) {
if (firstSelection) {
@ -911,7 +914,7 @@ class MangaScreenModel(
range.forEach {
val inbetweenItem = get(it)
if (!inbetweenItem.selected) {
selectedChapterIds.add(inbetweenItem.chapter.id)
selectedChapterIds.add(inbetweenItem.id)
set(it, inbetweenItem.copy(selected = true))
}
}
@ -939,7 +942,7 @@ class MangaScreenModel(
fun toggleAllSelection(selected: Boolean) {
updateSuccessState { successState ->
val newChapters = successState.chapters.map {
selectedChapterIds.addOrRemove(it.chapter.id, selected)
selectedChapterIds.addOrRemove(it.id, selected)
it.copy(selected = selected)
}
selectedPositions[0] = -1
@ -951,7 +954,7 @@ class MangaScreenModel(
fun invertSelection() {
updateSuccessState { successState ->
val newChapters = successState.chapters.map {
selectedChapterIds.addOrRemove(it.chapter.id, !it.selected)
selectedChapterIds.addOrRemove(it.id, !it.selected)
it.copy(selected = !it.selected)
}
selectedPositions[0] = -1
@ -1033,7 +1036,7 @@ class MangaScreenModel(
val manga: Manga,
val source: MangaSource,
val isFromSource: Boolean,
val chapters: List<ChapterItem>,
val chapters: List<ChapterList.Item>,
val trackItems: List<MangaTrackItem> = emptyList(),
val isRefreshingData: Boolean = false,
val dialog: Dialog? = null,
@ -1044,6 +1047,33 @@ class MangaScreenModel(
chapters.applyFilters(manga).toList()
}
val chapterListItems by lazy {
processedChapters.insertSeparators { before, after ->
val (lowerChapter, higherChapter) = if (manga.sortDescending()) {
after to before
} else {
before to after
}
if (higherChapter == null) return@insertSeparators null
if (lowerChapter == null) {
floor(higherChapter.chapter.chapterNumber)
.toInt()
.minus(1)
.coerceAtLeast(0)
} else {
calculateChapterGap(higherChapter.chapter, lowerChapter.chapter)
}
.takeIf { it > 0 }
?.let { missingCount ->
ChapterList.MissingCount(
id = "${lowerChapter?.id}-${higherChapter.id}",
count = missingCount,
)
}
}
}
val trackingAvailable: Boolean
get() = trackItems.isNotEmpty()
@ -1054,7 +1084,7 @@ class MangaScreenModel(
* Applies the view filters to the list of chapters obtained from the database.
* @return an observable of the list of chapters filtered and sorted.
*/
private fun List<ChapterItem>.applyFilters(manga: Manga): Sequence<ChapterItem> {
private fun List<ChapterList.Item>.applyFilters(manga: Manga): Sequence<ChapterList.Item> {
val isLocalManga = manga.isLocal()
val unreadFilter = manga.unreadFilter
val downloadedFilter = manga.downloadedFilter
@ -1075,11 +1105,21 @@ class MangaScreenModel(
}
@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 downloadState: MangaDownload.State,
val downloadProgress: Int,
val selected: Boolean = false,
) {
) : ChapterList() {
val id = chapter.id
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.applyFilter
import tachiyomi.domain.history.anime.interactor.GetNextEpisodes
import tachiyomi.domain.items.episode.interactor.GetEpisodeByAnimeId
import tachiyomi.domain.items.episode.interactor.GetEpisodesByAnimeId
import tachiyomi.domain.items.episode.model.Episode
import tachiyomi.domain.library.anime.LibraryAnime
import tachiyomi.domain.library.anime.model.AnimeLibrarySort
@ -79,7 +79,7 @@ class AnimeLibraryScreenModel(
private val getCategories: GetVisibleAnimeCategories = Injekt.get(),
private val getTracksPerAnime: GetTracksPerAnime = Injekt.get(),
private val getNextEpisodes: GetNextEpisodes = Injekt.get(),
private val getEpisodesByAnimeId: GetEpisodeByAnimeId = Injekt.get(),
private val getEpisodesByAnimeId: GetEpisodesByAnimeId = Injekt.get(),
private val setSeenStatus: SetSeenStatus = Injekt.get(),
private val updateAnime: UpdateAnime = Injekt.get(),
private val setAnimeCategories: SetAnimeCategories = Injekt.get(),

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

View file

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

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.UpsertAnimeHistory
import tachiyomi.domain.history.anime.model.AnimeHistoryUpdate
import tachiyomi.domain.items.episode.interactor.GetEpisodeByAnimeId
import tachiyomi.domain.items.episode.interactor.GetEpisodesByAnimeId
import tachiyomi.domain.items.episode.interactor.UpdateEpisode
import tachiyomi.domain.items.episode.model.EpisodeUpdate
import tachiyomi.domain.items.episode.service.getEpisodeSort
@ -81,7 +81,7 @@ class PlayerViewModel @JvmOverloads constructor(
private val trackEpisode: TrackEpisode = Injekt.get(),
private val getAnime: GetAnime = Injekt.get(),
private val getNextEpisodes: GetNextEpisodes = Injekt.get(),
private val getEpisodeByAnimeId: GetEpisodeByAnimeId = Injekt.get(),
private val getEpisodesByAnimeId: GetEpisodesByAnimeId = Injekt.get(),
private val getTracks: GetAnimeTracks = Injekt.get(),
private val upsertHistory: UpsertAnimeHistory = Injekt.get(),
private val updateEpisode: UpdateEpisode = Injekt.get(),
@ -284,7 +284,7 @@ class PlayerViewModel @JvmOverloads constructor(
)
private fun initEpisodeList(anime: Anime): List<Episode> {
val episodes = runBlocking { getEpisodeByAnimeId.await(anime.id) }
val episodes = runBlocking { getEpisodesByAnimeId.await(anime.id) }
return episodes
.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.UpsertMangaHistory
import tachiyomi.domain.history.manga.model.MangaHistoryUpdate
import tachiyomi.domain.items.chapter.interactor.GetChapterByMangaId
import tachiyomi.domain.items.chapter.interactor.GetChaptersByMangaId
import tachiyomi.domain.items.chapter.interactor.UpdateChapter
import tachiyomi.domain.items.chapter.model.ChapterUpdate
import tachiyomi.domain.items.chapter.service.getChapterSort
@ -92,7 +92,7 @@ class ReaderViewModel @JvmOverloads constructor(
private val trackPreferences: TrackPreferences = Injekt.get(),
private val trackChapter: TrackChapter = Injekt.get(),
private val getManga: GetManga = Injekt.get(),
private val getChapterByMangaId: GetChapterByMangaId = Injekt.get(),
private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(),
private val getNextChapters: GetNextChapters = Injekt.get(),
private val upsertHistory: UpsertMangaHistory = Injekt.get(),
private val updateChapter: UpdateChapter = Injekt.get(),
@ -147,7 +147,7 @@ class ReaderViewModel @JvmOverloads constructor(
*/
private val chapterList by lazy {
val manga = manga!!
val chapters = runBlocking { getChapterByMangaId.await(manga.id) }
val chapters = runBlocking { getChaptersByMangaId.await(manga.id) }
val selectedChapter = chapters.find { it.id == chapterId }
?: error("Requested chapter of id $chapterId not found in chapter list")

View file

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

View file

@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.util.chapter
import eu.kanade.domain.items.chapter.model.applyFilters
import eu.kanade.tachiyomi.data.download.manga.MangaDownloadManager
import eu.kanade.tachiyomi.ui.entries.manga.ChapterItem
import eu.kanade.tachiyomi.ui.entries.manga.ChapterList
import tachiyomi.domain.entries.manga.model.Manga
import tachiyomi.domain.items.chapter.model.Chapter
@ -22,7 +22,7 @@ fun List<Chapter>.getNextUnread(manga: Manga, downloadManager: MangaDownloadMana
/**
* 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 ->
if (manga.sortDescending()) {
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.tachiyomi.data.download.anime.AnimeDownloadManager
import eu.kanade.tachiyomi.ui.entries.anime.EpisodeItem
import eu.kanade.tachiyomi.ui.entries.anime.EpisodeList
import tachiyomi.domain.entries.anime.model.Anime
import tachiyomi.domain.items.episode.model.Episode
@ -22,7 +22,7 @@ fun List<Episode>.getNextUnseen(anime: Anime, downloadManager: AnimeDownloadMana
/**
* 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 ->
if (anime.sortDescending()) {
episodes.findLast { !it.episode.seen }

View file

@ -55,5 +55,5 @@ subprojects {
}
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.logging)
api(libs.okhttp.brotli)
api(libs.okhttp.dnsoverhttps)
api(libs.okio)

View file

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.core.security
import eu.kanade.tachiyomi.core.R
import tachiyomi.core.preference.Preference
import tachiyomi.core.preference.PreferenceStore
import tachiyomi.core.preference.getEnum
@ -20,7 +21,10 @@ class SecurityPreferences(
* For app lock. Will be set when there is a pending timed lock.
* Otherwise this pref should be deleted.
*/
fun lastAppClosed() = preferenceStore.getLong("last_app_closed", 0)
fun lastAppClosed() = preferenceStore.getLong(
Preference.appStateKey("last_app_closed"),
0,
)
enum class SecureScreenMode(val titleResId: Int) {
ALWAYS(R.string.lock_always),

View file

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

View file

@ -28,6 +28,30 @@ object DiskUtil {
return size
}
/**
* Gets the total space for the disk that a file path points to, in bytes.
*/
fun getTotalStorageSpace(file: File): Long {
return try {
val stat = StatFs(file.absolutePath)
stat.blockCountLong * stat.blockSizeLong
} catch (_: Exception) {
-1L
}
}
/**
* Gets the available space for the disk that a file path points to, in bytes.
*/
fun getAvailableStorageSpace(file: File): Long {
return try {
val stat = StatFs(file.absolutePath)
stat.availableBlocksLong * stat.blockSizeLong
} catch (_: Exception) {
-1L
}
}
/**
* Gets the available space for the disk that a file path points to, in bytes.
*/

View file

@ -21,6 +21,32 @@ interface Preference<T> {
fun changes(): Flow<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(

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

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

View file

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

View file

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

View file

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

View file

@ -18,6 +18,16 @@ data class Chapter(
val isRecognizedNumber: Boolean
get() = chapterNumber >= 0f
fun copyFrom(other: Chapter): Chapter {
return copy(
name = other.name,
url = other.url,
dateUpload = other.dateUpload,
chapterNumber = other.chapterNumber,
scanlator = other.scanlator?.ifBlank { null },
)
}
companion object {
fun create() = Chapter(
id = -1,

View file

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

View file

@ -19,6 +19,16 @@ data class Episode(
val isRecognizedNumber: Boolean
get() = episodeNumber >= 0f
fun copyFrom(other: Episode): Episode {
return copy(
name = other.name,
url = other.url,
dateUpload = other.dateUpload,
episodeNumber = other.episodeNumber,
scanlator = other.scanlator?.ifBlank { null },
)
}
companion object {
fun create() = Episode(
id = -1,

View file

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

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"
test-ext = "androidx.test.ext:junit-ktx:1.2.0-alpha01"
test-espresso-core = "androidx.test.espresso:espresso-core:3.6.0-alpha01"
test-uiautomator = "androidx.test.uiautomator:uiautomator:2.3.0-alpha04"
test-uiautomator = "androidx.test.uiautomator:uiautomator:2.3.0-alpha05"
[bundles]
lifecycle = ["lifecycle-common", "lifecycle-process", "lifecycle-runtimektx"]

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

View file

@ -158,6 +158,8 @@
<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_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="pref_auto_clear_chapter_cache">Clear episode/chapter cache on app launch</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_chapter_fetch_date">Chapter fetch date</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_search">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_canceled">Canceled restore</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 -->
<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="requires_app_restart">Requires app restart to take effect</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="download_cache_invalidated">Downloads index invalidated</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="updates_last_update_info">Library last updated: %s</string>
<string name="updates_last_update_info_just_now">Just now</string>
<string name="relative_time_span_never">Never</string>
<!-- History -->
<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.composed
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@ -69,7 +70,11 @@ fun TabText(
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Text(text = text)
Text(
text = text,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
if (badgeCount != null) {
Pill(
text = "$badgeCount",