mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-24 21:58:34 +03:00
merge24
Also a little test to see how this whole github team works.
Last commit merged: fe90546821
This commit is contained in:
parent
4cbf9a813e
commit
1b6301cc95
37 changed files with 863 additions and 348 deletions
|
@ -81,6 +81,7 @@ import tachiyomi.domain.entries.anime.interactor.GetLibraryAnime
|
||||||
import tachiyomi.domain.entries.anime.interactor.NetworkToLocalAnime
|
import tachiyomi.domain.entries.anime.interactor.NetworkToLocalAnime
|
||||||
import tachiyomi.domain.entries.anime.interactor.ResetAnimeViewerFlags
|
import tachiyomi.domain.entries.anime.interactor.ResetAnimeViewerFlags
|
||||||
import tachiyomi.domain.entries.anime.interactor.SetAnimeEpisodeFlags
|
import tachiyomi.domain.entries.anime.interactor.SetAnimeEpisodeFlags
|
||||||
|
import tachiyomi.domain.entries.anime.interactor.SetAnimeUpdateInterval
|
||||||
import tachiyomi.domain.entries.anime.repository.AnimeRepository
|
import tachiyomi.domain.entries.anime.repository.AnimeRepository
|
||||||
import tachiyomi.domain.entries.manga.interactor.GetDuplicateLibraryManga
|
import tachiyomi.domain.entries.manga.interactor.GetDuplicateLibraryManga
|
||||||
import tachiyomi.domain.entries.manga.interactor.GetLibraryManga
|
import tachiyomi.domain.entries.manga.interactor.GetLibraryManga
|
||||||
|
@ -90,6 +91,7 @@ import tachiyomi.domain.entries.manga.interactor.GetMangaWithChapters
|
||||||
import tachiyomi.domain.entries.manga.interactor.NetworkToLocalManga
|
import tachiyomi.domain.entries.manga.interactor.NetworkToLocalManga
|
||||||
import tachiyomi.domain.entries.manga.interactor.ResetMangaViewerFlags
|
import tachiyomi.domain.entries.manga.interactor.ResetMangaViewerFlags
|
||||||
import tachiyomi.domain.entries.manga.interactor.SetMangaChapterFlags
|
import tachiyomi.domain.entries.manga.interactor.SetMangaChapterFlags
|
||||||
|
import tachiyomi.domain.entries.manga.interactor.SetMangaUpdateInterval
|
||||||
import tachiyomi.domain.entries.manga.repository.MangaRepository
|
import tachiyomi.domain.entries.manga.repository.MangaRepository
|
||||||
import tachiyomi.domain.history.anime.interactor.GetAnimeHistory
|
import tachiyomi.domain.history.anime.interactor.GetAnimeHistory
|
||||||
import tachiyomi.domain.history.anime.interactor.GetNextEpisodes
|
import tachiyomi.domain.history.anime.interactor.GetNextEpisodes
|
||||||
|
@ -182,10 +184,11 @@ class DomainModule : InjektModule {
|
||||||
addFactory { GetNextEpisodes(get(), get(), get()) }
|
addFactory { GetNextEpisodes(get(), get(), get()) }
|
||||||
addFactory { ResetAnimeViewerFlags(get()) }
|
addFactory { ResetAnimeViewerFlags(get()) }
|
||||||
addFactory { SetAnimeEpisodeFlags(get()) }
|
addFactory { SetAnimeEpisodeFlags(get()) }
|
||||||
|
addFactory { SetAnimeUpdateInterval(get()) }
|
||||||
addFactory { SetAnimeDefaultEpisodeFlags(get(), get(), get()) }
|
addFactory { SetAnimeDefaultEpisodeFlags(get(), get(), get()) }
|
||||||
addFactory { SetAnimeViewerFlags(get()) }
|
addFactory { SetAnimeViewerFlags(get()) }
|
||||||
addFactory { NetworkToLocalAnime(get()) }
|
addFactory { NetworkToLocalAnime(get()) }
|
||||||
addFactory { UpdateAnime(get()) }
|
addFactory { UpdateAnime(get(), get()) }
|
||||||
addFactory { SetAnimeCategories(get()) }
|
addFactory { SetAnimeCategories(get()) }
|
||||||
|
|
||||||
addSingletonFactory<MangaRepository> { MangaRepositoryImpl(get()) }
|
addSingletonFactory<MangaRepository> { MangaRepositoryImpl(get()) }
|
||||||
|
@ -197,6 +200,7 @@ class DomainModule : InjektModule {
|
||||||
addFactory { GetNextChapters(get(), get(), get()) }
|
addFactory { GetNextChapters(get(), get(), get()) }
|
||||||
addFactory { ResetMangaViewerFlags(get()) }
|
addFactory { ResetMangaViewerFlags(get()) }
|
||||||
addFactory { SetMangaChapterFlags(get()) }
|
addFactory { SetMangaChapterFlags(get()) }
|
||||||
|
addFactory { SetMangaUpdateInterval(get()) }
|
||||||
addFactory {
|
addFactory {
|
||||||
SetMangaDefaultChapterFlags(
|
SetMangaDefaultChapterFlags(
|
||||||
get(),
|
get(),
|
||||||
|
@ -206,7 +210,7 @@ class DomainModule : InjektModule {
|
||||||
}
|
}
|
||||||
addFactory { SetMangaViewerFlags(get()) }
|
addFactory { SetMangaViewerFlags(get()) }
|
||||||
addFactory { NetworkToLocalManga(get()) }
|
addFactory { NetworkToLocalManga(get()) }
|
||||||
addFactory { UpdateManga(get()) }
|
addFactory { UpdateManga(get(), get()) }
|
||||||
addFactory { SetMangaCategories(get()) }
|
addFactory { SetMangaCategories(get()) }
|
||||||
|
|
||||||
addSingletonFactory<ReleaseService> { ReleaseServiceImpl(get(), get()) }
|
addSingletonFactory<ReleaseService> { ReleaseServiceImpl(get(), get()) }
|
||||||
|
|
|
@ -3,8 +3,7 @@ package eu.kanade.domain.entries.anime.interactor
|
||||||
import eu.kanade.domain.entries.anime.model.hasCustomCover
|
import eu.kanade.domain.entries.anime.model.hasCustomCover
|
||||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||||
import eu.kanade.tachiyomi.data.cache.AnimeCoverCache
|
import eu.kanade.tachiyomi.data.cache.AnimeCoverCache
|
||||||
import tachiyomi.domain.entries.anime.interactor.getCurrentFetchRange
|
import tachiyomi.domain.entries.anime.interactor.SetAnimeUpdateInterval
|
||||||
import tachiyomi.domain.entries.anime.interactor.updateIntervalMeta
|
|
||||||
import tachiyomi.domain.entries.anime.model.Anime
|
import tachiyomi.domain.entries.anime.model.Anime
|
||||||
import tachiyomi.domain.entries.anime.model.AnimeUpdate
|
import tachiyomi.domain.entries.anime.model.AnimeUpdate
|
||||||
import tachiyomi.domain.entries.anime.repository.AnimeRepository
|
import tachiyomi.domain.entries.anime.repository.AnimeRepository
|
||||||
|
@ -17,6 +16,7 @@ import java.util.Date
|
||||||
|
|
||||||
class UpdateAnime(
|
class UpdateAnime(
|
||||||
private val animeRepository: AnimeRepository,
|
private val animeRepository: AnimeRepository,
|
||||||
|
private val setAnimeUpdateInterval: SetAnimeUpdateInterval,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun await(animeUpdate: AnimeUpdate): Boolean {
|
suspend fun await(animeUpdate: AnimeUpdate): Boolean {
|
||||||
|
@ -77,16 +77,15 @@ class UpdateAnime(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun awaitUpdateIntervalMeta(
|
suspend fun awaitUpdateFetchInterval(
|
||||||
anime: Anime,
|
anime: Anime,
|
||||||
episodes: List<Episode>,
|
episodes: List<Episode>,
|
||||||
zonedDateTime: ZonedDateTime = ZonedDateTime.now(),
|
zonedDateTime: ZonedDateTime = ZonedDateTime.now(),
|
||||||
setCurrentFetchRange: Pair<Long, Long> = getCurrentFetchRange(zonedDateTime),
|
fetchRange: Pair<Long, Long> = setAnimeUpdateInterval.getCurrentFetchRange(zonedDateTime),
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val newMeta = updateIntervalMeta(anime, episodes, zonedDateTime, setCurrentFetchRange)
|
val updateAnime = setAnimeUpdateInterval.updateInterval(anime, episodes, zonedDateTime, fetchRange)
|
||||||
|
return if (updateAnime != null) {
|
||||||
return if (newMeta != null) {
|
animeRepository.updateAnime(updateAnime)
|
||||||
animeRepository.updateAnime(newMeta)
|
|
||||||
} else {
|
} else {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,7 @@ package eu.kanade.domain.entries.manga.interactor
|
||||||
import eu.kanade.domain.entries.manga.model.hasCustomCover
|
import eu.kanade.domain.entries.manga.model.hasCustomCover
|
||||||
import eu.kanade.tachiyomi.data.cache.MangaCoverCache
|
import eu.kanade.tachiyomi.data.cache.MangaCoverCache
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import tachiyomi.domain.entries.manga.interactor.getCurrentFetchRange
|
import tachiyomi.domain.entries.manga.interactor.SetMangaUpdateInterval
|
||||||
import tachiyomi.domain.entries.manga.interactor.updateIntervalMeta
|
|
||||||
import tachiyomi.domain.entries.manga.model.Manga
|
import tachiyomi.domain.entries.manga.model.Manga
|
||||||
import tachiyomi.domain.entries.manga.model.MangaUpdate
|
import tachiyomi.domain.entries.manga.model.MangaUpdate
|
||||||
import tachiyomi.domain.entries.manga.repository.MangaRepository
|
import tachiyomi.domain.entries.manga.repository.MangaRepository
|
||||||
|
@ -17,6 +16,7 @@ import java.util.Date
|
||||||
|
|
||||||
class UpdateManga(
|
class UpdateManga(
|
||||||
private val mangaRepository: MangaRepository,
|
private val mangaRepository: MangaRepository,
|
||||||
|
private val setMangaUpdateInterval: SetMangaUpdateInterval,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun await(mangaUpdate: MangaUpdate): Boolean {
|
suspend fun await(mangaUpdate: MangaUpdate): Boolean {
|
||||||
|
@ -77,16 +77,15 @@ class UpdateManga(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun awaitUpdateIntervalMeta(
|
suspend fun awaitUpdateFetchInterval(
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
chapters: List<Chapter>,
|
chapters: List<Chapter>,
|
||||||
zonedDateTime: ZonedDateTime = ZonedDateTime.now(),
|
zonedDateTime: ZonedDateTime = ZonedDateTime.now(),
|
||||||
setCurrentFetchRange: Pair<Long, Long> = getCurrentFetchRange(zonedDateTime),
|
fetchRange: Pair<Long, Long> = setMangaUpdateInterval.getCurrentFetchRange(zonedDateTime),
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val newMeta = updateIntervalMeta(manga, chapters, zonedDateTime, setCurrentFetchRange)
|
val updatedManga = setMangaUpdateInterval.updateInterval(manga, chapters, zonedDateTime, fetchRange)
|
||||||
|
return if (updatedManga != null) {
|
||||||
return if (newMeta != null) {
|
mangaRepository.updateManga(updatedManga)
|
||||||
mangaRepository.updateManga(newMeta)
|
|
||||||
} else {
|
} else {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import tachiyomi.source.local.entries.manga.isLocal
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.lang.Long.max
|
import java.lang.Long.max
|
||||||
|
import java.time.ZonedDateTime
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.TreeSet
|
import java.util.TreeSet
|
||||||
|
|
||||||
|
@ -48,6 +49,9 @@ class SyncChaptersWithSource(
|
||||||
rawSourceChapters: List<SChapter>,
|
rawSourceChapters: List<SChapter>,
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
source: MangaSource,
|
source: MangaSource,
|
||||||
|
manualFetch: Boolean = false,
|
||||||
|
zoneDateTime: ZonedDateTime = ZonedDateTime.now(),
|
||||||
|
fetchRange: Pair<Long, Long> = Pair(0, 0),
|
||||||
): List<Chapter> {
|
): List<Chapter> {
|
||||||
if (rawSourceChapters.isEmpty() && !source.isLocal()) {
|
if (rawSourceChapters.isEmpty() && !source.isLocal()) {
|
||||||
throw NoChaptersException()
|
throw NoChaptersException()
|
||||||
|
@ -134,6 +138,14 @@ class SyncChaptersWithSource(
|
||||||
|
|
||||||
// Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
|
// Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
|
||||||
if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) {
|
if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) {
|
||||||
|
if (manualFetch || manga.calculateInterval == 0 || manga.nextUpdate < fetchRange.first) {
|
||||||
|
updateManga.awaitUpdateFetchInterval(
|
||||||
|
manga,
|
||||||
|
dbChapters,
|
||||||
|
zoneDateTime,
|
||||||
|
fetchRange,
|
||||||
|
)
|
||||||
|
}
|
||||||
return emptyList()
|
return emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,6 +200,8 @@ class SyncChaptersWithSource(
|
||||||
val chapterUpdates = toChange.map { it.toChapterUpdate() }
|
val chapterUpdates = toChange.map { it.toChapterUpdate() }
|
||||||
updateChapter.awaitAll(chapterUpdates)
|
updateChapter.awaitAll(chapterUpdates)
|
||||||
}
|
}
|
||||||
|
val newChapters = chapterRepository.getChapterByMangaId(manga.id)
|
||||||
|
updateManga.awaitUpdateFetchInterval(manga, newChapters, zoneDateTime, fetchRange)
|
||||||
|
|
||||||
// Set this manga as updated since chapters were changed
|
// Set this manga as updated since chapters were changed
|
||||||
// Note that last_update actually represents last time the chapter list changed at all
|
// Note that last_update actually represents last time the chapter list changed at all
|
||||||
|
|
|
@ -23,6 +23,7 @@ import tachiyomi.source.local.entries.anime.isLocal
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.lang.Long.max
|
import java.lang.Long.max
|
||||||
|
import java.time.ZonedDateTime
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.TreeSet
|
import java.util.TreeSet
|
||||||
|
|
||||||
|
@ -48,6 +49,9 @@ class SyncEpisodesWithSource(
|
||||||
rawSourceEpisodes: List<SEpisode>,
|
rawSourceEpisodes: List<SEpisode>,
|
||||||
anime: Anime,
|
anime: Anime,
|
||||||
source: AnimeSource,
|
source: AnimeSource,
|
||||||
|
manualFetch: Boolean = false,
|
||||||
|
zoneDateTime: ZonedDateTime = ZonedDateTime.now(),
|
||||||
|
fetchRange: Pair<Long, Long> = Pair(0, 0),
|
||||||
): List<Episode> {
|
): List<Episode> {
|
||||||
if (rawSourceEpisodes.isEmpty() && !source.isLocal()) {
|
if (rawSourceEpisodes.isEmpty() && !source.isLocal()) {
|
||||||
throw NoEpisodesException()
|
throw NoEpisodesException()
|
||||||
|
@ -134,6 +138,14 @@ class SyncEpisodesWithSource(
|
||||||
|
|
||||||
// Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
|
// Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
|
||||||
if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) {
|
if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) {
|
||||||
|
if (manualFetch || anime.calculateInterval == 0 || anime.nextUpdate < fetchRange.first) {
|
||||||
|
updateAnime.awaitUpdateFetchInterval(
|
||||||
|
anime,
|
||||||
|
dbEpisodes,
|
||||||
|
zoneDateTime,
|
||||||
|
fetchRange,
|
||||||
|
)
|
||||||
|
}
|
||||||
return emptyList()
|
return emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,6 +200,8 @@ class SyncEpisodesWithSource(
|
||||||
val episodeUpdates = toChange.map { it.toEpisodeUpdate() }
|
val episodeUpdates = toChange.map { it.toEpisodeUpdate() }
|
||||||
updateEpisode.awaitAll(episodeUpdates)
|
updateEpisode.awaitAll(episodeUpdates)
|
||||||
}
|
}
|
||||||
|
val newChapters = episodeRepository.getEpisodeByAnimeId(anime.id)
|
||||||
|
updateAnime.awaitUpdateFetchInterval(anime, newChapters, zoneDateTime, fetchRange)
|
||||||
|
|
||||||
// Set this anime as updated since episodes were changed
|
// Set this anime as updated since episodes were changed
|
||||||
// Note that last_update actually represents last time the episode list changed at all
|
// Note that last_update actually represents last time the episode list changed at all
|
||||||
|
|
|
@ -13,18 +13,13 @@ import java.util.Date
|
||||||
fun RelativeDateHeader(
|
fun RelativeDateHeader(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
date: Date,
|
date: Date,
|
||||||
relativeTime: Int,
|
|
||||||
dateFormat: DateFormat,
|
dateFormat: DateFormat,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
ListGroupHeader(
|
ListGroupHeader(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
text = remember {
|
text = remember {
|
||||||
date.toRelativeString(
|
date.toRelativeString(context, dateFormat)
|
||||||
context,
|
|
||||||
relativeTime,
|
|
||||||
dateFormat,
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,23 @@
|
||||||
package eu.kanade.presentation.entries
|
package eu.kanade.presentation.entries
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.DpSize
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import tachiyomi.domain.entries.anime.interactor.MAX_GRACE_PERIOD
|
||||||
|
import tachiyomi.presentation.core.components.WheelTextPicker
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DeleteItemsDialog(
|
fun DeleteItemsDialog(
|
||||||
|
@ -39,3 +51,51 @@ fun DeleteItemsDialog(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SetIntervalDialog(
|
||||||
|
interval: Int,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onValueChanged: (Int) -> Unit,
|
||||||
|
) {
|
||||||
|
var intervalValue by rememberSaveable { mutableIntStateOf(interval) }
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
title = { Text(text = stringResource(R.string.manga_modify_calculated_interval_title)) },
|
||||||
|
text = {
|
||||||
|
BoxWithConstraints(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
val size = DpSize(width = maxWidth / 2, height = 128.dp)
|
||||||
|
val items = (0..MAX_GRACE_PERIOD).map {
|
||||||
|
if (it == 0) {
|
||||||
|
stringResource(R.string.label_default)
|
||||||
|
} else {
|
||||||
|
it.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WheelTextPicker(
|
||||||
|
size = size,
|
||||||
|
items = items,
|
||||||
|
startIndex = intervalValue,
|
||||||
|
onSelectionChanged = { intervalValue = it },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismissRequest) {
|
||||||
|
Text(text = stringResource(R.string.action_cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
onValueChanged(intervalValue)
|
||||||
|
onDismissRequest()
|
||||||
|
},) {
|
||||||
|
Text(text = stringResource(R.string.action_ok))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -97,7 +97,7 @@ import java.util.concurrent.TimeUnit
|
||||||
fun AnimeScreen(
|
fun AnimeScreen(
|
||||||
state: AnimeScreenModel.State.Success,
|
state: AnimeScreenModel.State.Success,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
dateRelativeTime: Int,
|
intervalDisplay: () -> Pair<Int, Int>?,
|
||||||
dateFormat: DateFormat,
|
dateFormat: DateFormat,
|
||||||
isTabletUi: Boolean,
|
isTabletUi: Boolean,
|
||||||
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
||||||
|
@ -127,6 +127,7 @@ fun AnimeScreen(
|
||||||
onShareClicked: (() -> Unit)?,
|
onShareClicked: (() -> Unit)?,
|
||||||
onDownloadActionClicked: ((DownloadAction) -> Unit)?,
|
onDownloadActionClicked: ((DownloadAction) -> Unit)?,
|
||||||
onEditCategoryClicked: (() -> Unit)?,
|
onEditCategoryClicked: (() -> Unit)?,
|
||||||
|
onEditIntervalClicked: (() -> Unit)?,
|
||||||
onMigrateClicked: (() -> Unit)?,
|
onMigrateClicked: (() -> Unit)?,
|
||||||
changeAnimeSkipIntro: (() -> Unit)?,
|
changeAnimeSkipIntro: (() -> Unit)?,
|
||||||
|
|
||||||
|
@ -160,8 +161,8 @@ fun AnimeScreen(
|
||||||
AnimeScreenSmallImpl(
|
AnimeScreenSmallImpl(
|
||||||
state = state,
|
state = state,
|
||||||
snackbarHostState = snackbarHostState,
|
snackbarHostState = snackbarHostState,
|
||||||
dateRelativeTime = dateRelativeTime,
|
|
||||||
dateFormat = dateFormat,
|
dateFormat = dateFormat,
|
||||||
|
intervalDisplay = intervalDisplay,
|
||||||
episodeSwipeStartAction = episodeSwipeStartAction,
|
episodeSwipeStartAction = episodeSwipeStartAction,
|
||||||
episodeSwipeEndAction = episodeSwipeEndAction,
|
episodeSwipeEndAction = episodeSwipeEndAction,
|
||||||
showNextEpisodeAirTime = showNextEpisodeAirTime,
|
showNextEpisodeAirTime = showNextEpisodeAirTime,
|
||||||
|
@ -183,6 +184,7 @@ fun AnimeScreen(
|
||||||
onShareClicked = onShareClicked,
|
onShareClicked = onShareClicked,
|
||||||
onDownloadActionClicked = onDownloadActionClicked,
|
onDownloadActionClicked = onDownloadActionClicked,
|
||||||
onEditCategoryClicked = onEditCategoryClicked,
|
onEditCategoryClicked = onEditCategoryClicked,
|
||||||
|
onEditIntervalClicked = onEditIntervalClicked,
|
||||||
onMigrateClicked = onMigrateClicked,
|
onMigrateClicked = onMigrateClicked,
|
||||||
changeAnimeSkipIntro = changeAnimeSkipIntro,
|
changeAnimeSkipIntro = changeAnimeSkipIntro,
|
||||||
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
||||||
|
@ -199,12 +201,12 @@ fun AnimeScreen(
|
||||||
AnimeScreenLargeImpl(
|
AnimeScreenLargeImpl(
|
||||||
state = state,
|
state = state,
|
||||||
snackbarHostState = snackbarHostState,
|
snackbarHostState = snackbarHostState,
|
||||||
dateRelativeTime = dateRelativeTime,
|
|
||||||
episodeSwipeStartAction = episodeSwipeStartAction,
|
episodeSwipeStartAction = episodeSwipeStartAction,
|
||||||
episodeSwipeEndAction = episodeSwipeEndAction,
|
episodeSwipeEndAction = episodeSwipeEndAction,
|
||||||
showNextEpisodeAirTime = showNextEpisodeAirTime,
|
showNextEpisodeAirTime = showNextEpisodeAirTime,
|
||||||
alwaysUseExternalPlayer = alwaysUseExternalPlayer,
|
alwaysUseExternalPlayer = alwaysUseExternalPlayer,
|
||||||
dateFormat = dateFormat,
|
dateFormat = dateFormat,
|
||||||
|
intervalDisplay = intervalDisplay,
|
||||||
onBackClicked = onBackClicked,
|
onBackClicked = onBackClicked,
|
||||||
onEpisodeClicked = onEpisodeClicked,
|
onEpisodeClicked = onEpisodeClicked,
|
||||||
onDownloadEpisode = onDownloadEpisode,
|
onDownloadEpisode = onDownloadEpisode,
|
||||||
|
@ -222,6 +224,7 @@ fun AnimeScreen(
|
||||||
onShareClicked = onShareClicked,
|
onShareClicked = onShareClicked,
|
||||||
onDownloadActionClicked = onDownloadActionClicked,
|
onDownloadActionClicked = onDownloadActionClicked,
|
||||||
onEditCategoryClicked = onEditCategoryClicked,
|
onEditCategoryClicked = onEditCategoryClicked,
|
||||||
|
onEditIntervalClicked = onEditIntervalClicked,
|
||||||
changeAnimeSkipIntro = changeAnimeSkipIntro,
|
changeAnimeSkipIntro = changeAnimeSkipIntro,
|
||||||
onMigrateClicked = onMigrateClicked,
|
onMigrateClicked = onMigrateClicked,
|
||||||
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
||||||
|
@ -242,8 +245,8 @@ fun AnimeScreen(
|
||||||
private fun AnimeScreenSmallImpl(
|
private fun AnimeScreenSmallImpl(
|
||||||
state: AnimeScreenModel.State.Success,
|
state: AnimeScreenModel.State.Success,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
dateRelativeTime: Int,
|
|
||||||
dateFormat: DateFormat,
|
dateFormat: DateFormat,
|
||||||
|
intervalDisplay: () -> Pair<Int, Int>?,
|
||||||
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
||||||
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
|
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
|
||||||
showNextEpisodeAirTime: Boolean,
|
showNextEpisodeAirTime: Boolean,
|
||||||
|
@ -272,6 +275,7 @@ private fun AnimeScreenSmallImpl(
|
||||||
onShareClicked: (() -> Unit)?,
|
onShareClicked: (() -> Unit)?,
|
||||||
onDownloadActionClicked: ((DownloadAction) -> Unit)?,
|
onDownloadActionClicked: ((DownloadAction) -> Unit)?,
|
||||||
onEditCategoryClicked: (() -> Unit)?,
|
onEditCategoryClicked: (() -> Unit)?,
|
||||||
|
onEditIntervalClicked: (() -> Unit)?,
|
||||||
onMigrateClicked: (() -> Unit)?,
|
onMigrateClicked: (() -> Unit)?,
|
||||||
changeAnimeSkipIntro: (() -> Unit)?,
|
changeAnimeSkipIntro: (() -> Unit)?,
|
||||||
onSettingsClicked: (() -> Unit)?,
|
onSettingsClicked: (() -> Unit)?,
|
||||||
|
@ -312,9 +316,11 @@ private fun AnimeScreenSmallImpl(
|
||||||
}
|
}
|
||||||
val animatedTitleAlpha by animateFloatAsState(
|
val animatedTitleAlpha by animateFloatAsState(
|
||||||
if (firstVisibleItemIndex > 0) 1f else 0f,
|
if (firstVisibleItemIndex > 0) 1f else 0f,
|
||||||
|
label = "titleAlpha",
|
||||||
)
|
)
|
||||||
val animatedBgAlpha by animateFloatAsState(
|
val animatedBgAlpha by animateFloatAsState(
|
||||||
if (firstVisibleItemIndex > 0 || firstVisibleItemScrollOffset > 0) 1f else 0f,
|
if (firstVisibleItemIndex > 0 || firstVisibleItemScrollOffset > 0) 1f else 0f,
|
||||||
|
label = "bgAlpha",
|
||||||
)
|
)
|
||||||
EntryToolbar(
|
EntryToolbar(
|
||||||
title = state.anime.title,
|
title = state.anime.title,
|
||||||
|
@ -426,10 +432,13 @@ private fun AnimeScreenSmallImpl(
|
||||||
AnimeActionRow(
|
AnimeActionRow(
|
||||||
favorite = state.anime.favorite,
|
favorite = state.anime.favorite,
|
||||||
trackingCount = state.trackingCount,
|
trackingCount = state.trackingCount,
|
||||||
|
intervalDisplay = intervalDisplay,
|
||||||
|
isUserIntervalMode = state.anime.calculateInterval < 0,
|
||||||
onAddToLibraryClicked = onAddToLibraryClicked,
|
onAddToLibraryClicked = onAddToLibraryClicked,
|
||||||
onWebViewClicked = onWebViewClicked,
|
onWebViewClicked = onWebViewClicked,
|
||||||
onWebViewLongClicked = onWebViewLongClicked,
|
onWebViewLongClicked = onWebViewLongClicked,
|
||||||
onTrackingClicked = onTrackingClicked,
|
onTrackingClicked = onTrackingClicked,
|
||||||
|
onEditIntervalClicked = onEditIntervalClicked,
|
||||||
onEditCategory = onEditCategoryClicked,
|
onEditCategory = onEditCategoryClicked,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -488,7 +497,6 @@ private fun AnimeScreenSmallImpl(
|
||||||
sharedEpisodeItems(
|
sharedEpisodeItems(
|
||||||
anime = state.anime,
|
anime = state.anime,
|
||||||
episodes = episodes,
|
episodes = episodes,
|
||||||
dateRelativeTime = dateRelativeTime,
|
|
||||||
dateFormat = dateFormat,
|
dateFormat = dateFormat,
|
||||||
episodeSwipeStartAction = episodeSwipeStartAction,
|
episodeSwipeStartAction = episodeSwipeStartAction,
|
||||||
episodeSwipeEndAction = episodeSwipeEndAction,
|
episodeSwipeEndAction = episodeSwipeEndAction,
|
||||||
|
@ -508,8 +516,8 @@ private fun AnimeScreenSmallImpl(
|
||||||
fun AnimeScreenLargeImpl(
|
fun AnimeScreenLargeImpl(
|
||||||
state: AnimeScreenModel.State.Success,
|
state: AnimeScreenModel.State.Success,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
dateRelativeTime: Int,
|
|
||||||
dateFormat: DateFormat,
|
dateFormat: DateFormat,
|
||||||
|
intervalDisplay: () -> Pair<Int, Int>?,
|
||||||
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
||||||
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
|
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
|
||||||
showNextEpisodeAirTime: Boolean,
|
showNextEpisodeAirTime: Boolean,
|
||||||
|
@ -538,6 +546,7 @@ fun AnimeScreenLargeImpl(
|
||||||
onShareClicked: (() -> Unit)?,
|
onShareClicked: (() -> Unit)?,
|
||||||
onDownloadActionClicked: ((DownloadAction) -> Unit)?,
|
onDownloadActionClicked: ((DownloadAction) -> Unit)?,
|
||||||
onEditCategoryClicked: (() -> Unit)?,
|
onEditCategoryClicked: (() -> Unit)?,
|
||||||
|
onEditIntervalClicked: (() -> Unit)?,
|
||||||
onMigrateClicked: (() -> Unit)?,
|
onMigrateClicked: (() -> Unit)?,
|
||||||
changeAnimeSkipIntro: (() -> Unit)?,
|
changeAnimeSkipIntro: (() -> Unit)?,
|
||||||
onSettingsClicked: (() -> Unit)?,
|
onSettingsClicked: (() -> Unit)?,
|
||||||
|
@ -676,10 +685,13 @@ fun AnimeScreenLargeImpl(
|
||||||
AnimeActionRow(
|
AnimeActionRow(
|
||||||
favorite = state.anime.favorite,
|
favorite = state.anime.favorite,
|
||||||
trackingCount = state.trackingCount,
|
trackingCount = state.trackingCount,
|
||||||
|
intervalDisplay = intervalDisplay,
|
||||||
|
isUserIntervalMode = state.anime.calculateInterval < 0,
|
||||||
onAddToLibraryClicked = onAddToLibraryClicked,
|
onAddToLibraryClicked = onAddToLibraryClicked,
|
||||||
onWebViewClicked = onWebViewClicked,
|
onWebViewClicked = onWebViewClicked,
|
||||||
onWebViewLongClicked = onWebViewLongClicked,
|
onWebViewLongClicked = onWebViewLongClicked,
|
||||||
onTrackingClicked = onTrackingClicked,
|
onTrackingClicked = onTrackingClicked,
|
||||||
|
onEditIntervalClicked = onEditIntervalClicked,
|
||||||
onEditCategory = onEditCategoryClicked,
|
onEditCategory = onEditCategoryClicked,
|
||||||
)
|
)
|
||||||
ExpandableAnimeDescription(
|
ExpandableAnimeDescription(
|
||||||
|
@ -745,7 +757,6 @@ fun AnimeScreenLargeImpl(
|
||||||
sharedEpisodeItems(
|
sharedEpisodeItems(
|
||||||
anime = state.anime,
|
anime = state.anime,
|
||||||
episodes = episodes,
|
episodes = episodes,
|
||||||
dateRelativeTime = dateRelativeTime,
|
|
||||||
dateFormat = dateFormat,
|
dateFormat = dateFormat,
|
||||||
episodeSwipeStartAction = episodeSwipeStartAction,
|
episodeSwipeStartAction = episodeSwipeStartAction,
|
||||||
episodeSwipeEndAction = episodeSwipeEndAction,
|
episodeSwipeEndAction = episodeSwipeEndAction,
|
||||||
|
@ -816,7 +827,6 @@ private fun SharedAnimeBottomActionMenu(
|
||||||
private fun LazyListScope.sharedEpisodeItems(
|
private fun LazyListScope.sharedEpisodeItems(
|
||||||
anime: Anime,
|
anime: Anime,
|
||||||
episodes: List<EpisodeItem>,
|
episodes: List<EpisodeItem>,
|
||||||
dateRelativeTime: Int,
|
|
||||||
dateFormat: DateFormat,
|
dateFormat: DateFormat,
|
||||||
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
||||||
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
|
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
|
||||||
|
@ -845,11 +855,7 @@ private fun LazyListScope.sharedEpisodeItems(
|
||||||
date = episodeItem.episode.dateUpload
|
date = episodeItem.episode.dateUpload
|
||||||
.takeIf { it > 0L }
|
.takeIf { it > 0L }
|
||||||
?.let {
|
?.let {
|
||||||
Date(it).toRelativeString(
|
Date(it).toRelativeString(context, dateFormat)
|
||||||
context,
|
|
||||||
dateRelativeTime,
|
|
||||||
dateFormat,
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
watchProgress = episodeItem.episode.lastSecondSeen
|
watchProgress = episodeItem.episode.lastSecondSeen
|
||||||
.takeIf { !episodeItem.episode.seen && it > 0L }
|
.takeIf { !episodeItem.episode.seen && it > 0L }
|
||||||
|
|
|
@ -26,6 +26,7 @@ import androidx.compose.foundation.text.selection.SelectionContainer
|
||||||
import androidx.compose.material.LocalMinimumInteractiveComponentEnforcement
|
import androidx.compose.material.LocalMinimumInteractiveComponentEnforcement
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Favorite
|
import androidx.compose.material.icons.filled.Favorite
|
||||||
|
import androidx.compose.material.icons.filled.HourglassEmpty
|
||||||
import androidx.compose.material.icons.outlined.AttachMoney
|
import androidx.compose.material.icons.outlined.AttachMoney
|
||||||
import androidx.compose.material.icons.outlined.Block
|
import androidx.compose.material.icons.outlined.Block
|
||||||
import androidx.compose.material.icons.outlined.Close
|
import androidx.compose.material.icons.outlined.Close
|
||||||
|
@ -166,14 +167,19 @@ fun AnimeActionRow(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
favorite: Boolean,
|
favorite: Boolean,
|
||||||
trackingCount: Int,
|
trackingCount: Int,
|
||||||
|
intervalDisplay: () -> Pair<Int, Int>?,
|
||||||
|
isUserIntervalMode: Boolean,
|
||||||
onAddToLibraryClicked: () -> Unit,
|
onAddToLibraryClicked: () -> Unit,
|
||||||
onWebViewClicked: (() -> Unit)?,
|
onWebViewClicked: (() -> Unit)?,
|
||||||
onWebViewLongClicked: (() -> Unit)?,
|
onWebViewLongClicked: (() -> Unit)?,
|
||||||
onTrackingClicked: (() -> Unit)?,
|
onTrackingClicked: (() -> Unit)?,
|
||||||
|
onEditIntervalClicked: (() -> Unit)?,
|
||||||
onEditCategory: (() -> Unit)?,
|
onEditCategory: (() -> Unit)?,
|
||||||
) {
|
) {
|
||||||
|
val interval: Pair<Int, Int>? = intervalDisplay()
|
||||||
|
val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f)
|
||||||
|
|
||||||
Row(modifier = modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) {
|
Row(modifier = modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) {
|
||||||
val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f)
|
|
||||||
AnimeActionButton(
|
AnimeActionButton(
|
||||||
title = if (favorite) {
|
title = if (favorite) {
|
||||||
stringResource(R.string.in_library)
|
stringResource(R.string.in_library)
|
||||||
|
@ -185,6 +191,19 @@ fun AnimeActionRow(
|
||||||
onClick = onAddToLibraryClicked,
|
onClick = onAddToLibraryClicked,
|
||||||
onLongClick = onEditCategory,
|
onLongClick = onEditCategory,
|
||||||
)
|
)
|
||||||
|
if (onEditIntervalClicked != null && interval != null) {
|
||||||
|
AnimeActionButton(
|
||||||
|
title =
|
||||||
|
if (interval.first == interval.second) {
|
||||||
|
pluralStringResource(id = R.plurals.day, count = interval.second, interval.second)
|
||||||
|
} else {
|
||||||
|
pluralStringResource(id = R.plurals.range_interval_day, count = interval.second, interval.first, interval.second)
|
||||||
|
},
|
||||||
|
icon = Icons.Default.HourglassEmpty,
|
||||||
|
color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
|
||||||
|
onClick = onEditIntervalClicked,
|
||||||
|
)
|
||||||
|
}
|
||||||
if (onTrackingClicked != null) {
|
if (onTrackingClicked != null) {
|
||||||
AnimeActionButton(
|
AnimeActionButton(
|
||||||
title = if (trackingCount == 0) {
|
title = if (trackingCount == 0) {
|
||||||
|
|
|
@ -90,7 +90,7 @@ import java.util.Date
|
||||||
fun MangaScreen(
|
fun MangaScreen(
|
||||||
state: MangaScreenModel.State.Success,
|
state: MangaScreenModel.State.Success,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
dateRelativeTime: Int,
|
intervalDisplay: () -> Pair<Int, Int>?,
|
||||||
dateFormat: DateFormat,
|
dateFormat: DateFormat,
|
||||||
isTabletUi: Boolean,
|
isTabletUi: Boolean,
|
||||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
|
@ -118,6 +118,7 @@ fun MangaScreen(
|
||||||
onShareClicked: (() -> Unit)?,
|
onShareClicked: (() -> Unit)?,
|
||||||
onDownloadActionClicked: ((DownloadAction) -> Unit)?,
|
onDownloadActionClicked: ((DownloadAction) -> Unit)?,
|
||||||
onEditCategoryClicked: (() -> Unit)?,
|
onEditCategoryClicked: (() -> Unit)?,
|
||||||
|
onEditIntervalClicked: (() -> Unit)?,
|
||||||
onMigrateClicked: (() -> Unit)?,
|
onMigrateClicked: (() -> Unit)?,
|
||||||
|
|
||||||
// For bottom action menu
|
// For bottom action menu
|
||||||
|
@ -150,8 +151,8 @@ fun MangaScreen(
|
||||||
MangaScreenSmallImpl(
|
MangaScreenSmallImpl(
|
||||||
state = state,
|
state = state,
|
||||||
snackbarHostState = snackbarHostState,
|
snackbarHostState = snackbarHostState,
|
||||||
dateRelativeTime = dateRelativeTime,
|
|
||||||
dateFormat = dateFormat,
|
dateFormat = dateFormat,
|
||||||
|
intervalDisplay = intervalDisplay,
|
||||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||||
onBackClicked = onBackClicked,
|
onBackClicked = onBackClicked,
|
||||||
|
@ -171,6 +172,7 @@ fun MangaScreen(
|
||||||
onShareClicked = onShareClicked,
|
onShareClicked = onShareClicked,
|
||||||
onDownloadActionClicked = onDownloadActionClicked,
|
onDownloadActionClicked = onDownloadActionClicked,
|
||||||
onEditCategoryClicked = onEditCategoryClicked,
|
onEditCategoryClicked = onEditCategoryClicked,
|
||||||
|
onEditIntervalClicked = onEditIntervalClicked,
|
||||||
onMigrateClicked = onMigrateClicked,
|
onMigrateClicked = onMigrateClicked,
|
||||||
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
||||||
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
|
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
|
||||||
|
@ -186,10 +188,10 @@ fun MangaScreen(
|
||||||
MangaScreenLargeImpl(
|
MangaScreenLargeImpl(
|
||||||
state = state,
|
state = state,
|
||||||
snackbarHostState = snackbarHostState,
|
snackbarHostState = snackbarHostState,
|
||||||
dateRelativeTime = dateRelativeTime,
|
|
||||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||||
dateFormat = dateFormat,
|
dateFormat = dateFormat,
|
||||||
|
intervalDisplay = intervalDisplay,
|
||||||
onBackClicked = onBackClicked,
|
onBackClicked = onBackClicked,
|
||||||
onChapterClicked = onChapterClicked,
|
onChapterClicked = onChapterClicked,
|
||||||
onDownloadChapter = onDownloadChapter,
|
onDownloadChapter = onDownloadChapter,
|
||||||
|
@ -207,6 +209,7 @@ fun MangaScreen(
|
||||||
onShareClicked = onShareClicked,
|
onShareClicked = onShareClicked,
|
||||||
onDownloadActionClicked = onDownloadActionClicked,
|
onDownloadActionClicked = onDownloadActionClicked,
|
||||||
onEditCategoryClicked = onEditCategoryClicked,
|
onEditCategoryClicked = onEditCategoryClicked,
|
||||||
|
onEditIntervalClicked = onEditIntervalClicked,
|
||||||
onMigrateClicked = onMigrateClicked,
|
onMigrateClicked = onMigrateClicked,
|
||||||
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
||||||
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
|
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
|
||||||
|
@ -225,8 +228,8 @@ fun MangaScreen(
|
||||||
private fun MangaScreenSmallImpl(
|
private fun MangaScreenSmallImpl(
|
||||||
state: MangaScreenModel.State.Success,
|
state: MangaScreenModel.State.Success,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
dateRelativeTime: Int,
|
|
||||||
dateFormat: DateFormat,
|
dateFormat: DateFormat,
|
||||||
|
intervalDisplay: () -> Pair<Int, Int>?,
|
||||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
onBackClicked: () -> Unit,
|
onBackClicked: () -> Unit,
|
||||||
|
@ -253,6 +256,7 @@ private fun MangaScreenSmallImpl(
|
||||||
onShareClicked: (() -> Unit)?,
|
onShareClicked: (() -> Unit)?,
|
||||||
onDownloadActionClicked: ((DownloadAction) -> Unit)?,
|
onDownloadActionClicked: ((DownloadAction) -> Unit)?,
|
||||||
onEditCategoryClicked: (() -> Unit)?,
|
onEditCategoryClicked: (() -> Unit)?,
|
||||||
|
onEditIntervalClicked: (() -> Unit)?,
|
||||||
onMigrateClicked: (() -> Unit)?,
|
onMigrateClicked: (() -> Unit)?,
|
||||||
onSettingsClicked: (() -> Unit)?,
|
onSettingsClicked: (() -> Unit)?,
|
||||||
|
|
||||||
|
@ -293,9 +297,11 @@ private fun MangaScreenSmallImpl(
|
||||||
}
|
}
|
||||||
val animatedTitleAlpha by animateFloatAsState(
|
val animatedTitleAlpha by animateFloatAsState(
|
||||||
if (firstVisibleItemIndex > 0) 1f else 0f,
|
if (firstVisibleItemIndex > 0) 1f else 0f,
|
||||||
|
label = "titleAlpha",
|
||||||
)
|
)
|
||||||
val animatedBgAlpha by animateFloatAsState(
|
val animatedBgAlpha by animateFloatAsState(
|
||||||
if (firstVisibleItemIndex > 0 || firstVisibleItemScrollOffset > 0) 1f else 0f,
|
if (firstVisibleItemIndex > 0 || firstVisibleItemScrollOffset > 0) 1f else 0f,
|
||||||
|
label = "bgAlpha",
|
||||||
)
|
)
|
||||||
EntryToolbar(
|
EntryToolbar(
|
||||||
title = state.manga.title,
|
title = state.manga.title,
|
||||||
|
@ -400,10 +406,13 @@ private fun MangaScreenSmallImpl(
|
||||||
MangaActionRow(
|
MangaActionRow(
|
||||||
favorite = state.manga.favorite,
|
favorite = state.manga.favorite,
|
||||||
trackingCount = state.trackingCount,
|
trackingCount = state.trackingCount,
|
||||||
|
intervalDisplay = intervalDisplay,
|
||||||
|
isUserIntervalMode = state.manga.calculateInterval < 0,
|
||||||
onAddToLibraryClicked = onAddToLibraryClicked,
|
onAddToLibraryClicked = onAddToLibraryClicked,
|
||||||
onWebViewClicked = onWebViewClicked,
|
onWebViewClicked = onWebViewClicked,
|
||||||
onWebViewLongClicked = onWebViewLongClicked,
|
onWebViewLongClicked = onWebViewLongClicked,
|
||||||
onTrackingClicked = onTrackingClicked,
|
onTrackingClicked = onTrackingClicked,
|
||||||
|
onEditIntervalClicked = onEditIntervalClicked,
|
||||||
onEditCategory = onEditCategoryClicked,
|
onEditCategory = onEditCategoryClicked,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -437,7 +446,6 @@ private fun MangaScreenSmallImpl(
|
||||||
sharedChapterItems(
|
sharedChapterItems(
|
||||||
manga = state.manga,
|
manga = state.manga,
|
||||||
chapters = chapters,
|
chapters = chapters,
|
||||||
dateRelativeTime = dateRelativeTime,
|
|
||||||
dateFormat = dateFormat,
|
dateFormat = dateFormat,
|
||||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||||
|
@ -456,8 +464,8 @@ private fun MangaScreenSmallImpl(
|
||||||
fun MangaScreenLargeImpl(
|
fun MangaScreenLargeImpl(
|
||||||
state: MangaScreenModel.State.Success,
|
state: MangaScreenModel.State.Success,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
dateRelativeTime: Int,
|
|
||||||
dateFormat: DateFormat,
|
dateFormat: DateFormat,
|
||||||
|
intervalDisplay: () -> Pair<Int, Int>?,
|
||||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
onBackClicked: () -> Unit,
|
onBackClicked: () -> Unit,
|
||||||
|
@ -484,6 +492,7 @@ fun MangaScreenLargeImpl(
|
||||||
onShareClicked: (() -> Unit)?,
|
onShareClicked: (() -> Unit)?,
|
||||||
onDownloadActionClicked: ((DownloadAction) -> Unit)?,
|
onDownloadActionClicked: ((DownloadAction) -> Unit)?,
|
||||||
onEditCategoryClicked: (() -> Unit)?,
|
onEditCategoryClicked: (() -> Unit)?,
|
||||||
|
onEditIntervalClicked: (() -> Unit)?,
|
||||||
onMigrateClicked: (() -> Unit)?,
|
onMigrateClicked: (() -> Unit)?,
|
||||||
onSettingsClicked: (() -> Unit)?,
|
onSettingsClicked: (() -> Unit)?,
|
||||||
|
|
||||||
|
@ -618,10 +627,13 @@ fun MangaScreenLargeImpl(
|
||||||
MangaActionRow(
|
MangaActionRow(
|
||||||
favorite = state.manga.favorite,
|
favorite = state.manga.favorite,
|
||||||
trackingCount = state.trackingCount,
|
trackingCount = state.trackingCount,
|
||||||
|
intervalDisplay = intervalDisplay,
|
||||||
|
isUserIntervalMode = state.manga.calculateInterval < 0,
|
||||||
onAddToLibraryClicked = onAddToLibraryClicked,
|
onAddToLibraryClicked = onAddToLibraryClicked,
|
||||||
onWebViewClicked = onWebViewClicked,
|
onWebViewClicked = onWebViewClicked,
|
||||||
onWebViewLongClicked = onWebViewLongClicked,
|
onWebViewLongClicked = onWebViewLongClicked,
|
||||||
onTrackingClicked = onTrackingClicked,
|
onTrackingClicked = onTrackingClicked,
|
||||||
|
onEditIntervalClicked = onEditIntervalClicked,
|
||||||
onEditCategory = onEditCategoryClicked,
|
onEditCategory = onEditCategoryClicked,
|
||||||
)
|
)
|
||||||
ExpandableMangaDescription(
|
ExpandableMangaDescription(
|
||||||
|
@ -662,7 +674,6 @@ fun MangaScreenLargeImpl(
|
||||||
sharedChapterItems(
|
sharedChapterItems(
|
||||||
manga = state.manga,
|
manga = state.manga,
|
||||||
chapters = chapters,
|
chapters = chapters,
|
||||||
dateRelativeTime = dateRelativeTime,
|
|
||||||
dateFormat = dateFormat,
|
dateFormat = dateFormat,
|
||||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||||
|
@ -725,7 +736,6 @@ private fun SharedMangaBottomActionMenu(
|
||||||
private fun LazyListScope.sharedChapterItems(
|
private fun LazyListScope.sharedChapterItems(
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
chapters: List<ChapterItem>,
|
chapters: List<ChapterItem>,
|
||||||
dateRelativeTime: Int,
|
|
||||||
dateFormat: DateFormat,
|
dateFormat: DateFormat,
|
||||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
|
@ -754,11 +764,7 @@ private fun LazyListScope.sharedChapterItems(
|
||||||
date = chapterItem.chapter.dateUpload
|
date = chapterItem.chapter.dateUpload
|
||||||
.takeIf { it > 0L }
|
.takeIf { it > 0L }
|
||||||
?.let {
|
?.let {
|
||||||
Date(it).toRelativeString(
|
Date(it).toRelativeString(context, dateFormat)
|
||||||
context,
|
|
||||||
dateRelativeTime,
|
|
||||||
dateFormat,
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
readProgress = chapterItem.chapter.lastPageRead
|
readProgress = chapterItem.chapter.lastPageRead
|
||||||
.takeIf { !chapterItem.chapter.read && it > 0L }
|
.takeIf { !chapterItem.chapter.read && it > 0L }
|
||||||
|
|
|
@ -26,6 +26,7 @@ import androidx.compose.foundation.text.selection.SelectionContainer
|
||||||
import androidx.compose.material.LocalMinimumInteractiveComponentEnforcement
|
import androidx.compose.material.LocalMinimumInteractiveComponentEnforcement
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Favorite
|
import androidx.compose.material.icons.filled.Favorite
|
||||||
|
import androidx.compose.material.icons.filled.HourglassEmpty
|
||||||
import androidx.compose.material.icons.filled.Warning
|
import androidx.compose.material.icons.filled.Warning
|
||||||
import androidx.compose.material.icons.outlined.AttachMoney
|
import androidx.compose.material.icons.outlined.AttachMoney
|
||||||
import androidx.compose.material.icons.outlined.Block
|
import androidx.compose.material.icons.outlined.Block
|
||||||
|
@ -166,14 +167,19 @@ fun MangaActionRow(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
favorite: Boolean,
|
favorite: Boolean,
|
||||||
trackingCount: Int,
|
trackingCount: Int,
|
||||||
|
intervalDisplay: () -> Pair<Int, Int>?,
|
||||||
|
isUserIntervalMode: Boolean,
|
||||||
onAddToLibraryClicked: () -> Unit,
|
onAddToLibraryClicked: () -> Unit,
|
||||||
onWebViewClicked: (() -> Unit)?,
|
onWebViewClicked: (() -> Unit)?,
|
||||||
onWebViewLongClicked: (() -> Unit)?,
|
onWebViewLongClicked: (() -> Unit)?,
|
||||||
onTrackingClicked: (() -> Unit)?,
|
onTrackingClicked: (() -> Unit)?,
|
||||||
|
onEditIntervalClicked: (() -> Unit)?,
|
||||||
onEditCategory: (() -> Unit)?,
|
onEditCategory: (() -> Unit)?,
|
||||||
) {
|
) {
|
||||||
|
val interval: Pair<Int, Int>? = intervalDisplay()
|
||||||
|
val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f)
|
||||||
|
|
||||||
Row(modifier = modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) {
|
Row(modifier = modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) {
|
||||||
val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f)
|
|
||||||
MangaActionButton(
|
MangaActionButton(
|
||||||
title = if (favorite) {
|
title = if (favorite) {
|
||||||
stringResource(R.string.in_library)
|
stringResource(R.string.in_library)
|
||||||
|
@ -185,6 +191,19 @@ fun MangaActionRow(
|
||||||
onClick = onAddToLibraryClicked,
|
onClick = onAddToLibraryClicked,
|
||||||
onLongClick = onEditCategory,
|
onLongClick = onEditCategory,
|
||||||
)
|
)
|
||||||
|
if (onEditIntervalClicked != null && interval != null) {
|
||||||
|
MangaActionButton(
|
||||||
|
title =
|
||||||
|
if (interval.first == interval.second) {
|
||||||
|
pluralStringResource(id = R.plurals.day, count = interval.second, interval.second)
|
||||||
|
} else {
|
||||||
|
pluralStringResource(id = R.plurals.range_interval_day, count = interval.second, interval.first, interval.second)
|
||||||
|
},
|
||||||
|
icon = Icons.Default.HourglassEmpty,
|
||||||
|
color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
|
||||||
|
onClick = onEditIntervalClicked,
|
||||||
|
)
|
||||||
|
}
|
||||||
if (onTrackingClicked != null) {
|
if (onTrackingClicked != null) {
|
||||||
MangaActionButton(
|
MangaActionButton(
|
||||||
title = if (trackingCount == 0) {
|
title = if (trackingCount == 0) {
|
||||||
|
|
|
@ -24,7 +24,6 @@ fun AnimeHistoryContent(
|
||||||
onClickDelete: (AnimeHistoryWithRelations) -> Unit,
|
onClickDelete: (AnimeHistoryWithRelations) -> Unit,
|
||||||
preferences: UiPreferences = Injekt.get(),
|
preferences: UiPreferences = Injekt.get(),
|
||||||
) {
|
) {
|
||||||
val relativeTime: Int = remember { preferences.relativeTime().get() }
|
|
||||||
val dateFormat: DateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }
|
val dateFormat: DateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }
|
||||||
|
|
||||||
FastScrollLazyColumn(
|
FastScrollLazyColumn(
|
||||||
|
@ -45,7 +44,6 @@ fun AnimeHistoryContent(
|
||||||
RelativeDateHeader(
|
RelativeDateHeader(
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItemPlacement(),
|
||||||
date = item.date,
|
date = item.date,
|
||||||
relativeTime = relativeTime,
|
|
||||||
dateFormat = dateFormat,
|
dateFormat = dateFormat,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,6 @@ fun MangaHistoryContent(
|
||||||
onClickDelete: (MangaHistoryWithRelations) -> Unit,
|
onClickDelete: (MangaHistoryWithRelations) -> Unit,
|
||||||
preferences: UiPreferences = Injekt.get(),
|
preferences: UiPreferences = Injekt.get(),
|
||||||
) {
|
) {
|
||||||
val relativeTime: Int = remember { preferences.relativeTime().get() }
|
|
||||||
val dateFormat: DateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }
|
val dateFormat: DateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }
|
||||||
|
|
||||||
FastScrollLazyColumn(
|
FastScrollLazyColumn(
|
||||||
|
@ -43,7 +42,6 @@ fun MangaHistoryContent(
|
||||||
RelativeDateHeader(
|
RelativeDateHeader(
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItemPlacement(),
|
||||||
date = item.date,
|
date = item.date,
|
||||||
relativeTime = relativeTime,
|
|
||||||
dateFormat = dateFormat,
|
dateFormat = dateFormat,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,7 +51,6 @@ object SettingsAppearanceScreen : SearchableSettings {
|
||||||
return listOf(
|
return listOf(
|
||||||
getThemeGroup(context = context, uiPreferences = uiPreferences),
|
getThemeGroup(context = context, uiPreferences = uiPreferences),
|
||||||
getDisplayGroup(context = context, uiPreferences = uiPreferences),
|
getDisplayGroup(context = context, uiPreferences = uiPreferences),
|
||||||
getTimestampGroup(uiPreferences = uiPreferences),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,6 +123,7 @@ object SettingsAppearanceScreen : SearchableSettings {
|
||||||
): Preference.PreferenceGroup {
|
): Preference.PreferenceGroup {
|
||||||
val langs = remember { getLangs(context) }
|
val langs = remember { getLangs(context) }
|
||||||
var currentLanguage by remember { mutableStateOf(AppCompatDelegate.getApplicationLocales().get(0)?.toLanguageTag() ?: "") }
|
var currentLanguage by remember { mutableStateOf(AppCompatDelegate.getApplicationLocales().get(0)?.toLanguageTag() ?: "") }
|
||||||
|
val now = remember { Date().time }
|
||||||
|
|
||||||
LaunchedEffect(currentLanguage) {
|
LaunchedEffect(currentLanguage) {
|
||||||
val locale = if (currentLanguage.isEmpty()) {
|
val locale = if (currentLanguage.isEmpty()) {
|
||||||
|
@ -186,25 +186,6 @@ object SettingsAppearanceScreen : SearchableSettings {
|
||||||
true
|
true
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun getTimestampGroup(uiPreferences: UiPreferences): Preference.PreferenceGroup {
|
|
||||||
val now = remember { Date().time }
|
|
||||||
return Preference.PreferenceGroup(
|
|
||||||
title = stringResource(R.string.pref_category_timestamps),
|
|
||||||
preferenceItems = listOf(
|
|
||||||
Preference.PreferenceItem.ListPreference(
|
|
||||||
pref = uiPreferences.relativeTime(),
|
|
||||||
title = stringResource(R.string.pref_relative_format),
|
|
||||||
entries = mapOf(
|
|
||||||
0 to stringResource(R.string.off),
|
|
||||||
2 to stringResource(R.string.pref_relative_time_short),
|
|
||||||
7 to stringResource(R.string.pref_relative_time_long),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = uiPreferences.dateFormat(),
|
pref = uiPreferences.dateFormat(),
|
||||||
title = stringResource(R.string.pref_date_format),
|
title = stringResource(R.string.pref_date_format),
|
||||||
|
@ -217,7 +198,6 @@ object SettingsAppearanceScreen : SearchableSettings {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getLangs(context: Context): Map<String, String> {
|
private fun getLangs(context: Context): Map<String, String> {
|
||||||
val langs = mutableListOf<Pair<String, String>>()
|
val langs = mutableListOf<Pair<String, String>>()
|
||||||
val parser = context.resources.getXml(R.xml.locales_config)
|
val parser = context.resources.getXml(R.xml.locales_config)
|
||||||
|
|
|
@ -292,20 +292,17 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||||
true
|
true
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
// TODO: remove isDevFlavor checks once functionality is available
|
|
||||||
Preference.PreferenceItem.MultiSelectListPreference(
|
Preference.PreferenceItem.MultiSelectListPreference(
|
||||||
pref = libraryUpdateDeviceRestrictionPref,
|
pref = libraryUpdateDeviceRestrictionPref,
|
||||||
enabled = libraryUpdateInterval > 0,
|
enabled = libraryUpdateInterval > 0,
|
||||||
title = stringResource(R.string.pref_library_update_restriction),
|
title = stringResource(R.string.pref_library_update_restriction),
|
||||||
subtitle = stringResource(R.string.restrictions),
|
subtitle = stringResource(R.string.restrictions),
|
||||||
entries = buildMap {
|
entries = mapOf(
|
||||||
put(ENTRY_HAS_UNVIEWED, stringResource(R.string.pref_update_only_completely_read))
|
ENTRY_HAS_UNVIEWED to stringResource(R.string.pref_update_only_completely_read),
|
||||||
put(ENTRY_NON_VIEWED, stringResource(R.string.pref_update_only_started))
|
ENTRY_NON_VIEWED to stringResource(R.string.pref_update_only_started),
|
||||||
put(ENTRY_NON_COMPLETED, stringResource(R.string.pref_update_only_non_completed))
|
ENTRY_NON_COMPLETED to stringResource(R.string.pref_update_only_non_completed),
|
||||||
if (isDevFlavor) {
|
ENTRY_OUTSIDE_RELEASE_PERIOD to stringResource(R.string.pref_update_only_in_release_period),
|
||||||
put(ENTRY_OUTSIDE_RELEASE_PERIOD, stringResource(R.string.pref_update_only_in_release_period))
|
),
|
||||||
}
|
|
||||||
},
|
|
||||||
onValueChanged = {
|
onValueChanged = {
|
||||||
// Post to event looper to allow the preference to be updated.
|
// Post to event looper to allow the preference to be updated.
|
||||||
ContextCompat.getMainExecutor(context).execute {
|
ContextCompat.getMainExecutor(context).execute {
|
||||||
|
|
|
@ -40,7 +40,6 @@ fun AnimeUpdateScreen(
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
lastUpdated: Long,
|
lastUpdated: Long,
|
||||||
relativeTime: Int,
|
|
||||||
onClickCover: (AnimeUpdatesItem) -> Unit,
|
onClickCover: (AnimeUpdatesItem) -> Unit,
|
||||||
onSelectAll: (Boolean) -> Unit,
|
onSelectAll: (Boolean) -> Unit,
|
||||||
onInvertSelection: () -> Unit,
|
onInvertSelection: () -> Unit,
|
||||||
|
@ -101,7 +100,7 @@ fun AnimeUpdateScreen(
|
||||||
animeUpdatesLastUpdatedItem(lastUpdated)
|
animeUpdatesLastUpdatedItem(lastUpdated)
|
||||||
}
|
}
|
||||||
animeUpdatesUiItems(
|
animeUpdatesUiItems(
|
||||||
uiModels = state.getUiModel(context, relativeTime),
|
uiModels = state.getUiModel(context),
|
||||||
selectionMode = state.selectionMode,
|
selectionMode = state.selectionMode,
|
||||||
onUpdateSelected = onUpdateSelected,
|
onUpdateSelected = onUpdateSelected,
|
||||||
onClickCover = onClickCover,
|
onClickCover = onClickCover,
|
||||||
|
|
|
@ -37,7 +37,6 @@ fun MangaUpdateScreen(
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
lastUpdated: Long,
|
lastUpdated: Long,
|
||||||
relativeTime: Int,
|
|
||||||
onClickCover: (MangaUpdatesItem) -> Unit,
|
onClickCover: (MangaUpdatesItem) -> Unit,
|
||||||
onSelectAll: (Boolean) -> Unit,
|
onSelectAll: (Boolean) -> Unit,
|
||||||
onInvertSelection: () -> Unit,
|
onInvertSelection: () -> Unit,
|
||||||
|
@ -98,7 +97,7 @@ fun MangaUpdateScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
mangaUpdatesUiItems(
|
mangaUpdatesUiItems(
|
||||||
uiModels = state.getUiModel(context, relativeTime),
|
uiModels = state.getUiModel(context),
|
||||||
selectionMode = state.selectionMode,
|
selectionMode = state.selectionMode,
|
||||||
onUpdateSelected = onUpdateSelected,
|
onUpdateSelected = onUpdateSelected,
|
||||||
onClickCover = onClickCover,
|
onClickCover = onClickCover,
|
||||||
|
|
|
@ -72,7 +72,7 @@ fun WebViewScreenContent(
|
||||||
super.onPageFinished(view, url)
|
super.onPageFinished(view, url)
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val html = view.getHtml()
|
val html = view.getHtml()
|
||||||
showCloudflareHelp = "Checking if the site connection is secure" in html
|
showCloudflareHelp = "window._cf_chl_opt" in html
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,8 @@ import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
|
import eu.kanade.domain.entries.anime.interactor.UpdateAnime
|
||||||
|
import eu.kanade.domain.entries.manga.interactor.UpdateManga
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupAnime
|
import eu.kanade.tachiyomi.data.backup.models.BackupAnime
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupAnimeHistory
|
import eu.kanade.tachiyomi.data.backup.models.BackupAnimeHistory
|
||||||
|
@ -28,14 +30,21 @@ import eu.kanade.tachiyomi.util.system.createFileInCacheDir
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import tachiyomi.core.util.system.logcat
|
import tachiyomi.core.util.system.logcat
|
||||||
|
import tachiyomi.domain.entries.anime.interactor.SetAnimeUpdateInterval
|
||||||
import tachiyomi.domain.entries.anime.model.Anime
|
import tachiyomi.domain.entries.anime.model.Anime
|
||||||
|
import tachiyomi.domain.entries.manga.interactor.SetMangaUpdateInterval
|
||||||
import tachiyomi.domain.entries.manga.model.Manga
|
import tachiyomi.domain.entries.manga.model.Manga
|
||||||
import tachiyomi.domain.items.chapter.model.Chapter
|
import tachiyomi.domain.items.chapter.model.Chapter
|
||||||
|
import tachiyomi.domain.items.chapter.repository.ChapterRepository
|
||||||
import tachiyomi.domain.items.episode.model.Episode
|
import tachiyomi.domain.items.episode.model.Episode
|
||||||
|
import tachiyomi.domain.items.episode.repository.EpisodeRepository
|
||||||
import tachiyomi.domain.track.anime.model.AnimeTrack
|
import tachiyomi.domain.track.anime.model.AnimeTrack
|
||||||
import tachiyomi.domain.track.manga.model.MangaTrack
|
import tachiyomi.domain.track.manga.model.MangaTrack
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
import java.time.ZonedDateTime
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
|
@ -43,6 +52,17 @@ class BackupRestorer(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val notifier: BackupNotifier,
|
private val notifier: BackupNotifier,
|
||||||
) {
|
) {
|
||||||
|
private val updateManga: UpdateManga = Injekt.get()
|
||||||
|
private val chapterRepository: ChapterRepository = Injekt.get()
|
||||||
|
private val setMangaUpdateInterval: SetMangaUpdateInterval = Injekt.get()
|
||||||
|
|
||||||
|
private val updateAnime: UpdateAnime = Injekt.get()
|
||||||
|
private val episodeRepository: EpisodeRepository = Injekt.get()
|
||||||
|
private val setAnimeUpdateInterval: SetAnimeUpdateInterval = Injekt.get()
|
||||||
|
|
||||||
|
private var zonedDateTime = ZonedDateTime.now()
|
||||||
|
private var currentMangaRange = setMangaUpdateInterval.getCurrentFetchRange(zonedDateTime)
|
||||||
|
private var currentAnimeRange = setAnimeUpdateInterval.getCurrentFetchRange(zonedDateTime)
|
||||||
|
|
||||||
private var backupManager = BackupManager(context)
|
private var backupManager = BackupManager(context)
|
||||||
|
|
||||||
|
@ -120,6 +140,10 @@ class BackupRestorer(
|
||||||
val backupAnimeMaps = backup.backupBrokenAnimeSources.map { BackupAnimeSource(it.name, it.sourceId) } + backup.backupAnimeSources
|
val backupAnimeMaps = backup.backupBrokenAnimeSources.map { BackupAnimeSource(it.name, it.sourceId) } + backup.backupAnimeSources
|
||||||
animeSourceMapping = backupAnimeMaps.associate { it.sourceId to it.name }
|
animeSourceMapping = backupAnimeMaps.associate { it.sourceId to it.name }
|
||||||
|
|
||||||
|
zonedDateTime = ZonedDateTime.now()
|
||||||
|
currentMangaRange = setMangaUpdateInterval.getCurrentFetchRange(zonedDateTime)
|
||||||
|
currentAnimeRange = setAnimeUpdateInterval.getCurrentFetchRange(zonedDateTime)
|
||||||
|
|
||||||
return coroutineScope {
|
return coroutineScope {
|
||||||
// Restore individual manga
|
// Restore individual manga
|
||||||
backup.backupManga.forEach {
|
backup.backupManga.forEach {
|
||||||
|
@ -182,7 +206,7 @@ class BackupRestorer(
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val dbManga = backupManager.getMangaFromDatabase(manga.url, manga.source)
|
val dbManga = backupManager.getMangaFromDatabase(manga.url, manga.source)
|
||||||
if (dbManga == null) {
|
val restoredManga = if (dbManga == null) {
|
||||||
// Manga not in database
|
// Manga not in database
|
||||||
restoreExistingManga(manga, chapters, categories, history, tracks, backupCategories)
|
restoreExistingManga(manga, chapters, categories, history, tracks, backupCategories)
|
||||||
} else {
|
} else {
|
||||||
|
@ -192,6 +216,8 @@ class BackupRestorer(
|
||||||
// Fetch rest of manga information
|
// Fetch rest of manga information
|
||||||
restoreNewManga(updateManga, chapters, categories, history, tracks, backupCategories)
|
restoreNewManga(updateManga, chapters, categories, history, tracks, backupCategories)
|
||||||
}
|
}
|
||||||
|
val updatedChapters = chapterRepository.getChapterByMangaId(restoredManga.id)
|
||||||
|
updateManga.awaitUpdateFetchInterval(restoredManga, updatedChapters, zonedDateTime, currentMangaRange)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
||||||
errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}")
|
errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}")
|
||||||
|
@ -219,10 +245,11 @@ class BackupRestorer(
|
||||||
history: List<BackupHistory>,
|
history: List<BackupHistory>,
|
||||||
tracks: List<MangaTrack>,
|
tracks: List<MangaTrack>,
|
||||||
backupCategories: List<BackupCategory>,
|
backupCategories: List<BackupCategory>,
|
||||||
) {
|
): Manga {
|
||||||
val fetchedManga = backupManager.restoreNewManga(manga)
|
val fetchedManga = backupManager.restoreNewManga(manga)
|
||||||
backupManager.restoreChapters(fetchedManga, chapters)
|
backupManager.restoreChapters(fetchedManga, chapters)
|
||||||
restoreExtras(fetchedManga, categories, history, tracks, backupCategories)
|
restoreExtras(fetchedManga, categories, history, tracks, backupCategories)
|
||||||
|
return fetchedManga
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun restoreNewManga(
|
private suspend fun restoreNewManga(
|
||||||
|
@ -232,9 +259,10 @@ class BackupRestorer(
|
||||||
history: List<BackupHistory>,
|
history: List<BackupHistory>,
|
||||||
tracks: List<MangaTrack>,
|
tracks: List<MangaTrack>,
|
||||||
backupCategories: List<BackupCategory>,
|
backupCategories: List<BackupCategory>,
|
||||||
) {
|
): Manga {
|
||||||
backupManager.restoreChapters(backupManga, chapters)
|
backupManager.restoreChapters(backupManga, chapters)
|
||||||
restoreExtras(backupManga, categories, history, tracks, backupCategories)
|
restoreExtras(backupManga, categories, history, tracks, backupCategories)
|
||||||
|
return backupManga
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun restoreExtras(manga: Manga, categories: List<Int>, history: List<BackupHistory>, tracks: List<MangaTrack>, backupCategories: List<BackupCategory>) {
|
private suspend fun restoreExtras(manga: Manga, categories: List<Int>, history: List<BackupHistory>, tracks: List<MangaTrack>, backupCategories: List<BackupCategory>) {
|
||||||
|
@ -253,7 +281,7 @@ class BackupRestorer(
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val dbAnime = backupManager.getAnimeFromDatabase(anime.url, anime.source)
|
val dbAnime = backupManager.getAnimeFromDatabase(anime.url, anime.source)
|
||||||
if (dbAnime == null) {
|
val restoredAnime = if (dbAnime == null) {
|
||||||
// Anime not in database
|
// Anime not in database
|
||||||
restoreExistingAnime(anime, episodes, categories, history, tracks, backupCategories)
|
restoreExistingAnime(anime, episodes, categories, history, tracks, backupCategories)
|
||||||
} else {
|
} else {
|
||||||
|
@ -263,6 +291,8 @@ class BackupRestorer(
|
||||||
// Fetch rest of anime information
|
// Fetch rest of anime information
|
||||||
restoreNewAnime(updateAnime, episodes, categories, history, tracks, backupCategories)
|
restoreNewAnime(updateAnime, episodes, categories, history, tracks, backupCategories)
|
||||||
}
|
}
|
||||||
|
val updatedEpisodes = episodeRepository.getEpisodeByAnimeId(restoredAnime.id)
|
||||||
|
updateAnime.awaitUpdateFetchInterval(restoredAnime, updatedEpisodes, zonedDateTime, currentAnimeRange)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
val sourceName = sourceMapping[anime.source] ?: anime.source.toString()
|
val sourceName = sourceMapping[anime.source] ?: anime.source.toString()
|
||||||
errors.add(Date() to "${anime.title} [$sourceName]: ${e.message}")
|
errors.add(Date() to "${anime.title} [$sourceName]: ${e.message}")
|
||||||
|
@ -290,10 +320,11 @@ class BackupRestorer(
|
||||||
history: List<BackupAnimeHistory>,
|
history: List<BackupAnimeHistory>,
|
||||||
tracks: List<AnimeTrack>,
|
tracks: List<AnimeTrack>,
|
||||||
backupCategories: List<BackupCategory>,
|
backupCategories: List<BackupCategory>,
|
||||||
) {
|
): Anime {
|
||||||
val fetchedAnime = backupManager.restoreNewAnime(anime)
|
val fetchedAnime = backupManager.restoreNewAnime(anime)
|
||||||
backupManager.restoreEpisodes(fetchedAnime, episodes)
|
backupManager.restoreEpisodes(fetchedAnime, episodes)
|
||||||
restoreExtras(fetchedAnime, categories, history, tracks, backupCategories)
|
restoreExtras(fetchedAnime, categories, history, tracks, backupCategories)
|
||||||
|
return fetchedAnime
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun restoreNewAnime(
|
private suspend fun restoreNewAnime(
|
||||||
|
@ -303,9 +334,10 @@ class BackupRestorer(
|
||||||
history: List<BackupAnimeHistory>,
|
history: List<BackupAnimeHistory>,
|
||||||
tracks: List<AnimeTrack>,
|
tracks: List<AnimeTrack>,
|
||||||
backupCategories: List<BackupCategory>,
|
backupCategories: List<BackupCategory>,
|
||||||
) {
|
): Anime {
|
||||||
backupManager.restoreEpisodes(backupAnime, episodes)
|
backupManager.restoreEpisodes(backupAnime, episodes)
|
||||||
restoreExtras(backupAnime, categories, history, tracks, backupCategories)
|
restoreExtras(backupAnime, categories, history, tracks, backupCategories)
|
||||||
|
return backupAnime
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun restoreExtras(anime: Anime, categories: List<Int>, history: List<BackupAnimeHistory>, tracks: List<AnimeTrack>, backupCategories: List<BackupCategory>) {
|
private suspend fun restoreExtras(anime: Anime, categories: List<Int>, history: List<BackupAnimeHistory>, tracks: List<AnimeTrack>, backupCategories: List<BackupCategory>) {
|
||||||
|
|
|
@ -56,6 +56,7 @@ import tachiyomi.domain.category.model.Category
|
||||||
import tachiyomi.domain.download.service.DownloadPreferences
|
import tachiyomi.domain.download.service.DownloadPreferences
|
||||||
import tachiyomi.domain.entries.anime.interactor.GetAnime
|
import tachiyomi.domain.entries.anime.interactor.GetAnime
|
||||||
import tachiyomi.domain.entries.anime.interactor.GetLibraryAnime
|
import tachiyomi.domain.entries.anime.interactor.GetLibraryAnime
|
||||||
|
import tachiyomi.domain.entries.anime.interactor.SetAnimeUpdateInterval
|
||||||
import tachiyomi.domain.entries.anime.model.Anime
|
import tachiyomi.domain.entries.anime.model.Anime
|
||||||
import tachiyomi.domain.entries.anime.model.toAnimeUpdate
|
import tachiyomi.domain.entries.anime.model.toAnimeUpdate
|
||||||
import tachiyomi.domain.items.episode.interactor.GetEpisodeByAnimeId
|
import tachiyomi.domain.items.episode.interactor.GetEpisodeByAnimeId
|
||||||
|
@ -70,6 +71,7 @@ import tachiyomi.domain.library.service.LibraryPreferences.Companion.DEVICE_ONLY
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_HAS_UNVIEWED
|
import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_HAS_UNVIEWED
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_NON_COMPLETED
|
import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_NON_COMPLETED
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_NON_VIEWED
|
import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_NON_VIEWED
|
||||||
|
import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_OUTSIDE_RELEASE_PERIOD
|
||||||
import tachiyomi.domain.source.anime.model.AnimeSourceNotInstalledException
|
import tachiyomi.domain.source.anime.model.AnimeSourceNotInstalledException
|
||||||
import tachiyomi.domain.source.anime.service.AnimeSourceManager
|
import tachiyomi.domain.source.anime.service.AnimeSourceManager
|
||||||
import tachiyomi.domain.track.anime.interactor.GetAnimeTracks
|
import tachiyomi.domain.track.anime.interactor.GetAnimeTracks
|
||||||
|
@ -77,6 +79,7 @@ import tachiyomi.domain.track.anime.interactor.InsertAnimeTrack
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.time.ZonedDateTime
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.concurrent.CopyOnWriteArrayList
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
@ -101,6 +104,7 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
||||||
private val getTracks: GetAnimeTracks = Injekt.get()
|
private val getTracks: GetAnimeTracks = Injekt.get()
|
||||||
private val insertTrack: InsertAnimeTrack = Injekt.get()
|
private val insertTrack: InsertAnimeTrack = Injekt.get()
|
||||||
private val syncEpisodesWithTrackServiceTwoWay: SyncEpisodesWithTrackServiceTwoWay = Injekt.get()
|
private val syncEpisodesWithTrackServiceTwoWay: SyncEpisodesWithTrackServiceTwoWay = Injekt.get()
|
||||||
|
private val setAnimeUpdateInterval: SetAnimeUpdateInterval = Injekt.get()
|
||||||
|
|
||||||
private val notifier = AnimeLibraryUpdateNotifier(context)
|
private val notifier = AnimeLibraryUpdateNotifier(context)
|
||||||
|
|
||||||
|
@ -227,6 +231,10 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
||||||
val hasDownloads = AtomicBoolean(false)
|
val hasDownloads = AtomicBoolean(false)
|
||||||
val restrictions = libraryPreferences.libraryUpdateItemRestriction().get()
|
val restrictions = libraryPreferences.libraryUpdateItemRestriction().get()
|
||||||
|
|
||||||
|
val now = ZonedDateTime.now()
|
||||||
|
val fetchRange = setAnimeUpdateInterval.getCurrentFetchRange(now)
|
||||||
|
val higherLimit = fetchRange.second
|
||||||
|
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
animeToUpdate.groupBy { it.anime.source }.values
|
animeToUpdate.groupBy { it.anime.source }.values
|
||||||
.map { animeInSource ->
|
.map { animeInSource ->
|
||||||
|
@ -247,6 +255,9 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
||||||
anime,
|
anime,
|
||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
|
ENTRY_OUTSIDE_RELEASE_PERIOD in restrictions && anime.nextUpdate > higherLimit ->
|
||||||
|
skippedUpdates.add(anime to context.getString(R.string.skipped_reason_not_in_release_period))
|
||||||
|
|
||||||
ENTRY_NON_COMPLETED in restrictions && anime.status.toInt() == SAnime.COMPLETED ->
|
ENTRY_NON_COMPLETED in restrictions && anime.status.toInt() == SAnime.COMPLETED ->
|
||||||
skippedUpdates.add(anime to context.getString(R.string.skipped_reason_completed))
|
skippedUpdates.add(anime to context.getString(R.string.skipped_reason_completed))
|
||||||
|
|
||||||
|
@ -261,7 +272,7 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
try {
|
try {
|
||||||
val newEpisodes = updateAnime(anime)
|
val newEpisodes = updateAnime(anime, now, fetchRange)
|
||||||
.sortedByDescending { it.sourceOrder }
|
.sortedByDescending { it.sourceOrder }
|
||||||
|
|
||||||
if (newEpisodes.isNotEmpty()) {
|
if (newEpisodes.isNotEmpty()) {
|
||||||
|
@ -333,7 +344,7 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
||||||
* @param anime the anime to update.
|
* @param anime the anime to update.
|
||||||
* @return a pair of the inserted and removed episodes.
|
* @return a pair of the inserted and removed episodes.
|
||||||
*/
|
*/
|
||||||
private suspend fun updateAnime(anime: Anime): List<Episode> {
|
private suspend fun updateAnime(anime: Anime, zoneDateTime: ZonedDateTime, fetchRange: Pair<Long, Long>): List<Episode> {
|
||||||
val source = sourceManager.getOrStub(anime.source)
|
val source = sourceManager.getOrStub(anime.source)
|
||||||
|
|
||||||
// Update anime metadata if needed
|
// Update anime metadata if needed
|
||||||
|
@ -348,7 +359,7 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
||||||
// to get latest data so it doesn't get overwritten later on
|
// to get latest data so it doesn't get overwritten later on
|
||||||
val dbAnime = getAnime.await(anime.id)?.takeIf { it.favorite } ?: return emptyList()
|
val dbAnime = getAnime.await(anime.id)?.takeIf { it.favorite } ?: return emptyList()
|
||||||
|
|
||||||
return syncEpisodesWithSource.await(episodes, dbAnime, source)
|
return syncEpisodesWithSource.await(episodes, dbAnime, source, false, zoneDateTime, fetchRange)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun updateCovers() {
|
private suspend fun updateCovers() {
|
||||||
|
|
|
@ -56,6 +56,7 @@ import tachiyomi.domain.category.model.Category
|
||||||
import tachiyomi.domain.download.service.DownloadPreferences
|
import tachiyomi.domain.download.service.DownloadPreferences
|
||||||
import tachiyomi.domain.entries.manga.interactor.GetLibraryManga
|
import tachiyomi.domain.entries.manga.interactor.GetLibraryManga
|
||||||
import tachiyomi.domain.entries.manga.interactor.GetManga
|
import tachiyomi.domain.entries.manga.interactor.GetManga
|
||||||
|
import tachiyomi.domain.entries.manga.interactor.SetMangaUpdateInterval
|
||||||
import tachiyomi.domain.entries.manga.model.Manga
|
import tachiyomi.domain.entries.manga.model.Manga
|
||||||
import tachiyomi.domain.entries.manga.model.toMangaUpdate
|
import tachiyomi.domain.entries.manga.model.toMangaUpdate
|
||||||
import tachiyomi.domain.items.chapter.interactor.GetChapterByMangaId
|
import tachiyomi.domain.items.chapter.interactor.GetChapterByMangaId
|
||||||
|
@ -70,6 +71,7 @@ import tachiyomi.domain.library.service.LibraryPreferences.Companion.DEVICE_ONLY
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_HAS_UNVIEWED
|
import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_HAS_UNVIEWED
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_NON_COMPLETED
|
import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_NON_COMPLETED
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_NON_VIEWED
|
import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_NON_VIEWED
|
||||||
|
import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_OUTSIDE_RELEASE_PERIOD
|
||||||
import tachiyomi.domain.source.manga.model.SourceNotInstalledException
|
import tachiyomi.domain.source.manga.model.SourceNotInstalledException
|
||||||
import tachiyomi.domain.source.manga.service.MangaSourceManager
|
import tachiyomi.domain.source.manga.service.MangaSourceManager
|
||||||
import tachiyomi.domain.track.manga.interactor.GetMangaTracks
|
import tachiyomi.domain.track.manga.interactor.GetMangaTracks
|
||||||
|
@ -77,6 +79,7 @@ import tachiyomi.domain.track.manga.interactor.InsertMangaTrack
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.time.ZonedDateTime
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.concurrent.CopyOnWriteArrayList
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
@ -101,6 +104,7 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
||||||
private val getTracks: GetMangaTracks = Injekt.get()
|
private val getTracks: GetMangaTracks = Injekt.get()
|
||||||
private val insertTrack: InsertMangaTrack = Injekt.get()
|
private val insertTrack: InsertMangaTrack = Injekt.get()
|
||||||
private val syncChaptersWithTrackServiceTwoWay: SyncChaptersWithTrackServiceTwoWay = Injekt.get()
|
private val syncChaptersWithTrackServiceTwoWay: SyncChaptersWithTrackServiceTwoWay = Injekt.get()
|
||||||
|
private val setMangaUpdateInterval: SetMangaUpdateInterval = Injekt.get()
|
||||||
|
|
||||||
private val notifier = MangaLibraryUpdateNotifier(context)
|
private val notifier = MangaLibraryUpdateNotifier(context)
|
||||||
|
|
||||||
|
@ -227,6 +231,10 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
||||||
val hasDownloads = AtomicBoolean(false)
|
val hasDownloads = AtomicBoolean(false)
|
||||||
val restrictions = libraryPreferences.libraryUpdateItemRestriction().get()
|
val restrictions = libraryPreferences.libraryUpdateItemRestriction().get()
|
||||||
|
|
||||||
|
val now = ZonedDateTime.now()
|
||||||
|
val fetchRange = setMangaUpdateInterval.getCurrentFetchRange(now)
|
||||||
|
val higherLimit = fetchRange.second
|
||||||
|
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
mangaToUpdate.groupBy { it.manga.source }.values
|
mangaToUpdate.groupBy { it.manga.source }.values
|
||||||
.map { mangaInSource ->
|
.map { mangaInSource ->
|
||||||
|
@ -247,6 +255,9 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
||||||
manga,
|
manga,
|
||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
|
ENTRY_OUTSIDE_RELEASE_PERIOD in restrictions && manga.nextUpdate > higherLimit ->
|
||||||
|
skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_in_release_period))
|
||||||
|
|
||||||
ENTRY_NON_COMPLETED in restrictions && manga.status.toInt() == SManga.COMPLETED ->
|
ENTRY_NON_COMPLETED in restrictions && manga.status.toInt() == SManga.COMPLETED ->
|
||||||
skippedUpdates.add(manga to context.getString(R.string.skipped_reason_completed))
|
skippedUpdates.add(manga to context.getString(R.string.skipped_reason_completed))
|
||||||
|
|
||||||
|
@ -261,7 +272,7 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
try {
|
try {
|
||||||
val newChapters = updateManga(manga)
|
val newChapters = updateManga(manga, now, fetchRange)
|
||||||
.sortedByDescending { it.sourceOrder }
|
.sortedByDescending { it.sourceOrder }
|
||||||
|
|
||||||
if (newChapters.isNotEmpty()) {
|
if (newChapters.isNotEmpty()) {
|
||||||
|
@ -333,7 +344,7 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
||||||
* @param manga the manga to update.
|
* @param manga the manga to update.
|
||||||
* @return a pair of the inserted and removed chapters.
|
* @return a pair of the inserted and removed chapters.
|
||||||
*/
|
*/
|
||||||
private suspend fun updateManga(manga: Manga): List<Chapter> {
|
private suspend fun updateManga(manga: Manga, zoneDateTime: ZonedDateTime, fetchRange: Pair<Long, Long>): List<Chapter> {
|
||||||
val source = sourceManager.getOrStub(manga.source)
|
val source = sourceManager.getOrStub(manga.source)
|
||||||
|
|
||||||
// Update manga metadata if needed
|
// Update manga metadata if needed
|
||||||
|
@ -348,7 +359,7 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
||||||
// to get latest data so it doesn't get overwritten later on
|
// to get latest data so it doesn't get overwritten later on
|
||||||
val dbManga = getManga.await(manga.id)?.takeIf { it.favorite } ?: return emptyList()
|
val dbManga = getManga.await(manga.id)?.takeIf { it.favorite } ?: return emptyList()
|
||||||
|
|
||||||
return syncChaptersWithSource.await(chapters, dbManga, source)
|
return syncChaptersWithSource.await(chapters, dbManga, source, false, zoneDateTime, fetchRange)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun updateCovers() {
|
private suspend fun updateCovers() {
|
||||||
|
|
|
@ -27,6 +27,7 @@ import eu.kanade.presentation.category.ChangeCategoryDialog
|
||||||
import eu.kanade.presentation.components.NavigatorAdaptiveSheet
|
import eu.kanade.presentation.components.NavigatorAdaptiveSheet
|
||||||
import eu.kanade.presentation.entries.DeleteItemsDialog
|
import eu.kanade.presentation.entries.DeleteItemsDialog
|
||||||
import eu.kanade.presentation.entries.EditCoverAction
|
import eu.kanade.presentation.entries.EditCoverAction
|
||||||
|
import eu.kanade.presentation.entries.SetIntervalDialog
|
||||||
import eu.kanade.presentation.entries.anime.AnimeScreen
|
import eu.kanade.presentation.entries.anime.AnimeScreen
|
||||||
import eu.kanade.presentation.entries.anime.DuplicateAnimeDialog
|
import eu.kanade.presentation.entries.anime.DuplicateAnimeDialog
|
||||||
import eu.kanade.presentation.entries.anime.EpisodeOptionsDialogScreen
|
import eu.kanade.presentation.entries.anime.EpisodeOptionsDialogScreen
|
||||||
|
@ -104,8 +105,8 @@ class AnimeScreen(
|
||||||
AnimeScreen(
|
AnimeScreen(
|
||||||
state = successState,
|
state = successState,
|
||||||
snackbarHostState = screenModel.snackbarHostState,
|
snackbarHostState = screenModel.snackbarHostState,
|
||||||
dateRelativeTime = screenModel.relativeTime,
|
|
||||||
dateFormat = screenModel.dateFormat,
|
dateFormat = screenModel.dateFormat,
|
||||||
|
intervalDisplay = screenModel::intervalDisplay,
|
||||||
isTabletUi = isTabletUi(),
|
isTabletUi = isTabletUi(),
|
||||||
episodeSwipeStartAction = screenModel.episodeSwipeStartAction,
|
episodeSwipeStartAction = screenModel.episodeSwipeStartAction,
|
||||||
episodeSwipeEndAction = screenModel.episodeSwipeEndAction,
|
episodeSwipeEndAction = screenModel.episodeSwipeEndAction,
|
||||||
|
@ -139,7 +140,8 @@ class AnimeScreen(
|
||||||
onCoverClicked = screenModel::showCoverDialog,
|
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() },
|
onDownloadActionClicked = screenModel::runDownloadAction.takeIf { !successState.source.isLocalOrStub() },
|
||||||
onEditCategoryClicked = screenModel::promptChangeCategories.takeIf { successState.anime.favorite },
|
onEditCategoryClicked = screenModel::showChangeCategoryDialog.takeIf { successState.anime.favorite },
|
||||||
|
onEditIntervalClicked = screenModel::showSetAnimeIntervalDialog.takeIf { screenModel.isIntervalEnabled && successState.anime.favorite },
|
||||||
onMigrateClicked = { navigator.push(MigrateAnimeSearchScreen(successState.anime.id)) }.takeIf { successState.anime.favorite },
|
onMigrateClicked = { navigator.push(MigrateAnimeSearchScreen(successState.anime.id)) }.takeIf { successState.anime.favorite },
|
||||||
changeAnimeSkipIntro = screenModel::showAnimeSkipIntroDialog.takeIf { successState.anime.favorite },
|
changeAnimeSkipIntro = screenModel::showAnimeSkipIntroDialog.takeIf { successState.anime.favorite },
|
||||||
onMultiBookmarkClicked = screenModel::bookmarkEpisodes,
|
onMultiBookmarkClicked = screenModel::bookmarkEpisodes,
|
||||||
|
@ -233,6 +235,13 @@ class AnimeScreen(
|
||||||
LoadingScreen(Modifier.systemBarsPadding())
|
LoadingScreen(Modifier.systemBarsPadding())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is AnimeScreenModel.Dialog.SetAnimeInterval -> {
|
||||||
|
SetIntervalDialog(
|
||||||
|
interval = if (dialog.anime.calculateInterval < 0) -dialog.anime.calculateInterval else 0,
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
onValueChanged = { screenModel.setFetchRangeInterval(dialog.anime, it) },
|
||||||
|
)
|
||||||
|
}
|
||||||
AnimeScreenModel.Dialog.ChangeAnimeSkipIntro -> {
|
AnimeScreenModel.Dialog.ChangeAnimeSkipIntro -> {
|
||||||
fun updateSkipIntroLength(newLength: Long) {
|
fun updateSkipIntroLength(newLength: Long) {
|
||||||
scope.launchIO {
|
scope.launchIO {
|
||||||
|
|
|
@ -8,7 +8,6 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
import cafe.adriel.voyager.core.model.coroutineScope
|
import cafe.adriel.voyager.core.model.coroutineScope
|
||||||
import eu.kanade.core.preference.asState
|
|
||||||
import eu.kanade.core.util.addOrRemove
|
import eu.kanade.core.util.addOrRemove
|
||||||
import eu.kanade.domain.entries.anime.interactor.SetAnimeViewerFlags
|
import eu.kanade.domain.entries.anime.interactor.SetAnimeViewerFlags
|
||||||
import eu.kanade.domain.entries.anime.interactor.UpdateAnime
|
import eu.kanade.domain.entries.anime.interactor.UpdateAnime
|
||||||
|
@ -65,6 +64,7 @@ import tachiyomi.domain.entries.anime.interactor.GetAnimeWithEpisodes
|
||||||
import tachiyomi.domain.entries.anime.interactor.GetDuplicateLibraryAnime
|
import tachiyomi.domain.entries.anime.interactor.GetDuplicateLibraryAnime
|
||||||
import tachiyomi.domain.entries.anime.interactor.SetAnimeEpisodeFlags
|
import tachiyomi.domain.entries.anime.interactor.SetAnimeEpisodeFlags
|
||||||
import tachiyomi.domain.entries.anime.model.Anime
|
import tachiyomi.domain.entries.anime.model.Anime
|
||||||
|
import tachiyomi.domain.entries.anime.repository.AnimeRepository
|
||||||
import tachiyomi.domain.entries.applyFilter
|
import tachiyomi.domain.entries.applyFilter
|
||||||
import tachiyomi.domain.items.episode.interactor.SetAnimeDefaultEpisodeFlags
|
import tachiyomi.domain.items.episode.interactor.SetAnimeDefaultEpisodeFlags
|
||||||
import tachiyomi.domain.items.episode.interactor.UpdateEpisode
|
import tachiyomi.domain.items.episode.interactor.UpdateEpisode
|
||||||
|
@ -79,6 +79,7 @@ import tachiyomi.source.local.entries.anime.isLocal
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
class AnimeScreenModel(
|
class AnimeScreenModel(
|
||||||
val context: Context,
|
val context: Context,
|
||||||
|
@ -103,6 +104,7 @@ class AnimeScreenModel(
|
||||||
private val getCategories: GetAnimeCategories = Injekt.get(),
|
private val getCategories: GetAnimeCategories = Injekt.get(),
|
||||||
private val getTracks: GetAnimeTracks = Injekt.get(),
|
private val getTracks: GetAnimeTracks = Injekt.get(),
|
||||||
private val setAnimeCategories: SetAnimeCategories = Injekt.get(),
|
private val setAnimeCategories: SetAnimeCategories = Injekt.get(),
|
||||||
|
private val animeRepository: AnimeRepository = Injekt.get(),
|
||||||
internal val setAnimeViewerFlags: SetAnimeViewerFlags = Injekt.get(),
|
internal val setAnimeViewerFlags: SetAnimeViewerFlags = Injekt.get(),
|
||||||
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
|
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
|
||||||
) : StateScreenModel<AnimeScreenModel.State>(State.Loading) {
|
) : StateScreenModel<AnimeScreenModel.State>(State.Loading) {
|
||||||
|
@ -131,9 +133,12 @@ class AnimeScreenModel(
|
||||||
val alwaysUseExternalPlayer = playerPreferences.alwaysUseExternalPlayer().get()
|
val alwaysUseExternalPlayer = playerPreferences.alwaysUseExternalPlayer().get()
|
||||||
val useExternalDownloader = downloadPreferences.useExternalDownloader().get()
|
val useExternalDownloader = downloadPreferences.useExternalDownloader().get()
|
||||||
|
|
||||||
val relativeTime by uiPreferences.relativeTime().asState(coroutineScope)
|
|
||||||
val dateFormat by mutableStateOf(UiPreferences.dateFormat(uiPreferences.dateFormat().get()))
|
val dateFormat by mutableStateOf(UiPreferences.dateFormat(uiPreferences.dateFormat().get()))
|
||||||
|
|
||||||
|
val isIntervalEnabled = LibraryPreferences.ENTRY_OUTSIDE_RELEASE_PERIOD in libraryPreferences.libraryUpdateItemRestriction().get()
|
||||||
|
private val leadDay = libraryPreferences.leadingAnimeExpectedDays().get()
|
||||||
|
private val followDay = libraryPreferences.followingAnimeExpectedDays().get()
|
||||||
|
|
||||||
private val selectedPositions: Array<Int> = arrayOf(-1, -1) // first and last selected index in list
|
private val selectedPositions: Array<Int> = arrayOf(-1, -1) // first and last selected index in list
|
||||||
private val selectedEpisodeIds: HashSet<Long> = HashSet()
|
private val selectedEpisodeIds: HashSet<Long> = HashSet()
|
||||||
|
|
||||||
|
@ -320,7 +325,7 @@ class AnimeScreenModel(
|
||||||
// Choose a category
|
// Choose a category
|
||||||
else -> {
|
else -> {
|
||||||
isFromChangeCategory = true
|
isFromChangeCategory = true
|
||||||
promptChangeCategories()
|
showChangeCategoryDialog()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -350,7 +355,7 @@ class AnimeScreenModel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun promptChangeCategories() {
|
fun showChangeCategoryDialog() {
|
||||||
val anime = successState?.anime ?: return
|
val anime = successState?.anime ?: return
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
val categories = getCategories()
|
val categories = getCategories()
|
||||||
|
@ -366,6 +371,37 @@ class AnimeScreenModel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showSetAnimeIntervalDialog() {
|
||||||
|
val anime = successState?.anime ?: return
|
||||||
|
updateSuccessState {
|
||||||
|
it.copy(dialog = Dialog.SetAnimeInterval(anime))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: this should be in the state/composables
|
||||||
|
fun intervalDisplay(): Pair<Int, Int>? {
|
||||||
|
val anime = successState?.anime ?: return null
|
||||||
|
val effInterval = anime.calculateInterval
|
||||||
|
return 1.coerceAtLeast(effInterval.absoluteValue - leadDay) to (effInterval.absoluteValue + followDay)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setFetchRangeInterval(anime: Anime, newInterval: Int) {
|
||||||
|
val interval = when (newInterval) {
|
||||||
|
// reset interval 0 default to trigger recalculation
|
||||||
|
// only reset if interval is custom, which is negative
|
||||||
|
0 -> if (anime.calculateInterval < 0) 0 else anime.calculateInterval
|
||||||
|
else -> -newInterval
|
||||||
|
}
|
||||||
|
coroutineScope.launchIO {
|
||||||
|
updateAnime.awaitUpdateFetchInterval(
|
||||||
|
anime.copy(calculateInterval = interval),
|
||||||
|
successState?.episodes?.map { it.episode }.orEmpty(),
|
||||||
|
)
|
||||||
|
val newAnime = animeRepository.getAnimeById(animeId)
|
||||||
|
updateSuccessState { it.copy(anime = newAnime) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the anime has any downloads.
|
* Returns true if the anime has any downloads.
|
||||||
*/
|
*/
|
||||||
|
@ -519,6 +555,7 @@ class AnimeScreenModel(
|
||||||
episodes,
|
episodes,
|
||||||
state.anime,
|
state.anime,
|
||||||
state.source,
|
state.source,
|
||||||
|
manualFetch,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (manualFetch) {
|
if (manualFetch) {
|
||||||
|
@ -536,6 +573,8 @@ class AnimeScreenModel(
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
snackbarHostState.showSnackbar(message = message)
|
snackbarHostState.showSnackbar(message = message)
|
||||||
}
|
}
|
||||||
|
val newAnime = animeRepository.getAnimeById(animeId)
|
||||||
|
updateSuccessState { it.copy(anime = newAnime, isRefreshingData = false) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -972,6 +1011,7 @@ class AnimeScreenModel(
|
||||||
data class ChangeCategory(val anime: Anime, val initialSelection: List<CheckboxState<Category>>) : Dialog
|
data class ChangeCategory(val anime: Anime, val initialSelection: List<CheckboxState<Category>>) : Dialog
|
||||||
data class DeleteEpisodes(val episodes: List<Episode>) : Dialog
|
data class DeleteEpisodes(val episodes: List<Episode>) : Dialog
|
||||||
data class DuplicateAnime(val anime: Anime, val duplicate: Anime) : Dialog
|
data class DuplicateAnime(val anime: Anime, val duplicate: Anime) : Dialog
|
||||||
|
data class SetAnimeInterval(val anime: Anime) : Dialog
|
||||||
data class ShowQualities(val episode: Episode, val anime: Anime, val source: AnimeSource) : Dialog
|
data class ShowQualities(val episode: Episode, val anime: Anime, val source: AnimeSource) : Dialog
|
||||||
data object ChangeAnimeSkipIntro : Dialog
|
data object ChangeAnimeSkipIntro : Dialog
|
||||||
data object SettingsSheet : Dialog
|
data object SettingsSheet : Dialog
|
||||||
|
@ -1009,7 +1049,7 @@ class AnimeScreenModel(
|
||||||
|
|
||||||
sealed interface State {
|
sealed interface State {
|
||||||
@Immutable
|
@Immutable
|
||||||
object Loading : State
|
data object Loading : State
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class Success(
|
data class Success(
|
||||||
|
|
|
@ -26,6 +26,7 @@ import eu.kanade.presentation.category.ChangeCategoryDialog
|
||||||
import eu.kanade.presentation.components.NavigatorAdaptiveSheet
|
import eu.kanade.presentation.components.NavigatorAdaptiveSheet
|
||||||
import eu.kanade.presentation.entries.DeleteItemsDialog
|
import eu.kanade.presentation.entries.DeleteItemsDialog
|
||||||
import eu.kanade.presentation.entries.EditCoverAction
|
import eu.kanade.presentation.entries.EditCoverAction
|
||||||
|
import eu.kanade.presentation.entries.SetIntervalDialog
|
||||||
import eu.kanade.presentation.entries.manga.ChapterSettingsDialog
|
import eu.kanade.presentation.entries.manga.ChapterSettingsDialog
|
||||||
import eu.kanade.presentation.entries.manga.DuplicateMangaDialog
|
import eu.kanade.presentation.entries.manga.DuplicateMangaDialog
|
||||||
import eu.kanade.presentation.entries.manga.MangaScreen
|
import eu.kanade.presentation.entries.manga.MangaScreen
|
||||||
|
@ -99,8 +100,8 @@ class MangaScreen(
|
||||||
MangaScreen(
|
MangaScreen(
|
||||||
state = successState,
|
state = successState,
|
||||||
snackbarHostState = screenModel.snackbarHostState,
|
snackbarHostState = screenModel.snackbarHostState,
|
||||||
dateRelativeTime = screenModel.relativeTime,
|
|
||||||
dateFormat = screenModel.dateFormat,
|
dateFormat = screenModel.dateFormat,
|
||||||
|
intervalDisplay = screenModel::intervalDisplay,
|
||||||
isTabletUi = isTabletUi(),
|
isTabletUi = isTabletUi(),
|
||||||
chapterSwipeStartAction = screenModel.chapterSwipeStartAction,
|
chapterSwipeStartAction = screenModel.chapterSwipeStartAction,
|
||||||
chapterSwipeEndAction = screenModel.chapterSwipeEndAction,
|
chapterSwipeEndAction = screenModel.chapterSwipeEndAction,
|
||||||
|
@ -122,7 +123,8 @@ class MangaScreen(
|
||||||
onCoverClicked = screenModel::showCoverDialog,
|
onCoverClicked = screenModel::showCoverDialog,
|
||||||
onShareClicked = { shareManga(context, screenModel.manga, screenModel.source) }.takeIf { isHttpSource },
|
onShareClicked = { shareManga(context, screenModel.manga, screenModel.source) }.takeIf { isHttpSource },
|
||||||
onDownloadActionClicked = screenModel::runDownloadAction.takeIf { !successState.source.isLocalOrStub() },
|
onDownloadActionClicked = screenModel::runDownloadAction.takeIf { !successState.source.isLocalOrStub() },
|
||||||
onEditCategoryClicked = screenModel::promptChangeCategories.takeIf { successState.manga.favorite },
|
onEditCategoryClicked = screenModel::showChangeCategoryDialog.takeIf { successState.manga.favorite },
|
||||||
|
onEditIntervalClicked = screenModel::showSetMangaIntervalDialog.takeIf { screenModel.isIntervalEnabled && successState.manga.favorite },
|
||||||
onMigrateClicked = { navigator.push(MigrateSearchScreen(successState.manga.id)) }.takeIf { successState.manga.favorite },
|
onMigrateClicked = { navigator.push(MigrateSearchScreen(successState.manga.id)) }.takeIf { successState.manga.favorite },
|
||||||
onMultiBookmarkClicked = screenModel::bookmarkChapters,
|
onMultiBookmarkClicked = screenModel::bookmarkChapters,
|
||||||
onMultiMarkAsReadClicked = screenModel::markChaptersRead,
|
onMultiMarkAsReadClicked = screenModel::markChaptersRead,
|
||||||
|
@ -215,6 +217,13 @@ class MangaScreen(
|
||||||
LoadingScreen(Modifier.systemBarsPadding())
|
LoadingScreen(Modifier.systemBarsPadding())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is MangaScreenModel.Dialog.SetMangaInterval -> {
|
||||||
|
SetIntervalDialog(
|
||||||
|
interval = if (dialog.manga.calculateInterval < 0) -dialog.manga.calculateInterval else 0,
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
onValueChanged = { screenModel.setFetchRangeInterval(dialog.manga, it) },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -63,6 +63,7 @@ import tachiyomi.domain.entries.manga.interactor.GetDuplicateLibraryManga
|
||||||
import tachiyomi.domain.entries.manga.interactor.GetMangaWithChapters
|
import tachiyomi.domain.entries.manga.interactor.GetMangaWithChapters
|
||||||
import tachiyomi.domain.entries.manga.interactor.SetMangaChapterFlags
|
import tachiyomi.domain.entries.manga.interactor.SetMangaChapterFlags
|
||||||
import tachiyomi.domain.entries.manga.model.Manga
|
import tachiyomi.domain.entries.manga.model.Manga
|
||||||
|
import tachiyomi.domain.entries.manga.repository.MangaRepository
|
||||||
import tachiyomi.domain.items.chapter.interactor.SetMangaDefaultChapterFlags
|
import tachiyomi.domain.items.chapter.interactor.SetMangaDefaultChapterFlags
|
||||||
import tachiyomi.domain.items.chapter.interactor.UpdateChapter
|
import tachiyomi.domain.items.chapter.interactor.UpdateChapter
|
||||||
import tachiyomi.domain.items.chapter.model.Chapter
|
import tachiyomi.domain.items.chapter.model.Chapter
|
||||||
|
@ -75,6 +76,7 @@ import tachiyomi.domain.track.manga.interactor.GetMangaTracks
|
||||||
import tachiyomi.source.local.entries.manga.isLocal
|
import tachiyomi.source.local.entries.manga.isLocal
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
class MangaScreenModel(
|
class MangaScreenModel(
|
||||||
val context: Context,
|
val context: Context,
|
||||||
|
@ -99,6 +101,7 @@ class MangaScreenModel(
|
||||||
private val getCategories: GetMangaCategories = Injekt.get(),
|
private val getCategories: GetMangaCategories = Injekt.get(),
|
||||||
private val getTracks: GetMangaTracks = Injekt.get(),
|
private val getTracks: GetMangaTracks = Injekt.get(),
|
||||||
private val setMangaCategories: SetMangaCategories = Injekt.get(),
|
private val setMangaCategories: SetMangaCategories = Injekt.get(),
|
||||||
|
private val mangaRepository: MangaRepository = Injekt.get(),
|
||||||
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
|
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
|
||||||
) : StateScreenModel<MangaScreenModel.State>(State.Loading) {
|
) : StateScreenModel<MangaScreenModel.State>(State.Loading) {
|
||||||
|
|
||||||
|
@ -125,10 +128,13 @@ class MangaScreenModel(
|
||||||
val chapterSwipeStartAction = libraryPreferences.swipeChapterEndAction().get()
|
val chapterSwipeStartAction = libraryPreferences.swipeChapterEndAction().get()
|
||||||
val chapterSwipeEndAction = libraryPreferences.swipeChapterStartAction().get()
|
val chapterSwipeEndAction = libraryPreferences.swipeChapterStartAction().get()
|
||||||
|
|
||||||
val relativeTime by uiPreferences.relativeTime().asState(coroutineScope)
|
|
||||||
val dateFormat by mutableStateOf(UiPreferences.dateFormat(uiPreferences.dateFormat().get()))
|
val dateFormat by mutableStateOf(UiPreferences.dateFormat(uiPreferences.dateFormat().get()))
|
||||||
val skipFiltered by readerPreferences.skipFiltered().asState(coroutineScope)
|
val skipFiltered by readerPreferences.skipFiltered().asState(coroutineScope)
|
||||||
|
|
||||||
|
val isIntervalEnabled = LibraryPreferences.ENTRY_OUTSIDE_RELEASE_PERIOD in libraryPreferences.libraryUpdateItemRestriction().get()
|
||||||
|
private val leadDay = libraryPreferences.leadingMangaExpectedDays().get()
|
||||||
|
private val followDay = libraryPreferences.followingMangaExpectedDays().get()
|
||||||
|
|
||||||
private val selectedPositions: Array<Int> = arrayOf(-1, -1) // first and last selected index in list
|
private val selectedPositions: Array<Int> = arrayOf(-1, -1) // first and last selected index in list
|
||||||
private val selectedChapterIds: HashSet<Long> = HashSet()
|
private val selectedChapterIds: HashSet<Long> = HashSet()
|
||||||
|
|
||||||
|
@ -316,7 +322,7 @@ class MangaScreenModel(
|
||||||
// Choose a category
|
// Choose a category
|
||||||
else -> {
|
else -> {
|
||||||
isFromChangeCategory = true
|
isFromChangeCategory = true
|
||||||
promptChangeCategories()
|
showChangeCategoryDialog()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -346,7 +352,7 @@ class MangaScreenModel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun promptChangeCategories() {
|
fun showChangeCategoryDialog() {
|
||||||
val manga = successState?.manga ?: return
|
val manga = successState?.manga ?: return
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
val categories = getCategories()
|
val categories = getCategories()
|
||||||
|
@ -362,6 +368,37 @@ class MangaScreenModel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showSetMangaIntervalDialog() {
|
||||||
|
val manga = successState?.manga ?: return
|
||||||
|
updateSuccessState {
|
||||||
|
it.copy(dialog = Dialog.SetMangaInterval(manga))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: this should be in the state/composables
|
||||||
|
fun intervalDisplay(): Pair<Int, Int>? {
|
||||||
|
val manga = successState?.manga ?: return null
|
||||||
|
val effInterval = manga.calculateInterval
|
||||||
|
return 1.coerceAtLeast(effInterval.absoluteValue - leadDay) to (effInterval.absoluteValue + followDay)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setFetchRangeInterval(manga: Manga, newInterval: Int) {
|
||||||
|
val interval = when (newInterval) {
|
||||||
|
// reset interval 0 default to trigger recalculation
|
||||||
|
// only reset if interval is custom, which is negative
|
||||||
|
0 -> if (manga.calculateInterval < 0) 0 else manga.calculateInterval
|
||||||
|
else -> -newInterval
|
||||||
|
}
|
||||||
|
coroutineScope.launchIO {
|
||||||
|
updateManga.awaitUpdateFetchInterval(
|
||||||
|
manga.copy(calculateInterval = interval),
|
||||||
|
successState?.chapters?.map { it.chapter }.orEmpty(),
|
||||||
|
)
|
||||||
|
val newManga = mangaRepository.getMangaById(mangaId)
|
||||||
|
updateSuccessState { it.copy(manga = newManga) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the manga has any downloads.
|
* Returns true if the manga has any downloads.
|
||||||
*/
|
*/
|
||||||
|
@ -515,6 +552,7 @@ class MangaScreenModel(
|
||||||
chapters,
|
chapters,
|
||||||
state.manga,
|
state.manga,
|
||||||
state.source,
|
state.source,
|
||||||
|
manualFetch,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (manualFetch) {
|
if (manualFetch) {
|
||||||
|
@ -532,6 +570,8 @@ class MangaScreenModel(
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
snackbarHostState.showSnackbar(message = message)
|
snackbarHostState.showSnackbar(message = message)
|
||||||
}
|
}
|
||||||
|
val newManga = mangaRepository.getMangaById(mangaId)
|
||||||
|
updateSuccessState { it.copy(manga = newManga, isRefreshingData = false) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -956,6 +996,7 @@ class MangaScreenModel(
|
||||||
data class ChangeCategory(val manga: Manga, val initialSelection: List<CheckboxState<Category>>) : Dialog
|
data class ChangeCategory(val manga: Manga, val initialSelection: List<CheckboxState<Category>>) : Dialog
|
||||||
data class DeleteChapters(val chapters: List<Chapter>) : Dialog
|
data class DeleteChapters(val chapters: List<Chapter>) : Dialog
|
||||||
data class DuplicateManga(val manga: Manga, val duplicate: Manga) : Dialog
|
data class DuplicateManga(val manga: Manga, val duplicate: Manga) : Dialog
|
||||||
|
data class SetMangaInterval(val manga: Manga) : Dialog
|
||||||
data object SettingsSheet : Dialog
|
data object SettingsSheet : Dialog
|
||||||
data object TrackSheet : Dialog
|
data object TrackSheet : Dialog
|
||||||
data object FullCover : Dialog
|
data object FullCover : Dialog
|
||||||
|
@ -983,7 +1024,7 @@ class MangaScreenModel(
|
||||||
|
|
||||||
sealed interface State {
|
sealed interface State {
|
||||||
@Immutable
|
@Immutable
|
||||||
object Loading : State
|
data object Loading : State
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class Success(
|
data class Success(
|
||||||
|
|
|
@ -92,11 +92,7 @@ fun EpisodeListDialog(
|
||||||
val date = episode.date_upload
|
val date = episode.date_upload
|
||||||
.takeIf { it > 0L }
|
.takeIf { it > 0L }
|
||||||
?.let {
|
?.let {
|
||||||
Date(it).toRelativeString(
|
Date(it).toRelativeString(context, dateFormat)
|
||||||
context,
|
|
||||||
relativeTime,
|
|
||||||
dateFormat,
|
|
||||||
)
|
|
||||||
} ?: ""
|
} ?: ""
|
||||||
|
|
||||||
EpisodeListItem(
|
EpisodeListItem(
|
||||||
|
|
|
@ -61,14 +61,12 @@ class AnimeUpdatesScreenModel(
|
||||||
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||||
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
|
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
|
||||||
downloadPreferences: DownloadPreferences = Injekt.get(),
|
downloadPreferences: DownloadPreferences = Injekt.get(),
|
||||||
uiPreferences: UiPreferences = Injekt.get(),
|
|
||||||
) : StateScreenModel<AnimeUpdatesScreenModel.State>(State()) {
|
) : StateScreenModel<AnimeUpdatesScreenModel.State>(State()) {
|
||||||
|
|
||||||
private val _events: Channel<Event> = Channel(Int.MAX_VALUE)
|
private val _events: Channel<Event> = Channel(Int.MAX_VALUE)
|
||||||
val events: Flow<Event> = _events.receiveAsFlow()
|
val events: Flow<Event> = _events.receiveAsFlow()
|
||||||
|
|
||||||
val lastUpdated by libraryPreferences.libraryUpdateLastTimestamp().asState(coroutineScope)
|
val lastUpdated by libraryPreferences.libraryUpdateLastTimestamp().asState(coroutineScope)
|
||||||
val relativeTime by uiPreferences.relativeTime().asState(coroutineScope)
|
|
||||||
|
|
||||||
val useExternalDownloader = downloadPreferences.useExternalDownloader().get()
|
val useExternalDownloader = downloadPreferences.useExternalDownloader().get()
|
||||||
|
|
||||||
|
@ -382,12 +380,12 @@ class AnimeUpdatesScreenModel(
|
||||||
data class State(
|
data class State(
|
||||||
val isLoading: Boolean = true,
|
val isLoading: Boolean = true,
|
||||||
val items: List<AnimeUpdatesItem> = emptyList(),
|
val items: List<AnimeUpdatesItem> = emptyList(),
|
||||||
val dialog: AnimeUpdatesScreenModel.Dialog? = null,
|
val dialog: Dialog? = null,
|
||||||
) {
|
) {
|
||||||
val selected = items.filter { it.selected }
|
val selected = items.filter { it.selected }
|
||||||
val selectionMode = selected.isNotEmpty()
|
val selectionMode = selected.isNotEmpty()
|
||||||
|
|
||||||
fun getUiModel(context: Context, relativeTime: Int): List<AnimeUpdatesUiModel> {
|
fun getUiModel(context: Context): List<AnimeUpdatesUiModel> {
|
||||||
val dateFormat by mutableStateOf(UiPreferences.dateFormat(Injekt.get<UiPreferences>().dateFormat().get()))
|
val dateFormat by mutableStateOf(UiPreferences.dateFormat(Injekt.get<UiPreferences>().dateFormat().get()))
|
||||||
|
|
||||||
return items
|
return items
|
||||||
|
@ -397,11 +395,7 @@ class AnimeUpdatesScreenModel(
|
||||||
val afterDate = after?.item?.update?.dateFetch?.toDateKey() ?: Date(0)
|
val afterDate = after?.item?.update?.dateFetch?.toDateKey() ?: Date(0)
|
||||||
when {
|
when {
|
||||||
beforeDate.time != afterDate.time && afterDate.time != 0L -> {
|
beforeDate.time != afterDate.time && afterDate.time != 0L -> {
|
||||||
val text = afterDate.toRelativeString(
|
val text = afterDate.toRelativeString(context, dateFormat)
|
||||||
context = context,
|
|
||||||
range = relativeTime,
|
|
||||||
dateFormat = dateFormat,
|
|
||||||
)
|
|
||||||
AnimeUpdatesUiModel.Header(text)
|
AnimeUpdatesUiModel.Header(text)
|
||||||
}
|
}
|
||||||
// Return null to avoid adding a separator between two items.
|
// Return null to avoid adding a separator between two items.
|
||||||
|
|
|
@ -59,7 +59,6 @@ fun Screen.animeUpdatesTab(
|
||||||
snackbarHostState = screenModel.snackbarHostState,
|
snackbarHostState = screenModel.snackbarHostState,
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
lastUpdated = screenModel.lastUpdated,
|
lastUpdated = screenModel.lastUpdated,
|
||||||
relativeTime = screenModel.relativeTime,
|
|
||||||
onClickCover = { item -> navigator.push(AnimeScreen(item.update.animeId)) },
|
onClickCover = { item -> navigator.push(AnimeScreen(item.update.animeId)) },
|
||||||
onSelectAll = screenModel::toggleAllSelection,
|
onSelectAll = screenModel::toggleAllSelection,
|
||||||
onInvertSelection = screenModel::invertSelection,
|
onInvertSelection = screenModel::invertSelection,
|
||||||
|
|
|
@ -59,14 +59,12 @@ class MangaUpdatesScreenModel(
|
||||||
private val getChapter: GetChapter = Injekt.get(),
|
private val getChapter: GetChapter = Injekt.get(),
|
||||||
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||||
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
|
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
|
||||||
uiPreferences: UiPreferences = Injekt.get(),
|
|
||||||
) : StateScreenModel<MangaUpdatesScreenModel.State>(State()) {
|
) : StateScreenModel<MangaUpdatesScreenModel.State>(State()) {
|
||||||
|
|
||||||
private val _events: Channel<Event> = Channel(Int.MAX_VALUE)
|
private val _events: Channel<Event> = Channel(Int.MAX_VALUE)
|
||||||
val events: Flow<Event> = _events.receiveAsFlow()
|
val events: Flow<Event> = _events.receiveAsFlow()
|
||||||
|
|
||||||
val lastUpdated by libraryPreferences.libraryUpdateLastTimestamp().asState(coroutineScope)
|
val lastUpdated by libraryPreferences.libraryUpdateLastTimestamp().asState(coroutineScope)
|
||||||
val relativeTime by uiPreferences.relativeTime().asState(coroutineScope)
|
|
||||||
|
|
||||||
// First and last selected index in list
|
// First and last selected index in list
|
||||||
private val selectedPositions: Array<Int> = arrayOf(-1, -1)
|
private val selectedPositions: Array<Int> = arrayOf(-1, -1)
|
||||||
|
@ -370,12 +368,12 @@ class MangaUpdatesScreenModel(
|
||||||
data class State(
|
data class State(
|
||||||
val isLoading: Boolean = true,
|
val isLoading: Boolean = true,
|
||||||
val items: List<MangaUpdatesItem> = emptyList(),
|
val items: List<MangaUpdatesItem> = emptyList(),
|
||||||
val dialog: MangaUpdatesScreenModel.Dialog? = null,
|
val dialog: Dialog? = null,
|
||||||
) {
|
) {
|
||||||
val selected = items.filter { it.selected }
|
val selected = items.filter { it.selected }
|
||||||
val selectionMode = selected.isNotEmpty()
|
val selectionMode = selected.isNotEmpty()
|
||||||
|
|
||||||
fun getUiModel(context: Context, relativeTime: Int): List<MangaUpdatesUiModel> {
|
fun getUiModel(context: Context): List<MangaUpdatesUiModel> {
|
||||||
val dateFormat by mutableStateOf(UiPreferences.dateFormat(Injekt.get<UiPreferences>().dateFormat().get()))
|
val dateFormat by mutableStateOf(UiPreferences.dateFormat(Injekt.get<UiPreferences>().dateFormat().get()))
|
||||||
|
|
||||||
return items
|
return items
|
||||||
|
@ -385,11 +383,7 @@ class MangaUpdatesScreenModel(
|
||||||
val afterDate = after?.item?.update?.dateFetch?.toDateKey() ?: Date(0)
|
val afterDate = after?.item?.update?.dateFetch?.toDateKey() ?: Date(0)
|
||||||
when {
|
when {
|
||||||
beforeDate.time != afterDate.time && afterDate.time != 0L -> {
|
beforeDate.time != afterDate.time && afterDate.time != 0L -> {
|
||||||
val text = afterDate.toRelativeString(
|
val text = afterDate.toRelativeString(context, dateFormat)
|
||||||
context = context,
|
|
||||||
range = relativeTime,
|
|
||||||
dateFormat = dateFormat,
|
|
||||||
)
|
|
||||||
MangaUpdatesUiModel.Header(text)
|
MangaUpdatesUiModel.Header(text)
|
||||||
}
|
}
|
||||||
// Return null to avoid adding a separator between two items.
|
// Return null to avoid adding a separator between two items.
|
||||||
|
|
|
@ -46,7 +46,6 @@ fun Screen.mangaUpdatesTab(
|
||||||
snackbarHostState = screenModel.snackbarHostState,
|
snackbarHostState = screenModel.snackbarHostState,
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
lastUpdated = screenModel.lastUpdated,
|
lastUpdated = screenModel.lastUpdated,
|
||||||
relativeTime = screenModel.relativeTime,
|
|
||||||
onClickCover = { item -> navigator.push(MangaScreen(item.update.mangaId)) },
|
onClickCover = { item -> navigator.push(MangaScreen(item.update.mangaId)) },
|
||||||
onSelectAll = screenModel::toggleAllSelection,
|
onSelectAll = screenModel::toggleAllSelection,
|
||||||
onInvertSelection = screenModel::invertSelection,
|
onInvertSelection = screenModel::invertSelection,
|
||||||
|
|
|
@ -114,19 +114,15 @@ private const val MILLISECONDS_IN_DAY = 86_400_000L
|
||||||
|
|
||||||
fun Date.toRelativeString(
|
fun Date.toRelativeString(
|
||||||
context: Context,
|
context: Context,
|
||||||
range: Int = 7,
|
|
||||||
dateFormat: DateFormat = DateFormat.getDateInstance(DateFormat.SHORT),
|
dateFormat: DateFormat = DateFormat.getDateInstance(DateFormat.SHORT),
|
||||||
): String {
|
): String {
|
||||||
if (range == 0) {
|
|
||||||
return dateFormat.format(this)
|
|
||||||
}
|
|
||||||
val now = Date()
|
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()
|
val days = difference.floorDiv(MILLISECONDS_IN_DAY).toInt()
|
||||||
return when {
|
return when {
|
||||||
difference < 0 -> context.getString(R.string.recently)
|
difference < 0 -> context.getString(R.string.recently)
|
||||||
difference < MILLISECONDS_IN_DAY -> context.getString(R.string.relative_time_today)
|
difference < MILLISECONDS_IN_DAY -> context.getString(R.string.relative_time_today)
|
||||||
difference < MILLISECONDS_IN_DAY.times(range) -> context.resources.getQuantityString(
|
difference < MILLISECONDS_IN_DAY.times(7) -> context.resources.getQuantityString(
|
||||||
R.plurals.relative_time,
|
R.plurals.relative_time,
|
||||||
days,
|
days,
|
||||||
days,
|
days,
|
||||||
|
|
|
@ -13,111 +13,115 @@ import kotlin.math.absoluteValue
|
||||||
|
|
||||||
const val MAX_GRACE_PERIOD = 28
|
const val MAX_GRACE_PERIOD = 28
|
||||||
|
|
||||||
fun updateIntervalMeta(
|
class SetAnimeUpdateInterval(
|
||||||
anime: Anime,
|
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||||
episodes: List<Episode>,
|
) {
|
||||||
zonedDateTime: ZonedDateTime = ZonedDateTime.now(),
|
|
||||||
setCurrentFetchRange: Pair<Long, Long> = getCurrentFetchRange(zonedDateTime),
|
|
||||||
): AnimeUpdate? {
|
|
||||||
val currentFetchRange = if (setCurrentFetchRange.first == 0L && setCurrentFetchRange.second == 0L) {
|
|
||||||
getCurrentFetchRange(ZonedDateTime.now())
|
|
||||||
} else {
|
|
||||||
setCurrentFetchRange
|
|
||||||
}
|
|
||||||
val interval = anime.calculateInterval.takeIf { it < 0 } ?: calculateInterval(episodes, zonedDateTime)
|
|
||||||
val nextUpdate = calculateNextUpdate(anime, interval, zonedDateTime, currentFetchRange)
|
|
||||||
|
|
||||||
return if (anime.nextUpdate == nextUpdate && anime.calculateInterval == interval) {
|
fun updateInterval(
|
||||||
null
|
anime: Anime,
|
||||||
} else { AnimeUpdate(id = anime.id, nextUpdate = nextUpdate, calculateInterval = interval) }
|
episodes: List<Episode>,
|
||||||
}
|
zonedDateTime: ZonedDateTime,
|
||||||
|
fetchRange: Pair<Long, Long>,
|
||||||
fun calculateInterval(episodes: List<Episode>, zonedDateTime: ZonedDateTime): Int {
|
): AnimeUpdate? {
|
||||||
val sortedEpisodes = episodes
|
val currentFetchRange = if (fetchRange.first == 0L && fetchRange.second == 0L) {
|
||||||
.sortedWith(compareByDescending<Episode> { it.dateUpload }.thenByDescending { it.dateFetch })
|
getCurrentFetchRange(ZonedDateTime.now())
|
||||||
.take(50)
|
} else {
|
||||||
|
fetchRange
|
||||||
val uploadDates = sortedEpisodes
|
|
||||||
.filter { it.dateUpload > 0L }
|
|
||||||
.map {
|
|
||||||
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateUpload), zonedDateTime.zone)
|
|
||||||
.toLocalDate()
|
|
||||||
.atStartOfDay()
|
|
||||||
}
|
}
|
||||||
.distinct()
|
val interval = anime.calculateInterval.takeIf { it < 0 } ?: calculateInterval(episodes, zonedDateTime)
|
||||||
val fetchDates = sortedEpisodes
|
val nextUpdate = calculateNextUpdate(anime, interval, zonedDateTime, currentFetchRange)
|
||||||
.map {
|
|
||||||
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateFetch), zonedDateTime.zone)
|
return if (anime.nextUpdate == nextUpdate && anime.calculateInterval == interval) {
|
||||||
.toLocalDate()
|
null
|
||||||
.atStartOfDay()
|
} else {
|
||||||
|
AnimeUpdate(id = anime.id, nextUpdate = nextUpdate, calculateInterval = interval)
|
||||||
}
|
}
|
||||||
.distinct()
|
}
|
||||||
|
|
||||||
val newInterval = when {
|
fun getCurrentFetchRange(timeToCal: ZonedDateTime): Pair<Long, Long> {
|
||||||
// Enough upload date from source
|
// lead range and the following range depend on if updateOnlyExpectedPeriod set.
|
||||||
uploadDates.size >= 3 -> {
|
var followRange = 0
|
||||||
val uploadDelta = uploadDates.last().until(uploadDates.first(), ChronoUnit.DAYS)
|
var leadRange = 0
|
||||||
val uploadPeriod = uploadDates.indexOf(uploadDates.last())
|
if (LibraryPreferences.ENTRY_OUTSIDE_RELEASE_PERIOD in libraryPreferences.libraryUpdateItemRestriction().get()) {
|
||||||
(uploadDelta).floorDiv(uploadPeriod).toInt()
|
followRange = libraryPreferences.followingAnimeExpectedDays().get()
|
||||||
|
leadRange = libraryPreferences.leadingAnimeExpectedDays().get()
|
||||||
}
|
}
|
||||||
// Enough fetch date from client
|
val startToday = timeToCal.toLocalDate().atStartOfDay(timeToCal.zone)
|
||||||
fetchDates.size >= 3 -> {
|
// revert math of (next_update + follow < now) become (next_update < now - follow)
|
||||||
val fetchDelta = fetchDates.last().until(fetchDates.first(), ChronoUnit.DAYS)
|
// so (now - follow) become lower limit
|
||||||
val uploadPeriod = fetchDates.indexOf(fetchDates.last())
|
val lowerRange = startToday.minusDays(followRange.toLong())
|
||||||
(fetchDelta).floorDiv(uploadPeriod).toInt()
|
val higherRange = startToday.plusDays(leadRange.toLong())
|
||||||
|
return Pair(lowerRange.toEpochSecond() * 1000, higherRange.toEpochSecond() * 1000 - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun calculateInterval(episodes: List<Episode>, zonedDateTime: ZonedDateTime): Int {
|
||||||
|
val sortedEpisodes = episodes
|
||||||
|
.sortedWith(compareByDescending<Episode> { it.dateUpload }.thenByDescending { it.dateFetch })
|
||||||
|
.take(50)
|
||||||
|
|
||||||
|
val uploadDates = sortedEpisodes
|
||||||
|
.filter { it.dateUpload > 0L }
|
||||||
|
.map {
|
||||||
|
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateUpload), zonedDateTime.zone)
|
||||||
|
.toLocalDate()
|
||||||
|
.atStartOfDay()
|
||||||
|
}
|
||||||
|
.distinct()
|
||||||
|
val fetchDates = sortedEpisodes
|
||||||
|
.map {
|
||||||
|
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateFetch), zonedDateTime.zone)
|
||||||
|
.toLocalDate()
|
||||||
|
.atStartOfDay()
|
||||||
|
}
|
||||||
|
.distinct()
|
||||||
|
|
||||||
|
val interval = when {
|
||||||
|
// Enough upload date from source
|
||||||
|
uploadDates.size >= 3 -> {
|
||||||
|
val uploadDelta = uploadDates.last().until(uploadDates.first(), ChronoUnit.DAYS)
|
||||||
|
val uploadPeriod = uploadDates.indexOf(uploadDates.last())
|
||||||
|
uploadDelta.floorDiv(uploadPeriod).toInt()
|
||||||
|
}
|
||||||
|
// Enough fetch date from client
|
||||||
|
fetchDates.size >= 3 -> {
|
||||||
|
val fetchDelta = fetchDates.last().until(fetchDates.first(), ChronoUnit.DAYS)
|
||||||
|
val uploadPeriod = fetchDates.indexOf(fetchDates.last())
|
||||||
|
fetchDelta.floorDiv(uploadPeriod).toInt()
|
||||||
|
}
|
||||||
|
// Default to 7 days
|
||||||
|
else -> 7
|
||||||
}
|
}
|
||||||
// Default to 7 days
|
// Min 1, max 28 days
|
||||||
else -> 7
|
return interval.coerceIn(1, MAX_GRACE_PERIOD)
|
||||||
}
|
}
|
||||||
// min 1, max 28 days
|
|
||||||
return newInterval.coerceIn(1, MAX_GRACE_PERIOD)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun calculateNextUpdate(
|
private fun calculateNextUpdate(
|
||||||
anime: Anime,
|
anime: Anime,
|
||||||
interval: Int,
|
interval: Int,
|
||||||
zonedDateTime: ZonedDateTime,
|
zonedDateTime: ZonedDateTime,
|
||||||
currentFetchRange: Pair<Long, Long>,
|
fetchRange: Pair<Long, Long>,
|
||||||
): Long {
|
): Long {
|
||||||
return if (anime.nextUpdate !in currentFetchRange.first.rangeTo(currentFetchRange.second + 1) ||
|
return if (
|
||||||
anime.calculateInterval == 0
|
anime.nextUpdate !in fetchRange.first.rangeTo(fetchRange.second + 1) ||
|
||||||
) {
|
anime.calculateInterval == 0
|
||||||
val latestDate = ZonedDateTime.ofInstant(Instant.ofEpochMilli(anime.lastUpdate), zonedDateTime.zone).toLocalDate().atStartOfDay()
|
) {
|
||||||
val timeSinceLatest = ChronoUnit.DAYS.between(latestDate, zonedDateTime).toInt()
|
val latestDate = ZonedDateTime.ofInstant(Instant.ofEpochMilli(anime.lastUpdate), zonedDateTime.zone).toLocalDate().atStartOfDay()
|
||||||
val cycle = timeSinceLatest.floorDiv(interval.absoluteValue.takeIf { interval < 0 } ?: doubleInterval(interval, timeSinceLatest, doubleWhenOver = 10, maxValue = 28))
|
val timeSinceLatest = ChronoUnit.DAYS.between(latestDate, zonedDateTime).toInt()
|
||||||
latestDate.plusDays((cycle + 1) * interval.toLong()).toEpochSecond(zonedDateTime.offset) * 1000
|
val cycle = timeSinceLatest.floorDiv(interval.absoluteValue.takeIf { interval < 0 } ?: doubleInterval(interval, timeSinceLatest, doubleWhenOver = 10, maxValue = 28))
|
||||||
} else {
|
latestDate.plusDays((cycle + 1) * interval.toLong()).toEpochSecond(zonedDateTime.offset) * 1000
|
||||||
anime.nextUpdate
|
} else {
|
||||||
|
anime.nextUpdate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun doubleInterval(delta: Int, timeSinceLatest: Int, doubleWhenOver: Int, maxValue: Int): Int {
|
||||||
|
if (delta >= maxValue) return maxValue
|
||||||
|
val cycle = timeSinceLatest.floorDiv(delta) + 1
|
||||||
|
// double delta again if missed more than 9 check in new delta
|
||||||
|
return if (cycle > doubleWhenOver) {
|
||||||
|
doubleInterval(delta * 2, timeSinceLatest, doubleWhenOver, maxValue)
|
||||||
|
} else {
|
||||||
|
delta
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun doubleInterval(delta: Int, timeSinceLatest: Int, doubleWhenOver: Int, maxValue: Int): Int {
|
|
||||||
if (delta >= maxValue) return maxValue
|
|
||||||
val cycle = timeSinceLatest.floorDiv(delta) + 1
|
|
||||||
// double delta again if missed more than 9 check in new delta
|
|
||||||
return if (cycle > doubleWhenOver) {
|
|
||||||
doubleInterval(delta * 2, timeSinceLatest, doubleWhenOver, maxValue)
|
|
||||||
} else {
|
|
||||||
delta
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getCurrentFetchRange(
|
|
||||||
timeToCal: ZonedDateTime,
|
|
||||||
): Pair<Long, Long> {
|
|
||||||
val preferences: LibraryPreferences = Injekt.get()
|
|
||||||
|
|
||||||
// lead range and the following range depend on if updateOnlyExpectedPeriod set.
|
|
||||||
var followRange = 0
|
|
||||||
var leadRange = 0
|
|
||||||
if (LibraryPreferences.ENTRY_OUTSIDE_RELEASE_PERIOD in preferences.libraryUpdateItemRestriction().get()) {
|
|
||||||
followRange = preferences.followingAnimeExpectedDays().get()
|
|
||||||
leadRange = preferences.leadingAnimeExpectedDays().get()
|
|
||||||
}
|
|
||||||
val startToday = timeToCal.toLocalDate().atStartOfDay(timeToCal.zone)
|
|
||||||
// revert math of (next_update + follow < now) become (next_update < now - follow)
|
|
||||||
// so (now - follow) become lower limit
|
|
||||||
val lowerRange = startToday.minusDays(followRange.toLong())
|
|
||||||
val higherRange = startToday.plusDays(leadRange.toLong())
|
|
||||||
return Pair(lowerRange.toEpochSecond() * 1000, higherRange.toEpochSecond() * 1000 - 1)
|
|
||||||
}
|
|
||||||
|
|
|
@ -13,111 +13,115 @@ import kotlin.math.absoluteValue
|
||||||
|
|
||||||
const val MAX_GRACE_PERIOD = 28
|
const val MAX_GRACE_PERIOD = 28
|
||||||
|
|
||||||
fun updateIntervalMeta(
|
class SetMangaUpdateInterval(
|
||||||
manga: Manga,
|
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||||
chapters: List<Chapter>,
|
) {
|
||||||
zonedDateTime: ZonedDateTime = ZonedDateTime.now(),
|
|
||||||
setCurrentFetchRange: Pair<Long, Long> = getCurrentFetchRange(zonedDateTime),
|
|
||||||
): MangaUpdate? {
|
|
||||||
val currentFetchRange = if (setCurrentFetchRange.first == 0L && setCurrentFetchRange.second == 0L) {
|
|
||||||
getCurrentFetchRange(ZonedDateTime.now())
|
|
||||||
} else {
|
|
||||||
setCurrentFetchRange
|
|
||||||
}
|
|
||||||
val interval = manga.calculateInterval.takeIf { it < 0 } ?: calculateInterval(chapters, zonedDateTime)
|
|
||||||
val nextUpdate = calculateNextUpdate(manga, interval, zonedDateTime, currentFetchRange)
|
|
||||||
|
|
||||||
return if (manga.nextUpdate == nextUpdate && manga.calculateInterval == interval) {
|
fun updateInterval(
|
||||||
null
|
manga: Manga,
|
||||||
} else { MangaUpdate(id = manga.id, nextUpdate = nextUpdate, calculateInterval = interval) }
|
chapters: List<Chapter>,
|
||||||
}
|
zonedDateTime: ZonedDateTime,
|
||||||
|
fetchRange: Pair<Long, Long>,
|
||||||
fun calculateInterval(chapters: List<Chapter>, zonedDateTime: ZonedDateTime): Int {
|
): MangaUpdate? {
|
||||||
val sortedChapters = chapters
|
val currentFetchRange = if (fetchRange.first == 0L && fetchRange.second == 0L) {
|
||||||
.sortedWith(compareByDescending<Chapter> { it.dateUpload }.thenByDescending { it.dateFetch })
|
getCurrentFetchRange(ZonedDateTime.now())
|
||||||
.take(50)
|
} else {
|
||||||
|
fetchRange
|
||||||
val uploadDates = sortedChapters
|
|
||||||
.filter { it.dateUpload > 0L }
|
|
||||||
.map {
|
|
||||||
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateUpload), zonedDateTime.zone)
|
|
||||||
.toLocalDate()
|
|
||||||
.atStartOfDay()
|
|
||||||
}
|
}
|
||||||
.distinct()
|
val interval = manga.calculateInterval.takeIf { it < 0 } ?: calculateInterval(chapters, zonedDateTime)
|
||||||
val fetchDates = sortedChapters
|
val nextUpdate = calculateNextUpdate(manga, interval, zonedDateTime, currentFetchRange)
|
||||||
.map {
|
|
||||||
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateFetch), zonedDateTime.zone)
|
return if (manga.nextUpdate == nextUpdate && manga.calculateInterval == interval) {
|
||||||
.toLocalDate()
|
null
|
||||||
.atStartOfDay()
|
} else {
|
||||||
|
MangaUpdate(id = manga.id, nextUpdate = nextUpdate, calculateInterval = interval)
|
||||||
}
|
}
|
||||||
.distinct()
|
}
|
||||||
|
|
||||||
val newInterval = when {
|
fun getCurrentFetchRange(timeToCal: ZonedDateTime): Pair<Long, Long> {
|
||||||
// Enough upload date from source
|
// lead range and the following range depend on if updateOnlyExpectedPeriod set.
|
||||||
uploadDates.size >= 3 -> {
|
var followRange = 0
|
||||||
val uploadDelta = uploadDates.last().until(uploadDates.first(), ChronoUnit.DAYS)
|
var leadRange = 0
|
||||||
val uploadPeriod = uploadDates.indexOf(uploadDates.last())
|
if (LibraryPreferences.ENTRY_OUTSIDE_RELEASE_PERIOD in libraryPreferences.libraryUpdateItemRestriction().get()) {
|
||||||
(uploadDelta).floorDiv(uploadPeriod).toInt()
|
followRange = libraryPreferences.followingAnimeExpectedDays().get()
|
||||||
|
leadRange = libraryPreferences.leadingAnimeExpectedDays().get()
|
||||||
}
|
}
|
||||||
// Enough fetch date from client
|
val startToday = timeToCal.toLocalDate().atStartOfDay(timeToCal.zone)
|
||||||
fetchDates.size >= 3 -> {
|
// revert math of (next_update + follow < now) become (next_update < now - follow)
|
||||||
val fetchDelta = fetchDates.last().until(fetchDates.first(), ChronoUnit.DAYS)
|
// so (now - follow) become lower limit
|
||||||
val uploadPeriod = fetchDates.indexOf(fetchDates.last())
|
val lowerRange = startToday.minusDays(followRange.toLong())
|
||||||
(fetchDelta).floorDiv(uploadPeriod).toInt()
|
val higherRange = startToday.plusDays(leadRange.toLong())
|
||||||
|
return Pair(lowerRange.toEpochSecond() * 1000, higherRange.toEpochSecond() * 1000 - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun calculateInterval(chapters: List<Chapter>, zonedDateTime: ZonedDateTime): Int {
|
||||||
|
val sortedChapters = chapters
|
||||||
|
.sortedWith(compareByDescending<Chapter> { it.dateUpload }.thenByDescending { it.dateFetch })
|
||||||
|
.take(50)
|
||||||
|
|
||||||
|
val uploadDates = sortedChapters
|
||||||
|
.filter { it.dateUpload > 0L }
|
||||||
|
.map {
|
||||||
|
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateUpload), zonedDateTime.zone)
|
||||||
|
.toLocalDate()
|
||||||
|
.atStartOfDay()
|
||||||
|
}
|
||||||
|
.distinct()
|
||||||
|
val fetchDates = sortedChapters
|
||||||
|
.map {
|
||||||
|
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateFetch), zonedDateTime.zone)
|
||||||
|
.toLocalDate()
|
||||||
|
.atStartOfDay()
|
||||||
|
}
|
||||||
|
.distinct()
|
||||||
|
|
||||||
|
val interval = when {
|
||||||
|
// Enough upload date from source
|
||||||
|
uploadDates.size >= 3 -> {
|
||||||
|
val uploadDelta = uploadDates.last().until(uploadDates.first(), ChronoUnit.DAYS)
|
||||||
|
val uploadPeriod = uploadDates.indexOf(uploadDates.last())
|
||||||
|
uploadDelta.floorDiv(uploadPeriod).toInt()
|
||||||
|
}
|
||||||
|
// Enough fetch date from client
|
||||||
|
fetchDates.size >= 3 -> {
|
||||||
|
val fetchDelta = fetchDates.last().until(fetchDates.first(), ChronoUnit.DAYS)
|
||||||
|
val uploadPeriod = fetchDates.indexOf(fetchDates.last())
|
||||||
|
fetchDelta.floorDiv(uploadPeriod).toInt()
|
||||||
|
}
|
||||||
|
// Default to 7 days
|
||||||
|
else -> 7
|
||||||
}
|
}
|
||||||
// Default to 7 days
|
// Min 1, max 28 days
|
||||||
else -> 7
|
return interval.coerceIn(1, MAX_GRACE_PERIOD)
|
||||||
}
|
}
|
||||||
// min 1, max 28 days
|
|
||||||
return newInterval.coerceIn(1, MAX_GRACE_PERIOD)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun calculateNextUpdate(
|
private fun calculateNextUpdate(
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
interval: Int,
|
interval: Int,
|
||||||
zonedDateTime: ZonedDateTime,
|
zonedDateTime: ZonedDateTime,
|
||||||
currentFetchRange: Pair<Long, Long>,
|
fetchRange: Pair<Long, Long>,
|
||||||
): Long {
|
): Long {
|
||||||
return if (manga.nextUpdate !in currentFetchRange.first.rangeTo(currentFetchRange.second + 1) ||
|
return if (
|
||||||
manga.calculateInterval == 0
|
manga.nextUpdate !in fetchRange.first.rangeTo(fetchRange.second + 1) ||
|
||||||
) {
|
manga.calculateInterval == 0
|
||||||
val latestDate = ZonedDateTime.ofInstant(Instant.ofEpochMilli(manga.lastUpdate), zonedDateTime.zone).toLocalDate().atStartOfDay()
|
) {
|
||||||
val timeSinceLatest = ChronoUnit.DAYS.between(latestDate, zonedDateTime).toInt()
|
val latestDate = ZonedDateTime.ofInstant(Instant.ofEpochMilli(manga.lastUpdate), zonedDateTime.zone).toLocalDate().atStartOfDay()
|
||||||
val cycle = timeSinceLatest.floorDiv(interval.absoluteValue.takeIf { interval < 0 } ?: doubleInterval(interval, timeSinceLatest, doubleWhenOver = 10, maxValue = 28))
|
val timeSinceLatest = ChronoUnit.DAYS.between(latestDate, zonedDateTime).toInt()
|
||||||
latestDate.plusDays((cycle + 1) * interval.toLong()).toEpochSecond(zonedDateTime.offset) * 1000
|
val cycle = timeSinceLatest.floorDiv(interval.absoluteValue.takeIf { interval < 0 } ?: doubleInterval(interval, timeSinceLatest, doubleWhenOver = 10, maxValue = 28))
|
||||||
} else {
|
latestDate.plusDays((cycle + 1) * interval.toLong()).toEpochSecond(zonedDateTime.offset) * 1000
|
||||||
manga.nextUpdate
|
} else {
|
||||||
|
manga.nextUpdate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun doubleInterval(delta: Int, timeSinceLatest: Int, doubleWhenOver: Int, maxValue: Int): Int {
|
||||||
|
if (delta >= maxValue) return maxValue
|
||||||
|
val cycle = timeSinceLatest.floorDiv(delta) + 1
|
||||||
|
// double delta again if missed more than 9 check in new delta
|
||||||
|
return if (cycle > doubleWhenOver) {
|
||||||
|
doubleInterval(delta * 2, timeSinceLatest, doubleWhenOver, maxValue)
|
||||||
|
} else {
|
||||||
|
delta
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun doubleInterval(delta: Int, timeSinceLatest: Int, doubleWhenOver: Int, maxValue: Int): Int {
|
|
||||||
if (delta >= maxValue) return maxValue
|
|
||||||
val cycle = timeSinceLatest.floorDiv(delta) + 1
|
|
||||||
// double delta again if missed more than 9 check in new delta
|
|
||||||
return if (cycle > doubleWhenOver) {
|
|
||||||
doubleInterval(delta * 2, timeSinceLatest, doubleWhenOver, maxValue)
|
|
||||||
} else {
|
|
||||||
delta
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getCurrentFetchRange(
|
|
||||||
timeToCal: ZonedDateTime,
|
|
||||||
): Pair<Long, Long> {
|
|
||||||
val preferences: LibraryPreferences = Injekt.get()
|
|
||||||
|
|
||||||
// lead range and the following range depend on if updateOnlyExpectedPeriod set.
|
|
||||||
var followRange = 0
|
|
||||||
var leadRange = 0
|
|
||||||
if (LibraryPreferences.ENTRY_OUTSIDE_RELEASE_PERIOD in preferences.libraryUpdateItemRestriction().get()) {
|
|
||||||
followRange = preferences.followingMangaExpectedDays().get()
|
|
||||||
leadRange = preferences.leadingMangaExpectedDays().get()
|
|
||||||
}
|
|
||||||
val startToday = timeToCal.toLocalDate().atStartOfDay(timeToCal.zone)
|
|
||||||
// revert math of (next_update + follow < now) become (next_update < now - follow)
|
|
||||||
// so (now - follow) become lower limit
|
|
||||||
val lowerRange = startToday.minusDays(followRange.toLong())
|
|
||||||
val higherRange = startToday.plusDays(leadRange.toLong())
|
|
||||||
return Pair(lowerRange.toEpochSecond() * 1000, higherRange.toEpochSecond() * 1000 - 1)
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,135 @@
|
||||||
|
package tachiyomi.domain.entries.anime.interactor
|
||||||
|
|
||||||
|
import io.kotest.matchers.shouldBe
|
||||||
|
import io.mockk.mockk
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.parallel.Execution
|
||||||
|
import org.junit.jupiter.api.parallel.ExecutionMode
|
||||||
|
import tachiyomi.domain.items.episode.model.Episode
|
||||||
|
import java.time.Duration
|
||||||
|
import java.time.ZonedDateTime
|
||||||
|
|
||||||
|
@Execution(ExecutionMode.CONCURRENT)
|
||||||
|
class SetAnimeUpdateIntervalTest {
|
||||||
|
private val testTime = ZonedDateTime.parse("2020-01-01T00:00:00Z")
|
||||||
|
private var episode = Episode.create().copy(
|
||||||
|
dateFetch = testTime.toEpochSecond() * 1000,
|
||||||
|
dateUpload = testTime.toEpochSecond() * 1000,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val setAnimeUpdateInterval = SetAnimeUpdateInterval(mockk())
|
||||||
|
|
||||||
|
private fun episodeAddTime(episode: Episode, duration: Duration): Episode {
|
||||||
|
val newTime = testTime.plus(duration).toEpochSecond() * 1000
|
||||||
|
return episode.copy(dateFetch = newTime, dateUpload = newTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
// default 7 when less than 3 distinct day
|
||||||
|
@Test
|
||||||
|
fun `calculateInterval returns 7 when 1 episodes in 1 day`() {
|
||||||
|
val episodes = mutableListOf<Episode>()
|
||||||
|
(1..1).forEach {
|
||||||
|
val duration = Duration.ofHours(10)
|
||||||
|
val newEpisode = episodeAddTime(episode, duration)
|
||||||
|
episodes.add(newEpisode)
|
||||||
|
}
|
||||||
|
setAnimeUpdateInterval.calculateInterval(episodes, testTime) shouldBe 7
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `calculateInterval returns 7 when 5 episodes in 1 day`() {
|
||||||
|
val episodes = mutableListOf<Episode>()
|
||||||
|
(1..5).forEach {
|
||||||
|
val duration = Duration.ofHours(10)
|
||||||
|
val newEpisode = episodeAddTime(episode, duration)
|
||||||
|
episodes.add(newEpisode)
|
||||||
|
}
|
||||||
|
setAnimeUpdateInterval.calculateInterval(episodes, testTime) shouldBe 7
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `calculateInterval returns 7 when 7 episodes in 48 hours, 2 day`() {
|
||||||
|
val episodes = mutableListOf<Episode>()
|
||||||
|
(1..2).forEach {
|
||||||
|
val duration = Duration.ofHours(24L)
|
||||||
|
val newEpisode = episodeAddTime(episode, duration)
|
||||||
|
episodes.add(newEpisode)
|
||||||
|
}
|
||||||
|
(1..5).forEach {
|
||||||
|
val duration = Duration.ofHours(48L)
|
||||||
|
val newEpisode = episodeAddTime(episode, duration)
|
||||||
|
episodes.add(newEpisode)
|
||||||
|
}
|
||||||
|
setAnimeUpdateInterval.calculateInterval(episodes, testTime) shouldBe 7
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default 1 if interval less than 1
|
||||||
|
@Test
|
||||||
|
fun `calculateInterval returns 1 when 5 episodes in 75 hours, 3 days`() {
|
||||||
|
val episodes = mutableListOf<Episode>()
|
||||||
|
(1..5).forEach {
|
||||||
|
val duration = Duration.ofHours(15L * it)
|
||||||
|
val newEpisode = episodeAddTime(episode, duration)
|
||||||
|
episodes.add(newEpisode)
|
||||||
|
}
|
||||||
|
setAnimeUpdateInterval.calculateInterval(episodes, testTime) shouldBe 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal interval calculation
|
||||||
|
@Test
|
||||||
|
fun `calculateInterval returns 1 when 5 episodes in 120 hours, 5 days`() {
|
||||||
|
val episodes = mutableListOf<Episode>()
|
||||||
|
(1..5).forEach {
|
||||||
|
val duration = Duration.ofHours(24L * it)
|
||||||
|
val newEpisode = episodeAddTime(episode, duration)
|
||||||
|
episodes.add(newEpisode)
|
||||||
|
}
|
||||||
|
setAnimeUpdateInterval.calculateInterval(episodes, testTime) shouldBe 1
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `calculateInterval returns 2 when 5 episodes in 240 hours, 10 days`() {
|
||||||
|
val episodes = mutableListOf<Episode>()
|
||||||
|
(1..5).forEach {
|
||||||
|
val duration = Duration.ofHours(48L * it)
|
||||||
|
val newEpisode = episodeAddTime(episode, duration)
|
||||||
|
episodes.add(newEpisode)
|
||||||
|
}
|
||||||
|
setAnimeUpdateInterval.calculateInterval(episodes, testTime) shouldBe 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// If interval is decimal, floor to closest integer
|
||||||
|
@Test
|
||||||
|
fun `calculateInterval returns 1 when 5 episodes in 125 hours, 5 days`() {
|
||||||
|
val episodes = mutableListOf<Episode>()
|
||||||
|
(1..5).forEach {
|
||||||
|
val duration = Duration.ofHours(25L * it)
|
||||||
|
val newEpisode = episodeAddTime(episode, duration)
|
||||||
|
episodes.add(newEpisode)
|
||||||
|
}
|
||||||
|
setAnimeUpdateInterval.calculateInterval(episodes, testTime) shouldBe 1
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `calculateInterval returns 1 when 5 episodes in 215 hours, 5 days`() {
|
||||||
|
val episodes = mutableListOf<Episode>()
|
||||||
|
(1..5).forEach {
|
||||||
|
val duration = Duration.ofHours(43L * it)
|
||||||
|
val newEpisode = episodeAddTime(episode, duration)
|
||||||
|
episodes.add(newEpisode)
|
||||||
|
}
|
||||||
|
setAnimeUpdateInterval.calculateInterval(episodes, testTime) shouldBe 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use fetch time if upload time not available
|
||||||
|
@Test
|
||||||
|
fun `calculateInterval returns 1 when 5 episodes in 125 hours, 5 days of dateFetch`() {
|
||||||
|
val episodes = mutableListOf<Episode>()
|
||||||
|
(1..5).forEach {
|
||||||
|
val duration = Duration.ofHours(25L * it)
|
||||||
|
val newEpisode = episodeAddTime(episode, duration).copy(dateUpload = 0L)
|
||||||
|
episodes.add(newEpisode)
|
||||||
|
}
|
||||||
|
setAnimeUpdateInterval.calculateInterval(episodes, testTime) shouldBe 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,135 @@
|
||||||
|
package tachiyomi.domain.entries.manga.interactor
|
||||||
|
|
||||||
|
import io.kotest.matchers.shouldBe
|
||||||
|
import io.mockk.mockk
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.parallel.Execution
|
||||||
|
import org.junit.jupiter.api.parallel.ExecutionMode
|
||||||
|
import tachiyomi.domain.items.chapter.model.Chapter
|
||||||
|
import java.time.Duration
|
||||||
|
import java.time.ZonedDateTime
|
||||||
|
|
||||||
|
@Execution(ExecutionMode.CONCURRENT)
|
||||||
|
class SetMangaUpdateIntervalTest {
|
||||||
|
private val testTime = ZonedDateTime.parse("2020-01-01T00:00:00Z")
|
||||||
|
private var chapter = Chapter.create().copy(
|
||||||
|
dateFetch = testTime.toEpochSecond() * 1000,
|
||||||
|
dateUpload = testTime.toEpochSecond() * 1000,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val setMangaUpdateInterval = SetMangaUpdateInterval(mockk())
|
||||||
|
|
||||||
|
private fun chapterAddTime(chapter: Chapter, duration: Duration): Chapter {
|
||||||
|
val newTime = testTime.plus(duration).toEpochSecond() * 1000
|
||||||
|
return chapter.copy(dateFetch = newTime, dateUpload = newTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
// default 7 when less than 3 distinct day
|
||||||
|
@Test
|
||||||
|
fun `calculateInterval returns 7 when 1 chapters in 1 day`() {
|
||||||
|
val chapters = mutableListOf<Chapter>()
|
||||||
|
(1..1).forEach {
|
||||||
|
val duration = Duration.ofHours(10)
|
||||||
|
val newChapter = chapterAddTime(chapter, duration)
|
||||||
|
chapters.add(newChapter)
|
||||||
|
}
|
||||||
|
setMangaUpdateInterval.calculateInterval(chapters, testTime) shouldBe 7
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `calculateInterval returns 7 when 5 chapters in 1 day`() {
|
||||||
|
val chapters = mutableListOf<Chapter>()
|
||||||
|
(1..5).forEach {
|
||||||
|
val duration = Duration.ofHours(10)
|
||||||
|
val newChapter = chapterAddTime(chapter, duration)
|
||||||
|
chapters.add(newChapter)
|
||||||
|
}
|
||||||
|
setMangaUpdateInterval.calculateInterval(chapters, testTime) shouldBe 7
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `calculateInterval returns 7 when 7 chapters in 48 hours, 2 day`() {
|
||||||
|
val chapters = mutableListOf<Chapter>()
|
||||||
|
(1..2).forEach {
|
||||||
|
val duration = Duration.ofHours(24L)
|
||||||
|
val newChapter = chapterAddTime(chapter, duration)
|
||||||
|
chapters.add(newChapter)
|
||||||
|
}
|
||||||
|
(1..5).forEach {
|
||||||
|
val duration = Duration.ofHours(48L)
|
||||||
|
val newChapter = chapterAddTime(chapter, duration)
|
||||||
|
chapters.add(newChapter)
|
||||||
|
}
|
||||||
|
setMangaUpdateInterval.calculateInterval(chapters, testTime) shouldBe 7
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default 1 if interval less than 1
|
||||||
|
@Test
|
||||||
|
fun `calculateInterval returns 1 when 5 chapters in 75 hours, 3 days`() {
|
||||||
|
val chapters = mutableListOf<Chapter>()
|
||||||
|
(1..5).forEach {
|
||||||
|
val duration = Duration.ofHours(15L * it)
|
||||||
|
val newChapter = chapterAddTime(chapter, duration)
|
||||||
|
chapters.add(newChapter)
|
||||||
|
}
|
||||||
|
setMangaUpdateInterval.calculateInterval(chapters, testTime) shouldBe 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal interval calculation
|
||||||
|
@Test
|
||||||
|
fun `calculateInterval returns 1 when 5 chapters in 120 hours, 5 days`() {
|
||||||
|
val chapters = mutableListOf<Chapter>()
|
||||||
|
(1..5).forEach {
|
||||||
|
val duration = Duration.ofHours(24L * it)
|
||||||
|
val newChapter = chapterAddTime(chapter, duration)
|
||||||
|
chapters.add(newChapter)
|
||||||
|
}
|
||||||
|
setMangaUpdateInterval.calculateInterval(chapters, testTime) shouldBe 1
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `calculateInterval returns 2 when 5 chapters in 240 hours, 10 days`() {
|
||||||
|
val chapters = mutableListOf<Chapter>()
|
||||||
|
(1..5).forEach {
|
||||||
|
val duration = Duration.ofHours(48L * it)
|
||||||
|
val newChapter = chapterAddTime(chapter, duration)
|
||||||
|
chapters.add(newChapter)
|
||||||
|
}
|
||||||
|
setMangaUpdateInterval.calculateInterval(chapters, testTime) shouldBe 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// If interval is decimal, floor to closest integer
|
||||||
|
@Test
|
||||||
|
fun `calculateInterval returns 1 when 5 chapters in 125 hours, 5 days`() {
|
||||||
|
val chapters = mutableListOf<Chapter>()
|
||||||
|
(1..5).forEach {
|
||||||
|
val duration = Duration.ofHours(25L * it)
|
||||||
|
val newChapter = chapterAddTime(chapter, duration)
|
||||||
|
chapters.add(newChapter)
|
||||||
|
}
|
||||||
|
setMangaUpdateInterval.calculateInterval(chapters, testTime) shouldBe 1
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `calculateInterval returns 1 when 5 chapters in 215 hours, 5 days`() {
|
||||||
|
val chapters = mutableListOf<Chapter>()
|
||||||
|
(1..5).forEach {
|
||||||
|
val duration = Duration.ofHours(43L * it)
|
||||||
|
val newChapter = chapterAddTime(chapter, duration)
|
||||||
|
chapters.add(newChapter)
|
||||||
|
}
|
||||||
|
setMangaUpdateInterval.calculateInterval(chapters, testTime) shouldBe 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use fetch time if upload time not available
|
||||||
|
@Test
|
||||||
|
fun `calculateInterval returns 1 when 5 chapters in 125 hours, 5 days of dateFetch`() {
|
||||||
|
val chapters = mutableListOf<Chapter>()
|
||||||
|
(1..5).forEach {
|
||||||
|
val duration = Duration.ofHours(25L * it)
|
||||||
|
val newChapter = chapterAddTime(chapter, duration).copy(dateUpload = 0L)
|
||||||
|
chapters.add(newChapter)
|
||||||
|
}
|
||||||
|
setMangaUpdateInterval.calculateInterval(chapters, testTime) shouldBe 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -204,10 +204,6 @@
|
||||||
<string name="theme_matrix">Matrix</string>
|
<string name="theme_matrix">Matrix</string>
|
||||||
<string name="theme_tidalwave">Tidal Wave</string>
|
<string name="theme_tidalwave">Tidal Wave</string>
|
||||||
<string name="pref_dark_theme_pure_black">Pure black dark mode</string>
|
<string name="pref_dark_theme_pure_black">Pure black dark mode</string>
|
||||||
<string name="pref_category_timestamps">Timestamps</string>
|
|
||||||
<string name="pref_relative_format">Relative timestamps</string>
|
|
||||||
<string name="pref_relative_time_short">Short (Today, Yesterday)</string>
|
|
||||||
<string name="pref_relative_time_long">Long (Short+, n days ago)</string>
|
|
||||||
<string name="pref_date_format">Date format</string>
|
<string name="pref_date_format">Date format</string>
|
||||||
|
|
||||||
<string name="pref_manage_notifications">Manage notifications</string>
|
<string name="pref_manage_notifications">Manage notifications</string>
|
||||||
|
@ -628,6 +624,10 @@
|
||||||
<item quantity="one">1 day</item>
|
<item quantity="one">1 day</item>
|
||||||
<item quantity="other">%d days</item>
|
<item quantity="other">%d days</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
<plurals name="range_interval_day">
|
||||||
|
<item quantity="one">%1$d - %2$d day</item>
|
||||||
|
<item quantity="other">%1$d - %2$d days</item>
|
||||||
|
</plurals>
|
||||||
|
|
||||||
<!-- Item info -->
|
<!-- Item info -->
|
||||||
<plurals name="missing_items">
|
<plurals name="missing_items">
|
||||||
|
@ -669,8 +669,7 @@
|
||||||
<string name="display_mode_chapter">Chapter %1$s</string>
|
<string name="display_mode_chapter">Chapter %1$s</string>
|
||||||
<string name="manga_display_interval_title">Estimate every</string>
|
<string name="manga_display_interval_title">Estimate every</string>
|
||||||
<string name="manga_display_modified_interval_title">Set to update every</string>
|
<string name="manga_display_modified_interval_title">Set to update every</string>
|
||||||
<string name="manga_modify_interval_title">Modify interval</string>
|
<string name="manga_modify_calculated_interval_title">Customize interval</string>
|
||||||
<string name="manga_modify_calculated_interval_title">Customize Interval</string>
|
|
||||||
<string name="chapter_downloading_progress">Downloading (%1$d/%2$d)</string>
|
<string name="chapter_downloading_progress">Downloading (%1$d/%2$d)</string>
|
||||||
<string name="show_title">Source title</string>
|
<string name="show_title">Source title</string>
|
||||||
<string name="show_chapter_number">Chapter number</string>
|
<string name="show_chapter_number">Chapter number</string>
|
||||||
|
|
|
@ -78,6 +78,7 @@ fun AdaptiveSheet(
|
||||||
val alpha by animateFloatAsState(
|
val alpha by animateFloatAsState(
|
||||||
targetValue = targetAlpha,
|
targetValue = targetAlpha,
|
||||||
animationSpec = sheetAnimationSpec,
|
animationSpec = sheetAnimationSpec,
|
||||||
|
label = "alpha",
|
||||||
)
|
)
|
||||||
val internalOnDismissRequest: () -> Unit = {
|
val internalOnDismissRequest: () -> Unit = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
|
Loading…
Reference in a new issue