mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-21 12:17:12 +03:00
parent
89777e98b0
commit
d7aee03688
119 changed files with 1087 additions and 463 deletions
|
@ -1,7 +1,8 @@
|
|||
[*.{kt,kts}]
|
||||
indent_size=4
|
||||
insert_final_newline=true
|
||||
ij_kotlin_allow_trailing_comma=true
|
||||
ij_kotlin_allow_trailing_comma_on_call_site=true
|
||||
max_line_length = 120
|
||||
indent_size = 4
|
||||
insert_final_newline = true
|
||||
ij_kotlin_allow_trailing_comma = true
|
||||
ij_kotlin_allow_trailing_comma_on_call_site = true
|
||||
ij_kotlin_name_count_to_use_star_import = 2147483647
|
||||
ij_kotlin_name_count_to_use_star_import_for_members = 2147483647
|
|
@ -230,7 +230,7 @@ class DomainModule : InjektModule {
|
|||
|
||||
addSingletonFactory<AnimeTrackRepository> { AnimeTrackRepositoryImpl(get()) }
|
||||
addFactory { TrackEpisode(get(), get(), get(), get()) }
|
||||
addFactory { AddAnimeTracks(get(), get(), get()) }
|
||||
addFactory { AddAnimeTracks(get(), get(), get(), get()) }
|
||||
addFactory { RefreshAnimeTracks(get(), get(), get(), get()) }
|
||||
addFactory { DeleteAnimeTrack(get()) }
|
||||
addFactory { GetTracksPerAnime(get()) }
|
||||
|
@ -240,7 +240,7 @@ class DomainModule : InjektModule {
|
|||
|
||||
addSingletonFactory<MangaTrackRepository> { MangaTrackRepositoryImpl(get()) }
|
||||
addFactory { TrackChapter(get(), get(), get(), get()) }
|
||||
addFactory { AddMangaTracks(get(), get(), get()) }
|
||||
addFactory { AddMangaTracks(get(), get(), get(), get()) }
|
||||
addFactory { RefreshMangaTracks(get(), get(), get(), get()) }
|
||||
addFactory { DeleteMangaTrack(get()) }
|
||||
addFactory { GetTracksPerManga(get()) }
|
||||
|
|
|
@ -4,12 +4,11 @@ import eu.kanade.domain.source.service.SetMigrateSorting
|
|||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import tachiyomi.core.util.lang.compareToWithCollator
|
||||
import tachiyomi.domain.source.anime.model.AnimeSource
|
||||
import tachiyomi.domain.source.anime.repository.AnimeSourceRepository
|
||||
import tachiyomi.source.local.entries.anime.LocalAnimeSource
|
||||
import java.text.Collator
|
||||
import java.util.Collections
|
||||
import java.util.Locale
|
||||
|
||||
class GetAnimeSourcesWithFavoriteCount(
|
||||
private val repository: AnimeSourceRepository,
|
||||
|
@ -32,20 +31,13 @@ class GetAnimeSourcesWithFavoriteCount(
|
|||
direction: SetMigrateSorting.Direction,
|
||||
sorting: SetMigrateSorting.Mode,
|
||||
): java.util.Comparator<Pair<AnimeSource, Long>> {
|
||||
val locale = Locale.getDefault()
|
||||
val collator = Collator.getInstance(locale).apply {
|
||||
strength = Collator.PRIMARY
|
||||
}
|
||||
val sortFn: (Pair<AnimeSource, Long>, Pair<AnimeSource, Long>) -> Int = { a, b ->
|
||||
when (sorting) {
|
||||
SetMigrateSorting.Mode.ALPHABETICAL -> {
|
||||
when {
|
||||
a.first.isStub && b.first.isStub.not() -> -1
|
||||
b.first.isStub && a.first.isStub.not() -> 1
|
||||
else -> collator.compare(
|
||||
a.first.name.lowercase(locale),
|
||||
b.first.name.lowercase(locale),
|
||||
)
|
||||
else -> a.first.name.lowercase().compareToWithCollator(b.first.name.lowercase())
|
||||
}
|
||||
}
|
||||
SetMigrateSorting.Mode.TOTAL -> {
|
||||
|
|
|
@ -4,12 +4,11 @@ import eu.kanade.domain.source.service.SetMigrateSorting
|
|||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import tachiyomi.core.util.lang.compareToWithCollator
|
||||
import tachiyomi.domain.source.manga.model.Source
|
||||
import tachiyomi.domain.source.manga.repository.MangaSourceRepository
|
||||
import tachiyomi.source.local.entries.manga.LocalMangaSource
|
||||
import java.text.Collator
|
||||
import java.util.Collections
|
||||
import java.util.Locale
|
||||
|
||||
class GetMangaSourcesWithFavoriteCount(
|
||||
private val repository: MangaSourceRepository,
|
||||
|
@ -32,20 +31,13 @@ class GetMangaSourcesWithFavoriteCount(
|
|||
direction: SetMigrateSorting.Direction,
|
||||
sorting: SetMigrateSorting.Mode,
|
||||
): java.util.Comparator<Pair<Source, Long>> {
|
||||
val locale = Locale.getDefault()
|
||||
val collator = Collator.getInstance(locale).apply {
|
||||
strength = Collator.PRIMARY
|
||||
}
|
||||
val sortFn: (Pair<Source, Long>, Pair<Source, Long>) -> Int = { a, b ->
|
||||
when (sorting) {
|
||||
SetMigrateSorting.Mode.ALPHABETICAL -> {
|
||||
when {
|
||||
a.first.isStub && b.first.isStub.not() -> -1
|
||||
b.first.isStub && a.first.isStub.not() -> 1
|
||||
else -> collator.compare(
|
||||
a.first.name.lowercase(locale),
|
||||
b.first.name.lowercase(locale),
|
||||
)
|
||||
else -> a.first.name.lowercase().compareToWithCollator(b.first.name.lowercase())
|
||||
}
|
||||
}
|
||||
SetMigrateSorting.Mode.TOTAL -> {
|
||||
|
|
|
@ -1,45 +1,108 @@
|
|||
package eu.kanade.domain.track.anime.interactor
|
||||
|
||||
import eu.kanade.domain.track.anime.model.toDbTrack
|
||||
import eu.kanade.domain.track.anime.model.toDomainTrack
|
||||
import eu.kanade.tachiyomi.animesource.AnimeSource
|
||||
import eu.kanade.tachiyomi.data.database.models.anime.AnimeTrack
|
||||
import eu.kanade.tachiyomi.data.track.AnimeTracker
|
||||
import eu.kanade.tachiyomi.data.track.EnhancedAnimeTracker
|
||||
import eu.kanade.tachiyomi.data.track.Tracker
|
||||
import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.util.lang.withIOContext
|
||||
import tachiyomi.core.util.lang.withNonCancellableContext
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.entries.anime.model.Anime
|
||||
import tachiyomi.domain.history.anime.interactor.GetAnimeHistory
|
||||
import tachiyomi.domain.items.episode.interactor.GetEpisodesByAnimeId
|
||||
import tachiyomi.domain.track.anime.interactor.GetAnimeTracks
|
||||
import tachiyomi.domain.track.anime.interactor.InsertAnimeTrack
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.time.ZoneOffset
|
||||
|
||||
class AddAnimeTracks(
|
||||
private val getTracks: GetAnimeTracks,
|
||||
private val insertTrack: InsertAnimeTrack,
|
||||
private val syncChapterProgressWithTrack: SyncEpisodeProgressWithTrack,
|
||||
private val getEpisodesByAnimeId: GetEpisodesByAnimeId,
|
||||
) {
|
||||
|
||||
suspend fun bindEnhancedTracks(anime: Anime, source: AnimeSource) = withNonCancellableContext {
|
||||
getTracks.await(anime.id)
|
||||
.filterIsInstance<EnhancedAnimeTracker>()
|
||||
.filter { it.accept(source) }
|
||||
.forEach { service ->
|
||||
try {
|
||||
service.match(anime)?.let { track ->
|
||||
track.anime_id = anime.id
|
||||
(service as Tracker).animeService.bind(track)
|
||||
insertTrack.await(track.toDomainTrack()!!)
|
||||
// TODO: update all trackers based on common data
|
||||
suspend fun bind(tracker: AnimeTracker, item: AnimeTrack, animeId: Long) = withNonCancellableContext {
|
||||
withIOContext {
|
||||
val allChapters = getEpisodesByAnimeId.await(animeId)
|
||||
val hasSeenEpisodes = allChapters.any { it.seen }
|
||||
tracker.bind(item, hasSeenEpisodes)
|
||||
|
||||
syncChapterProgressWithTrack.await(
|
||||
anime.id,
|
||||
track.toDomainTrack()!!,
|
||||
service.animeService,
|
||||
var track = item.toDomainTrack(idRequired = false) ?: return@withIOContext
|
||||
|
||||
insertTrack.await(track)
|
||||
|
||||
// TODO: merge into [SyncChapterProgressWithTrack]?
|
||||
// Update chapter progress if newer chapters marked read locally
|
||||
if (hasSeenEpisodes) {
|
||||
val latestLocalReadChapterNumber = allChapters
|
||||
.sortedBy { it.episodeNumber }
|
||||
.takeWhile { it.seen }
|
||||
.lastOrNull()
|
||||
?.episodeNumber ?: -1.0
|
||||
|
||||
if (latestLocalReadChapterNumber > track.lastEpisodeSeen) {
|
||||
track = track.copy(
|
||||
lastEpisodeSeen = latestLocalReadChapterNumber,
|
||||
)
|
||||
tracker.setRemoteLastEpisodeSeen(track.toDbTrack(), latestLocalReadChapterNumber.toInt())
|
||||
}
|
||||
|
||||
if (track.startDate <= 0) {
|
||||
val firstReadChapterDate = Injekt.get<GetAnimeHistory>().await(animeId)
|
||||
.sortedBy { it.seenAt }
|
||||
.firstOrNull()
|
||||
?.seenAt
|
||||
|
||||
firstReadChapterDate?.let {
|
||||
val startDate = firstReadChapterDate.time.convertEpochMillisZone(
|
||||
ZoneOffset.systemDefault(),
|
||||
ZoneOffset.UTC,
|
||||
)
|
||||
track = track.copy(
|
||||
startDate = startDate,
|
||||
)
|
||||
tracker.setRemoteStartDate(track.toDbTrack(), startDate)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logcat(
|
||||
LogPriority.WARN,
|
||||
e,
|
||||
) { "Could not match anime: ${anime.title} with service $service" }
|
||||
}
|
||||
}
|
||||
|
||||
syncChapterProgressWithTrack.await(animeId, track, tracker)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun bindEnhancedTrackers(anime: Anime, source: AnimeSource) = withNonCancellableContext {
|
||||
withIOContext {
|
||||
getTracks.await(anime.id)
|
||||
.filterIsInstance<EnhancedAnimeTracker>()
|
||||
.filter { it.accept(source) }
|
||||
.forEach { service ->
|
||||
try {
|
||||
service.match(anime)?.let { track ->
|
||||
track.anime_id = anime.id
|
||||
(service as Tracker).animeService.bind(track)
|
||||
insertTrack.await(track.toDomainTrack()!!)
|
||||
|
||||
syncChapterProgressWithTrack.await(
|
||||
anime.id,
|
||||
track.toDomainTrack()!!,
|
||||
service.animeService,
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logcat(
|
||||
LogPriority.WARN,
|
||||
e,
|
||||
) { "Could not match anime: ${anime.title} with service $service" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,7 +43,10 @@ class DelayedAnimeTrackingUpdateJob(private val context: Context, workerParams:
|
|||
track?.copy(lastEpisodeSeen = it.lastEpisodeSeen.toDouble())
|
||||
}
|
||||
.forEach { animeTrack ->
|
||||
logcat(LogPriority.DEBUG) { "Updating delayed track item: ${animeTrack.animeId}, last chapter read: ${animeTrack.lastEpisodeSeen}" }
|
||||
logcat(LogPriority.DEBUG) {
|
||||
"Updating delayed track item: ${animeTrack.animeId}" +
|
||||
", last chapter read: ${animeTrack.lastEpisodeSeen}"
|
||||
}
|
||||
trackEpisode.await(context, animeTrack.animeId, animeTrack.lastEpisodeSeen)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,45 +1,108 @@
|
|||
package eu.kanade.domain.track.manga.interactor
|
||||
|
||||
import eu.kanade.domain.track.manga.model.toDbTrack
|
||||
import eu.kanade.domain.track.manga.model.toDomainTrack
|
||||
import eu.kanade.tachiyomi.data.database.models.manga.MangaTrack
|
||||
import eu.kanade.tachiyomi.data.track.EnhancedMangaTracker
|
||||
import eu.kanade.tachiyomi.data.track.MangaTracker
|
||||
import eu.kanade.tachiyomi.data.track.Tracker
|
||||
import eu.kanade.tachiyomi.source.MangaSource
|
||||
import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.util.lang.withIOContext
|
||||
import tachiyomi.core.util.lang.withNonCancellableContext
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.entries.manga.model.Manga
|
||||
import tachiyomi.domain.history.manga.interactor.GetMangaHistory
|
||||
import tachiyomi.domain.items.chapter.interactor.GetChaptersByMangaId
|
||||
import tachiyomi.domain.track.manga.interactor.GetMangaTracks
|
||||
import tachiyomi.domain.track.manga.interactor.InsertMangaTrack
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.time.ZoneOffset
|
||||
|
||||
class AddMangaTracks(
|
||||
private val getTracks: GetMangaTracks,
|
||||
private val insertTrack: InsertMangaTrack,
|
||||
private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack,
|
||||
private val getChaptersByMangaId: GetChaptersByMangaId,
|
||||
) {
|
||||
|
||||
suspend fun bindEnhancedTracks(manga: Manga, source: MangaSource) = withNonCancellableContext {
|
||||
getTracks.await(manga.id)
|
||||
.filterIsInstance<EnhancedMangaTracker>()
|
||||
.filter { it.accept(source) }
|
||||
.forEach { service ->
|
||||
try {
|
||||
service.match(manga)?.let { track ->
|
||||
track.manga_id = manga.id
|
||||
(service as Tracker).mangaService.bind(track)
|
||||
insertTrack.await(track.toDomainTrack()!!)
|
||||
// TODO: update all trackers based on common data
|
||||
suspend fun bind(tracker: MangaTracker, item: MangaTrack, mangaId: Long) = withNonCancellableContext {
|
||||
withIOContext {
|
||||
val allChapters = getChaptersByMangaId.await(mangaId)
|
||||
val hasReadChapters = allChapters.any { it.read }
|
||||
tracker.bind(item, hasReadChapters)
|
||||
|
||||
syncChapterProgressWithTrack.await(
|
||||
manga.id,
|
||||
track.toDomainTrack()!!,
|
||||
service.mangaService,
|
||||
var track = item.toDomainTrack(idRequired = false) ?: return@withIOContext
|
||||
|
||||
insertTrack.await(track)
|
||||
|
||||
// TODO: merge into [SyncChapterProgressWithTrack]?
|
||||
// Update chapter progress if newer chapters marked read locally
|
||||
if (hasReadChapters) {
|
||||
val latestLocalReadChapterNumber = allChapters
|
||||
.sortedBy { it.chapterNumber }
|
||||
.takeWhile { it.read }
|
||||
.lastOrNull()
|
||||
?.chapterNumber ?: -1.0
|
||||
|
||||
if (latestLocalReadChapterNumber > track.lastChapterRead) {
|
||||
track = track.copy(
|
||||
lastChapterRead = latestLocalReadChapterNumber,
|
||||
)
|
||||
tracker.setRemoteLastChapterRead(track.toDbTrack(), latestLocalReadChapterNumber.toInt())
|
||||
}
|
||||
|
||||
if (track.startDate <= 0) {
|
||||
val firstReadChapterDate = Injekt.get<GetMangaHistory>().await(mangaId)
|
||||
.sortedBy { it.readAt }
|
||||
.firstOrNull()
|
||||
?.readAt
|
||||
|
||||
firstReadChapterDate?.let {
|
||||
val startDate = firstReadChapterDate.time.convertEpochMillisZone(
|
||||
ZoneOffset.systemDefault(),
|
||||
ZoneOffset.UTC,
|
||||
)
|
||||
track = track.copy(
|
||||
startDate = startDate,
|
||||
)
|
||||
tracker.setRemoteStartDate(track.toDbTrack(), startDate)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logcat(
|
||||
LogPriority.WARN,
|
||||
e,
|
||||
) { "Could not match manga: ${manga.title} with service $service" }
|
||||
}
|
||||
}
|
||||
|
||||
syncChapterProgressWithTrack.await(mangaId, track, tracker)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun bindEnhancedTrackers(manga: Manga, source: MangaSource) = withNonCancellableContext {
|
||||
withIOContext {
|
||||
getTracks.await(manga.id)
|
||||
.filterIsInstance<EnhancedMangaTracker>()
|
||||
.filter { it.accept(source) }
|
||||
.forEach { service ->
|
||||
try {
|
||||
service.match(manga)?.let { track ->
|
||||
track.manga_id = manga.id
|
||||
(service as Tracker).mangaService.bind(track)
|
||||
insertTrack.await(track.toDomainTrack()!!)
|
||||
|
||||
syncChapterProgressWithTrack.await(
|
||||
manga.id,
|
||||
track.toDomainTrack()!!,
|
||||
service.mangaService,
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logcat(
|
||||
LogPriority.WARN,
|
||||
e,
|
||||
) { "Could not match manga: ${manga.title} with service $service" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,7 +43,9 @@ class DelayedMangaTrackingUpdateJob(private val context: Context, workerParams:
|
|||
track?.copy(lastChapterRead = it.lastChapterRead.toDouble())
|
||||
}
|
||||
.forEach { track ->
|
||||
logcat(LogPriority.DEBUG) { "Updating delayed track item: ${track.mangaId}, last chapter read: ${track.lastChapterRead}" }
|
||||
logcat(LogPriority.DEBUG) {
|
||||
"Updating delayed track item: ${track.mangaId}, last chapter read: ${track.lastChapterRead}"
|
||||
}
|
||||
trackChapter.await(context, track.mangaId, track.lastChapterRead)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -74,7 +74,8 @@ internal fun GlobalSearchContent(
|
|||
items.forEach { (source, result) ->
|
||||
item(key = source.id) {
|
||||
GlobalSearchResultItem(
|
||||
title = fromSourceId?.let { "▶ ${source.name}".takeIf { source.id == fromSourceId } } ?: source.name,
|
||||
title = fromSourceId
|
||||
?.let { "▶ ${source.name}".takeIf { source.id == fromSourceId } } ?: source.name,
|
||||
subtitle = LocaleHelper.getDisplayName(source.lang),
|
||||
onClick = { onClickSource(source) },
|
||||
) {
|
||||
|
|
|
@ -26,7 +26,9 @@ fun BaseAnimeSourceItem(
|
|||
action: @Composable RowScope.(AnimeSource) -> Unit = {},
|
||||
content: @Composable RowScope.(AnimeSource, String?) -> Unit = defaultContent,
|
||||
) {
|
||||
val sourceLangString = LocaleHelper.getSourceDisplayName(source.lang, LocalContext.current).takeIf { showLanguageInContent }
|
||||
val sourceLangString = LocaleHelper.getSourceDisplayName(source.lang, LocalContext.current).takeIf {
|
||||
showLanguageInContent
|
||||
}
|
||||
BaseBrowseItem(
|
||||
modifier = modifier,
|
||||
onClickItem = onClickItem,
|
||||
|
|
|
@ -56,7 +56,11 @@ fun BrowseAnimeSourceToolbar(
|
|||
actions = listOfNotNull(
|
||||
AppBar.Action(
|
||||
title = stringResource(R.string.action_display_mode),
|
||||
icon = if (displayMode == LibraryDisplayMode.List) Icons.Filled.ViewList else Icons.Filled.ViewModule,
|
||||
icon = if (displayMode == LibraryDisplayMode.List) {
|
||||
Icons.Filled.ViewList
|
||||
} else {
|
||||
Icons.Filled.ViewModule
|
||||
},
|
||||
onClick = { selectingDisplayMode = true },
|
||||
),
|
||||
if (isLocalSource) {
|
||||
|
|
|
@ -74,7 +74,8 @@ internal fun GlobalSearchContent(
|
|||
items.forEach { (source, result) ->
|
||||
item(key = source.id) {
|
||||
GlobalSearchResultItem(
|
||||
title = fromSourceId?.let { "▶ ${source.name}".takeIf { source.id == fromSourceId } } ?: source.name,
|
||||
title = fromSourceId
|
||||
?.let { "▶ ${source.name}".takeIf { source.id == fromSourceId } } ?: source.name,
|
||||
subtitle = LocaleHelper.getDisplayName(source.lang),
|
||||
onClick = { onClickSource(source) },
|
||||
) {
|
||||
|
|
|
@ -26,7 +26,9 @@ fun BaseMangaSourceItem(
|
|||
action: @Composable RowScope.(Source) -> Unit = {},
|
||||
content: @Composable RowScope.(Source, String?) -> Unit = defaultContent,
|
||||
) {
|
||||
val sourceLangString = LocaleHelper.getSourceDisplayName(source.lang, LocalContext.current).takeIf { showLanguageInContent }
|
||||
val sourceLangString = LocaleHelper.getSourceDisplayName(source.lang, LocalContext.current).takeIf {
|
||||
showLanguageInContent
|
||||
}
|
||||
BaseBrowseItem(
|
||||
modifier = modifier,
|
||||
onClickItem = onClickItem,
|
||||
|
|
|
@ -56,7 +56,11 @@ fun BrowseMangaSourceToolbar(
|
|||
actions = listOfNotNull(
|
||||
AppBar.Action(
|
||||
title = stringResource(R.string.action_display_mode),
|
||||
icon = if (displayMode == LibraryDisplayMode.List) Icons.Filled.ViewList else Icons.Filled.ViewModule,
|
||||
icon = if (displayMode == LibraryDisplayMode.List) {
|
||||
Icons.Filled.ViewList
|
||||
} else {
|
||||
Icons.Filled.ViewModule
|
||||
},
|
||||
onClick = { selectingDisplayMode = true },
|
||||
),
|
||||
if (isLocalSource) {
|
||||
|
|
|
@ -76,8 +76,14 @@ fun ChangeCategoryDialog(
|
|||
onClick = {
|
||||
onDismissRequest()
|
||||
onConfirm(
|
||||
selection.filter { it is CheckboxState.State.Checked || it is CheckboxState.TriState.Include }.map { it.value.id },
|
||||
selection.filter { it is CheckboxState.State.None || it is CheckboxState.TriState.None }.map { it.value.id },
|
||||
selection.filter {
|
||||
it is CheckboxState.State.Checked ||
|
||||
it is CheckboxState.TriState.Include
|
||||
}.map { it.value.id },
|
||||
selection.filter {
|
||||
it is CheckboxState.State.None ||
|
||||
it is CheckboxState.TriState.None
|
||||
}.map { it.value.id },
|
||||
)
|
||||
},
|
||||
) {
|
||||
|
|
|
@ -87,7 +87,11 @@ fun CategoryCreateDialog(
|
|||
onValueChange = { name = it },
|
||||
label = { Text(text = stringResource(R.string.name)) },
|
||||
supportingText = {
|
||||
val msgRes = if (name.isNotEmpty() && nameAlreadyExists) R.string.error_category_exists else R.string.information_required_plain
|
||||
val msgRes = if (name.isNotEmpty() && nameAlreadyExists) {
|
||||
R.string.error_category_exists
|
||||
} else {
|
||||
R.string.information_required_plain
|
||||
}
|
||||
Text(text = stringResource(msgRes))
|
||||
},
|
||||
isError = name.isNotEmpty() && nameAlreadyExists,
|
||||
|
@ -147,7 +151,11 @@ fun CategoryRenameDialog(
|
|||
},
|
||||
label = { Text(text = stringResource(R.string.name)) },
|
||||
supportingText = {
|
||||
val msgRes = if (valueHasChanged && nameAlreadyExists) R.string.error_category_exists else R.string.information_required_plain
|
||||
val msgRes = if (valueHasChanged && nameAlreadyExists) {
|
||||
R.string.error_category_exists
|
||||
} else {
|
||||
R.string.information_required_plain
|
||||
}
|
||||
Text(text = stringResource(msgRes))
|
||||
},
|
||||
isError = valueHasChanged && nameAlreadyExists,
|
||||
|
|
|
@ -126,7 +126,11 @@ fun EntryBottomActionMenu(
|
|||
)
|
||||
}
|
||||
if (onRemoveBookmarkClicked != null) {
|
||||
val removeBookmark = if (isManga) R.string.action_remove_bookmark else R.string.action_remove_bookmark_episode
|
||||
val removeBookmark = if (isManga) {
|
||||
R.string.action_remove_bookmark
|
||||
} else {
|
||||
R.string.action_remove_bookmark_episode
|
||||
}
|
||||
Button(
|
||||
title = stringResource(removeBookmark),
|
||||
icon = Icons.Outlined.BookmarkRemove,
|
||||
|
@ -156,7 +160,11 @@ fun EntryBottomActionMenu(
|
|||
)
|
||||
}
|
||||
if (onMarkPreviousAsViewedClicked != null) {
|
||||
val previousUnviewed = if (isManga) R.string.action_mark_previous_as_read else R.string.action_mark_previous_as_seen
|
||||
val previousUnviewed = if (isManga) {
|
||||
R.string.action_mark_previous_as_read
|
||||
} else {
|
||||
R.string.action_mark_previous_as_seen
|
||||
}
|
||||
Button(
|
||||
title = stringResource(previousUnviewed),
|
||||
icon = ImageVector.vectorResource(R.drawable.ic_done_prev_24dp),
|
||||
|
|
|
@ -510,7 +510,9 @@ private fun AnimeScreenSmallImpl(
|
|||
timer -= 1000L
|
||||
}
|
||||
}
|
||||
if (timer > 0L && showNextEpisodeAirTime && state.anime.status.toInt() != SAnime.COMPLETED) {
|
||||
if (timer > 0L && showNextEpisodeAirTime &&
|
||||
state.anime.status.toInt() != SAnime.COMPLETED
|
||||
) {
|
||||
NextEpisodeAiringListItem(
|
||||
title = stringResource(
|
||||
R.string.display_mode_episode,
|
||||
|
@ -694,7 +696,11 @@ fun AnimeScreenLargeImpl(
|
|||
val isWatching = remember(state.episodes) {
|
||||
state.episodes.fastAny { it.episode.seen }
|
||||
}
|
||||
Text(text = stringResource(if (isWatching) R.string.action_resume else R.string.action_start))
|
||||
Text(
|
||||
text = stringResource(
|
||||
if (isWatching) R.string.action_resume else R.string.action_start,
|
||||
),
|
||||
)
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
|
@ -795,7 +801,9 @@ fun AnimeScreenLargeImpl(
|
|||
timer -= 1000L
|
||||
}
|
||||
}
|
||||
if (timer > 0L && showNextEpisodeAirTime && state.anime.status.toInt() != SAnime.COMPLETED) {
|
||||
if (timer > 0L && showNextEpisodeAirTime &&
|
||||
state.anime.status.toInt() != SAnime.COMPLETED
|
||||
) {
|
||||
NextEpisodeAiringListItem(
|
||||
title = stringResource(
|
||||
R.string.display_mode_episode,
|
||||
|
|
|
@ -74,7 +74,8 @@ fun EpisodeSettingsDialog(
|
|||
0 -> {
|
||||
FilterPage(
|
||||
downloadFilter = anime?.downloadedFilter ?: TriState.DISABLED,
|
||||
onDownloadFilterChanged = onDownloadFilterChanged.takeUnless { anime?.forceDownloaded() == true },
|
||||
onDownloadFilterChanged = onDownloadFilterChanged
|
||||
.takeUnless { anime?.forceDownloaded() == true },
|
||||
unseenFilter = anime?.unseenFilter ?: TriState.DISABLED,
|
||||
onUnseenFilterChanged = onUnseenFilterChanged,
|
||||
bookmarkedFilter = anime?.bookmarkedFilter ?: TriState.DISABLED,
|
||||
|
@ -146,6 +147,11 @@ private fun SortPage(
|
|||
sortDescending = sortDescending.takeIf { sortingMode == Anime.EPISODE_SORTING_UPLOAD_DATE },
|
||||
onClick = { onItemSelected(Anime.EPISODE_SORTING_UPLOAD_DATE) },
|
||||
)
|
||||
SortItem(
|
||||
label = stringResource(R.string.action_sort_alpha),
|
||||
sortDescending = sortDescending.takeIf { sortingMode == Anime.EPISODE_SORTING_ALPHABET },
|
||||
onClick = { onItemSelected(Anime.EPISODE_SORTING_ALPHABET) },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
|
@ -600,7 +600,9 @@ private fun AnimeSummary(
|
|||
val image = AnimatedImageVector.animatedVectorResource(R.drawable.anim_caret_down)
|
||||
Icon(
|
||||
painter = rememberAnimatedVectorPainter(image, !expanded),
|
||||
contentDescription = stringResource(if (expanded) R.string.manga_info_collapse else R.string.manga_info_expand),
|
||||
contentDescription = stringResource(
|
||||
if (expanded) R.string.manga_info_collapse else R.string.manga_info_expand,
|
||||
),
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
modifier = Modifier.background(Brush.radialGradient(colors = colors.asReversed())),
|
||||
)
|
||||
|
|
|
@ -82,7 +82,8 @@ fun ChapterSettingsDialog(
|
|||
0 -> {
|
||||
FilterPage(
|
||||
downloadFilter = manga?.downloadedFilter ?: TriState.DISABLED,
|
||||
onDownloadFilterChanged = onDownloadFilterChanged.takeUnless { manga?.forceDownloaded() == true },
|
||||
onDownloadFilterChanged = onDownloadFilterChanged
|
||||
.takeUnless { manga?.forceDownloaded() == true },
|
||||
unreadFilter = manga?.unreadFilter ?: TriState.DISABLED,
|
||||
onUnreadFilterChanged = onUnreadFilterChanged,
|
||||
bookmarkedFilter = manga?.bookmarkedFilter ?: TriState.DISABLED,
|
||||
|
@ -154,6 +155,11 @@ private fun SortPage(
|
|||
sortDescending = sortDescending.takeIf { sortingMode == Manga.CHAPTER_SORTING_UPLOAD_DATE },
|
||||
onClick = { onItemSelected(Manga.CHAPTER_SORTING_UPLOAD_DATE) },
|
||||
)
|
||||
SortItem(
|
||||
label = stringResource(R.string.action_sort_alpha),
|
||||
sortDescending = sortDescending.takeIf { sortingMode == Manga.CHAPTER_SORTING_ALPHABET },
|
||||
onClick = { onItemSelected(Manga.CHAPTER_SORTING_ALPHABET) },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
|
@ -636,7 +636,9 @@ fun MangaScreenLargeImpl(
|
|||
val isReading = remember(state.chapters) {
|
||||
state.chapters.fastAny { it.chapter.read }
|
||||
}
|
||||
Text(text = stringResource(if (isReading) R.string.action_resume else R.string.action_start))
|
||||
Text(
|
||||
text = stringResource(if (isReading) R.string.action_resume else R.string.action_start),
|
||||
)
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
|
|
|
@ -599,7 +599,9 @@ private fun MangaSummary(
|
|||
val image = AnimatedImageVector.animatedVectorResource(R.drawable.anim_caret_down)
|
||||
Icon(
|
||||
painter = rememberAnimatedVectorPainter(image, !expanded),
|
||||
contentDescription = stringResource(if (expanded) R.string.manga_info_collapse else R.string.manga_info_expand),
|
||||
contentDescription = stringResource(
|
||||
if (expanded) R.string.manga_info_collapse else R.string.manga_info_expand,
|
||||
),
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
modifier = Modifier.background(Brush.radialGradient(colors = colors.asReversed())),
|
||||
)
|
||||
|
|
|
@ -34,7 +34,11 @@ fun HistoryDeleteDialog(
|
|||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
val subtitle = if (isManga) R.string.dialog_with_checkbox_remove_description else R.string.dialog_with_checkbox_remove_description_anime
|
||||
val subtitle = if (isManga) {
|
||||
R.string.dialog_with_checkbox_remove_description
|
||||
} else {
|
||||
R.string.dialog_with_checkbox_remove_description_anime
|
||||
}
|
||||
Text(text = stringResource(subtitle))
|
||||
|
||||
LabeledCheckbox(
|
||||
|
|
|
@ -165,8 +165,16 @@ private fun ColumnScope.SortPage(
|
|||
onClick = {
|
||||
val isTogglingDirection = sortingMode == mode
|
||||
val direction = when {
|
||||
isTogglingDirection -> if (sortDescending) AnimeLibrarySort.Direction.Ascending else AnimeLibrarySort.Direction.Descending
|
||||
else -> if (sortDescending) AnimeLibrarySort.Direction.Descending else AnimeLibrarySort.Direction.Ascending
|
||||
isTogglingDirection -> if (sortDescending) {
|
||||
AnimeLibrarySort.Direction.Ascending
|
||||
} else {
|
||||
AnimeLibrarySort.Direction.Descending
|
||||
}
|
||||
else -> if (sortDescending) {
|
||||
AnimeLibrarySort.Direction.Descending
|
||||
} else {
|
||||
AnimeLibrarySort.Direction.Ascending
|
||||
}
|
||||
}
|
||||
screenModel.setSort(category, mode, direction)
|
||||
},
|
||||
|
|
|
@ -148,6 +148,13 @@ private fun ColumnScope.SortPage(
|
|||
val sortingMode = category.sort.type
|
||||
val sortDescending = !category.sort.isAscending
|
||||
|
||||
val trackerSortOption =
|
||||
if (screenModel.trackers.isEmpty()) {
|
||||
emptyList()
|
||||
} else {
|
||||
listOf(R.string.action_sort_tracker_score to MangaLibrarySort.Type.TrackerMean)
|
||||
}
|
||||
|
||||
listOf(
|
||||
R.string.action_sort_alpha to MangaLibrarySort.Type.Alphabetical,
|
||||
R.string.action_sort_total to MangaLibrarySort.Type.TotalChapters,
|
||||
|
@ -157,15 +164,23 @@ private fun ColumnScope.SortPage(
|
|||
R.string.action_sort_latest_chapter to MangaLibrarySort.Type.LatestChapter,
|
||||
R.string.action_sort_chapter_fetch_date to MangaLibrarySort.Type.ChapterFetchDate,
|
||||
R.string.action_sort_date_added to MangaLibrarySort.Type.DateAdded,
|
||||
).map { (titleRes, mode) ->
|
||||
).plus(trackerSortOption).map { (titleRes, mode) ->
|
||||
SortItem(
|
||||
label = stringResource(titleRes),
|
||||
sortDescending = sortDescending.takeIf { sortingMode == mode },
|
||||
onClick = {
|
||||
val isTogglingDirection = sortingMode == mode
|
||||
val direction = when {
|
||||
isTogglingDirection -> if (sortDescending) MangaLibrarySort.Direction.Ascending else MangaLibrarySort.Direction.Descending
|
||||
else -> if (sortDescending) MangaLibrarySort.Direction.Descending else MangaLibrarySort.Direction.Ascending
|
||||
isTogglingDirection -> if (sortDescending) {
|
||||
MangaLibrarySort.Direction.Ascending
|
||||
} else {
|
||||
MangaLibrarySort.Direction.Descending
|
||||
}
|
||||
else -> if (sortDescending) {
|
||||
MangaLibrarySort.Direction.Descending
|
||||
} else {
|
||||
MangaLibrarySort.Direction.Ascending
|
||||
}
|
||||
}
|
||||
screenModel.setSort(category, mode, direction)
|
||||
},
|
||||
|
|
|
@ -31,11 +31,13 @@ fun getCategoriesLabel(
|
|||
|
||||
val includedItemsText = when {
|
||||
// Some selected, but not all
|
||||
includedCategories.isNotEmpty() && includedCategories.size != allCategories.size -> includedCategories.joinToString {
|
||||
it.visualName(
|
||||
context,
|
||||
)
|
||||
}
|
||||
includedCategories.isNotEmpty() &&
|
||||
includedCategories.size != allCategories.size ->
|
||||
includedCategories.joinToString {
|
||||
it.visualName(
|
||||
context,
|
||||
)
|
||||
}
|
||||
// All explicitly selected
|
||||
includedCategories.size == allCategories.size -> stringResource(R.string.all)
|
||||
allExcluded -> stringResource(R.string.none)
|
||||
|
|
|
@ -162,7 +162,8 @@ object SettingsDownloadScreen : SearchableSettings {
|
|||
return remember {
|
||||
val file = UniFile.fromFile(
|
||||
File(
|
||||
"${Environment.getExternalStorageDirectory().absolutePath}${File.separator}${Environment.DIRECTORY_DOWNLOADS}${File.separator}$appName",
|
||||
Environment.getExternalStorageDirectory().absolutePath +
|
||||
"${File.separator}${Environment.DIRECTORY_DOWNLOADS}${File.separator}$appName",
|
||||
"downloads",
|
||||
),
|
||||
)!!
|
||||
|
|
|
@ -371,6 +371,11 @@ object SettingsReaderScreen : SearchableSettings {
|
|||
pref = readerPreferences.readWithLongTap(),
|
||||
title = stringResource(R.string.pref_read_with_long_tap),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.folderPerManga(),
|
||||
title = stringResource(R.string.pref_create_folder_per_manga),
|
||||
subtitle = stringResource(R.string.pref_create_folder_per_manga_summary),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -227,6 +227,9 @@ object AboutScreen : Screen() {
|
|||
is GetApplicationRelease.Result.NoNewUpdate -> {
|
||||
context.toast(R.string.update_check_no_new_updates)
|
||||
}
|
||||
is GetApplicationRelease.Result.OsTooOld -> {
|
||||
context.toast(R.string.update_check_eol)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
|
|
@ -76,15 +76,28 @@ class DebugInfoScreen : Screen() {
|
|||
val status by produceState(initialValue = "-") {
|
||||
val result = ProfileVerifier.getCompilationStatusAsync().await().profileInstallResultCode
|
||||
value = when (result) {
|
||||
ProfileVerifier.CompilationStatus.RESULT_CODE_NO_PROFILE -> "No profile installed"
|
||||
ProfileVerifier.CompilationStatus.RESULT_CODE_COMPILED_WITH_PROFILE -> "Compiled"
|
||||
ProfileVerifier.CompilationStatus.RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING -> "Compiled non-matching"
|
||||
ProfileVerifier.CompilationStatus.RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ,
|
||||
ProfileVerifier.CompilationStatus.RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE,
|
||||
ProfileVerifier.CompilationStatus.RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST,
|
||||
ProfileVerifier.CompilationStatus
|
||||
.RESULT_CODE_NO_PROFILE,
|
||||
-> "No profile installed"
|
||||
ProfileVerifier.CompilationStatus
|
||||
.RESULT_CODE_COMPILED_WITH_PROFILE,
|
||||
-> "Compiled"
|
||||
ProfileVerifier.CompilationStatus
|
||||
.RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING,
|
||||
-> "Compiled non-matching"
|
||||
ProfileVerifier.CompilationStatus
|
||||
.RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ,
|
||||
ProfileVerifier.CompilationStatus
|
||||
.RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE,
|
||||
ProfileVerifier.CompilationStatus
|
||||
.RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST,
|
||||
-> "Error $result"
|
||||
ProfileVerifier.CompilationStatus.RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION -> "Not supported"
|
||||
ProfileVerifier.CompilationStatus.RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION -> "Pending compilation"
|
||||
ProfileVerifier.CompilationStatus
|
||||
.RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION,
|
||||
-> "Not supported"
|
||||
ProfileVerifier.CompilationStatus
|
||||
.RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION,
|
||||
-> "Pending compilation"
|
||||
else -> "Unknown code $result"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,11 +4,14 @@ import androidx.compose.foundation.layout.Box
|
|||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||
import tachiyomi.presentation.core.util.ThemePreviews
|
||||
|
||||
@Composable
|
||||
fun PageIndicatorText(
|
||||
|
@ -19,24 +22,36 @@ fun PageIndicatorText(
|
|||
|
||||
val text = "$currentPage / $totalPages"
|
||||
|
||||
Box {
|
||||
val style = TextStyle(
|
||||
color = Color(235, 235, 235),
|
||||
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
||||
fontWeight = FontWeight.Bold,
|
||||
letterSpacing = 1.sp,
|
||||
)
|
||||
val strokeStyle = style.copy(
|
||||
color = Color(45, 45, 45),
|
||||
drawStyle = Stroke(width = 4f),
|
||||
)
|
||||
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
color = Color(45, 45, 45),
|
||||
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
||||
fontWeight = FontWeight.Bold,
|
||||
letterSpacing = 1.sp,
|
||||
style = TextStyle.Default.copy(
|
||||
drawStyle = Stroke(width = 4f),
|
||||
),
|
||||
style = strokeStyle,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = text,
|
||||
color = Color(235, 235, 235),
|
||||
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
||||
fontWeight = FontWeight.Bold,
|
||||
letterSpacing = 1.sp,
|
||||
style = style,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
private fun PageIndicatorTextPreview() {
|
||||
TachiyomiTheme {
|
||||
PageIndicatorText(currentPage = 10, totalPages = 69)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -99,7 +99,9 @@ fun ReaderAppBars(
|
|||
AppBarActions(
|
||||
listOfNotNull(
|
||||
AppBar.Action(
|
||||
title = stringResource(if (bookmarked) R.string.action_remove_bookmark else R.string.action_bookmark),
|
||||
title = stringResource(
|
||||
if (bookmarked) R.string.action_remove_bookmark else R.string.action_bookmark,
|
||||
),
|
||||
icon = if (bookmarked) Icons.Outlined.Bookmark else Icons.Outlined.BookmarkBorder,
|
||||
onClick = onToggleBookmarked,
|
||||
),
|
||||
|
|
|
@ -167,7 +167,9 @@ fun WebViewScreenContent(
|
|||
modifier = Modifier
|
||||
.clip(MaterialTheme.shapes.small)
|
||||
.clickable {
|
||||
uriHandler.openUri("https://tachiyomi.org/docs/guides/troubleshooting/#cloudflare")
|
||||
uriHandler.openUri(
|
||||
"https://tachiyomi.org/docs/guides/troubleshooting/#cloudflare",
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import android.content.IntentFilter
|
|||
import android.os.Build
|
||||
import android.os.Looper
|
||||
import android.webkit.WebView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
|
@ -203,7 +204,7 @@ class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
|||
if (chromiumElement?.methodName.equals("getAll", ignoreCase = true)) {
|
||||
return WebViewUtil.SPOOF_PACKAGE_NAME
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
return super.getPackageName()
|
||||
|
@ -247,7 +248,12 @@ class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
|||
|
||||
fun register() {
|
||||
if (!registered) {
|
||||
registerReceiver(this, IntentFilter(ACTION_DISABLE_INCOGNITO_MODE))
|
||||
ContextCompat.registerReceiver(
|
||||
this@App,
|
||||
this,
|
||||
IntentFilter(ACTION_DISABLE_INCOGNITO_MODE),
|
||||
ContextCompat.RECEIVER_NOT_EXPORTED,
|
||||
)
|
||||
registered = true
|
||||
}
|
||||
}
|
||||
|
@ -266,7 +272,7 @@ private const val ACTION_DISABLE_INCOGNITO_MODE = "tachi.action.DISABLE_INCOGNIT
|
|||
/**
|
||||
* Direct copy of Coil's internal SingletonDiskCache so that [MangaCoverFetcher] can access it.
|
||||
*/
|
||||
internal object CoilDiskCache {
|
||||
private object CoilDiskCache {
|
||||
|
||||
private const val FOLDER_NAME = "image_cache"
|
||||
private var instance: DiskCache? = null
|
||||
|
|
|
@ -304,7 +304,9 @@ object Migrations {
|
|||
SecurityPreferences.SecureScreenMode.ALWAYS,
|
||||
)
|
||||
}
|
||||
if (DeviceUtil.isMiui && basePreferences.extensionInstaller().get() == BasePreferences.ExtensionInstaller.PACKAGEINSTALLER) {
|
||||
if (DeviceUtil.isMiui &&
|
||||
basePreferences.extensionInstaller().get() == BasePreferences.ExtensionInstaller.PACKAGEINSTALLER
|
||||
) {
|
||||
basePreferences.extensionInstaller().set(
|
||||
BasePreferences.ExtensionInstaller.LEGACY,
|
||||
)
|
||||
|
@ -507,7 +509,10 @@ object Migrations {
|
|||
if (oldVersion < 107) {
|
||||
replacePreferences(
|
||||
preferenceStore = preferenceStore,
|
||||
filterPredicate = { it.key.startsWith("pref_mangasync_") || it.key.startsWith("track_token_") },
|
||||
filterPredicate = {
|
||||
it.key.startsWith("pref_mangasync_") ||
|
||||
it.key.startsWith("track_token_")
|
||||
},
|
||||
newKey = { Preference.privateKey(it) },
|
||||
)
|
||||
}
|
||||
|
|
|
@ -150,7 +150,9 @@ class BackupRestorer(
|
|||
private suspend fun performRestore(uri: Uri, sync: Boolean): Boolean {
|
||||
val backup = BackupUtil.decodeBackup(context, uri)
|
||||
|
||||
restoreAmount = backup.backupManga.size + backup.backupAnime.size + 3 // +3 for categories, app prefs, source prefs
|
||||
restoreAmount =
|
||||
backup.backupManga.size +
|
||||
backup.backupAnime.size + 3 // +3 for categories, app prefs, source prefs
|
||||
|
||||
// Restore categories
|
||||
if (backup.backupCategories.isNotEmpty()) {
|
||||
|
@ -1262,7 +1264,12 @@ class BackupRestorer(
|
|||
}
|
||||
|
||||
restoreProgress += 1
|
||||
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.extension_settings), context.getString(R.string.restoring_backup))
|
||||
showRestoreProgress(
|
||||
restoreProgress,
|
||||
restoreAmount,
|
||||
context.getString(R.string.extension_settings),
|
||||
context.getString(R.string.restoring_backup),
|
||||
)
|
||||
}
|
||||
|
||||
private fun restoreExtensions(extensions: List<BackupExtension>) {
|
||||
|
|
|
@ -72,7 +72,22 @@ data class BackupAnimeTracking(
|
|||
}
|
||||
}
|
||||
|
||||
val backupAnimeTrackMapper = { _id: Long, anime_id: Long, syncId: Long, mediaId: Long, libraryId: Long?, title: String, lastEpisodeSeen: Double, totalEpisodes: Long, status: Long, score: Double, remoteUrl: String, startDate: Long, finishDate: Long ->
|
||||
val backupAnimeTrackMapper = {
|
||||
_id: Long,
|
||||
anime_id:
|
||||
Long,
|
||||
syncId: Long,
|
||||
mediaId: Long,
|
||||
libraryId: Long?,
|
||||
title: String,
|
||||
lastEpisodeSeen: Double,
|
||||
totalEpisodes: Long,
|
||||
status: Long,
|
||||
score: Double,
|
||||
remoteUrl: String,
|
||||
startDate: Long,
|
||||
finishDate: Long,
|
||||
->
|
||||
BackupAnimeTracking(
|
||||
syncId = syncId.toInt(),
|
||||
mediaId = mediaId,
|
||||
|
|
|
@ -39,7 +39,21 @@ data class BackupChapter(
|
|||
}
|
||||
}
|
||||
|
||||
val backupChapterMapper = { _: Long, _: Long, url: String, name: String, scanlator: String?, read: Boolean, bookmark: Boolean, lastPageRead: Long, chapterNumber: Double, source_order: Long, dateFetch: Long, dateUpload: Long, lastModifiedAt: Long ->
|
||||
val backupChapterMapper = {
|
||||
_: Long,
|
||||
_: Long,
|
||||
url: String,
|
||||
name: String,
|
||||
scanlator: String?,
|
||||
read: Boolean,
|
||||
bookmark: Boolean,
|
||||
lastPageRead: Long,
|
||||
chapterNumber: Double,
|
||||
source_order: Long,
|
||||
dateFetch: Long,
|
||||
dateUpload: Long,
|
||||
lastModifiedAt: Long,
|
||||
->
|
||||
BackupChapter(
|
||||
url = url,
|
||||
name = name,
|
||||
|
|
|
@ -41,7 +41,22 @@ data class BackupEpisode(
|
|||
}
|
||||
}
|
||||
|
||||
val backupEpisodeMapper = { _: Long, _: Long, url: String, name: String, scanlator: String?, seen: Boolean, bookmark: Boolean, lastSecondSeen: Long, totalSeconds: Long, episodeNumber: Double, source_order: Long, dateFetch: Long, dateUpload: Long, lastModifiedAt: Long ->
|
||||
val backupEpisodeMapper = {
|
||||
_: Long,
|
||||
_: Long,
|
||||
url: String,
|
||||
name: String,
|
||||
scanlator: String?,
|
||||
seen: Boolean,
|
||||
bookmark: Boolean,
|
||||
lastSecondSeen: Long,
|
||||
totalSeconds: Long,
|
||||
episodeNumber: Double,
|
||||
source_order: Long,
|
||||
dateFetch: Long,
|
||||
dateUpload: Long,
|
||||
lastModifiedAt: Long,
|
||||
->
|
||||
BackupEpisode(
|
||||
url = url,
|
||||
name = name,
|
||||
|
|
|
@ -54,7 +54,20 @@ data class BackupTracking(
|
|||
}
|
||||
|
||||
val backupTrackMapper = {
|
||||
_: Long, _: Long, syncId: Long, mediaId: Long, libraryId: Long?, title: String, lastChapterRead: Double, totalChapters: Long, status: Long, score: Double, remoteUrl: String, startDate: Long, finishDate: Long ->
|
||||
_: Long,
|
||||
_: Long,
|
||||
syncId: Long,
|
||||
mediaId: Long,
|
||||
libraryId: Long?,
|
||||
title: String,
|
||||
lastChapterRead: Double,
|
||||
totalChapters: Long,
|
||||
status: Long,
|
||||
score: Double,
|
||||
remoteUrl: String,
|
||||
startDate: Long,
|
||||
finishDate: Long,
|
||||
->
|
||||
BackupTracking(
|
||||
syncId = syncId.toInt(),
|
||||
mediaId = mediaId,
|
||||
|
|
|
@ -138,7 +138,10 @@ class AnimeDownloadCache(
|
|||
if (sourceDir != null) {
|
||||
val animeDir = sourceDir.animeDirs[provider.getAnimeDirName(animeTitle)]
|
||||
if (animeDir != null) {
|
||||
return provider.getValidEpisodeDirNames(episodeName, episodeScanlator).any { it in animeDir.episodeDirs }
|
||||
return provider.getValidEpisodeDirNames(
|
||||
episodeName,
|
||||
episodeScanlator,
|
||||
).any { it in animeDir.episodeDirs }
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
|
|
@ -206,7 +206,9 @@ class AnimeDownloader(
|
|||
val activeDownloadsFlow = queueState.transformLatest { queue ->
|
||||
while (true) {
|
||||
val activeDownloads = queue.asSequence()
|
||||
.filter { it.status.value <= AnimeDownload.State.DOWNLOADING.value } // Ignore completed downloads, leave them in the queue
|
||||
.filter {
|
||||
it.status.value <= AnimeDownload.State.DOWNLOADING.value
|
||||
} // Ignore completed downloads, leave them in the queue
|
||||
.groupBy { it.source }
|
||||
.toList().take(3) // Concurrently download from 5 different sources
|
||||
.map { (_, downloads) -> downloads.first() }
|
||||
|
@ -554,7 +556,8 @@ class AnimeDownloader(
|
|||
val ffmpegOptions = getFFmpegOptions(video, headerOptions, ffmpegFilename())
|
||||
val ffprobeCommand = { file: String, ffprobeHeaders: String? ->
|
||||
FFmpegKitConfig.parseArguments(
|
||||
"${ffprobeHeaders?.plus(" ") ?: ""}-v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 \"$file\"",
|
||||
"${ffprobeHeaders?.plus(" ") ?: ""}-v error -show_entries " +
|
||||
"format=duration -of default=noprint_wrappers=1:nokey=1 \"$file\"",
|
||||
)
|
||||
}
|
||||
var duration = 0L
|
||||
|
@ -905,7 +908,9 @@ class AnimeDownloader(
|
|||
val downloads = queue.filter { predicate(it) }
|
||||
store.removeAll(downloads)
|
||||
downloads.forEach { download ->
|
||||
if (download.status == AnimeDownload.State.DOWNLOADING || download.status == AnimeDownload.State.QUEUE) {
|
||||
if (download.status == AnimeDownload.State.DOWNLOADING ||
|
||||
download.status == AnimeDownload.State.QUEUE
|
||||
) {
|
||||
download.status = AnimeDownload.State.NOT_DOWNLOADED
|
||||
}
|
||||
}
|
||||
|
@ -927,7 +932,9 @@ class AnimeDownloader(
|
|||
it.forEach { download ->
|
||||
download.progressSubject = null
|
||||
download.progressCallback = null
|
||||
if (download.status == AnimeDownload.State.DOWNLOADING || download.status == AnimeDownload.State.QUEUE) {
|
||||
if (download.status == AnimeDownload.State.DOWNLOADING ||
|
||||
download.status == AnimeDownload.State.QUEUE
|
||||
) {
|
||||
download.status = AnimeDownload.State.NOT_DOWNLOADED
|
||||
}
|
||||
}
|
||||
|
|
|
@ -391,9 +391,10 @@ class MangaDownloadCache(
|
|||
// Folder of images
|
||||
it.isDirectory -> it.name
|
||||
// CBZ files
|
||||
it.isFile && it.name?.endsWith(".cbz") == true -> it.name!!.substringBeforeLast(
|
||||
".cbz",
|
||||
)
|
||||
it.isFile && it.name?.endsWith(".cbz") == true ->
|
||||
it.name!!.substringBeforeLast(
|
||||
".cbz",
|
||||
)
|
||||
// Anything else is irrelevant
|
||||
else -> null
|
||||
}
|
||||
|
|
|
@ -206,7 +206,9 @@ class MangaDownloader(
|
|||
val activeDownloadsFlow = queueState.transformLatest { queue ->
|
||||
while (true) {
|
||||
val activeDownloads = queue.asSequence()
|
||||
.filter { it.status.value <= MangaDownload.State.DOWNLOADING.value } // Ignore completed downloads, leave them in the queue
|
||||
.filter {
|
||||
it.status.value <= MangaDownload.State.DOWNLOADING.value
|
||||
} // Ignore completed downloads, leave them in the queue
|
||||
.groupBy { it.source }
|
||||
.toList().take(5) // Concurrently download from 5 different sources
|
||||
.map { (_, downloads) -> downloads.first() }
|
||||
|
@ -726,7 +728,9 @@ class MangaDownloader(
|
|||
val downloads = queue.filter { predicate(it) }
|
||||
store.removeAll(downloads)
|
||||
downloads.forEach { download ->
|
||||
if (download.status == MangaDownload.State.DOWNLOADING || download.status == MangaDownload.State.QUEUE) {
|
||||
if (download.status == MangaDownload.State.DOWNLOADING ||
|
||||
download.status == MangaDownload.State.QUEUE
|
||||
) {
|
||||
download.status = MangaDownload.State.NOT_DOWNLOADED
|
||||
}
|
||||
}
|
||||
|
@ -746,7 +750,9 @@ class MangaDownloader(
|
|||
private fun _clearQueue() {
|
||||
_queueState.update {
|
||||
it.forEach { download ->
|
||||
if (download.status == MangaDownload.State.DOWNLOADING || download.status == MangaDownload.State.QUEUE) {
|
||||
if (download.status == MangaDownload.State.DOWNLOADING ||
|
||||
download.status == MangaDownload.State.QUEUE
|
||||
) {
|
||||
download.status = MangaDownload.State.NOT_DOWNLOADED
|
||||
}
|
||||
}
|
||||
|
|
|
@ -287,7 +287,8 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
|||
hasDownloads.set(true)
|
||||
}
|
||||
|
||||
libraryPreferences.newAnimeUpdatesCount().getAndSet { it + newChapters.size }
|
||||
libraryPreferences.newAnimeUpdatesCount()
|
||||
.getAndSet { it + newChapters.size }
|
||||
|
||||
// Convert to the manga that contains new chapters
|
||||
newUpdates.add(anime to newChapters.toTypedArray())
|
||||
|
@ -296,7 +297,9 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
|||
val errorMessage = when (e) {
|
||||
is NoEpisodesException -> context.getString(R.string.no_chapters_error)
|
||||
// failedUpdates will already have the source, don't need to copy it into the message
|
||||
is AnimeSourceNotInstalledException -> context.getString(R.string.loader_not_implemented_error)
|
||||
is AnimeSourceNotInstalledException -> context.getString(
|
||||
R.string.loader_not_implemented_error,
|
||||
)
|
||||
else -> e.message
|
||||
}
|
||||
failedUpdates.add(anime to errorMessage)
|
||||
|
@ -506,7 +509,9 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
|||
if (interval > 0) {
|
||||
val restrictions = preferences.autoUpdateDeviceRestrictions().get()
|
||||
val constraints = Constraints(
|
||||
requiredNetworkType = if (DEVICE_NETWORK_NOT_METERED in restrictions) { NetworkType.UNMETERED } else { NetworkType.CONNECTED },
|
||||
requiredNetworkType = if (DEVICE_NETWORK_NOT_METERED in restrictions) {
|
||||
NetworkType.UNMETERED
|
||||
} else { NetworkType.CONNECTED },
|
||||
requiresCharging = DEVICE_CHARGING in restrictions,
|
||||
requiresBatteryNotLow = true,
|
||||
)
|
||||
|
|
|
@ -86,7 +86,12 @@ class AnimeLibraryUpdateNotifier(private val context: Context) {
|
|||
} else {
|
||||
val updatingText = anime.joinToString("\n") { it.title.chop(40) }
|
||||
progressNotificationBuilder
|
||||
.setContentTitle(context.getString(R.string.notification_updating_progress, percentFormatter.format(current.toFloat() / total)))
|
||||
.setContentTitle(
|
||||
context.getString(
|
||||
R.string.notification_updating_progress,
|
||||
percentFormatter.format(current.toFloat() / total),
|
||||
),
|
||||
)
|
||||
.setStyle(NotificationCompat.BigTextStyle().bigText(updatingText))
|
||||
}
|
||||
|
||||
|
@ -381,7 +386,8 @@ class AnimeLibraryUpdateNotifier(private val context: Context) {
|
|||
|
||||
companion object {
|
||||
// TODO: Change when implemented on Aniyomi website
|
||||
const val HELP_WARNING_URL = "https://aniyomi.org/docs/faq/library#why-am-i-warned-about-large-bulk-updates-and-downloads"
|
||||
const val HELP_WARNING_URL =
|
||||
"https://aniyomi.org/docs/faq/library#why-am-i-warned-about-large-bulk-updates-and-downloads"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -286,7 +286,8 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
|||
downloadChapters(manga, newChapters)
|
||||
hasDownloads.set(true)
|
||||
}
|
||||
libraryPreferences.newMangaUpdatesCount().getAndSet { it + newChapters.size }
|
||||
libraryPreferences.newMangaUpdatesCount()
|
||||
.getAndSet { it + newChapters.size }
|
||||
|
||||
// Convert to the manga that contains new chapters
|
||||
newUpdates.add(manga to newChapters.toTypedArray())
|
||||
|
@ -295,7 +296,9 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
|||
val errorMessage = when (e) {
|
||||
is NoChaptersException -> context.getString(R.string.no_chapters_error)
|
||||
// failedUpdates will already have the source, don't need to copy it into the message
|
||||
is SourceNotInstalledException -> context.getString(R.string.loader_not_implemented_error)
|
||||
is SourceNotInstalledException -> context.getString(
|
||||
R.string.loader_not_implemented_error,
|
||||
)
|
||||
else -> e.message
|
||||
}
|
||||
failedUpdates.add(manga to errorMessage)
|
||||
|
@ -504,7 +507,9 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
|||
if (interval > 0) {
|
||||
val restrictions = preferences.autoUpdateDeviceRestrictions().get()
|
||||
val constraints = Constraints(
|
||||
requiredNetworkType = if (DEVICE_NETWORK_NOT_METERED in restrictions) { NetworkType.UNMETERED } else { NetworkType.CONNECTED },
|
||||
requiredNetworkType = if (DEVICE_NETWORK_NOT_METERED in restrictions) {
|
||||
NetworkType.UNMETERED
|
||||
} else { NetworkType.CONNECTED },
|
||||
requiresCharging = DEVICE_CHARGING in restrictions,
|
||||
requiresBatteryNotLow = true,
|
||||
)
|
||||
|
|
|
@ -86,7 +86,12 @@ class MangaLibraryUpdateNotifier(private val context: Context) {
|
|||
} else {
|
||||
val updatingText = manga.joinToString("\n") { it.title.chop(40) }
|
||||
progressNotificationBuilder
|
||||
.setContentTitle(context.getString(R.string.notification_updating_progress, percentFormatter.format(current.toFloat() / total)))
|
||||
.setContentTitle(
|
||||
context.getString(
|
||||
R.string.notification_updating_progress,
|
||||
percentFormatter.format(current.toFloat() / total),
|
||||
),
|
||||
)
|
||||
.setStyle(NotificationCompat.BigTextStyle().bigText(updatingText))
|
||||
}
|
||||
|
||||
|
@ -384,7 +389,8 @@ class MangaLibraryUpdateNotifier(private val context: Context) {
|
|||
|
||||
companion object {
|
||||
// TODO: Change when implemented on Aniyomi website
|
||||
const val HELP_WARNING_URL = "https://aniyomi.org/docs/faq/library#why-am-i-warned-about-large-bulk-updates-and-downloads"
|
||||
const val HELP_WARNING_URL =
|
||||
"https://aniyomi.org/docs/faq/library#why-am-i-warned-about-large-bulk-updates-and-downloads"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -892,12 +892,21 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||
* @param context context of application
|
||||
* @return [PendingIntent]
|
||||
*/
|
||||
internal fun downloadAppUpdatePendingBroadcast(context: Context, url: String, title: String? = null): PendingIntent {
|
||||
internal fun downloadAppUpdatePendingBroadcast(
|
||||
context: Context,
|
||||
url: String,
|
||||
title: String? = null,
|
||||
): PendingIntent {
|
||||
return Intent(context, NotificationReceiver::class.java).run {
|
||||
action = ACTION_START_APP_UPDATE
|
||||
putExtra(AppUpdateDownloadJob.EXTRA_DOWNLOAD_URL, url)
|
||||
title?.let { putExtra(AppUpdateDownloadJob.EXTRA_DOWNLOAD_TITLE, it) }
|
||||
PendingIntent.getBroadcast(context, 0, this, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
PendingIntent.getBroadcast(
|
||||
context,
|
||||
0,
|
||||
this,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -175,12 +175,19 @@ sealed class Image(
|
|||
}
|
||||
|
||||
sealed interface Location {
|
||||
data class Pictures(val relativePath: String) : Location
|
||||
data class Pictures(val relativePath: String) : Location {
|
||||
companion object {
|
||||
fun create(relativePath: String = ""): Pictures {
|
||||
return Pictures(relativePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data object Cache : Location
|
||||
|
||||
fun directory(context: Context): File {
|
||||
return when (this) {
|
||||
Cache -> context.cacheImageDir
|
||||
is Pictures -> {
|
||||
val file = File(
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
|
||||
|
@ -194,7 +201,6 @@ sealed interface Location {
|
|||
}
|
||||
file
|
||||
}
|
||||
Cache -> context.cacheImageDir
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,28 +1,23 @@
|
|||
package eu.kanade.tachiyomi.data.track
|
||||
|
||||
import android.app.Application
|
||||
import eu.kanade.domain.track.anime.interactor.SyncEpisodeProgressWithTrack
|
||||
import eu.kanade.domain.track.anime.model.toDbTrack
|
||||
import eu.kanade.domain.track.anime.interactor.AddAnimeTracks
|
||||
import eu.kanade.domain.track.anime.model.toDomainTrack
|
||||
import eu.kanade.tachiyomi.data.database.models.anime.AnimeTrack
|
||||
import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch
|
||||
import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import logcat.LogPriority
|
||||
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.GetEpisodesByAnimeId
|
||||
import tachiyomi.domain.track.anime.interactor.InsertAnimeTrack
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.time.ZoneOffset
|
||||
import tachiyomi.domain.track.anime.model.AnimeTrack as DomainAnimeTrack
|
||||
|
||||
private val addTracks: AddAnimeTracks by injectLazy()
|
||||
private val insertTrack: InsertAnimeTrack by injectLazy()
|
||||
private val syncEpisodeProgressWithTrack: SyncEpisodeProgressWithTrack by injectLazy()
|
||||
|
||||
interface AnimeTracker {
|
||||
|
||||
|
@ -61,55 +56,7 @@ interface AnimeTracker {
|
|||
suspend fun register(item: AnimeTrack, animeId: Long) {
|
||||
item.anime_id = animeId
|
||||
try {
|
||||
withIOContext {
|
||||
val allEpisodes = Injekt.get<GetEpisodesByAnimeId>().await(animeId)
|
||||
val hasSeenEpisodes = allEpisodes.any { it.seen }
|
||||
bind(item, hasSeenEpisodes)
|
||||
|
||||
var track = item.toDomainTrack(idRequired = false) ?: return@withIOContext
|
||||
|
||||
insertTrack.await(track)
|
||||
|
||||
// TODO: merge into SyncChaptersWithTrackServiceTwoWay?
|
||||
// Update episode progress if newer episodes marked seen locally
|
||||
if (hasSeenEpisodes) {
|
||||
val latestLocalSeenEpisodeNumber = allEpisodes
|
||||
.sortedBy { it.episodeNumber }
|
||||
.takeWhile { it.seen }
|
||||
.lastOrNull()
|
||||
?.episodeNumber ?: -1.0
|
||||
|
||||
if (latestLocalSeenEpisodeNumber > track.lastEpisodeSeen) {
|
||||
track = track.copy(
|
||||
lastEpisodeSeen = latestLocalSeenEpisodeNumber,
|
||||
)
|
||||
setRemoteLastEpisodeSeen(
|
||||
track.toDbTrack(),
|
||||
latestLocalSeenEpisodeNumber.toInt(),
|
||||
)
|
||||
}
|
||||
|
||||
if (track.startDate <= 0) {
|
||||
val firstReadChapterDate = Injekt.get<GetAnimeHistory>().await(animeId)
|
||||
.sortedBy { it.seenAt }
|
||||
.firstOrNull()
|
||||
?.seenAt
|
||||
|
||||
firstReadChapterDate?.let {
|
||||
val startDate = firstReadChapterDate.time.convertEpochMillisZone(
|
||||
ZoneOffset.systemDefault(),
|
||||
ZoneOffset.UTC,
|
||||
)
|
||||
track = track.copy(
|
||||
startDate = startDate,
|
||||
)
|
||||
setRemoteStartDate(track.toDbTrack(), startDate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
syncEpisodeProgressWithTrack.await(animeId, track, this@AnimeTracker)
|
||||
}
|
||||
addTracks.bind(this, item, animeId)
|
||||
} catch (e: Throwable) {
|
||||
withUIContext { Injekt.get<Application>().toast(e.message) }
|
||||
}
|
||||
|
@ -120,11 +67,14 @@ interface AnimeTracker {
|
|||
if (track.status == getCompletionStatus() && track.total_episodes != 0) {
|
||||
track.last_episode_seen = track.total_episodes.toFloat()
|
||||
}
|
||||
withIOContext { updateRemote(track) }
|
||||
updateRemote(track)
|
||||
}
|
||||
|
||||
suspend fun setRemoteLastEpisodeSeen(track: AnimeTrack, episodeNumber: Int) {
|
||||
if (track.last_episode_seen == 0f && track.last_episode_seen < episodeNumber && track.status != getRewatchingStatus()) {
|
||||
if (track.last_episode_seen == 0f &&
|
||||
track.last_episode_seen < episodeNumber &&
|
||||
track.status != getRewatchingStatus()
|
||||
) {
|
||||
track.status = getWatchingStatus()
|
||||
}
|
||||
track.last_episode_seen = episodeNumber.toFloat()
|
||||
|
@ -132,35 +82,33 @@ interface AnimeTracker {
|
|||
track.status = getCompletionStatus()
|
||||
track.finished_watching_date = System.currentTimeMillis()
|
||||
}
|
||||
withIOContext { updateRemote(track) }
|
||||
updateRemote(track)
|
||||
}
|
||||
|
||||
suspend fun setRemoteScore(track: AnimeTrack, scoreString: String) {
|
||||
track.score = indexToScore(getScoreList().indexOf(scoreString))
|
||||
withIOContext { updateRemote(track) }
|
||||
updateRemote(track)
|
||||
}
|
||||
|
||||
suspend fun setRemoteStartDate(track: AnimeTrack, epochMillis: Long) {
|
||||
track.started_watching_date = epochMillis
|
||||
withIOContext { updateRemote(track) }
|
||||
updateRemote(track)
|
||||
}
|
||||
|
||||
suspend fun setRemoteFinishDate(track: AnimeTrack, epochMillis: Long) {
|
||||
track.finished_watching_date = epochMillis
|
||||
withIOContext { updateRemote(track) }
|
||||
updateRemote(track)
|
||||
}
|
||||
|
||||
private suspend fun updateRemote(track: AnimeTrack) {
|
||||
withIOContext {
|
||||
try {
|
||||
update(track)
|
||||
track.toDomainTrack(idRequired = false)?.let {
|
||||
insertTrack.await(it)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e) { "Failed to update remote track data id=${track.id}" }
|
||||
withUIContext { Injekt.get<Application>().toast(e.message) }
|
||||
private suspend fun updateRemote(track: AnimeTrack): Unit = withIOContext {
|
||||
try {
|
||||
update(track)
|
||||
track.toDomainTrack(idRequired = false)?.let {
|
||||
insertTrack.await(it)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e) { "Failed to update remote track data id=${track.id}" }
|
||||
withUIContext { Injekt.get<Application>().toast(e.message) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,28 +1,23 @@
|
|||
package eu.kanade.tachiyomi.data.track
|
||||
|
||||
import android.app.Application
|
||||
import eu.kanade.domain.track.manga.interactor.SyncChapterProgressWithTrack
|
||||
import eu.kanade.domain.track.manga.model.toDbTrack
|
||||
import eu.kanade.domain.track.manga.interactor.AddMangaTracks
|
||||
import eu.kanade.domain.track.manga.model.toDomainTrack
|
||||
import eu.kanade.tachiyomi.data.database.models.manga.MangaTrack
|
||||
import eu.kanade.tachiyomi.data.track.model.MangaTrackSearch
|
||||
import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import logcat.LogPriority
|
||||
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.GetChaptersByMangaId
|
||||
import tachiyomi.domain.track.manga.interactor.InsertMangaTrack
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.time.ZoneOffset
|
||||
import tachiyomi.domain.track.manga.model.MangaTrack as DomainTrack
|
||||
|
||||
private val addTracks: AddMangaTracks by injectLazy()
|
||||
private val insertTrack: InsertMangaTrack by injectLazy()
|
||||
private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack by injectLazy()
|
||||
|
||||
interface MangaTracker {
|
||||
|
||||
|
@ -57,59 +52,10 @@ interface MangaTracker {
|
|||
|
||||
suspend fun refresh(track: MangaTrack): MangaTrack
|
||||
|
||||
// TODO: move this to an interactor, and update all trackers based on common data
|
||||
suspend fun register(item: MangaTrack, mangaId: Long) {
|
||||
item.manga_id = mangaId
|
||||
try {
|
||||
withIOContext {
|
||||
val allChapters = Injekt.get<GetChaptersByMangaId>().await(mangaId)
|
||||
val hasReadChapters = allChapters.any { it.read }
|
||||
bind(item, hasReadChapters)
|
||||
|
||||
var track = item.toDomainTrack(idRequired = false) ?: return@withIOContext
|
||||
|
||||
insertTrack.await(track)
|
||||
|
||||
// TODO: merge into SyncChaptersWithTrackServiceTwoWay?
|
||||
// Update chapter progress if newer chapters marked read locally
|
||||
if (hasReadChapters) {
|
||||
val latestLocalReadChapterNumber = allChapters
|
||||
.sortedBy { it.chapterNumber }
|
||||
.takeWhile { it.read }
|
||||
.lastOrNull()
|
||||
?.chapterNumber ?: -1.0
|
||||
|
||||
if (latestLocalReadChapterNumber > track.lastChapterRead) {
|
||||
track = track.copy(
|
||||
lastChapterRead = latestLocalReadChapterNumber,
|
||||
)
|
||||
setRemoteLastChapterRead(
|
||||
track.toDbTrack(),
|
||||
latestLocalReadChapterNumber.toInt(),
|
||||
)
|
||||
}
|
||||
|
||||
if (track.startDate <= 0) {
|
||||
val firstReadChapterDate = Injekt.get<GetMangaHistory>().await(mangaId)
|
||||
.sortedBy { it.readAt }
|
||||
.firstOrNull()
|
||||
?.readAt
|
||||
|
||||
firstReadChapterDate?.let {
|
||||
val startDate = firstReadChapterDate.time.convertEpochMillisZone(
|
||||
ZoneOffset.systemDefault(),
|
||||
ZoneOffset.UTC,
|
||||
)
|
||||
track = track.copy(
|
||||
startDate = startDate,
|
||||
)
|
||||
setRemoteStartDate(track.toDbTrack(), startDate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
syncChapterProgressWithTrack.await(mangaId, track, this@MangaTracker)
|
||||
}
|
||||
addTracks.bind(this, item, mangaId)
|
||||
} catch (e: Throwable) {
|
||||
withUIContext { Injekt.get<Application>().toast(e.message) }
|
||||
}
|
||||
|
@ -120,47 +66,49 @@ interface MangaTracker {
|
|||
if (track.status == getCompletionStatus() && track.total_chapters != 0) {
|
||||
track.last_chapter_read = track.total_chapters.toFloat()
|
||||
}
|
||||
withIOContext { updateRemote(track) }
|
||||
updateRemote(track)
|
||||
}
|
||||
|
||||
suspend fun setRemoteLastChapterRead(track: MangaTrack, chapterNumber: Int) {
|
||||
if (track.last_chapter_read == 0f && track.last_chapter_read < chapterNumber && track.status != getRereadingStatus()) {
|
||||
if (track.last_chapter_read == 0f &&
|
||||
track.last_chapter_read < chapterNumber && track.status != getRereadingStatus()
|
||||
) {
|
||||
track.status = getReadingStatus()
|
||||
}
|
||||
track.last_chapter_read = chapterNumber.toFloat()
|
||||
if (track.total_chapters != 0 && track.last_chapter_read.toInt() == track.total_chapters) {
|
||||
if (track.total_chapters != 0 &&
|
||||
track.last_chapter_read.toInt() == track.total_chapters
|
||||
) {
|
||||
track.status = getCompletionStatus()
|
||||
track.finished_reading_date = System.currentTimeMillis()
|
||||
}
|
||||
withIOContext { updateRemote(track) }
|
||||
updateRemote(track)
|
||||
}
|
||||
|
||||
suspend fun setRemoteScore(track: MangaTrack, scoreString: String) {
|
||||
track.score = indexToScore(getScoreList().indexOf(scoreString))
|
||||
withIOContext { updateRemote(track) }
|
||||
updateRemote(track)
|
||||
}
|
||||
|
||||
suspend fun setRemoteStartDate(track: MangaTrack, epochMillis: Long) {
|
||||
track.started_reading_date = epochMillis
|
||||
withIOContext { updateRemote(track) }
|
||||
updateRemote(track)
|
||||
}
|
||||
|
||||
suspend fun setRemoteFinishDate(track: MangaTrack, epochMillis: Long) {
|
||||
track.finished_reading_date = epochMillis
|
||||
withIOContext { updateRemote(track) }
|
||||
updateRemote(track)
|
||||
}
|
||||
|
||||
private suspend fun updateRemote(track: MangaTrack) {
|
||||
withIOContext {
|
||||
try {
|
||||
update(track)
|
||||
track.toDomainTrack(idRequired = false)?.let {
|
||||
insertTrack.await(it)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e) { "Failed to update remote track data id=${track.id}" }
|
||||
withUIContext { Injekt.get<Application>().toast(e.message) }
|
||||
private suspend fun updateRemote(track: MangaTrack): Unit = withIOContext {
|
||||
try {
|
||||
update(track)
|
||||
track.toDomainTrack(idRequired = false)?.let {
|
||||
insertTrack.await(it)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e) { "Failed to update remote track data id=${track.id}" }
|
||||
withUIContext { Injekt.get<Application>().toast(e.message) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,6 +34,8 @@ class TrackerManager(context: Context) {
|
|||
|
||||
val trackers = listOf(myAnimeList, aniList, kitsu, shikimori, bangumi, komga, mangaUpdates, kavita, suwayomi, simkl)
|
||||
|
||||
fun loggedInTrackers() = trackers.filter { it.isLoggedIn }
|
||||
|
||||
fun get(id: Long) = trackers.find { it.id == id }
|
||||
|
||||
fun hasLoggedIn() = trackers.any { it.isLoggedIn }
|
||||
|
|
|
@ -19,7 +19,15 @@ import uy.kohesive.injekt.injectLazy
|
|||
import tachiyomi.domain.track.anime.model.AnimeTrack as DomainAnimeTrack
|
||||
import tachiyomi.domain.track.manga.model.MangaTrack as DomainTrack
|
||||
|
||||
class Anilist(id: Long) : BaseTracker(id, "AniList"), MangaTracker, AnimeTracker, DeletableMangaTracker, DeletableAnimeTracker {
|
||||
class Anilist(id: Long) :
|
||||
BaseTracker(
|
||||
id,
|
||||
"AniList",
|
||||
),
|
||||
MangaTracker,
|
||||
AnimeTracker,
|
||||
DeletableMangaTracker,
|
||||
DeletableAnimeTracker {
|
||||
|
||||
companion object {
|
||||
const val READING = 1
|
||||
|
|
|
@ -48,11 +48,19 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor
|
|||
when (it.code) {
|
||||
200 -> return it.parseAs<AuthenticationDto>().token
|
||||
401 -> {
|
||||
logcat(LogPriority.WARN) { "Unauthorized / api key not valid: Cleaned api URL: $apiUrl, Api key is empty: ${apiKey.isEmpty()}" }
|
||||
logcat(LogPriority.WARN) {
|
||||
"Unauthorized / api key not valid: Cleaned api URL: " +
|
||||
"$apiUrl, Api key is empty: ${apiKey.isEmpty()}"
|
||||
}
|
||||
throw IOException("Unauthorized / api key not valid")
|
||||
}
|
||||
500 -> {
|
||||
logcat(LogPriority.WARN) { "Error fetching JWT token. Cleaned api URL: $apiUrl, Api key is empty: ${apiKey.isEmpty()}" }
|
||||
logcat(
|
||||
LogPriority.WARN,
|
||||
) {
|
||||
"Error fetching JWT token. Cleaned api URL: " +
|
||||
"$apiUrl, Api key is empty: ${apiKey.isEmpty()}"
|
||||
}
|
||||
throw IOException("Error fetching JWT token")
|
||||
}
|
||||
else -> {}
|
||||
|
@ -62,7 +70,8 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor
|
|||
// Not sure which one to catch
|
||||
} catch (e: SocketTimeoutException) {
|
||||
logcat(LogPriority.WARN) {
|
||||
"Could not fetch JWT token. Probably due to connectivity issue or the url '$apiUrl' is not available, skipping"
|
||||
"Could not fetch JWT token. Probably due to connectivity " +
|
||||
"issue or the url '$apiUrl' is not available, skipping"
|
||||
}
|
||||
return null
|
||||
} catch (e: Exception) {
|
||||
|
@ -129,7 +138,10 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor
|
|||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.WARN, e) { "Exception getting latest chapter read. Could not get itemRequest: $requestUrl" }
|
||||
logcat(
|
||||
LogPriority.WARN,
|
||||
e,
|
||||
) { "Exception getting latest chapter read. Could not get itemRequest: $requestUrl" }
|
||||
throw e
|
||||
}
|
||||
return 0F
|
||||
|
@ -164,7 +176,9 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor
|
|||
}
|
||||
|
||||
suspend fun updateProgress(track: MangaTrack): MangaTrack {
|
||||
val requestUrl = "${getApiFromUrl(track.tracking_url)}/Tachiyomi/mark-chapter-until-as-read?seriesId=${getIdFromUrl(
|
||||
val requestUrl = "${getApiFromUrl(
|
||||
track.tracking_url,
|
||||
)}/Tachiyomi/mark-chapter-until-as-read?seriesId=${getIdFromUrl(
|
||||
track.tracking_url,
|
||||
)}&chapterNumber=${track.last_chapter_read}"
|
||||
authClient.newCall(
|
||||
|
|
|
@ -17,7 +17,15 @@ import kotlinx.serialization.json.Json
|
|||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.DecimalFormat
|
||||
|
||||
class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), AnimeTracker, MangaTracker, DeletableMangaTracker, DeletableAnimeTracker {
|
||||
class Kitsu(id: Long) :
|
||||
BaseTracker(
|
||||
id,
|
||||
"Kitsu",
|
||||
),
|
||||
AnimeTracker,
|
||||
MangaTracker,
|
||||
DeletableMangaTracker,
|
||||
DeletableAnimeTracker {
|
||||
|
||||
companion object {
|
||||
const val READING = 1
|
||||
|
|
|
@ -483,9 +483,13 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||
"https://AWQO5J657S-dsn.algolia.net/1/indexes/production_media/query/"
|
||||
private const val algoliaAppId = "AWQO5J657S"
|
||||
private const val algoliaFilter =
|
||||
"&facetFilters=%5B%22kind%3Amanga%22%5D&attributesToRetrieve=%5B%22synopsis%22%2C%22canonicalTitle%22%2C%22chapterCount%22%2C%22posterImage%22%2C%22startDate%22%2C%22subtype%22%2C%22endDate%22%2C%20%22id%22%5D"
|
||||
"&facetFilters=%5B%22kind%3Amanga%22%5D&attributesToRetrieve=%5B%22synopsis%22%2C%22canonicalTitle%22%2C" +
|
||||
"%22chapterCount%22%2C%22posterImage%22%2C%22" +
|
||||
"startDate%22%2C%22subtype%22%2C%22endDate%22%2C%20%22id%22%5D"
|
||||
private const val algoliaFilterAnime =
|
||||
"&facetFilters=%5B%22kind%3Aanime%22%5D&attributesToRetrieve=%5B%22synopsis%22%2C%22canonicalTitle%22%2C%22episodeCount%22%2C%22posterImage%22%2C%22startDate%22%2C%22subtype%22%2C%22endDate%22%2C%20%22id%22%5D"
|
||||
"&facetFilters=%5B%22kind%3Aanime%22%5D&attributesToRetrieve=%5B%22synopsis%22%2C%22canonicalTitle%22%2C" +
|
||||
"%22episodeCount%22%2C%22posterImage%22%2C%22" +
|
||||
"startDate%22%2C%22subtype%22%2C%22endDate%22%2C%20%22id%22%5D"
|
||||
|
||||
fun mangaUrl(remoteId: Long): String {
|
||||
return baseMangaUrl + remoteId
|
||||
|
|
|
@ -17,7 +17,15 @@ import kotlinx.serialization.encodeToString
|
|||
import kotlinx.serialization.json.Json
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class MyAnimeList(id: Long) : BaseTracker(id, "MyAnimeList"), MangaTracker, AnimeTracker, DeletableMangaTracker, DeletableAnimeTracker {
|
||||
class MyAnimeList(id: Long) :
|
||||
BaseTracker(
|
||||
id,
|
||||
"MyAnimeList",
|
||||
),
|
||||
MangaTracker,
|
||||
AnimeTracker,
|
||||
DeletableMangaTracker,
|
||||
DeletableAnimeTracker {
|
||||
|
||||
companion object {
|
||||
const val READING = 1
|
||||
|
|
|
@ -17,7 +17,15 @@ import kotlinx.serialization.encodeToString
|
|||
import kotlinx.serialization.json.Json
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class Shikimori(id: Long) : BaseTracker(id, "Shikimori"), MangaTracker, AnimeTracker, DeletableMangaTracker, DeletableAnimeTracker {
|
||||
class Shikimori(id: Long) :
|
||||
BaseTracker(
|
||||
id,
|
||||
"Shikimori",
|
||||
),
|
||||
MangaTracker,
|
||||
AnimeTracker,
|
||||
DeletableMangaTracker,
|
||||
DeletableAnimeTracker {
|
||||
|
||||
companion object {
|
||||
const val READING = 1
|
||||
|
|
|
@ -61,7 +61,8 @@ class ShikimoriApi(
|
|||
).awaitSuccess()
|
||||
.parseAs<JsonObject>()
|
||||
.let {
|
||||
track.library_id = it["id"]!!.jsonPrimitive.long // save id of the entry for possible future delete request
|
||||
track.library_id =
|
||||
it["id"]!!.jsonPrimitive.long // save id of the entry for possible future delete request
|
||||
}
|
||||
track
|
||||
}
|
||||
|
@ -105,7 +106,8 @@ class ShikimoriApi(
|
|||
).awaitSuccess()
|
||||
.parseAs<JsonObject>()
|
||||
.let {
|
||||
track.library_id = it["id"]!!.jsonPrimitive.long // save id of the entry for possible future delete request
|
||||
track.library_id =
|
||||
it["id"]!!.jsonPrimitive.long // save id of the entry for possible future delete request
|
||||
}
|
||||
track
|
||||
}
|
||||
|
|
|
@ -172,7 +172,9 @@ class SimklApi(private val client: OkHttpClient, interceptor: SimklInterceptor)
|
|||
title = obj["title_romaji"]?.jsonPrimitive?.content ?: obj["title"]!!.jsonPrimitive.content
|
||||
total_episodes = obj["ep_count"]?.jsonPrimitive?.intOrNull ?: 1
|
||||
cover_url = "https://simkl.in/posters/" + obj["poster"]!!.jsonPrimitive.content + "_m.webp"
|
||||
summary = obj["all_titles"]?.jsonArray?.joinToString("\n", "All titles:\n") { it.jsonPrimitive.content } ?: ""
|
||||
summary = obj["all_titles"]?.jsonArray
|
||||
?.joinToString("\n", "All titles:\n") { it.jsonPrimitive.content } ?: ""
|
||||
|
||||
tracking_url = obj["url"]!!.jsonPrimitive.content
|
||||
publishing_status = obj["status"]?.jsonPrimitive?.content ?: "ended"
|
||||
publishing_type = obj["type"]?.jsonPrimitive?.content ?: type
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package eu.kanade.tachiyomi.data.updater
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.util.system.isInstalledFromFDroid
|
||||
import tachiyomi.core.util.lang.withIOContext
|
||||
|
@ -12,6 +13,11 @@ class AppUpdateChecker {
|
|||
private val getApplicationRelease: GetApplicationRelease by injectLazy()
|
||||
|
||||
suspend fun checkForUpdate(context: Context, forceCheck: Boolean = false): GetApplicationRelease.Result {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||
return GetApplicationRelease.Result.OsTooOld
|
||||
}
|
||||
// Disabling app update checks for older Android versions that we're going to drop support for
|
||||
|
||||
return withIOContext {
|
||||
val result = getApplicationRelease.await(
|
||||
GetApplicationRelease.Arguments(
|
||||
|
@ -28,7 +34,9 @@ class AppUpdateChecker {
|
|||
is GetApplicationRelease.Result.NewUpdate -> AppUpdateNotifier(context).promptUpdate(
|
||||
result.release,
|
||||
)
|
||||
is GetApplicationRelease.Result.ThirdPartyInstallation -> AppUpdateNotifier(context).promptFdroidUpdate()
|
||||
is GetApplicationRelease.Result.ThirdPartyInstallation -> AppUpdateNotifier(
|
||||
context,
|
||||
).promptFdroidUpdate()
|
||||
else -> {}
|
||||
}
|
||||
|
||||
|
|
|
@ -72,7 +72,10 @@ internal class AnimeExtensionGithubApi {
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun checkForUpdates(context: Context, fromAvailableExtensionList: Boolean = false): List<AnimeExtension.Installed>? {
|
||||
suspend fun checkForUpdates(
|
||||
context: Context,
|
||||
fromAvailableExtensionList: Boolean = false,
|
||||
): List<AnimeExtension.Installed>? {
|
||||
// Limit checks to once a day at most
|
||||
if (fromAvailableExtensionList && Date().time < lastExtCheck.get() + 1.days.inWholeMilliseconds) {
|
||||
return null
|
||||
|
|
|
@ -105,7 +105,12 @@ class PackageInstallerInstallerAnime(private val service: Service) : InstallerAn
|
|||
}
|
||||
|
||||
init {
|
||||
ContextCompat.registerReceiver(service, packageActionReceiver, IntentFilter(INSTALL_ACTION), ContextCompat.RECEIVER_EXPORTED)
|
||||
ContextCompat.registerReceiver(
|
||||
service,
|
||||
packageActionReceiver,
|
||||
IntentFilter(INSTALL_ACTION),
|
||||
ContextCompat.RECEIVER_EXPORTED,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -138,7 +138,9 @@ internal class AnimeExtensionInstaller(private val context: Context) {
|
|||
emit(downloadStatus)
|
||||
|
||||
// Stop polling when the download fails or finishes
|
||||
if (downloadStatus == DownloadManager.STATUS_SUCCESSFUL || downloadStatus == DownloadManager.STATUS_FAILED) {
|
||||
if (downloadStatus == DownloadManager.STATUS_SUCCESSFUL ||
|
||||
downloadStatus == DownloadManager.STATUS_FAILED
|
||||
) {
|
||||
return@flow
|
||||
}
|
||||
|
||||
|
|
|
@ -72,7 +72,10 @@ internal class MangaExtensionGithubApi {
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun checkForUpdates(context: Context, fromAvailableExtensionList: Boolean = false): List<MangaExtension.Installed>? {
|
||||
suspend fun checkForUpdates(
|
||||
context: Context,
|
||||
fromAvailableExtensionList: Boolean = false,
|
||||
): List<MangaExtension.Installed>? {
|
||||
// Limit checks to once a day at most
|
||||
if (fromAvailableExtensionList && Date().time < lastExtCheck.get() + 1.days.inWholeMilliseconds) {
|
||||
return null
|
||||
|
|
|
@ -105,7 +105,12 @@ class PackageInstallerInstallerManga(private val service: Service) : InstallerMa
|
|||
}
|
||||
|
||||
init {
|
||||
ContextCompat.registerReceiver(service, packageActionReceiver, IntentFilter(INSTALL_ACTION), ContextCompat.RECEIVER_EXPORTED)
|
||||
ContextCompat.registerReceiver(
|
||||
service,
|
||||
packageActionReceiver,
|
||||
IntentFilter(INSTALL_ACTION),
|
||||
ContextCompat.RECEIVER_EXPORTED,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -138,7 +138,9 @@ internal class MangaExtensionInstaller(private val context: Context) {
|
|||
emit(downloadStatus)
|
||||
|
||||
// Stop polling when the download fails or finishes
|
||||
if (downloadStatus == DownloadManager.STATUS_SUCCESSFUL || downloadStatus == DownloadManager.STATUS_FAILED) {
|
||||
if (downloadStatus == DownloadManager.STATUS_SUCCESSFUL ||
|
||||
downloadStatus == DownloadManager.STATUS_FAILED
|
||||
) {
|
||||
return@flow
|
||||
}
|
||||
|
||||
|
|
|
@ -37,7 +37,9 @@ class AndroidAnimeSourceManager(
|
|||
|
||||
private val stubSourcesMap = ConcurrentHashMap<Long, StubAnimeSource>()
|
||||
|
||||
override val catalogueSources: Flow<List<AnimeCatalogueSource>> = sourcesMapFlow.map { it.values.filterIsInstance<AnimeCatalogueSource>() }
|
||||
override val catalogueSources: Flow<List<AnimeCatalogueSource>> = sourcesMapFlow.map {
|
||||
it.values.filterIsInstance<AnimeCatalogueSource>()
|
||||
}
|
||||
|
||||
init {
|
||||
scope.launch {
|
||||
|
|
|
@ -37,7 +37,9 @@ class AndroidMangaSourceManager(
|
|||
|
||||
private val stubSourcesMap = ConcurrentHashMap<Long, StubMangaSource>()
|
||||
|
||||
override val catalogueSources: Flow<List<CatalogueSource>> = sourcesMapFlow.map { it.values.filterIsInstance<CatalogueSource>() }
|
||||
override val catalogueSources: Flow<List<CatalogueSource>> = sourcesMapFlow.map {
|
||||
it.values.filterIsInstance<CatalogueSource>()
|
||||
}
|
||||
|
||||
init {
|
||||
scope.launch {
|
||||
|
|
|
@ -232,7 +232,7 @@ class BrowseAnimeSourceScreenModel(
|
|||
new = new.removeCovers(coverCache)
|
||||
} else {
|
||||
setAnimeDefaultEpisodeFlags.await(anime)
|
||||
addTracks.bindEnhancedTracks(anime, source)
|
||||
addTracks.bindEnhancedTrackers(anime, source)
|
||||
}
|
||||
|
||||
updateAnime.await(new.toAnimeUpdate())
|
||||
|
|
|
@ -233,7 +233,7 @@ class BrowseMangaSourceScreenModel(
|
|||
new = new.removeCovers(coverCache)
|
||||
} else {
|
||||
setMangaDefaultChapterFlags.await(manga)
|
||||
addTracks.bindEnhancedTracks(manga, source)
|
||||
addTracks.bindEnhancedTrackers(manga, source)
|
||||
}
|
||||
|
||||
updateManga.await(new.toMangaUpdate())
|
||||
|
|
|
@ -132,7 +132,13 @@ class AnimeScreen(
|
|||
screenModel.source,
|
||||
)
|
||||
}.takeIf { isAnimeHttpSource },
|
||||
onWebViewLongClicked = { copyAnimeUrl(context, screenModel.anime, screenModel.source) }.takeIf { isAnimeHttpSource },
|
||||
onWebViewLongClicked = {
|
||||
copyAnimeUrl(
|
||||
context,
|
||||
screenModel.anime,
|
||||
screenModel.source,
|
||||
)
|
||||
}.takeIf { isAnimeHttpSource },
|
||||
onTrackingClicked = screenModel::showTrackDialog.takeIf { successState.trackingAvailable },
|
||||
onTagSearch = { scope.launch { performGenreSearch(navigator, it, screenModel.source!!) } },
|
||||
onFilterButtonClicked = screenModel::showSettingsDialog,
|
||||
|
@ -145,11 +151,21 @@ class AnimeScreen(
|
|||
},
|
||||
onSearch = { query, global -> scope.launch { performSearch(navigator, query, global) } },
|
||||
onCoverClicked = screenModel::showCoverDialog,
|
||||
onShareClicked = { shareAnime(context, screenModel.anime, screenModel.source) }.takeIf { isAnimeHttpSource },
|
||||
onShareClicked = {
|
||||
shareAnime(
|
||||
context,
|
||||
screenModel.anime,
|
||||
screenModel.source,
|
||||
)
|
||||
}.takeIf { isAnimeHttpSource },
|
||||
onDownloadActionClicked = screenModel::runDownloadAction.takeIf { !successState.source.isLocalOrStub() },
|
||||
onEditCategoryClicked = screenModel::showChangeCategoryDialog.takeIf { successState.anime.favorite },
|
||||
onEditFetchIntervalClicked = screenModel::showSetAnimeFetchIntervalDialog.takeIf { screenModel.isUpdateIntervalEnabled && successState.anime.favorite },
|
||||
onMigrateClicked = { navigator.push(MigrateAnimeSearchScreen(successState.anime.id)) }.takeIf { successState.anime.favorite },
|
||||
onEditFetchIntervalClicked = screenModel::showSetAnimeFetchIntervalDialog.takeIf {
|
||||
screenModel.isUpdateIntervalEnabled && successState.anime.favorite
|
||||
},
|
||||
onMigrateClicked = {
|
||||
navigator.push(MigrateAnimeSearchScreen(successState.anime.id))
|
||||
}.takeIf { successState.anime.favorite },
|
||||
changeAnimeSkipIntro = screenModel::showAnimeSkipIntroDialog.takeIf { successState.anime.favorite },
|
||||
onMultiBookmarkClicked = screenModel::bookmarkEpisodes,
|
||||
onMultiMarkAsSeenClicked = screenModel::markEpisodesSeen,
|
||||
|
|
|
@ -141,7 +141,8 @@ class AnimeScreenModel(
|
|||
val relativeTime by uiPreferences.relativeTime().asState(screenModelScope)
|
||||
val dateFormat by mutableStateOf(UiPreferences.dateFormat(uiPreferences.dateFormat().get()))
|
||||
|
||||
val isUpdateIntervalEnabled = LibraryPreferences.ENTRY_OUTSIDE_RELEASE_PERIOD in libraryPreferences.autoUpdateItemRestrictions().get()
|
||||
val isUpdateIntervalEnabled =
|
||||
LibraryPreferences.ENTRY_OUTSIDE_RELEASE_PERIOD in libraryPreferences.autoUpdateItemRestrictions().get()
|
||||
|
||||
private val selectedPositions: Array<Int> = arrayOf(-1, -1) // first and last selected index in list
|
||||
private val selectedEpisodeIds: HashSet<Long> = HashSet()
|
||||
|
@ -338,7 +339,7 @@ class AnimeScreenModel(
|
|||
}
|
||||
|
||||
// Finally match with enhanced tracking when available
|
||||
addTracks.bindEnhancedTracks(anime, state.source)
|
||||
addTracks.bindEnhancedTrackers(anime, state.source)
|
||||
if (autoOpenTrack) {
|
||||
showTrackDialog()
|
||||
}
|
||||
|
|
|
@ -102,8 +102,8 @@ class MangaCoverScreenModel(
|
|||
imageSaver.save(
|
||||
Image.Cover(
|
||||
bitmap = bitmap,
|
||||
name = "cover",
|
||||
location = if (temp) Location.Cache else Location.Pictures(manga.title),
|
||||
name = manga.title,
|
||||
location = if (temp) Location.Cache else Location.Pictures.create(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -120,7 +120,13 @@ class MangaScreen(
|
|||
screenModel.source,
|
||||
)
|
||||
}.takeIf { isHttpSource },
|
||||
onWebViewLongClicked = { copyMangaUrl(context, screenModel.manga, screenModel.source) }.takeIf { isHttpSource },
|
||||
onWebViewLongClicked = {
|
||||
copyMangaUrl(
|
||||
context,
|
||||
screenModel.manga,
|
||||
screenModel.source,
|
||||
)
|
||||
}.takeIf { isHttpSource },
|
||||
onTrackingClicked = screenModel::showTrackDialog.takeIf { successState.trackingAvailable },
|
||||
onTagSearch = { scope.launch { performGenreSearch(navigator, it, screenModel.source!!) } },
|
||||
onFilterButtonClicked = screenModel::showSettingsDialog,
|
||||
|
@ -131,8 +137,12 @@ class MangaScreen(
|
|||
onShareClicked = { shareManga(context, screenModel.manga, screenModel.source) }.takeIf { isHttpSource },
|
||||
onDownloadActionClicked = screenModel::runDownloadAction.takeIf { !successState.source.isLocalOrStub() },
|
||||
onEditCategoryClicked = screenModel::showChangeCategoryDialog.takeIf { successState.manga.favorite },
|
||||
onEditFetchIntervalClicked = screenModel::showSetMangaFetchIntervalDialog.takeIf { screenModel.isUpdateIntervalEnabled && successState.manga.favorite },
|
||||
onMigrateClicked = { navigator.push(MigrateMangaSearchScreen(successState.manga.id)) }.takeIf { successState.manga.favorite },
|
||||
onEditFetchIntervalClicked = screenModel::showSetMangaFetchIntervalDialog.takeIf {
|
||||
screenModel.isUpdateIntervalEnabled && successState.manga.favorite
|
||||
},
|
||||
onMigrateClicked = {
|
||||
navigator.push(MigrateMangaSearchScreen(successState.manga.id))
|
||||
}.takeIf { successState.manga.favorite },
|
||||
onMultiBookmarkClicked = screenModel::bookmarkChapters,
|
||||
onMultiMarkAsReadClicked = screenModel::markChaptersRead,
|
||||
onMarkPreviousAsReadClicked = screenModel::markPreviousChapterRead,
|
||||
|
|
|
@ -136,7 +136,8 @@ class MangaScreenModel(
|
|||
val dateFormat by mutableStateOf(UiPreferences.dateFormat(uiPreferences.dateFormat().get()))
|
||||
val skipFiltered by readerPreferences.skipFiltered().asState(screenModelScope)
|
||||
|
||||
val isUpdateIntervalEnabled = LibraryPreferences.ENTRY_OUTSIDE_RELEASE_PERIOD in libraryPreferences.autoUpdateItemRestrictions().get()
|
||||
val isUpdateIntervalEnabled =
|
||||
LibraryPreferences.ENTRY_OUTSIDE_RELEASE_PERIOD in libraryPreferences.autoUpdateItemRestrictions().get()
|
||||
|
||||
private val selectedPositions: Array<Int> = arrayOf(-1, -1) // first and last selected index in list
|
||||
private val selectedChapterIds: HashSet<Long> = HashSet()
|
||||
|
@ -334,7 +335,7 @@ class MangaScreenModel(
|
|||
}
|
||||
|
||||
// Finally match with enhanced tracking when available
|
||||
addTracks.bindEnhancedTracks(manga, state.source)
|
||||
addTracks.bindEnhancedTrackers(manga, state.source)
|
||||
if (autoOpenTrack) {
|
||||
showTrackDialog()
|
||||
}
|
||||
|
|
|
@ -25,7 +25,6 @@ import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
|||
import eu.kanade.tachiyomi.data.cache.AnimeCoverCache
|
||||
import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadCache
|
||||
import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadManager
|
||||
import eu.kanade.tachiyomi.data.track.AnimeTracker
|
||||
import eu.kanade.tachiyomi.data.track.TrackerManager
|
||||
import eu.kanade.tachiyomi.util.episode.getNextUnseen
|
||||
import eu.kanade.tachiyomi.util.removeCovers
|
||||
|
@ -42,6 +41,7 @@ import kotlinx.coroutines.flow.onEach
|
|||
import kotlinx.coroutines.flow.update
|
||||
import tachiyomi.core.preference.CheckboxState
|
||||
import tachiyomi.core.preference.TriState
|
||||
import tachiyomi.core.util.lang.compareToWithCollator
|
||||
import tachiyomi.core.util.lang.launchIO
|
||||
import tachiyomi.core.util.lang.launchNonCancellable
|
||||
import tachiyomi.core.util.lang.withIOContext
|
||||
|
@ -62,12 +62,11 @@ import tachiyomi.domain.library.model.LibraryDisplayMode
|
|||
import tachiyomi.domain.library.service.LibraryPreferences
|
||||
import tachiyomi.domain.source.anime.service.AnimeSourceManager
|
||||
import tachiyomi.domain.track.anime.interactor.GetTracksPerAnime
|
||||
import tachiyomi.domain.track.anime.model.AnimeTrack
|
||||
import tachiyomi.source.local.entries.anime.isLocal
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.text.Collator
|
||||
import java.util.Collections
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Typealias for the library anime, using the category as keys, and list of anime as values.
|
||||
|
@ -107,7 +106,7 @@ class AnimeLibraryScreenModel(
|
|||
) { searchQuery, library, tracks, loggedInTrackers, _ ->
|
||||
library
|
||||
.applyFilters(tracks, loggedInTrackers)
|
||||
.applySort()
|
||||
.applySort(tracks)
|
||||
.mapValues { (_, value) ->
|
||||
if (searchQuery != null) {
|
||||
// Filter query
|
||||
|
@ -171,7 +170,7 @@ class AnimeLibraryScreenModel(
|
|||
* Applies library filters to the given map of anime.
|
||||
*/
|
||||
private suspend fun AnimeLibraryMap.applyFilters(
|
||||
trackMap: Map<Long, List<Long>>,
|
||||
trackMap: Map<Long, List<AnimeTrack>>,
|
||||
loggedInTrackers: Map<Long, TriState>,
|
||||
): AnimeLibraryMap {
|
||||
val prefs = getAnimelibItemPreferencesFlow().first()
|
||||
|
@ -216,7 +215,9 @@ class AnimeLibraryScreenModel(
|
|||
val filterFnTracking: (AnimeLibraryItem) -> Boolean = tracking@{ item ->
|
||||
if (isNotLoggedInAnyTrack || trackFiltersIsIgnored) return@tracking true
|
||||
|
||||
val animeTracks = trackMap[item.libraryAnime.id].orEmpty()
|
||||
val animeTracks = trackMap
|
||||
.mapValues { entry -> entry.value.map { it.syncId } }[item.libraryAnime.id]
|
||||
.orEmpty()
|
||||
|
||||
val isExcluded = excludedTracks.isNotEmpty() && animeTracks.fastAny { it in excludedTracks }
|
||||
val isIncluded = includedTracks.isEmpty() || animeTracks.fastAny { it in includedTracks }
|
||||
|
@ -239,16 +240,26 @@ class AnimeLibraryScreenModel(
|
|||
/**
|
||||
* Applies library sorting to the given map of anime.
|
||||
*/
|
||||
private fun AnimeLibraryMap.applySort(): AnimeLibraryMap {
|
||||
val locale = Locale.getDefault()
|
||||
val collator = Collator.getInstance(locale).apply {
|
||||
strength = Collator.PRIMARY
|
||||
}
|
||||
private fun AnimeLibraryMap.applySort(
|
||||
// Map<MangaId, List<Track>>
|
||||
trackMap: Map<Long, List<AnimeTrack>>,
|
||||
): AnimeLibraryMap {
|
||||
val sortAlphabetically: (AnimeLibraryItem, AnimeLibraryItem) -> Int = { i1, i2 ->
|
||||
collator.compare(
|
||||
i1.libraryAnime.anime.title.lowercase(locale),
|
||||
i2.libraryAnime.anime.title.lowercase(locale),
|
||||
)
|
||||
i1.libraryAnime.anime.title.lowercase().compareToWithCollator(i2.libraryAnime.anime.title.lowercase())
|
||||
}
|
||||
|
||||
val defaultTrackerScoreSortValue = -1.0
|
||||
val trackerScores by lazy {
|
||||
val trackerMap = trackerManager.loggedInTrackers().associateBy { e -> e.id }
|
||||
trackMap.mapValues { entry ->
|
||||
when {
|
||||
entry.value.isEmpty() -> null
|
||||
else ->
|
||||
entry.value
|
||||
.mapNotNull { trackerMap[it.syncId]?.animeService?.get10PointScore(it) }
|
||||
.average()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val sortFn: (AnimeLibraryItem, AnimeLibraryItem) -> Int = { i1, i2 ->
|
||||
|
@ -282,12 +293,18 @@ class AnimeLibraryScreenModel(
|
|||
AnimeLibrarySort.Type.DateAdded -> {
|
||||
i1.libraryAnime.anime.dateAdded.compareTo(i2.libraryAnime.anime.dateAdded)
|
||||
}
|
||||
AnimeLibrarySort.Type.TrackerMean -> {
|
||||
val item1Score = trackerScores[i1.libraryAnime.id] ?: defaultTrackerScoreSortValue
|
||||
val item2Score = trackerScores[i2.libraryAnime.id] ?: defaultTrackerScoreSortValue
|
||||
item1Score.compareTo(item2Score)
|
||||
}
|
||||
AnimeLibrarySort.Type.AiringTime -> when {
|
||||
i1.libraryAnime.anime.nextEpisodeAiringAt == 0L -> if (sort.isAscending) 1 else -1
|
||||
i2.libraryAnime.anime.nextEpisodeAiringAt == 0L -> if (sort.isAscending) -1 else 1
|
||||
i1.libraryAnime.unseenCount == i2.libraryAnime.unseenCount -> i1.libraryAnime.anime.nextEpisodeAiringAt.compareTo(
|
||||
i2.libraryAnime.anime.nextEpisodeAiringAt,
|
||||
)
|
||||
i1.libraryAnime.unseenCount == i2.libraryAnime.unseenCount ->
|
||||
i1.libraryAnime.anime.nextEpisodeAiringAt.compareTo(
|
||||
i2.libraryAnime.anime.nextEpisodeAiringAt,
|
||||
)
|
||||
else -> i1.libraryAnime.unseenCount.compareTo(i2.libraryAnime.unseenCount)
|
||||
}
|
||||
}
|
||||
|
@ -380,7 +397,7 @@ class AnimeLibraryScreenModel(
|
|||
* @return map of track id with the filter value
|
||||
*/
|
||||
private fun getTrackingFilterFlow(): Flow<Map<Long, TriState>> {
|
||||
val loggedInTrackers = trackerManager.trackers.filter { it.isLoggedIn && it is AnimeTracker }
|
||||
val loggedInTrackers = trackerManager.loggedInTrackers()
|
||||
return if (loggedInTrackers.isNotEmpty()) {
|
||||
val prefFlows = loggedInTrackers
|
||||
.map { libraryPreferences.filterTrackedAnime(it.id.toInt()).changes() }
|
||||
|
@ -541,7 +558,13 @@ class AnimeLibraryScreenModel(
|
|||
}
|
||||
|
||||
fun getColumnsPreferenceForCurrentOrientation(isLandscape: Boolean): PreferenceMutableState<Int> {
|
||||
return (if (isLandscape) libraryPreferences.animeLandscapeColumns() else libraryPreferences.animePortraitColumns()).asState(
|
||||
return (
|
||||
if (isLandscape) {
|
||||
libraryPreferences.animeLandscapeColumns()
|
||||
} else {
|
||||
libraryPreferences.animePortraitColumns()
|
||||
}
|
||||
).asState(
|
||||
screenModelScope,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -23,7 +23,6 @@ import eu.kanade.presentation.library.LibraryToolbarTitle
|
|||
import eu.kanade.tachiyomi.data.cache.MangaCoverCache
|
||||
import eu.kanade.tachiyomi.data.download.manga.MangaDownloadCache
|
||||
import eu.kanade.tachiyomi.data.download.manga.MangaDownloadManager
|
||||
import eu.kanade.tachiyomi.data.track.MangaTracker
|
||||
import eu.kanade.tachiyomi.data.track.TrackerManager
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
|
@ -42,6 +41,7 @@ import kotlinx.coroutines.flow.onEach
|
|||
import kotlinx.coroutines.flow.update
|
||||
import tachiyomi.core.preference.CheckboxState
|
||||
import tachiyomi.core.preference.TriState
|
||||
import tachiyomi.core.util.lang.compareToWithCollator
|
||||
import tachiyomi.core.util.lang.launchIO
|
||||
import tachiyomi.core.util.lang.launchNonCancellable
|
||||
import tachiyomi.core.util.lang.withIOContext
|
||||
|
@ -62,12 +62,11 @@ import tachiyomi.domain.library.model.LibraryDisplayMode
|
|||
import tachiyomi.domain.library.service.LibraryPreferences
|
||||
import tachiyomi.domain.source.manga.service.MangaSourceManager
|
||||
import tachiyomi.domain.track.manga.interactor.GetTracksPerManga
|
||||
import tachiyomi.domain.track.manga.model.MangaTrack
|
||||
import tachiyomi.source.local.entries.manga.isLocal
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.text.Collator
|
||||
import java.util.Collections
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Typealias for the library manga, using the category as keys, and list of manga as values.
|
||||
|
@ -107,7 +106,7 @@ class MangaLibraryScreenModel(
|
|||
) { searchQuery, library, tracks, loggedInTrackers, _ ->
|
||||
library
|
||||
.applyFilters(tracks, loggedInTrackers)
|
||||
.applySort()
|
||||
.applySort(tracks)
|
||||
.mapValues { (_, value) ->
|
||||
if (searchQuery != null) {
|
||||
// Filter query
|
||||
|
@ -171,7 +170,7 @@ class MangaLibraryScreenModel(
|
|||
* Applies library filters to the given map of manga.
|
||||
*/
|
||||
private suspend fun MangaLibraryMap.applyFilters(
|
||||
trackMap: Map<Long, List<Long>>,
|
||||
trackMap: Map<Long, List<MangaTrack>>,
|
||||
loggedInTrackers: Map<Long, TriState>,
|
||||
): MangaLibraryMap {
|
||||
val prefs = getLibraryItemPreferencesFlow().first()
|
||||
|
@ -216,7 +215,9 @@ class MangaLibraryScreenModel(
|
|||
val filterFnTracking: (MangaLibraryItem) -> Boolean = tracking@{ item ->
|
||||
if (isNotLoggedInAnyTrack || trackFiltersIsIgnored) return@tracking true
|
||||
|
||||
val mangaTracks = trackMap[item.libraryManga.id].orEmpty()
|
||||
val mangaTracks = trackMap
|
||||
.mapValues { entry -> entry.value.map { it.syncId } }[item.libraryManga.id]
|
||||
.orEmpty()
|
||||
|
||||
val isExcluded = excludedTracks.isNotEmpty() && mangaTracks.fastAny { it in excludedTracks }
|
||||
val isIncluded = includedTracks.isEmpty() || mangaTracks.fastAny { it in includedTracks }
|
||||
|
@ -239,16 +240,26 @@ class MangaLibraryScreenModel(
|
|||
/**
|
||||
* Applies library sorting to the given map of manga.
|
||||
*/
|
||||
private fun MangaLibraryMap.applySort(): MangaLibraryMap {
|
||||
val locale = Locale.getDefault()
|
||||
val collator = Collator.getInstance(locale).apply {
|
||||
strength = Collator.PRIMARY
|
||||
}
|
||||
private fun MangaLibraryMap.applySort(
|
||||
// Map<MangaId, List<Track>>
|
||||
trackMap: Map<Long, List<MangaTrack>>,
|
||||
): MangaLibraryMap {
|
||||
val sortAlphabetically: (MangaLibraryItem, MangaLibraryItem) -> Int = { i1, i2 ->
|
||||
collator.compare(
|
||||
i1.libraryManga.manga.title.lowercase(locale),
|
||||
i2.libraryManga.manga.title.lowercase(locale),
|
||||
)
|
||||
i1.libraryManga.manga.title.lowercase().compareToWithCollator(i2.libraryManga.manga.title.lowercase())
|
||||
}
|
||||
|
||||
val defaultTrackerScoreSortValue = -1.0
|
||||
val trackerScores by lazy {
|
||||
val trackerMap = trackerManager.loggedInTrackers().associateBy { e -> e.id }
|
||||
trackMap.mapValues { entry ->
|
||||
when {
|
||||
entry.value.isEmpty() -> null
|
||||
else ->
|
||||
entry.value
|
||||
.mapNotNull { trackerMap[it.syncId]?.mangaService?.get10PointScore(it) }
|
||||
.average()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val sortFn: (MangaLibraryItem, MangaLibraryItem) -> Int = { i1, i2 ->
|
||||
|
@ -282,6 +293,11 @@ class MangaLibraryScreenModel(
|
|||
MangaLibrarySort.Type.DateAdded -> {
|
||||
i1.libraryManga.manga.dateAdded.compareTo(i2.libraryManga.manga.dateAdded)
|
||||
}
|
||||
MangaLibrarySort.Type.TrackerMean -> {
|
||||
val item1Score = trackerScores[i1.libraryManga.id] ?: defaultTrackerScoreSortValue
|
||||
val item2Score = trackerScores[i2.libraryManga.id] ?: defaultTrackerScoreSortValue
|
||||
item1Score.compareTo(item2Score)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -372,7 +388,7 @@ class MangaLibraryScreenModel(
|
|||
* @return map of track id with the filter value
|
||||
*/
|
||||
private fun getTrackingFilterFlow(): Flow<Map<Long, TriState>> {
|
||||
val loggedInTrackers = trackerManager.trackers.filter { it.isLoggedIn && it is MangaTracker }
|
||||
val loggedInTrackers = trackerManager.loggedInTrackers()
|
||||
return if (loggedInTrackers.isNotEmpty()) {
|
||||
val prefFlows = loggedInTrackers
|
||||
.map { libraryPreferences.filterTrackedManga(it.id.toInt()).changes() }
|
||||
|
@ -533,7 +549,13 @@ class MangaLibraryScreenModel(
|
|||
}
|
||||
|
||||
fun getColumnsPreferenceForCurrentOrientation(isLandscape: Boolean): PreferenceMutableState<Int> {
|
||||
return (if (isLandscape) libraryPreferences.mangaLandscapeColumns() else libraryPreferences.mangaPortraitColumns()).asState(
|
||||
return (
|
||||
if (isLandscape) {
|
||||
libraryPreferences.mangaLandscapeColumns()
|
||||
} else {
|
||||
libraryPreferences.mangaPortraitColumns()
|
||||
}
|
||||
).asState(
|
||||
screenModelScope,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -263,8 +263,14 @@ class MainActivity : BaseActivity() {
|
|||
.filter { !it }
|
||||
.onEach {
|
||||
val currentScreen = navigator.lastItem
|
||||
if ((currentScreen is BrowseMangaSourceScreen || (currentScreen is MangaScreen && currentScreen.fromSource)) ||
|
||||
(currentScreen is BrowseAnimeSourceScreen || (currentScreen is AnimeScreen && currentScreen.fromSource))
|
||||
if ((
|
||||
currentScreen is BrowseMangaSourceScreen ||
|
||||
(currentScreen is MangaScreen && currentScreen.fromSource)
|
||||
) ||
|
||||
(
|
||||
currentScreen is BrowseAnimeSourceScreen ||
|
||||
(currentScreen is AnimeScreen && currentScreen.fromSource)
|
||||
)
|
||||
) {
|
||||
navigator.popUntilRoot()
|
||||
}
|
||||
|
|
|
@ -432,7 +432,11 @@ class PlayerActivity : BaseActivity() {
|
|||
}
|
||||
|
||||
fun onSubtitleSelected(index: Int) {
|
||||
if (streams.subtitle.index == index || streams.subtitle.index > subtitleTracks.lastIndex) return
|
||||
if (streams.subtitle.index == index ||
|
||||
streams.subtitle.index > subtitleTracks.lastIndex
|
||||
) {
|
||||
return
|
||||
}
|
||||
streams.subtitle.index = index
|
||||
if (index == 0) {
|
||||
player.sid = -1
|
||||
|
@ -631,7 +635,10 @@ class PlayerActivity : BaseActivity() {
|
|||
}
|
||||
|
||||
private fun setupPlayerBrightness() {
|
||||
val useDeviceBrightness = playerPreferences.playerBrightnessValue().get() == -1.0F || !playerPreferences.rememberPlayerBrightness().get()
|
||||
val useDeviceBrightness =
|
||||
playerPreferences.playerBrightnessValue().get() == -1.0F ||
|
||||
!playerPreferences.rememberPlayerBrightness().get()
|
||||
|
||||
brightness = if (useDeviceBrightness) {
|
||||
Utils.getScreenBrightness(this) ?: 0.5F
|
||||
} else {
|
||||
|
@ -1621,10 +1628,14 @@ class PlayerActivity : BaseActivity() {
|
|||
viewModel.viewModelScope.launchUI {
|
||||
if (playerPreferences.adjustOrientationVideoDimensions().get()) {
|
||||
if ((player.videoW ?: 1) / (player.videoH ?: 1) >= 1) {
|
||||
this@PlayerActivity.requestedOrientation = playerPreferences.defaultPlayerOrientationLandscape().get()
|
||||
this@PlayerActivity.requestedOrientation =
|
||||
playerPreferences.defaultPlayerOrientationLandscape().get()
|
||||
|
||||
switchControlsOrientation(true)
|
||||
} else {
|
||||
this@PlayerActivity.requestedOrientation = playerPreferences.defaultPlayerOrientationPortrait().get()
|
||||
this@PlayerActivity.requestedOrientation =
|
||||
playerPreferences.defaultPlayerOrientationPortrait().get()
|
||||
|
||||
switchControlsOrientation(false)
|
||||
}
|
||||
}
|
||||
|
@ -1731,7 +1742,11 @@ class PlayerActivity : BaseActivity() {
|
|||
val autoSkipAniSkip = playerPreferences.autoSkipAniSkip().get()
|
||||
|
||||
skipType =
|
||||
aniSkipInterval?.firstOrNull { it.interval.startTime <= position && it.interval.endTime > position }?.skipType
|
||||
aniSkipInterval
|
||||
?.firstOrNull {
|
||||
it.interval.startTime <= position &&
|
||||
it.interval.endTime > position
|
||||
}?.skipType
|
||||
skipType?.let { skipType ->
|
||||
val aniSkipPlayerUtils = AniSkipApi.PlayerUtils(binding, aniSkipInterval!!)
|
||||
if (netflixStyle) {
|
||||
|
|
|
@ -30,14 +30,16 @@ class PlayerObserver(val activity: PlayerActivity) :
|
|||
|
||||
override fun event(eventId: Int) {
|
||||
when (eventId) {
|
||||
MPVLib.mpvEventId.MPV_EVENT_FILE_LOADED -> activity.viewModel.viewModelScope.launchIO { activity.fileLoaded() }
|
||||
MPVLib.mpvEventId.MPV_EVENT_START_FILE -> activity.viewModel.viewModelScope.launchUI {
|
||||
activity.player.paused = false
|
||||
activity.refreshUi()
|
||||
// Fixes a minor Ui bug but I have no idea why
|
||||
val isEpisodeOnline = withIOContext { activity.viewModel.isEpisodeOnline() != true }
|
||||
if (isEpisodeOnline) activity.showLoadingIndicator(false)
|
||||
}
|
||||
MPVLib.mpvEventId.MPV_EVENT_FILE_LOADED ->
|
||||
activity.viewModel.viewModelScope.launchIO { activity.fileLoaded() }
|
||||
MPVLib.mpvEventId.MPV_EVENT_START_FILE ->
|
||||
activity.viewModel.viewModelScope.launchUI {
|
||||
activity.player.paused = false
|
||||
activity.refreshUi()
|
||||
// Fixes a minor Ui bug but I have no idea why
|
||||
val isEpisodeOnline = withIOContext { activity.viewModel.isEpisodeOnline() != true }
|
||||
if (isEpisodeOnline) activity.showLoadingIndicator(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -153,7 +153,13 @@ private fun StreamsPageBuilder(
|
|||
}
|
||||
}
|
||||
|
||||
val addTrackRes = if (externalTrackCode == "sub") R.string.player_add_external_subtitles else R.string.player_add_external_audio
|
||||
val addTrackRes =
|
||||
if (externalTrackCode == "sub") {
|
||||
R.string.player_add_external_subtitles
|
||||
} else {
|
||||
R.string.player_add_external_audio
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
|
@ -63,7 +63,13 @@ fun VideoChaptersSheet(
|
|||
val nextChapterTime = videoChapters.getOrNull(index + 1)?.time?.toInt()
|
||||
|
||||
val selected = (index == videoChapters.lastIndex && currentTimePosition >= videoChapterTime) ||
|
||||
(currentTimePosition >= videoChapterTime && (nextChapterTime == null || currentTimePosition < nextChapterTime))
|
||||
(
|
||||
currentTimePosition >= videoChapterTime &&
|
||||
(
|
||||
nextChapterTime == null ||
|
||||
currentTimePosition < nextChapterTime
|
||||
)
|
||||
)
|
||||
|
||||
val onClick = {
|
||||
currentTimePosition = videoChapter.time.roundToInt()
|
||||
|
|
|
@ -29,7 +29,14 @@ class GestureHandler(
|
|||
private val playerControls = activity.playerControls
|
||||
|
||||
override fun onSingleTapUp(e: MotionEvent): Boolean {
|
||||
if (SeekState.mode == SeekState.LOCKED || SeekState.mode != SeekState.DOUBLE_TAP || activity.player.timePos == null || activity.player.duration == null) return false
|
||||
if (SeekState.mode == SeekState.LOCKED ||
|
||||
SeekState.mode != SeekState.DOUBLE_TAP ||
|
||||
activity.player.timePos == null ||
|
||||
activity.player.duration == null
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
when {
|
||||
e.x < width * 0.4F && interval != 0 -> if (activity.player.timePos!! > 0) {
|
||||
activity.doubleTapSeek(
|
||||
|
|
|
@ -239,7 +239,12 @@ class PlayerControlsView @JvmOverloads constructor(context: Context, attrs: Attr
|
|||
animationHandler.removeCallbacks(fadeOutControlsRunnable)
|
||||
animationHandler.removeCallbacks(hideUiForSeekRunnable)
|
||||
|
||||
if (!(binding.topControlsGroup.visibility == View.INVISIBLE && binding.middleControlsGroup.visibility == INVISIBLE && binding.bottomControlsGroup.visibility == INVISIBLE)) {
|
||||
if (!(
|
||||
binding.topControlsGroup.visibility == View.INVISIBLE &&
|
||||
binding.middleControlsGroup.visibility == INVISIBLE &&
|
||||
binding.bottomControlsGroup.visibility == INVISIBLE
|
||||
)
|
||||
) {
|
||||
wasPausedBeforeSeeking = player.paused!!
|
||||
showControls = binding.unlockedView.isVisible
|
||||
binding.topControlsGroup.visibility = View.INVISIBLE
|
||||
|
|
|
@ -477,7 +477,8 @@ class ReaderActivity : BaseActivity() {
|
|||
} else {
|
||||
if (readerPreferences.fullscreen().get()) {
|
||||
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
|
||||
windowInsetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
windowInsetsController.systemBarsBehavior =
|
||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -161,20 +161,22 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||
(manga.unreadFilterRaw == Manga.CHAPTER_SHOW_READ && !it.read) ||
|
||||
(manga.unreadFilterRaw == Manga.CHAPTER_SHOW_UNREAD && it.read) ||
|
||||
(
|
||||
manga.downloadedFilterRaw == Manga.CHAPTER_SHOW_DOWNLOADED && !downloadManager.isChapterDownloaded(
|
||||
it.name,
|
||||
it.scanlator,
|
||||
manga.title,
|
||||
manga.source,
|
||||
)
|
||||
manga.downloadedFilterRaw ==
|
||||
Manga.CHAPTER_SHOW_DOWNLOADED && !downloadManager.isChapterDownloaded(
|
||||
it.name,
|
||||
it.scanlator,
|
||||
manga.title,
|
||||
manga.source,
|
||||
)
|
||||
) ||
|
||||
(
|
||||
manga.downloadedFilterRaw == Manga.CHAPTER_SHOW_NOT_DOWNLOADED && downloadManager.isChapterDownloaded(
|
||||
it.name,
|
||||
it.scanlator,
|
||||
manga.title,
|
||||
manga.source,
|
||||
)
|
||||
manga.downloadedFilterRaw ==
|
||||
Manga.CHAPTER_SHOW_NOT_DOWNLOADED && downloadManager.isChapterDownloaded(
|
||||
it.name,
|
||||
it.scanlator,
|
||||
manga.title,
|
||||
manga.source,
|
||||
)
|
||||
) ||
|
||||
(manga.bookmarkedFilterRaw == Manga.CHAPTER_SHOW_BOOKMARKED && !it.bookmark) ||
|
||||
(manga.bookmarkedFilterRaw == Manga.CHAPTER_SHOW_NOT_BOOKMARKED && it.bookmark)
|
||||
|
@ -783,14 +785,23 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||
|
||||
val filename = generateFilename(manga, page)
|
||||
|
||||
// Copy file in background
|
||||
// Pictures directory.
|
||||
val relativePath = if (readerPreferences.folderPerManga().get()) {
|
||||
DiskUtil.buildValidFilename(
|
||||
manga.title,
|
||||
)
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
// Copy file in background.
|
||||
viewModelScope.launchNonCancellable {
|
||||
try {
|
||||
val uri = imageSaver.save(
|
||||
image = Image.Page(
|
||||
inputStream = page.stream!!,
|
||||
name = filename,
|
||||
location = Location.Pictures(DiskUtil.buildValidFilename(manga.title)),
|
||||
location = Location.Pictures.create(relativePath),
|
||||
),
|
||||
)
|
||||
withUIContext {
|
||||
|
|
|
@ -71,10 +71,9 @@ class ReaderPreferences(
|
|||
|
||||
fun webtoonSidePadding() = preferenceStore.getInt("webtoon_side_padding", WEBTOON_PADDING_MIN)
|
||||
|
||||
fun readerHideThreshold() = preferenceStore.getEnum(
|
||||
"reader_hide_threshold",
|
||||
ReaderHideThreshold.LOW,
|
||||
)
|
||||
fun readerHideThreshold() = preferenceStore.getEnum("reader_hide_threshold", ReaderHideThreshold.LOW)
|
||||
|
||||
fun folderPerManga() = preferenceStore.getBoolean("create_folder_per_manga", false)
|
||||
|
||||
fun skipRead() = preferenceStore.getBoolean("skip_read", false)
|
||||
|
||||
|
|
|
@ -110,7 +110,12 @@ open class ReaderPageImageView @JvmOverloads constructor(
|
|||
}
|
||||
|
||||
private fun SubsamplingScaleImageView.landscapeZoom(forward: Boolean) {
|
||||
if (config != null && config!!.landscapeZoom && config!!.minimumScaleType == SCALE_TYPE_CENTER_INSIDE && sWidth > sHeight && scale == minScale) {
|
||||
if (config != null &&
|
||||
config!!.landscapeZoom &&
|
||||
config!!.minimumScaleType == SCALE_TYPE_CENTER_INSIDE &&
|
||||
sWidth > sHeight &&
|
||||
scale == minScale
|
||||
) {
|
||||
handler?.postDelayed(500) {
|
||||
val point = when (config!!.zoomStartPosition) {
|
||||
ZoomStartPosition.LEFT -> if (forward) {
|
||||
|
|
|
@ -29,7 +29,8 @@ class AniSkipApi {
|
|||
// credits: https://github.com/saikou-app/saikou/blob/main/app/src/main/java/ani/saikou/others/AniSkip.kt
|
||||
fun getResult(malId: Int, episodeNumber: Int, episodeLength: Long): List<Stamp>? {
|
||||
val url =
|
||||
"https://api.aniskip.com/v2/skip-times/$malId/$episodeNumber?types[]=ed&types[]=mixed-ed&types[]=mixed-op&types[]=op&types[]=recap&episodeLength=$episodeLength"
|
||||
"https://api.aniskip.com/v2/skip-times/$malId/$episodeNumber?types[]=ed" +
|
||||
"&types[]=mixed-ed&types[]=mixed-op&types[]=op&types[]=recap&episodeLength=$episodeLength"
|
||||
return try {
|
||||
val a = client.newCall(GET(url)).execute().body.string()
|
||||
val res = json.decodeFromString<AniSkipResponse>(a)
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
package eu.kanade.tachiyomi.util
|
||||
|
||||
import android.util.Base64
|
||||
import java.security.SecureRandom
|
||||
import java.util.Base64
|
||||
|
||||
object PkceUtil {
|
||||
|
||||
private const val PKCE_BASE64_ENCODE_SETTINGS = Base64.NO_WRAP or Base64.NO_PADDING or Base64.URL_SAFE
|
||||
|
||||
fun generateCodeVerifier(): String {
|
||||
val codeVerifier = ByteArray(50)
|
||||
SecureRandom().nextBytes(codeVerifier)
|
||||
return Base64.encodeToString(codeVerifier, PKCE_BASE64_ENCODE_SETTINGS)
|
||||
return Base64.getUrlEncoder()
|
||||
.withoutPadding()
|
||||
.encodeToString(codeVerifier)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -56,7 +56,8 @@ fun Date.toRelativeString(
|
|||
return dateFormat.format(this)
|
||||
}
|
||||
val now = Date()
|
||||
val difference = now.timeWithOffset.floorNearest(MILLISECONDS_IN_DAY) - this.timeWithOffset.floorNearest(MILLISECONDS_IN_DAY)
|
||||
val difference = now.timeWithOffset.floorNearest(MILLISECONDS_IN_DAY) -
|
||||
this.timeWithOffset.floorNearest(MILLISECONDS_IN_DAY)
|
||||
val days = difference.floorDiv(MILLISECONDS_IN_DAY).toInt()
|
||||
return when {
|
||||
difference < 0 -> dateFormat.format(this)
|
||||
|
|
|
@ -99,4 +99,15 @@ private fun isRequestHeaderSafe(_name: String, _value: String): Boolean {
|
|||
if (name == "connection" && value == "upgrade") return false
|
||||
return true
|
||||
}
|
||||
private val unsafeHeaderNames = listOf("content-length", "host", "trailer", "te", "upgrade", "cookie2", "keep-alive", "transfer-encoding", "set-cookie")
|
||||
private val unsafeHeaderNames =
|
||||
listOf(
|
||||
"content-length",
|
||||
"host",
|
||||
"trailer",
|
||||
"te",
|
||||
"upgrade",
|
||||
"cookie2",
|
||||
"keep-alive",
|
||||
"transfer-encoding",
|
||||
"set-cookie",
|
||||
)
|
||||
|
|
|
@ -35,9 +35,17 @@ fun File.copyAndSetReadOnlyTo(target: File, overwrite: Boolean = false, bufferSi
|
|||
|
||||
if (target.exists()) {
|
||||
if (!overwrite) {
|
||||
throw FileAlreadyExistsException(file = this, other = target, reason = "The destination file already exists.")
|
||||
throw FileAlreadyExistsException(
|
||||
file = this,
|
||||
other = target,
|
||||
reason = "The destination file already exists.",
|
||||
)
|
||||
} else if (!target.delete()) {
|
||||
throw FileAlreadyExistsException(file = this, other = target, reason = "Tried to overwrite the destination, but failed to delete it.")
|
||||
throw FileAlreadyExistsException(
|
||||
file = this,
|
||||
other = target,
|
||||
reason = "Tried to overwrite the destination, but failed to delete it.",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
15
core/src/main/java/tachiyomi/core/util/lang/SortUtil.kt
Normal file
15
core/src/main/java/tachiyomi/core/util/lang/SortUtil.kt
Normal file
|
@ -0,0 +1,15 @@
|
|||
package tachiyomi.core.util.lang
|
||||
|
||||
import java.text.Collator
|
||||
import java.util.Locale
|
||||
|
||||
private val collator by lazy {
|
||||
val locale = Locale.getDefault()
|
||||
Collator.getInstance(locale).apply {
|
||||
strength = Collator.PRIMARY
|
||||
}
|
||||
}
|
||||
|
||||
fun String.compareToWithCollator(other: String): Int {
|
||||
return collator.compare(this, other)
|
||||
}
|
|
@ -366,8 +366,26 @@ object ImageUtil {
|
|||
val botLeftIsDark = botLeftPixel.isDark()
|
||||
val botRightIsDark = botRightPixel.isDark()
|
||||
|
||||
var darkBG = (topLeftIsDark && (botLeftIsDark || botRightIsDark || topRightIsDark || midLeftIsDark || topMidIsDark)) ||
|
||||
(topRightIsDark && (botRightIsDark || botLeftIsDark || midRightIsDark || topMidIsDark))
|
||||
var darkBG =
|
||||
(
|
||||
topLeftIsDark &&
|
||||
(
|
||||
botLeftIsDark ||
|
||||
botRightIsDark ||
|
||||
topRightIsDark ||
|
||||
midLeftIsDark ||
|
||||
topMidIsDark
|
||||
)
|
||||
) ||
|
||||
(
|
||||
topRightIsDark &&
|
||||
(
|
||||
botRightIsDark ||
|
||||
botLeftIsDark ||
|
||||
midRightIsDark ||
|
||||
topMidIsDark
|
||||
)
|
||||
)
|
||||
|
||||
val topAndBotPixels = listOf(
|
||||
topLeftPixel,
|
||||
|
@ -521,10 +539,26 @@ object ImageUtil {
|
|||
darkBG -> {
|
||||
return ColorDrawable(blackColor)
|
||||
}
|
||||
topIsBlackStreak || (topCornersIsDark && topOffsetCornersIsDark && (topMidIsDark || overallBlackPixels > 9)) -> {
|
||||
topIsBlackStreak ||
|
||||
(
|
||||
topCornersIsDark &&
|
||||
topOffsetCornersIsDark &&
|
||||
(
|
||||
topMidIsDark ||
|
||||
overallBlackPixels > 9
|
||||
)
|
||||
) -> {
|
||||
intArrayOf(blackColor, blackColor, whiteColor, whiteColor)
|
||||
}
|
||||
bottomIsBlackStreak || (botCornersIsDark && botOffsetCornersIsDark && (bottomCenterPixel.isDark() || overallBlackPixels > 9)) -> {
|
||||
bottomIsBlackStreak ||
|
||||
(
|
||||
botCornersIsDark &&
|
||||
botOffsetCornersIsDark &&
|
||||
(
|
||||
bottomCenterPixel.isDark() ||
|
||||
overallBlackPixels > 9
|
||||
)
|
||||
) -> {
|
||||
intArrayOf(whiteColor, whiteColor, blackColor, blackColor)
|
||||
}
|
||||
else -> {
|
||||
|
|
|
@ -9,9 +9,14 @@ import tachiyomi.core.util.lang.withIOContext
|
|||
import tachiyomi.domain.items.chapter.model.NoChaptersException
|
||||
import tachiyomi.domain.source.manga.repository.SourcePagingSourceType
|
||||
|
||||
class SourceSearchPagingSource(source: CatalogueSource, val query: String, val filters: FilterList) : SourcePagingSource(
|
||||
source,
|
||||
) {
|
||||
class SourceSearchPagingSource(
|
||||
source: CatalogueSource,
|
||||
val query: String,
|
||||
val filters: FilterList,
|
||||
) :
|
||||
SourcePagingSource(
|
||||
source,
|
||||
) {
|
||||
override suspend fun requestNextPage(currentPage: Int): MangasPage {
|
||||
return source.getSearchManga(currentPage, query, filters)
|
||||
}
|
||||
|
|
|
@ -100,6 +100,7 @@ data class Anime(
|
|||
const val EPISODE_SORTING_SOURCE = 0x00000000L
|
||||
const val EPISODE_SORTING_NUMBER = 0x00000100L
|
||||
const val EPISODE_SORTING_UPLOAD_DATE = 0x00000200L
|
||||
const val EPISODE_SORTING_ALPHABET = 0x00000300L
|
||||
const val EPISODE_SORTING_MASK = 0x00000300L
|
||||
|
||||
const val EPISODE_DISPLAY_NAME = 0x00000000L
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue