mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-21 12:17:12 +03:00
parent
3fba4cbc2b
commit
89777e98b0
62 changed files with 1063 additions and 866 deletions
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()) }
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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>()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 }
|
||||
|
||||
|
|
|
@ -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 }
|
||||
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,100 +881,123 @@ 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
|
||||
|
||||
AnimeEpisodeListItem(
|
||||
title = if (anime.displayMode == Anime.EPISODE_DISPLAY_NUMBER) {
|
||||
stringResource(
|
||||
R.string.display_mode_episode,
|
||||
formatEpisodeNumber(episodeItem.episode.episodeNumber),
|
||||
)
|
||||
} else {
|
||||
episodeItem.episode.name
|
||||
},
|
||||
date = episodeItem.episode.dateUpload
|
||||
.takeIf { it > 0L }
|
||||
?.let {
|
||||
Date(it).toRelativeString(
|
||||
context,
|
||||
dateRelativeTime,
|
||||
dateFormat,
|
||||
when (item) {
|
||||
is EpisodeList.MissingCount -> {
|
||||
Row(
|
||||
modifier = Modifier.padding(
|
||||
horizontal = MaterialTheme.padding.medium,
|
||||
vertical = MaterialTheme.padding.small,
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium),
|
||||
) {
|
||||
HorizontalDivider(modifier = Modifier.weight(1f))
|
||||
Text(
|
||||
text = pluralStringResource(
|
||||
id = R.plurals.missing_items,
|
||||
count = item.count,
|
||||
item.count,
|
||||
),
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
)
|
||||
},
|
||||
watchProgress = episodeItem.episode.lastSecondSeen
|
||||
.takeIf { !episodeItem.episode.seen && it > 0L }
|
||||
?.let {
|
||||
stringResource(
|
||||
R.string.episode_progress,
|
||||
formatTime(it),
|
||||
formatTime(episodeItem.episode.totalSeconds),
|
||||
)
|
||||
},
|
||||
scanlator = episodeItem.episode.scanlator.takeIf { !it.isNullOrBlank() },
|
||||
seen = episodeItem.episode.seen,
|
||||
bookmark = episodeItem.episode.bookmark,
|
||||
selected = episodeItem.selected,
|
||||
downloadIndicatorEnabled = episodes.fastAll { !it.selected },
|
||||
downloadStateProvider = { episodeItem.downloadState },
|
||||
downloadProgressProvider = { episodeItem.downloadProgress },
|
||||
episodeSwipeStartAction = episodeSwipeStartAction,
|
||||
episodeSwipeEndAction = episodeSwipeEndAction,
|
||||
onLongClick = {
|
||||
onEpisodeSelected(episodeItem, !episodeItem.selected, true, true)
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
},
|
||||
onClick = {
|
||||
onEpisodeItemClick(
|
||||
episodeItem = episodeItem,
|
||||
episodes = episodes,
|
||||
onToggleSelection = {
|
||||
onEpisodeSelected(
|
||||
episodeItem,
|
||||
!episodeItem.selected,
|
||||
true,
|
||||
false,
|
||||
HorizontalDivider(modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
is EpisodeList.Item -> {
|
||||
AnimeEpisodeListItem(
|
||||
title = if (anime.displayMode == Anime.EPISODE_DISPLAY_NUMBER) {
|
||||
stringResource(
|
||||
R.string.display_mode_episode,
|
||||
formatEpisodeNumber(item.episode.episodeNumber),
|
||||
)
|
||||
} else {
|
||||
item.episode.name
|
||||
},
|
||||
date = item.episode.dateUpload
|
||||
.takeIf { it > 0L }
|
||||
?.let {
|
||||
Date(it).toRelativeString(
|
||||
context,
|
||||
dateRelativeTime,
|
||||
dateFormat,
|
||||
)
|
||||
},
|
||||
watchProgress = item.episode.lastSecondSeen
|
||||
.takeIf { !item.episode.seen && it > 0L }
|
||||
?.let {
|
||||
stringResource(
|
||||
R.string.episode_progress,
|
||||
it + 1,
|
||||
)
|
||||
},
|
||||
scanlator = item.episode.scanlator.takeIf { !it.isNullOrBlank() },
|
||||
seen = item.episode.seen,
|
||||
bookmark = item.episode.bookmark,
|
||||
selected = item.selected,
|
||||
downloadIndicatorEnabled = !isAnyEpisodeSelected,
|
||||
downloadStateProvider = { item.downloadState },
|
||||
downloadProgressProvider = { item.downloadProgress },
|
||||
episodeSwipeStartAction = episodeSwipeStartAction,
|
||||
episodeSwipeEndAction = episodeSwipeEndAction,
|
||||
onLongClick = {
|
||||
onEpisodeSelected(item, !item.selected, true, true)
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
},
|
||||
onClick = {
|
||||
onEpisodeItemClick(
|
||||
episodeItem = item,
|
||||
isAnyEpisodeSelected = isAnyEpisodeSelected,
|
||||
onToggleSelection = { onEpisodeSelected(item, !item.selected, true, false) },
|
||||
onEpisodeClicked = onEpisodeClicked,
|
||||
)
|
||||
},
|
||||
onEpisodeClicked = onEpisodeClicked,
|
||||
onDownloadClick = if (onDownloadEpisode != null) {
|
||||
{ onDownloadEpisode(listOf(item), it) }
|
||||
} else {
|
||||
null
|
||||
},
|
||||
onEpisodeSwipe = {
|
||||
onEpisodeSwipe(item, it)
|
||||
},
|
||||
)
|
||||
},
|
||||
onDownloadClick = if (onDownloadEpisode != null) {
|
||||
{ onDownloadEpisode(listOf(episodeItem), it) }
|
||||
} else {
|
||||
null
|
||||
},
|
||||
onEpisodeSwipe = {
|
||||
onEpisodeSwipe(episodeItem, it)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onEpisodeItemClick(
|
||||
episodeItem: EpisodeItem,
|
||||
episodes: List<EpisodeItem>,
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package eu.kanade.presentation.entries.anime.components
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.graphics.res.animatedVectorResource
|
||||
|
@ -29,6 +28,7 @@ import androidx.compose.material.icons.filled.Brush
|
|||
import androidx.compose.material.icons.filled.Favorite
|
||||
import androidx.compose.material.icons.filled.HourglassEmpty
|
||||
import androidx.compose.material.icons.filled.PersonOutline
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.material.icons.outlined.AttachMoney
|
||||
import androidx.compose.material.icons.outlined.Block
|
||||
import androidx.compose.material.icons.outlined.Close
|
||||
|
@ -43,6 +43,7 @@ import androidx.compose.material.icons.outlined.Warning
|
|||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ProvideTextStyle
|
||||
import androidx.compose.material3.SuggestionChip
|
||||
|
@ -128,7 +129,7 @@ fun AnimeInfoBox(
|
|||
.alpha(.2f),
|
||||
)
|
||||
|
||||
// Manga & source info
|
||||
// Anime & source info
|
||||
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) {
|
||||
if (!isTabletUi) {
|
||||
AnimeAndSourceTitlesSmall(
|
||||
|
@ -136,7 +137,6 @@ fun AnimeInfoBox(
|
|||
coverDataProvider = coverDataProvider,
|
||||
onCoverClick = onCoverClick,
|
||||
title = title,
|
||||
context = LocalContext.current,
|
||||
doSearch = doSearch,
|
||||
author = author,
|
||||
artist = artist,
|
||||
|
@ -150,7 +150,6 @@ fun AnimeInfoBox(
|
|||
coverDataProvider = coverDataProvider,
|
||||
onCoverClick = onCoverClick,
|
||||
title = title,
|
||||
context = LocalContext.current,
|
||||
doSearch = doSearch,
|
||||
author = author,
|
||||
artist = artist,
|
||||
|
@ -335,7 +334,6 @@ private fun AnimeAndSourceTitlesLarge(
|
|||
coverDataProvider: () -> Anime,
|
||||
onCoverClick: () -> Unit,
|
||||
title: String,
|
||||
context: Context,
|
||||
doSearch: (query: String, global: Boolean) -> Unit,
|
||||
author: String?,
|
||||
artist: String?,
|
||||
|
@ -356,104 +354,16 @@ private fun AnimeAndSourceTitlesLarge(
|
|||
onClick = onCoverClick,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = title.ifBlank { stringResource(R.string.unknown_title) },
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier.clickableNoIndication(
|
||||
onLongClick = { if (title.isNotBlank()) context.copyToClipboard(title, title) },
|
||||
onClick = { if (title.isNotBlank()) doSearch(title, true) },
|
||||
),
|
||||
AnimeContentInfo(
|
||||
title = title,
|
||||
doSearch = doSearch,
|
||||
author = author,
|
||||
artist = artist,
|
||||
status = status,
|
||||
sourceName = sourceName,
|
||||
isStubSource = isStubSource,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Text(
|
||||
text = author?.takeIf { it.isNotBlank() } ?: stringResource(R.string.unknown_studio),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
modifier = Modifier
|
||||
.secondaryItemAlpha()
|
||||
.padding(top = 2.dp)
|
||||
.clickableNoIndication(
|
||||
onLongClick = {
|
||||
if (!author.isNullOrBlank()) {
|
||||
context.copyToClipboard(
|
||||
author,
|
||||
author,
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = { if (!author.isNullOrBlank()) doSearch(author, true) },
|
||||
),
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
if (!artist.isNullOrBlank() && author != artist) {
|
||||
Text(
|
||||
text = artist,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
modifier = Modifier
|
||||
.secondaryItemAlpha()
|
||||
.padding(top = 2.dp)
|
||||
.clickableNoIndication(
|
||||
onLongClick = { context.copyToClipboard(artist, artist) },
|
||||
onClick = { doSearch(artist, true) },
|
||||
),
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Row(
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = when (status) {
|
||||
SAnime.ONGOING.toLong() -> Icons.Outlined.Schedule
|
||||
SAnime.COMPLETED.toLong() -> Icons.Outlined.DoneAll
|
||||
SAnime.LICENSED.toLong() -> Icons.Outlined.AttachMoney
|
||||
SAnime.PUBLISHING_FINISHED.toLong() -> Icons.Outlined.Done
|
||||
SAnime.CANCELLED.toLong() -> Icons.Outlined.Close
|
||||
SAnime.ON_HIATUS.toLong() -> Icons.Outlined.Pause
|
||||
else -> Icons.Outlined.Block
|
||||
},
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(end = 4.dp)
|
||||
.size(16.dp),
|
||||
)
|
||||
ProvideTextStyle(MaterialTheme.typography.bodyMedium) {
|
||||
Text(
|
||||
text = when (status) {
|
||||
SAnime.ONGOING.toLong() -> stringResource(R.string.ongoing)
|
||||
SAnime.COMPLETED.toLong() -> stringResource(R.string.completed)
|
||||
SAnime.LICENSED.toLong() -> stringResource(R.string.licensed)
|
||||
SAnime.PUBLISHING_FINISHED.toLong() -> stringResource(
|
||||
R.string.publishing_finished,
|
||||
)
|
||||
SAnime.CANCELLED.toLong() -> stringResource(R.string.cancelled)
|
||||
SAnime.ON_HIATUS.toLong() -> stringResource(R.string.on_hiatus)
|
||||
else -> stringResource(R.string.unknown)
|
||||
},
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
DotSeparatorText()
|
||||
if (isStubSource) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Warning,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(end = 4.dp)
|
||||
.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = sourceName,
|
||||
modifier = Modifier.clickableNoIndication { doSearch(sourceName, false) },
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -463,7 +373,6 @@ private fun AnimeAndSourceTitlesSmall(
|
|||
coverDataProvider: () -> Anime,
|
||||
onCoverClick: () -> Unit,
|
||||
title: String,
|
||||
context: Context,
|
||||
doSearch: (query: String, global: Boolean) -> Unit,
|
||||
author: String?,
|
||||
artist: String?,
|
||||
|
@ -489,137 +398,161 @@ private fun AnimeAndSourceTitlesSmall(
|
|||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||
) {
|
||||
Text(
|
||||
text = title.ifBlank { stringResource(R.string.unknown_title) },
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier.clickableNoIndication(
|
||||
AnimeContentInfo(
|
||||
title = title,
|
||||
doSearch = doSearch,
|
||||
author = author,
|
||||
artist = artist,
|
||||
status = status,
|
||||
sourceName = sourceName,
|
||||
isStubSource = isStubSource,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AnimeContentInfo(
|
||||
title: String,
|
||||
textAlign: TextAlign? = LocalTextStyle.current.textAlign,
|
||||
doSearch: (query: String, global: Boolean) -> Unit,
|
||||
author: String?,
|
||||
artist: String?,
|
||||
status: Long,
|
||||
sourceName: String,
|
||||
isStubSource: Boolean,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
Text(
|
||||
text = title.ifBlank { stringResource(R.string.unknown_title) },
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier.clickableNoIndication(
|
||||
onLongClick = {
|
||||
if (title.isNotBlank()) {
|
||||
context.copyToClipboard(
|
||||
title,
|
||||
title,
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = { if (title.isNotBlank()) doSearch(title, true) },
|
||||
),
|
||||
textAlign = textAlign,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.PersonOutline,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
)
|
||||
Text(
|
||||
text = author?.takeIf { it.isNotBlank() }
|
||||
?: stringResource(R.string.unknown_author),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
modifier = Modifier
|
||||
.clickableNoIndication(
|
||||
onLongClick = {
|
||||
if (title.isNotBlank()) {
|
||||
if (!author.isNullOrBlank()) {
|
||||
context.copyToClipboard(
|
||||
title,
|
||||
title,
|
||||
author,
|
||||
author,
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = { if (title.isNotBlank()) doSearch(title, true) },
|
||||
onClick = { if (!author.isNullOrBlank()) doSearch(author, true) },
|
||||
),
|
||||
textAlign = textAlign,
|
||||
)
|
||||
}
|
||||
|
||||
if (!artist.isNullOrBlank() && author != artist) {
|
||||
Row(
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Brush,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
)
|
||||
Text(
|
||||
text = artist,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
modifier = Modifier
|
||||
.clickableNoIndication(
|
||||
onLongClick = { context.copyToClipboard(artist, artist) },
|
||||
onClick = { doSearch(artist, true) },
|
||||
),
|
||||
textAlign = textAlign,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = when (status) {
|
||||
SAnime.ONGOING.toLong() -> Icons.Outlined.Schedule
|
||||
SAnime.COMPLETED.toLong() -> Icons.Outlined.DoneAll
|
||||
SAnime.LICENSED.toLong() -> Icons.Outlined.AttachMoney
|
||||
SAnime.PUBLISHING_FINISHED.toLong() -> Icons.Outlined.Done
|
||||
SAnime.CANCELLED.toLong() -> Icons.Outlined.Close
|
||||
SAnime.ON_HIATUS.toLong() -> Icons.Outlined.Pause
|
||||
else -> Icons.Outlined.Block
|
||||
},
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(end = 4.dp)
|
||||
.size(16.dp),
|
||||
)
|
||||
ProvideTextStyle(MaterialTheme.typography.bodyMedium) {
|
||||
Text(
|
||||
text = when (status) {
|
||||
SAnime.ONGOING.toLong() -> stringResource(R.string.ongoing)
|
||||
SAnime.COMPLETED.toLong() -> stringResource(R.string.completed)
|
||||
SAnime.LICENSED.toLong() -> stringResource(R.string.licensed)
|
||||
SAnime.PUBLISHING_FINISHED.toLong() -> stringResource(R.string.publishing_finished)
|
||||
SAnime.CANCELLED.toLong() -> stringResource(R.string.cancelled)
|
||||
SAnime.ON_HIATUS.toLong() -> stringResource(R.string.on_hiatus)
|
||||
else -> stringResource(R.string.unknown)
|
||||
},
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
DotSeparatorText()
|
||||
if (isStubSource) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.PersonOutline,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
)
|
||||
Text(
|
||||
text = author?.takeIf { it.isNotBlank() }
|
||||
?: stringResource(R.string.unknown_author),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
modifier = Modifier
|
||||
.clickableNoIndication(
|
||||
onLongClick = {
|
||||
if (!author.isNullOrBlank()) {
|
||||
context.copyToClipboard(
|
||||
author,
|
||||
author,
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = { if (!author.isNullOrBlank()) doSearch(author, true) },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if (!artist.isNullOrBlank() && author != artist) {
|
||||
Row(
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Brush,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
)
|
||||
Text(
|
||||
text = artist,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
modifier = Modifier
|
||||
.clickableNoIndication(
|
||||
onLongClick = { context.copyToClipboard(artist, artist) },
|
||||
onClick = { doSearch(artist, true) },
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = when (status) {
|
||||
SAnime.ONGOING.toLong() -> Icons.Outlined.Schedule
|
||||
SAnime.COMPLETED.toLong() -> Icons.Outlined.DoneAll
|
||||
SAnime.LICENSED.toLong() -> Icons.Outlined.AttachMoney
|
||||
SAnime.PUBLISHING_FINISHED.toLong() -> Icons.Outlined.Done
|
||||
SAnime.CANCELLED.toLong() -> Icons.Outlined.Close
|
||||
SAnime.ON_HIATUS.toLong() -> Icons.Outlined.Pause
|
||||
else -> Icons.Outlined.Block
|
||||
},
|
||||
imageVector = Icons.Filled.Warning,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(end = 4.dp)
|
||||
.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
ProvideTextStyle(MaterialTheme.typography.bodyMedium) {
|
||||
Text(
|
||||
text = when (status) {
|
||||
SAnime.ONGOING.toLong() -> stringResource(R.string.ongoing)
|
||||
SAnime.COMPLETED.toLong() -> stringResource(R.string.completed)
|
||||
SAnime.LICENSED.toLong() -> stringResource(R.string.licensed)
|
||||
SAnime.PUBLISHING_FINISHED.toLong() -> stringResource(
|
||||
R.string.publishing_finished,
|
||||
)
|
||||
SAnime.CANCELLED.toLong() -> stringResource(R.string.cancelled)
|
||||
SAnime.ON_HIATUS.toLong() -> stringResource(R.string.on_hiatus)
|
||||
else -> stringResource(R.string.unknown)
|
||||
},
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
DotSeparatorText()
|
||||
if (isStubSource) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Warning,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(end = 4.dp)
|
||||
.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = sourceName,
|
||||
modifier = Modifier.clickableNoIndication {
|
||||
doSearch(
|
||||
sourceName,
|
||||
false,
|
||||
)
|
||||
},
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = sourceName,
|
||||
modifier = Modifier.clickableNoIndication {
|
||||
doSearch(
|
||||
sourceName,
|
||||
false,
|
||||
)
|
||||
},
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,99 +790,123 @@ 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
|
||||
|
||||
MangaChapterListItem(
|
||||
title = if (manga.displayMode == Manga.CHAPTER_DISPLAY_NUMBER) {
|
||||
stringResource(
|
||||
R.string.display_mode_chapter,
|
||||
formatChapterNumber(chapterItem.chapter.chapterNumber),
|
||||
)
|
||||
} else {
|
||||
chapterItem.chapter.name
|
||||
},
|
||||
date = chapterItem.chapter.dateUpload
|
||||
.takeIf { it > 0L }
|
||||
?.let {
|
||||
Date(it).toRelativeString(
|
||||
context,
|
||||
dateRelativeTime,
|
||||
dateFormat,
|
||||
when (item) {
|
||||
is ChapterList.MissingCount -> {
|
||||
Row(
|
||||
modifier = Modifier.padding(
|
||||
horizontal = MaterialTheme.padding.medium,
|
||||
vertical = MaterialTheme.padding.small,
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium),
|
||||
) {
|
||||
HorizontalDivider(modifier = Modifier.weight(1f))
|
||||
Text(
|
||||
text = pluralStringResource(
|
||||
id = R.plurals.missing_items,
|
||||
count = item.count,
|
||||
item.count,
|
||||
),
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
)
|
||||
},
|
||||
readProgress = chapterItem.chapter.lastPageRead
|
||||
.takeIf { !chapterItem.chapter.read && it > 0L }
|
||||
?.let {
|
||||
stringResource(
|
||||
R.string.chapter_progress,
|
||||
it + 1,
|
||||
)
|
||||
},
|
||||
scanlator = chapterItem.chapter.scanlator.takeIf { !it.isNullOrBlank() },
|
||||
read = chapterItem.chapter.read,
|
||||
bookmark = chapterItem.chapter.bookmark,
|
||||
selected = chapterItem.selected,
|
||||
downloadIndicatorEnabled = chapters.fastAll { !it.selected },
|
||||
downloadStateProvider = { chapterItem.downloadState },
|
||||
downloadProgressProvider = { chapterItem.downloadProgress },
|
||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||
onLongClick = {
|
||||
onChapterSelected(chapterItem, !chapterItem.selected, true, true)
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
},
|
||||
onClick = {
|
||||
onChapterItemClick(
|
||||
chapterItem = chapterItem,
|
||||
chapters = chapters,
|
||||
onToggleSelection = {
|
||||
onChapterSelected(
|
||||
chapterItem,
|
||||
!chapterItem.selected,
|
||||
true,
|
||||
false,
|
||||
HorizontalDivider(modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
is ChapterList.Item -> {
|
||||
MangaChapterListItem(
|
||||
title = if (manga.displayMode == Manga.CHAPTER_DISPLAY_NUMBER) {
|
||||
stringResource(
|
||||
R.string.display_mode_chapter,
|
||||
formatChapterNumber(item.chapter.chapterNumber),
|
||||
)
|
||||
} else {
|
||||
item.chapter.name
|
||||
},
|
||||
date = item.chapter.dateUpload
|
||||
.takeIf { it > 0L }
|
||||
?.let {
|
||||
Date(it).toRelativeString(
|
||||
context,
|
||||
dateRelativeTime,
|
||||
dateFormat,
|
||||
)
|
||||
},
|
||||
readProgress = item.chapter.lastPageRead
|
||||
.takeIf { !item.chapter.read && it > 0L }
|
||||
?.let {
|
||||
stringResource(
|
||||
R.string.chapter_progress,
|
||||
it + 1,
|
||||
)
|
||||
},
|
||||
scanlator = item.chapter.scanlator.takeIf { !it.isNullOrBlank() },
|
||||
read = item.chapter.read,
|
||||
bookmark = item.chapter.bookmark,
|
||||
selected = item.selected,
|
||||
downloadIndicatorEnabled = !isAnyChapterSelected,
|
||||
downloadStateProvider = { item.downloadState },
|
||||
downloadProgressProvider = { item.downloadProgress },
|
||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||
onLongClick = {
|
||||
onChapterSelected(item, !item.selected, true, true)
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
},
|
||||
onClick = {
|
||||
onChapterItemClick(
|
||||
chapterItem = item,
|
||||
isAnyChapterSelected = isAnyChapterSelected,
|
||||
onToggleSelection = { onChapterSelected(item, !item.selected, true, false) },
|
||||
onChapterClicked = onChapterClicked,
|
||||
)
|
||||
},
|
||||
onChapterClicked = onChapterClicked,
|
||||
onDownloadClick = if (onDownloadChapter != null) {
|
||||
{ onDownloadChapter(listOf(item), it) }
|
||||
} else {
|
||||
null
|
||||
},
|
||||
onChapterSwipe = {
|
||||
onChapterSwipe(item, it)
|
||||
},
|
||||
)
|
||||
},
|
||||
onDownloadClick = if (onDownloadChapter != null) {
|
||||
{ onDownloadChapter(listOf(chapterItem), it) }
|
||||
} else {
|
||||
null
|
||||
},
|
||||
onChapterSwipe = {
|
||||
onChapterSwipe(chapterItem, it)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onChapterItemClick(
|
||||
chapterItem: ChapterItem,
|
||||
chapters: List<ChapterItem>,
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package eu.kanade.presentation.entries.manga.components
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.graphics.res.animatedVectorResource
|
||||
|
@ -43,6 +42,7 @@ import androidx.compose.material.icons.outlined.Sync
|
|||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ProvideTextStyle
|
||||
import androidx.compose.material3.SuggestionChip
|
||||
|
@ -136,7 +136,6 @@ fun MangaInfoBox(
|
|||
coverDataProvider = coverDataProvider,
|
||||
onCoverClick = onCoverClick,
|
||||
title = title,
|
||||
context = LocalContext.current,
|
||||
doSearch = doSearch,
|
||||
author = author,
|
||||
artist = artist,
|
||||
|
@ -150,7 +149,6 @@ fun MangaInfoBox(
|
|||
coverDataProvider = coverDataProvider,
|
||||
onCoverClick = onCoverClick,
|
||||
title = title,
|
||||
context = LocalContext.current,
|
||||
doSearch = doSearch,
|
||||
author = author,
|
||||
artist = artist,
|
||||
|
@ -335,7 +333,6 @@ private fun MangaAndSourceTitlesLarge(
|
|||
coverDataProvider: () -> Manga,
|
||||
onCoverClick: () -> Unit,
|
||||
title: String,
|
||||
context: Context,
|
||||
doSearch: (query: String, global: Boolean) -> Unit,
|
||||
author: String?,
|
||||
artist: String?,
|
||||
|
@ -356,104 +353,16 @@ private fun MangaAndSourceTitlesLarge(
|
|||
onClick = onCoverClick,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = title.ifBlank { stringResource(R.string.unknown_title) },
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier.clickableNoIndication(
|
||||
onLongClick = { if (title.isNotBlank()) context.copyToClipboard(title, title) },
|
||||
onClick = { if (title.isNotBlank()) doSearch(title, true) },
|
||||
),
|
||||
MangaContentInfo(
|
||||
title = title,
|
||||
doSearch = doSearch,
|
||||
author = author,
|
||||
artist = artist,
|
||||
status = status,
|
||||
sourceName = sourceName,
|
||||
isStubSource = isStubSource,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Text(
|
||||
text = author?.takeIf { it.isNotBlank() } ?: stringResource(R.string.unknown_author),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
modifier = Modifier
|
||||
.secondaryItemAlpha()
|
||||
.padding(top = 2.dp)
|
||||
.clickableNoIndication(
|
||||
onLongClick = {
|
||||
if (!author.isNullOrBlank()) {
|
||||
context.copyToClipboard(
|
||||
author,
|
||||
author,
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = { if (!author.isNullOrBlank()) doSearch(author, true) },
|
||||
),
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
if (!artist.isNullOrBlank() && author != artist) {
|
||||
Text(
|
||||
text = artist,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
modifier = Modifier
|
||||
.secondaryItemAlpha()
|
||||
.padding(top = 2.dp)
|
||||
.clickableNoIndication(
|
||||
onLongClick = { context.copyToClipboard(artist, artist) },
|
||||
onClick = { doSearch(artist, true) },
|
||||
),
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Row(
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = when (status) {
|
||||
SManga.ONGOING.toLong() -> Icons.Outlined.Schedule
|
||||
SManga.COMPLETED.toLong() -> Icons.Outlined.DoneAll
|
||||
SManga.LICENSED.toLong() -> Icons.Outlined.AttachMoney
|
||||
SManga.PUBLISHING_FINISHED.toLong() -> Icons.Outlined.Done
|
||||
SManga.CANCELLED.toLong() -> Icons.Outlined.Close
|
||||
SManga.ON_HIATUS.toLong() -> Icons.Outlined.Pause
|
||||
else -> Icons.Outlined.Block
|
||||
},
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(end = 4.dp)
|
||||
.size(16.dp),
|
||||
)
|
||||
ProvideTextStyle(MaterialTheme.typography.bodyMedium) {
|
||||
Text(
|
||||
text = when (status) {
|
||||
SManga.ONGOING.toLong() -> stringResource(R.string.ongoing)
|
||||
SManga.COMPLETED.toLong() -> stringResource(R.string.completed)
|
||||
SManga.LICENSED.toLong() -> stringResource(R.string.licensed)
|
||||
SManga.PUBLISHING_FINISHED.toLong() -> stringResource(
|
||||
R.string.publishing_finished,
|
||||
)
|
||||
SManga.CANCELLED.toLong() -> stringResource(R.string.cancelled)
|
||||
SManga.ON_HIATUS.toLong() -> stringResource(R.string.on_hiatus)
|
||||
else -> stringResource(R.string.unknown)
|
||||
},
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
DotSeparatorText()
|
||||
if (isStubSource) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Warning,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(end = 4.dp)
|
||||
.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = sourceName,
|
||||
modifier = Modifier.clickableNoIndication { doSearch(sourceName, false) },
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -463,7 +372,6 @@ private fun MangaAndSourceTitlesSmall(
|
|||
coverDataProvider: () -> Manga,
|
||||
onCoverClick: () -> Unit,
|
||||
title: String,
|
||||
context: Context,
|
||||
doSearch: (query: String, global: Boolean) -> Unit,
|
||||
author: String?,
|
||||
artist: String?,
|
||||
|
@ -489,136 +397,161 @@ private fun MangaAndSourceTitlesSmall(
|
|||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||
) {
|
||||
Text(
|
||||
text = title.ifBlank { stringResource(R.string.unknown_title) },
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier.clickableNoIndication(
|
||||
MangaContentInfo(
|
||||
title = title,
|
||||
doSearch = doSearch,
|
||||
author = author,
|
||||
artist = artist,
|
||||
status = status,
|
||||
sourceName = sourceName,
|
||||
isStubSource = isStubSource,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MangaContentInfo(
|
||||
title: String,
|
||||
textAlign: TextAlign? = LocalTextStyle.current.textAlign,
|
||||
doSearch: (query: String, global: Boolean) -> Unit,
|
||||
author: String?,
|
||||
artist: String?,
|
||||
status: Long,
|
||||
sourceName: String,
|
||||
isStubSource: Boolean,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
Text(
|
||||
text = title.ifBlank { stringResource(R.string.unknown_title) },
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier.clickableNoIndication(
|
||||
onLongClick = {
|
||||
if (title.isNotBlank()) {
|
||||
context.copyToClipboard(
|
||||
title,
|
||||
title,
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = { if (title.isNotBlank()) doSearch(title, true) },
|
||||
),
|
||||
textAlign = textAlign,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.PersonOutline,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
)
|
||||
Text(
|
||||
text = author?.takeIf { it.isNotBlank() }
|
||||
?: stringResource(R.string.unknown_author),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
modifier = Modifier
|
||||
.clickableNoIndication(
|
||||
onLongClick = {
|
||||
if (title.isNotBlank()) {
|
||||
if (!author.isNullOrBlank()) {
|
||||
context.copyToClipboard(
|
||||
title,
|
||||
title,
|
||||
author,
|
||||
author,
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = { if (title.isNotBlank()) doSearch(title, true) },
|
||||
onClick = { if (!author.isNullOrBlank()) doSearch(author, true) },
|
||||
),
|
||||
textAlign = textAlign,
|
||||
)
|
||||
}
|
||||
|
||||
if (!artist.isNullOrBlank() && author != artist) {
|
||||
Row(
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Brush,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
)
|
||||
Text(
|
||||
text = artist,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
modifier = Modifier
|
||||
.clickableNoIndication(
|
||||
onLongClick = { context.copyToClipboard(artist, artist) },
|
||||
onClick = { doSearch(artist, true) },
|
||||
),
|
||||
textAlign = textAlign,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Row(
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = when (status) {
|
||||
SManga.ONGOING.toLong() -> Icons.Outlined.Schedule
|
||||
SManga.COMPLETED.toLong() -> Icons.Outlined.DoneAll
|
||||
SManga.LICENSED.toLong() -> Icons.Outlined.AttachMoney
|
||||
SManga.PUBLISHING_FINISHED.toLong() -> Icons.Outlined.Done
|
||||
SManga.CANCELLED.toLong() -> Icons.Outlined.Close
|
||||
SManga.ON_HIATUS.toLong() -> Icons.Outlined.Pause
|
||||
else -> Icons.Outlined.Block
|
||||
},
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(end = 4.dp)
|
||||
.size(16.dp),
|
||||
)
|
||||
ProvideTextStyle(MaterialTheme.typography.bodyMedium) {
|
||||
Text(
|
||||
text = when (status) {
|
||||
SManga.ONGOING.toLong() -> stringResource(R.string.ongoing)
|
||||
SManga.COMPLETED.toLong() -> stringResource(R.string.completed)
|
||||
SManga.LICENSED.toLong() -> stringResource(R.string.licensed)
|
||||
SManga.PUBLISHING_FINISHED.toLong() -> stringResource(R.string.publishing_finished)
|
||||
SManga.CANCELLED.toLong() -> stringResource(R.string.cancelled)
|
||||
SManga.ON_HIATUS.toLong() -> stringResource(R.string.on_hiatus)
|
||||
else -> stringResource(R.string.unknown)
|
||||
},
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
DotSeparatorText()
|
||||
if (isStubSource) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.PersonOutline,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
)
|
||||
Text(
|
||||
text = author?.takeIf { it.isNotBlank() }
|
||||
?: stringResource(R.string.unknown_author),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
modifier = Modifier
|
||||
.clickableNoIndication(
|
||||
onLongClick = {
|
||||
if (!author.isNullOrBlank()) {
|
||||
context.copyToClipboard(
|
||||
author,
|
||||
author,
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = { if (!author.isNullOrBlank()) doSearch(author, true) },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if (!artist.isNullOrBlank() && author != artist) {
|
||||
Row(
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Brush,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
)
|
||||
Text(
|
||||
text = artist,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
modifier = Modifier
|
||||
.clickableNoIndication(
|
||||
onLongClick = { context.copyToClipboard(artist, artist) },
|
||||
onClick = { doSearch(artist, true) },
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = when (status) {
|
||||
SManga.ONGOING.toLong() -> Icons.Outlined.Schedule
|
||||
SManga.COMPLETED.toLong() -> Icons.Outlined.DoneAll
|
||||
SManga.LICENSED.toLong() -> Icons.Outlined.AttachMoney
|
||||
SManga.PUBLISHING_FINISHED.toLong() -> Icons.Outlined.Done
|
||||
SManga.CANCELLED.toLong() -> Icons.Outlined.Close
|
||||
SManga.ON_HIATUS.toLong() -> Icons.Outlined.Pause
|
||||
else -> Icons.Outlined.Block
|
||||
},
|
||||
imageVector = Icons.Filled.Warning,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(end = 4.dp)
|
||||
.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
ProvideTextStyle(MaterialTheme.typography.bodyMedium) {
|
||||
Text(
|
||||
text = when (status) {
|
||||
SManga.ONGOING.toLong() -> stringResource(R.string.ongoing)
|
||||
SManga.COMPLETED.toLong() -> stringResource(R.string.completed)
|
||||
SManga.LICENSED.toLong() -> stringResource(R.string.licensed)
|
||||
SManga.PUBLISHING_FINISHED.toLong() -> stringResource(
|
||||
R.string.publishing_finished,
|
||||
)
|
||||
SManga.CANCELLED.toLong() -> stringResource(R.string.cancelled)
|
||||
SManga.ON_HIATUS.toLong() -> stringResource(R.string.on_hiatus)
|
||||
else -> stringResource(R.string.unknown)
|
||||
},
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
DotSeparatorText()
|
||||
if (isStubSource) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Warning,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(end = 4.dp)
|
||||
.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = sourceName,
|
||||
modifier = Modifier.clickableNoIndication {
|
||||
doSearch(
|
||||
sourceName,
|
||||
false,
|
||||
)
|
||||
},
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = sourceName,
|
||||
modifier = Modifier.clickableNoIndication {
|
||||
doSearch(
|
||||
sourceName,
|
||||
false,
|
||||
)
|
||||
},
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -97,9 +97,8 @@ fun AnimeUpdateScreen(
|
|||
FastScrollLazyColumn(
|
||||
contentPadding = contentPadding,
|
||||
) {
|
||||
if (lastUpdated > 0L) {
|
||||
animeUpdatesLastUpdatedItem(lastUpdated)
|
||||
}
|
||||
animeUpdatesLastUpdatedItem(lastUpdated)
|
||||
|
||||
animeUpdatesUiItems(
|
||||
uiModels = state.getUiModel(context, relativeTime),
|
||||
selectionMode = state.selectionMode,
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -93,9 +93,7 @@ fun MangaUpdateScreen(
|
|||
FastScrollLazyColumn(
|
||||
contentPadding = contentPadding,
|
||||
) {
|
||||
if (lastUpdated > 0L) {
|
||||
mangaUpdatesLastUpdatedItem(lastUpdated)
|
||||
}
|
||||
mangaUpdatesLastUpdatedItem(lastUpdated)
|
||||
|
||||
mangaUpdatesUiItems(
|
||||
uiModels = state.getUiModel(context, relativeTime),
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 ->
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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(
|
||||
val episode: Episode,
|
||||
val downloadState: AnimeDownload.State,
|
||||
val downloadProgress: Int,
|
||||
val selected: Boolean = false,
|
||||
) {
|
||||
val isDownloaded = downloadState == AnimeDownload.State.DOWNLOADED
|
||||
sealed class EpisodeList {
|
||||
@Immutable
|
||||
data class MissingCount(
|
||||
val id: String,
|
||||
val count: Int,
|
||||
) : EpisodeList()
|
||||
|
||||
@Immutable
|
||||
data class Item(
|
||||
val episode: Episode,
|
||||
val downloadState: AnimeDownload.State,
|
||||
val downloadProgress: Int,
|
||||
val selected: Boolean = false,
|
||||
) : EpisodeList() {
|
||||
val id = episode.id
|
||||
val isDownloaded = downloadState == AnimeDownload.State.DOWNLOADED
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
val chapter: Chapter,
|
||||
val downloadState: MangaDownload.State,
|
||||
val downloadProgress: Int,
|
||||
val selected: Boolean = false,
|
||||
) {
|
||||
val isDownloaded = downloadState == MangaDownload.State.DOWNLOADED
|
||||
sealed class ChapterList {
|
||||
@Immutable
|
||||
data class MissingCount(
|
||||
val id: String,
|
||||
val count: Int,
|
||||
) : ChapterList()
|
||||
|
||||
@Immutable
|
||||
data class Item(
|
||||
val chapter: Chapter,
|
||||
val downloadState: MangaDownload.State,
|
||||
val downloadProgress: Int,
|
||||
val selected: Boolean = false,
|
||||
) : ChapterList() {
|
||||
val id = chapter.id
|
||||
val isDownloaded = downloadState == MangaDownload.State.DOWNLOADED
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -55,5 +55,5 @@ subprojects {
|
|||
}
|
||||
|
||||
tasks.register<Delete>("clean") {
|
||||
delete(rootProject.buildDir)
|
||||
delete(rootProject.layout.buildDirectory)
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ dependencies {
|
|||
|
||||
api(libs.okhttp.core)
|
||||
api(libs.okhttp.logging)
|
||||
api(libs.okhttp.brotli)
|
||||
api(libs.okhttp.dnsoverhttps)
|
||||
api(libs.okio)
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
) {
|
||||
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
) {
|
||||
|
|
@ -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,
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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"]
|
|
@ -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"]
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in a new issue