Last commit merged: 1d144e6767
This commit is contained in:
LuftVerbot 2023-11-25 19:42:37 +01:00
parent 89777e98b0
commit d7aee03688
119 changed files with 1087 additions and 463 deletions

View file

@ -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

View file

@ -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()) }

View file

@ -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 -> {

View file

@ -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 -> {

View file

@ -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" }
}
}
}
}
}

View file

@ -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)
}
}

View file

@ -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" }
}
}
}
}
}

View file

@ -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)
}
}

View file

@ -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) },
) {

View file

@ -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,

View file

@ -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) {

View file

@ -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) },
) {

View file

@ -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,

View file

@ -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) {

View file

@ -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 },
)
},
) {

View file

@ -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,

View file

@ -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),

View file

@ -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,

View file

@ -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

View file

@ -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())),
)

View file

@ -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

View file

@ -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(

View file

@ -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())),
)

View file

@ -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(

View file

@ -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)
},

View file

@ -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)
},

View file

@ -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)

View file

@ -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",
),
)!!

View file

@ -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),
),
),
)
}

View file

@ -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) {

View file

@ -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"
}
}

View file

@ -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)
}
}

View file

@ -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,
),

View file

@ -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",
)
},
)
}

View file

@ -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

View file

@ -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) },
)
}

View file

@ -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>) {

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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,
)

View file

@ -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"
}
}

View file

@ -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,
)

View file

@ -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"
}
}

View file

@ -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,
)
}
}

View file

@ -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
}
}
}

View file

@ -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) }
}
}
}

View file

@ -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) }
}
}
}

View file

@ -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 }

View file

@ -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

View file

@ -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(

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
}

View file

@ -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

View file

@ -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 -> {}
}

View file

@ -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

View file

@ -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,
)
}
}

View file

@ -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
}

View file

@ -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

View file

@ -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,
)
}
}

View file

@ -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
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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())

View file

@ -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())

View file

@ -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,

View file

@ -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()
}

View file

@ -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(),
),
)
}

View file

@ -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,

View file

@ -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()
}

View file

@ -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,
)
}

View file

@ -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,
)
}

View file

@ -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()
}

View file

@ -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) {

View file

@ -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)
}
}
}

View file

@ -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()

View file

@ -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()

View file

@ -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(

View file

@ -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

View file

@ -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
}
}
}

View file

@ -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 {

View file

@ -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)

View file

@ -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) {

View file

@ -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)

View file

@ -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)
}
}

View file

@ -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)

View file

@ -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",
)

View file

@ -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.",
)
}
}

View 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)
}

View file

@ -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 -> {

View file

@ -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)
}

View file

@ -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