diff --git a/app/src/main/java/eu/kanade/domain/entries/anime/interactor/UpdateAnime.kt b/app/src/main/java/eu/kanade/domain/entries/anime/interactor/UpdateAnime.kt index 4b9ebdd35..de98c68e6 100644 --- a/app/src/main/java/eu/kanade/domain/entries/anime/interactor/UpdateAnime.kt +++ b/app/src/main/java/eu/kanade/domain/entries/anime/interactor/UpdateAnime.kt @@ -3,12 +3,16 @@ package eu.kanade.domain.entries.anime.interactor import eu.kanade.domain.entries.anime.model.hasCustomCover import eu.kanade.tachiyomi.animesource.model.SAnime import eu.kanade.tachiyomi.data.cache.AnimeCoverCache +import tachiyomi.domain.entries.anime.interactor.getCurrentFetchRange +import tachiyomi.domain.entries.anime.interactor.updateIntervalMeta import tachiyomi.domain.entries.anime.model.Anime import tachiyomi.domain.entries.anime.model.AnimeUpdate import tachiyomi.domain.entries.anime.repository.AnimeRepository +import tachiyomi.domain.items.episode.model.Episode import tachiyomi.source.local.entries.anime.isLocal import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import java.time.ZonedDateTime import java.util.Date class UpdateAnime( @@ -73,6 +77,21 @@ class UpdateAnime( ) } + suspend fun awaitUpdateIntervalMeta( + anime: Anime, + episodes: List, + zonedDateTime: ZonedDateTime = ZonedDateTime.now(), + setCurrentFetchRange: Pair = getCurrentFetchRange(zonedDateTime), + ): Boolean { + val newMeta = updateIntervalMeta(anime, episodes, zonedDateTime, setCurrentFetchRange) + + return if (newMeta != null) { + animeRepository.updateAnime(newMeta) + } else { + true + } + } + suspend fun awaitUpdateLastUpdate(animeId: Long): Boolean { return animeRepository.updateAnime(AnimeUpdate(id = animeId, lastUpdate = Date().time)) } diff --git a/app/src/main/java/eu/kanade/domain/entries/manga/interactor/UpdateManga.kt b/app/src/main/java/eu/kanade/domain/entries/manga/interactor/UpdateManga.kt index 8d159cfe0..579f985e5 100644 --- a/app/src/main/java/eu/kanade/domain/entries/manga/interactor/UpdateManga.kt +++ b/app/src/main/java/eu/kanade/domain/entries/manga/interactor/UpdateManga.kt @@ -3,12 +3,16 @@ package eu.kanade.domain.entries.manga.interactor import eu.kanade.domain.entries.manga.model.hasCustomCover import eu.kanade.tachiyomi.data.cache.MangaCoverCache import eu.kanade.tachiyomi.source.model.SManga +import tachiyomi.domain.entries.manga.interactor.getCurrentFetchRange +import tachiyomi.domain.entries.manga.interactor.updateIntervalMeta import tachiyomi.domain.entries.manga.model.Manga import tachiyomi.domain.entries.manga.model.MangaUpdate import tachiyomi.domain.entries.manga.repository.MangaRepository +import tachiyomi.domain.items.chapter.model.Chapter import tachiyomi.source.local.entries.manga.isLocal import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import java.time.ZonedDateTime import java.util.Date class UpdateManga( @@ -73,6 +77,21 @@ class UpdateManga( ) } + suspend fun awaitUpdateIntervalMeta( + manga: Manga, + chapters: List, + zonedDateTime: ZonedDateTime = ZonedDateTime.now(), + setCurrentFetchRange: Pair = getCurrentFetchRange(zonedDateTime), + ): Boolean { + val newMeta = updateIntervalMeta(manga, chapters, zonedDateTime, setCurrentFetchRange) + + return if (newMeta != null) { + mangaRepository.updateManga(newMeta) + } else { + true + } + } + suspend fun awaitUpdateLastUpdate(mangaId: Long): Boolean { return mangaRepository.updateManga(MangaUpdate(id = mangaId, lastUpdate = Date().time)) } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt index 3f62bfb52..a766c38e4 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt @@ -1,18 +1,33 @@ package eu.kanade.presentation.more.settings.screen import androidx.annotation.StringRes +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope 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.platform.LocalContext import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastMap import androidx.core.content.ContextCompat import cafe.adriel.voyager.navigator.LocalNavigator @@ -41,6 +56,8 @@ 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_NON_COMPLETED import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_NON_VIEWED +import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_OUTSIDE_RELEASE_PERIOD +import tachiyomi.presentation.core.components.WheelTextPicker import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -174,11 +191,12 @@ object SettingsLibraryScreen : SearchableSettings { val animelibUpdateCategoriesPref = libraryPreferences.animeLibraryUpdateCategories() val animelibUpdateCategoriesExcludePref = libraryPreferences.animeLibraryUpdateCategoriesExclude() + val libraryUpdateAnimeRestriction by libraryUpdateMangaRestrictionPref.collectAsState() val includedAnime by animelibUpdateCategoriesPref.collectAsState() val excludedAnime by animelibUpdateCategoriesExcludePref.collectAsState() - var showAnimeDialog by rememberSaveable { mutableStateOf(false) } - if (showAnimeDialog) { + var showAnimeCategoriesDialog by rememberSaveable { mutableStateOf(false) } + if (showAnimeCategoriesDialog) { TriStateListDialog( title = stringResource(R.string.anime_categories), message = stringResource(R.string.pref_anime_library_update_categories_details), @@ -186,14 +204,30 @@ object SettingsLibraryScreen : SearchableSettings { initialChecked = includedAnime.mapNotNull { id -> allAnimeCategories.find { it.id.toString() == id } }, initialInversed = excludedAnime.mapNotNull { id -> allAnimeCategories.find { it.id.toString() == id } }, itemLabel = { it.visualName }, - onDismissRequest = { showAnimeDialog = false }, + onDismissRequest = { showAnimeCategoriesDialog = false }, onValueChanged = { newIncluded, newExcluded -> animelibUpdateCategoriesPref.set(newIncluded.map { it.id.toString() }.toSet()) animelibUpdateCategoriesExcludePref.set( newExcluded.map { it.id.toString() } .toSet(), ) - showAnimeDialog = false + showAnimeCategoriesDialog = false + }, + ) + } + val leadAnimeRange by libraryPreferences.leadingAnimeExpectedDays().collectAsState() + val followAnimeRange by libraryPreferences.followingAnimeExpectedDays().collectAsState() + + var showFetchAnimeRangesDialog by rememberSaveable { mutableStateOf(false) } + if (showFetchAnimeRangesDialog) { + LibraryExpectedRangeDialog( + initialLead = leadAnimeRange, + initialFollow = followAnimeRange, + onDismissRequest = { showFetchAnimeRangesDialog = false }, + onValueChanged = { leadValue, followValue -> + libraryPreferences.leadingAnimeExpectedDays().set(leadValue) + libraryPreferences.followingAnimeExpectedDays().set(followValue) + showFetchAnimeRangesDialog = false }, ) } @@ -201,11 +235,12 @@ object SettingsLibraryScreen : SearchableSettings { val libraryUpdateCategoriesPref = libraryPreferences.mangaLibraryUpdateCategories() val libraryUpdateCategoriesExcludePref = libraryPreferences.mangaLibraryUpdateCategoriesExclude() + val libraryUpdateMangaRestriction by libraryUpdateMangaRestrictionPref.collectAsState() val includedManga by libraryUpdateCategoriesPref.collectAsState() val excludedManga by libraryUpdateCategoriesExcludePref.collectAsState() - var showMangaDialog by rememberSaveable { mutableStateOf(false) } - if (showMangaDialog) { + var showMangaCategoriesDialog by rememberSaveable { mutableStateOf(false) } + if (showMangaCategoriesDialog) { TriStateListDialog( title = stringResource(R.string.manga_categories), message = stringResource(R.string.pref_manga_library_update_categories_details), @@ -213,20 +248,36 @@ object SettingsLibraryScreen : SearchableSettings { initialChecked = includedManga.mapNotNull { id -> allMangaCategories.find { it.id.toString() == id } }, initialInversed = excludedManga.mapNotNull { id -> allMangaCategories.find { it.id.toString() == id } }, itemLabel = { it.visualName }, - onDismissRequest = { showMangaDialog = false }, + onDismissRequest = { showMangaCategoriesDialog = false }, onValueChanged = { newIncluded, newExcluded -> libraryUpdateCategoriesPref.set(newIncluded.map { it.id.toString() }.toSet()) libraryUpdateCategoriesExcludePref.set( newExcluded.map { it.id.toString() } .toSet(), ) - showMangaDialog = false + showMangaCategoriesDialog = false + }, + ) + } + val leadMangaRange by libraryPreferences.leadingMangaExpectedDays().collectAsState() + val followMangaRange by libraryPreferences.followingMangaExpectedDays().collectAsState() + + var showFetchMangaRangesDialog by rememberSaveable { mutableStateOf(false) } + if (showFetchMangaRangesDialog) { + LibraryExpectedRangeDialog( + initialLead = leadMangaRange, + initialFollow = followMangaRange, + onDismissRequest = { showFetchMangaRangesDialog = false }, + onValueChanged = { leadValue, followValue -> + libraryPreferences.leadingMangaExpectedDays().set(leadValue) + libraryPreferences.followingMangaExpectedDays().set(followValue) + showFetchMangaRangesDialog = false }, ) } return Preference.PreferenceGroup( title = stringResource(R.string.pref_category_library_update), - preferenceItems = listOf( + preferenceItems = listOfNotNull( Preference.PreferenceItem.ListPreference( pref = libraryUpdateIntervalPref, title = stringResource(R.string.pref_library_update_interval), @@ -264,15 +315,6 @@ object SettingsLibraryScreen : SearchableSettings { true }, ), - Preference.PreferenceItem.MultiSelectListPreference( - pref = libraryUpdateMangaRestrictionPref, - title = stringResource(R.string.pref_library_update_manga_restriction), - entries = mapOf( - ENTRY_HAS_UNVIEWED to stringResource(R.string.pref_update_only_completely_read), - ENTRY_NON_VIEWED to stringResource(R.string.pref_update_only_started), - ENTRY_NON_COMPLETED to stringResource(R.string.pref_update_only_non_completed), - ), - ), Preference.PreferenceItem.TextPreference( title = stringResource(R.string.anime_categories), subtitle = getCategoriesLabel( @@ -280,7 +322,7 @@ object SettingsLibraryScreen : SearchableSettings { included = includedAnime, excluded = excludedAnime, ), - onClick = { showAnimeDialog = true }, + onClick = { showAnimeCategoriesDialog = true }, ), Preference.PreferenceItem.TextPreference( title = stringResource(R.string.manga_categories), @@ -289,7 +331,7 @@ object SettingsLibraryScreen : SearchableSettings { included = includedManga, excluded = excludedManga, ), - onClick = { showMangaDialog = true }, + onClick = { showMangaCategoriesDialog = true }, ), Preference.PreferenceItem.SwitchPreference( pref = libraryPreferences.autoUpdateMetadata(), @@ -302,6 +344,39 @@ object SettingsLibraryScreen : SearchableSettings { title = stringResource(R.string.pref_library_update_refresh_trackers), subtitle = stringResource(R.string.pref_library_update_refresh_trackers_summary), ), + Preference.PreferenceItem.MultiSelectListPreference( + pref = libraryUpdateMangaRestrictionPref, + title = stringResource(R.string.pref_library_update_manga_restriction), + entries = mapOf( + ENTRY_HAS_UNVIEWED to stringResource(R.string.pref_update_only_completely_read), + ENTRY_NON_VIEWED to stringResource(R.string.pref_update_only_started), + ENTRY_NON_COMPLETED to stringResource(R.string.pref_update_only_non_completed), + ENTRY_OUTSIDE_RELEASE_PERIOD to stringResource(R.string.pref_update_only_in_release_period), + ), + ), + Preference.PreferenceItem.TextPreference( + title = stringResource(R.string.pref_update_release_grace_period), + subtitle = listOf( + pluralStringResource(R.plurals.pref_update_release_leading_days, leadMangaRange, leadMangaRange), + pluralStringResource(R.plurals.pref_update_release_following_days, followMangaRange, followMangaRange), + ).joinToString(), + onClick = { showFetchMangaRangesDialog = true }, + ).takeIf { ENTRY_OUTSIDE_RELEASE_PERIOD in libraryUpdateMangaRestriction }, + Preference.PreferenceItem.InfoPreference( + title = stringResource(R.string.pref_update_release_grace_period_info), + ).takeIf { ENTRY_OUTSIDE_RELEASE_PERIOD in libraryUpdateMangaRestriction }, + + Preference.PreferenceItem.TextPreference( + title = stringResource(R.string.pref_update_anime_release_grace_period), + subtitle = listOf( + pluralStringResource(R.plurals.pref_update_release_leading_days, leadAnimeRange, leadAnimeRange), + pluralStringResource(R.plurals.pref_update_release_following_days, followAnimeRange, followAnimeRange), + ).joinToString(), + onClick = { showFetchAnimeRangesDialog = true }, + ).takeIf { ENTRY_OUTSIDE_RELEASE_PERIOD in libraryUpdateAnimeRestriction }, + Preference.PreferenceItem.InfoPreference( + title = stringResource(R.string.pref_update_release_grace_period_info), + ).takeIf { ENTRY_OUTSIDE_RELEASE_PERIOD in libraryUpdateAnimeRestriction }, ), ) } @@ -367,4 +442,85 @@ object SettingsLibraryScreen : SearchableSettings { ), ) } + + @Composable + private fun LibraryExpectedRangeDialog( + initialLead: Int, + initialFollow: Int, + onDismissRequest: () -> Unit, + onValueChanged: (portrait: Int, landscape: Int) -> Unit, + ) { + var leadValue by rememberSaveable { mutableIntStateOf(initialLead) } + var followValue by rememberSaveable { mutableIntStateOf(initialFollow) } + + AlertDialog( + onDismissRequest = onDismissRequest, + title = { Text(text = stringResource(R.string.pref_update_release_grace_period)) }, + text = { + Column { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + modifier = Modifier.weight(1f), + text = pluralStringResource(R.plurals.pref_update_release_leading_days, leadValue, leadValue), + textAlign = TextAlign.Center, + maxLines = 1, + style = MaterialTheme.typography.labelMedium, + ) + Text( + modifier = Modifier.weight(1f), + text = pluralStringResource(R.plurals.pref_update_release_following_days, followValue, followValue), + textAlign = TextAlign.Center, + maxLines = 1, + style = MaterialTheme.typography.labelMedium, + ) + } + } + BoxWithConstraints( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center, + ) { + val size = DpSize(width = maxWidth / 2, height = 128.dp) + val items = (0..28).map { + if (it == 0) { + stringResource(R.string.label_default) + } else { + it.toString() + } + } + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + WheelTextPicker( + size = size, + items = items, + startIndex = leadValue, + onSelectionChanged = { + leadValue = it + }, + ) + WheelTextPicker( + size = size, + items = items, + startIndex = followValue, + onSelectionChanged = { + followValue = it + }, + ) + } + } + }, + dismissButton = { + TextButton(onClick = onDismissRequest) { + Text(text = stringResource(android.R.string.cancel)) + } + }, + confirmButton = { + TextButton(onClick = { onValueChanged(leadValue, followValue) }) { + Text(text = stringResource(android.R.string.ok)) + } + }, + ) + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/App.kt b/app/src/main/java/eu/kanade/tachiyomi/App.kt index e2ac59aa6..7bdacee67 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/App.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/App.kt @@ -139,11 +139,11 @@ class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory { setAppCompatDelegateThemeMode(Injekt.get().themeMode().get()) // Updates widget update - with(TachiyomiMangaWidgetManager(Injekt.get())) { + with(TachiyomiMangaWidgetManager(Injekt.get(), Injekt.get())) { init(ProcessLifecycleOwner.get().lifecycleScope) } - with(TachiyomiAnimeWidgetManager(Injekt.get())) { + with(TachiyomiAnimeWidgetManager(Injekt.get(), Injekt.get())) { init(ProcessLifecycleOwner.get().lifecycleScope) } diff --git a/data/src/main/java/tachiyomi/data/updates/anime/AnimeUpdatesRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/updates/anime/AnimeUpdatesRepositoryImpl.kt index 8cdd74069..c60ddf2c1 100644 --- a/data/src/main/java/tachiyomi/data/updates/anime/AnimeUpdatesRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/updates/anime/AnimeUpdatesRepositoryImpl.kt @@ -19,9 +19,9 @@ class AnimeUpdatesRepositoryImpl( } } - override fun subscribeAllAnimeUpdates(after: Long): Flow> { + override fun subscribeAllAnimeUpdates(after: Long, limit: Long): Flow> { return databaseHandler.subscribeToList { - animeupdatesViewQueries.animeupdates(after, animeUpdateWithRelationMapper) + animeupdatesViewQueries.getRecentAnimeUpdates(after, limit, animeUpdateWithRelationMapper) } } diff --git a/data/src/main/java/tachiyomi/data/updates/manga/MangaUpdatesRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/updates/manga/MangaUpdatesRepositoryImpl.kt index acf960237..5a7674195 100644 --- a/data/src/main/java/tachiyomi/data/updates/manga/MangaUpdatesRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/updates/manga/MangaUpdatesRepositoryImpl.kt @@ -19,9 +19,9 @@ class MangaUpdatesRepositoryImpl( } } - override fun subscribeAllMangaUpdates(after: Long): Flow> { + override fun subscribeAllMangaUpdates(after: Long, limit: Long): Flow> { return databaseHandler.subscribeToList { - updatesViewQueries.updates(after, mangaUpdateWithRelationMapper) + updatesViewQueries.getRecentUpdates(after, limit, mangaUpdateWithRelationMapper) } } diff --git a/data/src/main/sqldelight/view/updatesView.sq b/data/src/main/sqldelight/view/updatesView.sq index fc6953818..6e9fd95c2 100644 --- a/data/src/main/sqldelight/view/updatesView.sq +++ b/data/src/main/sqldelight/view/updatesView.sq @@ -20,10 +20,11 @@ WHERE favorite = 1 AND date_fetch > date_added ORDER BY date_fetch DESC; -updates: +getRecentUpdates: SELECT * FROM updatesView -WHERE dateUpload > :after; +WHERE dateUpload > :after +LIMIT :limit; getUpdatesByReadStatus: SELECT * diff --git a/data/src/main/sqldelightanime/view/animeupdatesView.sq b/data/src/main/sqldelightanime/view/animeupdatesView.sq index ff1e8b1e3..be30e10b1 100644 --- a/data/src/main/sqldelightanime/view/animeupdatesView.sq +++ b/data/src/main/sqldelightanime/view/animeupdatesView.sq @@ -21,10 +21,11 @@ WHERE favorite = 1 AND date_fetch > date_added ORDER BY date_fetch DESC; -animeupdates: +getRecentAnimeUpdates: SELECT * FROM animeupdatesView -WHERE dateUpload > :after; +WHERE dateUpload > :after +LIMIT :limit; getUpdatesBySeenStatus: SELECT * diff --git a/domain/src/main/java/tachiyomi/domain/entries/anime/interactor/SetAnimeUpdateInterval.kt b/domain/src/main/java/tachiyomi/domain/entries/anime/interactor/SetAnimeUpdateInterval.kt new file mode 100644 index 000000000..03c3a4b25 --- /dev/null +++ b/domain/src/main/java/tachiyomi/domain/entries/anime/interactor/SetAnimeUpdateInterval.kt @@ -0,0 +1,123 @@ +package tachiyomi.domain.entries.anime.interactor + +import tachiyomi.domain.entries.anime.model.Anime +import tachiyomi.domain.entries.anime.model.AnimeUpdate +import tachiyomi.domain.items.episode.model.Episode +import tachiyomi.domain.library.service.LibraryPreferences +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.time.Instant +import java.time.ZonedDateTime +import java.time.temporal.ChronoUnit +import kotlin.math.absoluteValue + +const val MAX_GRACE_PERIOD = 28 + +fun updateIntervalMeta( + anime: Anime, + episodes: List, + zonedDateTime: ZonedDateTime = ZonedDateTime.now(), + setCurrentFetchRange: Pair = 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) { + null + } else { AnimeUpdate(id = anime.id, nextUpdate = nextUpdate, calculateInterval = interval) } +} + +fun calculateInterval(episodes: List, zonedDateTime: ZonedDateTime): Int { + val sortedEpisodes = episodes + .sortedWith(compareByDescending { 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 newInterval = 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 + } + // min 1, max 28 days + return newInterval.coerceIn(1, MAX_GRACE_PERIOD) +} + +private fun calculateNextUpdate( + anime: Anime, + interval: Int, + zonedDateTime: ZonedDateTime, + currentFetchRange: Pair, +): Long { + return if (anime.nextUpdate !in currentFetchRange.first.rangeTo(currentFetchRange.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 cycle = timeSinceLatest.floorDiv(interval.absoluteValue.takeIf { interval < 0 } ?: doubleInterval(interval, timeSinceLatest, doubleWhenOver = 10, maxValue = 28)) + latestDate.plusDays((cycle + 1) * interval.toLong()).toEpochSecond(zonedDateTime.offset) * 1000 + } 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 + } +} + +fun getCurrentFetchRange( + timeToCal: ZonedDateTime, +): Pair { + 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) +} diff --git a/domain/src/main/java/tachiyomi/domain/entries/manga/interactor/SetMangaUpdateInterval.kt b/domain/src/main/java/tachiyomi/domain/entries/manga/interactor/SetMangaUpdateInterval.kt new file mode 100644 index 000000000..32f10e7d6 --- /dev/null +++ b/domain/src/main/java/tachiyomi/domain/entries/manga/interactor/SetMangaUpdateInterval.kt @@ -0,0 +1,123 @@ +package tachiyomi.domain.entries.manga.interactor + +import tachiyomi.domain.entries.manga.model.Manga +import tachiyomi.domain.entries.manga.model.MangaUpdate +import tachiyomi.domain.items.chapter.model.Chapter +import tachiyomi.domain.library.service.LibraryPreferences +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.time.Instant +import java.time.ZonedDateTime +import java.time.temporal.ChronoUnit +import kotlin.math.absoluteValue + +const val MAX_GRACE_PERIOD = 28 + +fun updateIntervalMeta( + manga: Manga, + chapters: List, + zonedDateTime: ZonedDateTime = ZonedDateTime.now(), + setCurrentFetchRange: Pair = 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) { + null + } else { MangaUpdate(id = manga.id, nextUpdate = nextUpdate, calculateInterval = interval) } +} + +fun calculateInterval(chapters: List, zonedDateTime: ZonedDateTime): Int { + val sortedChapters = chapters + .sortedWith(compareByDescending { 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 newInterval = 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 + } + // min 1, max 28 days + return newInterval.coerceIn(1, MAX_GRACE_PERIOD) +} + +private fun calculateNextUpdate( + manga: Manga, + interval: Int, + zonedDateTime: ZonedDateTime, + currentFetchRange: Pair, +): Long { + return if (manga.nextUpdate !in currentFetchRange.first.rangeTo(currentFetchRange.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 cycle = timeSinceLatest.floorDiv(interval.absoluteValue.takeIf { interval < 0 } ?: doubleInterval(interval, timeSinceLatest, doubleWhenOver = 10, maxValue = 28)) + latestDate.plusDays((cycle + 1) * interval.toLong()).toEpochSecond(zonedDateTime.offset) * 1000 + } 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 + } +} + +fun getCurrentFetchRange( + timeToCal: ZonedDateTime, +): Pair { + 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) +} diff --git a/domain/src/main/java/tachiyomi/domain/library/service/LibraryPreferences.kt b/domain/src/main/java/tachiyomi/domain/library/service/LibraryPreferences.kt index 37459d213..178cdfb5d 100644 --- a/domain/src/main/java/tachiyomi/domain/library/service/LibraryPreferences.kt +++ b/domain/src/main/java/tachiyomi/domain/library/service/LibraryPreferences.kt @@ -58,9 +58,16 @@ class LibraryPreferences( ENTRY_HAS_UNVIEWED, ENTRY_NON_COMPLETED, ENTRY_NON_VIEWED, + ENTRY_OUTSIDE_RELEASE_PERIOD, ), ) + fun leadingAnimeExpectedDays() = preferenceStore.getInt("pref_library_before_expect_key", 1) + fun followingAnimeExpectedDays() = preferenceStore.getInt("pref_library_after_expect_key", 1) + + fun leadingMangaExpectedDays() = preferenceStore.getInt("pref_library_before_expect_key", 1) + fun followingMangaExpectedDays() = preferenceStore.getInt("pref_library_after_expect_key", 1) + fun autoUpdateMetadata() = preferenceStore.getBoolean("auto_update_metadata", false) fun autoUpdateTrackers() = preferenceStore.getBoolean("auto_update_trackers", false) @@ -132,6 +139,26 @@ class LibraryPreferences( fun filterCompletedManga() = preferenceStore.getEnum("pref_filter_library_completed_v2", TriStateFilter.DISABLED) + fun filterIntervalCustomAnime() = preferenceStore.getEnum("pref_filter_anime_library_interval_custom", TriStateFilter.DISABLED) + + fun filterIntervalCustomManga() = preferenceStore.getEnum("pref_filter_manga_library_interval_custom", TriStateFilter.DISABLED) + + fun filterIntervalLongAnime() = preferenceStore.getEnum("pref_filter_anime_library_interval_long", TriStateFilter.DISABLED) + + fun filterIntervalLongManga() = preferenceStore.getEnum("pref_filter_manga_library_interval_long", TriStateFilter.DISABLED) + + fun filterIntervalLateAnime() = preferenceStore.getEnum("pref_filter_anime_library_interval_late", TriStateFilter.DISABLED) + + fun filterIntervalLateManga() = preferenceStore.getEnum("pref_filter_manga_library_interval_late", TriStateFilter.DISABLED) + + fun filterIntervalDroppedAnime() = preferenceStore.getEnum("pref_filter_anime_library_interval_dropped", TriStateFilter.DISABLED) + + fun filterIntervalDroppedManga() = preferenceStore.getEnum("pref_filter_manga_library_interval_dropped", TriStateFilter.DISABLED) + + fun filterIntervalPassedAnime() = preferenceStore.getEnum("pref_filter_anime_library_interval_passed", TriStateFilter.DISABLED) + + fun filterIntervalPassedManga() = preferenceStore.getEnum("pref_filter_manga_library_interval_passed", TriStateFilter.DISABLED) + fun filterTrackedAnime(id: Int) = preferenceStore.getEnum("pref_filter_animelib_tracked_${id}_v2", TriStateFilter.DISABLED) @@ -275,5 +302,6 @@ class LibraryPreferences( const val ENTRY_NON_COMPLETED = "manga_ongoing" const val ENTRY_HAS_UNVIEWED = "manga_fully_read" const val ENTRY_NON_VIEWED = "manga_started" + const val ENTRY_OUTSIDE_RELEASE_PERIOD = "manga_outside_release_period" } } diff --git a/domain/src/main/java/tachiyomi/domain/updates/anime/interactor/GetAnimeUpdates.kt b/domain/src/main/java/tachiyomi/domain/updates/anime/interactor/GetAnimeUpdates.kt index ec2d1153d..fdeb48792 100644 --- a/domain/src/main/java/tachiyomi/domain/updates/anime/interactor/GetAnimeUpdates.kt +++ b/domain/src/main/java/tachiyomi/domain/updates/anime/interactor/GetAnimeUpdates.kt @@ -13,10 +13,8 @@ class GetAnimeUpdates( return repository.awaitWithSeen(seen, after) } - fun subscribe(calendar: Calendar): Flow> = subscribe(calendar.time.time) - - fun subscribe(after: Long): Flow> { - return repository.subscribeAllAnimeUpdates(after) + fun subscribe(calendar: Calendar): Flow> { + return repository.subscribeAllAnimeUpdates(calendar.time.time, limit = 250) } fun subscribe(seen: Boolean, after: Long): Flow> { diff --git a/domain/src/main/java/tachiyomi/domain/updates/anime/repository/AnimeUpdatesRepository.kt b/domain/src/main/java/tachiyomi/domain/updates/anime/repository/AnimeUpdatesRepository.kt index 0df253276..1e5495f42 100644 --- a/domain/src/main/java/tachiyomi/domain/updates/anime/repository/AnimeUpdatesRepository.kt +++ b/domain/src/main/java/tachiyomi/domain/updates/anime/repository/AnimeUpdatesRepository.kt @@ -7,7 +7,7 @@ interface AnimeUpdatesRepository { suspend fun awaitWithSeen(seen: Boolean, after: Long): List - fun subscribeAllAnimeUpdates(after: Long): Flow> + fun subscribeAllAnimeUpdates(after: Long, limit: Long): Flow> fun subscribeWithSeen(seen: Boolean, after: Long): Flow> } diff --git a/domain/src/main/java/tachiyomi/domain/updates/manga/interactor/GetMangaUpdates.kt b/domain/src/main/java/tachiyomi/domain/updates/manga/interactor/GetMangaUpdates.kt index 4a19dabef..f15af204c 100644 --- a/domain/src/main/java/tachiyomi/domain/updates/manga/interactor/GetMangaUpdates.kt +++ b/domain/src/main/java/tachiyomi/domain/updates/manga/interactor/GetMangaUpdates.kt @@ -13,10 +13,8 @@ class GetMangaUpdates( return repository.awaitWithRead(read, after) } - fun subscribe(calendar: Calendar): Flow> = subscribe(calendar.time.time) - - fun subscribe(after: Long): Flow> { - return repository.subscribeAllMangaUpdates(after) + fun subscribe(calendar: Calendar): Flow> { + return repository.subscribeAllMangaUpdates(calendar.time.time, limit = 250) } fun subscribe(read: Boolean, after: Long): Flow> { diff --git a/domain/src/main/java/tachiyomi/domain/updates/manga/repository/MangaUpdatesRepository.kt b/domain/src/main/java/tachiyomi/domain/updates/manga/repository/MangaUpdatesRepository.kt index 7b7cd22b4..0d1a8f461 100644 --- a/domain/src/main/java/tachiyomi/domain/updates/manga/repository/MangaUpdatesRepository.kt +++ b/domain/src/main/java/tachiyomi/domain/updates/manga/repository/MangaUpdatesRepository.kt @@ -7,7 +7,7 @@ interface MangaUpdatesRepository { suspend fun awaitWithRead(read: Boolean, after: Long): List - fun subscribeAllMangaUpdates(after: Long): Flow> + fun subscribeAllMangaUpdates(after: Long, limit: Long): Flow> fun subscribeWithRead(read: Boolean, after: Long): Flow> } diff --git a/gradle/androidx.versions.toml b/gradle/androidx.versions.toml index 55c0852ee..f724f12b2 100644 --- a/gradle/androidx.versions.toml +++ b/gradle/androidx.versions.toml @@ -13,7 +13,7 @@ corektx = "androidx.core:core-ktx:1.11.0-beta01" splashscreen = "androidx.core:core-splashscreen:1.0.0-alpha02" recyclerview = "androidx.recyclerview:recyclerview:1.3.1-rc01" viewpager = "androidx.viewpager:viewpager:1.1.0-alpha01" -glance = "androidx.glance:glance-appwidget:1.0.0-alpha03" +glance = "androidx.glance:glance-appwidget:1.0.0-beta01" profileinstaller = "androidx.profileinstaller:profileinstaller:1.3.1" mediasession = "androidx.media:media:1.6.0" diff --git a/i18n/src/main/res/values/strings-aniyomi.xml b/i18n/src/main/res/values/strings-aniyomi.xml index 67898bcd6..748197ae9 100644 --- a/i18n/src/main/res/values/strings-aniyomi.xml +++ b/i18n/src/main/res/values/strings-aniyomi.xml @@ -320,6 +320,8 @@ Hide hidden categories from categories screen + Expected anime release grace period + Exclude from data saver Stop excluding from data saver diff --git a/i18n/src/main/res/values/strings.xml b/i18n/src/main/res/values/strings.xml index a2fc61785..220c311a7 100644 --- a/i18n/src/main/res/values/strings.xml +++ b/i18n/src/main/res/values/strings.xml @@ -42,10 +42,16 @@ Settings Menu Filter + Set interval Bookmarked Tracked Unread Started + Customized fetch interval + Fetch monthly (28 days) + Late 10+ check + Dropped? Late 20+ and 2 months + Passed check period Remove filter Alphabetically @@ -53,6 +59,7 @@ Total chapters Last read Unread count + Next expected update Latest chapter Chapter fetch date Date added @@ -255,6 +262,18 @@ Skip updating entries With \"Completed\" status That haven\'t been started + Outside expected release period + + Expected manga release grace period + + %d day before + %d days before + + + %d day after + %d days after + + A low grace period is recommended to minimize stress on sources. The more checks for an entry that are missed, the longer the interval in between checks will be with a maximum of 28 days. Automatically refresh metadata Check for new cover and details when updating library Automatically refresh trackers @@ -580,6 +599,7 @@ Updating category Local Downloaded chapters + Intervals Overlay Tabs @@ -605,6 +625,10 @@ Invalid chapter format Order by Date + + 1 day + %d days + @@ -644,6 +668,10 @@ Chapter %1$s + Estimate every + Set to update every + Modify interval + Customize Interval Downloading (%1$d/%2$d) Source title Chapter number @@ -837,6 +865,7 @@ Skipped because there are unread chapters Skipped because no chapters are read Skipped because series does not require updates + Skipped because no release was expected today Select cover image diff --git a/presentation-widget/src/main/java/tachiyomi/presentation/widget/entries/anime/AnimeUpdatesGridGlanceReceiver.kt b/presentation-widget/src/main/java/tachiyomi/presentation/widget/entries/anime/AnimeUpdatesGridGlanceReceiver.kt index 2b228d273..5e5500b46 100644 --- a/presentation-widget/src/main/java/tachiyomi/presentation/widget/entries/anime/AnimeUpdatesGridGlanceReceiver.kt +++ b/presentation-widget/src/main/java/tachiyomi/presentation/widget/entries/anime/AnimeUpdatesGridGlanceReceiver.kt @@ -4,5 +4,5 @@ import androidx.glance.appwidget.GlanceAppWidget import androidx.glance.appwidget.GlanceAppWidgetReceiver class AnimeUpdatesGridGlanceReceiver : GlanceAppWidgetReceiver() { - override val glanceAppWidget: GlanceAppWidget = AnimeUpdatesGridGlanceWidget().apply { loadData() } + override val glanceAppWidget: GlanceAppWidget = AnimeUpdatesGridGlanceWidget() } diff --git a/presentation-widget/src/main/java/tachiyomi/presentation/widget/entries/anime/AnimeUpdatesGridGlanceWidget.kt b/presentation-widget/src/main/java/tachiyomi/presentation/widget/entries/anime/AnimeUpdatesGridGlanceWidget.kt index b9614ecfa..e8165315d 100644 --- a/presentation-widget/src/main/java/tachiyomi/presentation/widget/entries/anime/AnimeUpdatesGridGlanceWidget.kt +++ b/presentation-widget/src/main/java/tachiyomi/presentation/widget/entries/anime/AnimeUpdatesGridGlanceWidget.kt @@ -1,17 +1,18 @@ package tachiyomi.presentation.widget.entries.anime import android.app.Application +import android.content.Context import android.graphics.Bitmap import android.os.Build -import androidx.compose.runtime.Composable import androidx.core.graphics.drawable.toBitmap +import androidx.glance.GlanceId import androidx.glance.GlanceModifier import androidx.glance.ImageProvider import androidx.glance.appwidget.GlanceAppWidget import androidx.glance.appwidget.GlanceAppWidgetManager import androidx.glance.appwidget.SizeMode import androidx.glance.appwidget.appWidgetBackground -import androidx.glance.appwidget.updateAll +import androidx.glance.appwidget.provideContent import androidx.glance.background import androidx.glance.layout.fillMaxSize import coil.executeBlocking @@ -23,8 +24,7 @@ import coil.size.Scale import coil.transform.RoundedCornersTransformation import eu.kanade.tachiyomi.core.security.SecurityPreferences import eu.kanade.tachiyomi.util.system.dpToPx -import kotlinx.coroutines.MainScope -import tachiyomi.core.util.lang.launchIO +import tachiyomi.core.util.lang.withIOContext import tachiyomi.domain.entries.anime.model.AnimeCover import tachiyomi.domain.updates.anime.interactor.GetAnimeUpdates import tachiyomi.domain.updates.anime.model.AnimeUpdatesWithRelations @@ -46,33 +46,29 @@ class AnimeUpdatesGridGlanceWidget : GlanceAppWidget() { private val app: Application by injectLazy() private val preferences: SecurityPreferences by injectLazy() - private val coroutineScope = MainScope() - private var data: List>? = null override val sizeMode = SizeMode.Exact - @Composable - override fun Content() { - // If app lock enabled, don't do anything - if (preferences.useAuthenticator().get()) { - LockedAnimeWidget() - return + override suspend fun provideGlance(context: Context, id: GlanceId) { + val locked = preferences.useAuthenticator().get() + if (!locked) loadData() + + provideContent { + // If app lock enabled, don't do anything + if (locked) { + LockedAnimeWidget() + return@provideContent + } + UpdatesAnimeWidget(data) } - UpdatesAnimeWidget(data) } - fun loadData(list: List? = null) { - coroutineScope.launchIO { - // Don't show anything when lock is active - if (preferences.useAuthenticator().get()) { - updateAll(app) - return@launchIO - } - + private suspend fun loadData(list: List? = null) { + withIOContext { val manager = GlanceAppWidgetManager(app) val ids = manager.getGlanceIds(this@AnimeUpdatesGridGlanceWidget::class.java) - if (ids.isEmpty()) return@launchIO + if (ids.isEmpty()) return@withIOContext val processList = list ?: Injekt.get().await( @@ -85,7 +81,6 @@ class AnimeUpdatesGridGlanceWidget : GlanceAppWidget() { .calculateRowAndColumnCount() data = prepareList(processList, rowCount * columnCount) - ids.forEach { update(app, it) } } } diff --git a/presentation-widget/src/main/java/tachiyomi/presentation/widget/entries/anime/TachiyomiAnimeWidgetManager.kt b/presentation-widget/src/main/java/tachiyomi/presentation/widget/entries/anime/TachiyomiAnimeWidgetManager.kt index b849c9842..2690d0509 100644 --- a/presentation-widget/src/main/java/tachiyomi/presentation/widget/entries/anime/TachiyomiAnimeWidgetManager.kt +++ b/presentation-widget/src/main/java/tachiyomi/presentation/widget/entries/anime/TachiyomiAnimeWidgetManager.kt @@ -1,29 +1,36 @@ package tachiyomi.presentation.widget.entries.anime import android.content.Context -import androidx.glance.appwidget.GlanceAppWidgetManager +import androidx.glance.appwidget.updateAll import androidx.lifecycle.LifecycleCoroutineScope +import eu.kanade.tachiyomi.core.security.SecurityPreferences +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import logcat.LogPriority +import tachiyomi.core.util.system.logcat import tachiyomi.domain.updates.anime.interactor.GetAnimeUpdates class TachiyomiAnimeWidgetManager( private val getUpdates: GetAnimeUpdates, + private val securityPreferences: SecurityPreferences, ) { fun Context.init(scope: LifecycleCoroutineScope) { - getUpdates.subscribe( - seen = false, - after = AnimeUpdatesGridGlanceWidget.DateLimit.timeInMillis, + combine( + getUpdates.subscribe(seen = false, after = AnimeUpdatesGridGlanceWidget.DateLimit.timeInMillis), + securityPreferences.useAuthenticator().changes(), + transform = { a, _ -> a }, ) .drop(1) .distinctUntilChanged() .onEach { - val manager = GlanceAppWidgetManager(this) - if (manager.getGlanceIds(AnimeUpdatesGridGlanceWidget::class.java).isNotEmpty()) { - AnimeUpdatesGridGlanceWidget().loadData(it) + try { + AnimeUpdatesGridGlanceWidget().updateAll(this) + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) { "Failed to update widget" } } } .launchIn(scope) diff --git a/presentation-widget/src/main/java/tachiyomi/presentation/widget/entries/manga/MangaUpdatesGridGlanceReceiver.kt b/presentation-widget/src/main/java/tachiyomi/presentation/widget/entries/manga/MangaUpdatesGridGlanceReceiver.kt index 46971593d..463c809da 100644 --- a/presentation-widget/src/main/java/tachiyomi/presentation/widget/entries/manga/MangaUpdatesGridGlanceReceiver.kt +++ b/presentation-widget/src/main/java/tachiyomi/presentation/widget/entries/manga/MangaUpdatesGridGlanceReceiver.kt @@ -4,5 +4,5 @@ import androidx.glance.appwidget.GlanceAppWidget import androidx.glance.appwidget.GlanceAppWidgetReceiver class MangaUpdatesGridGlanceReceiver : GlanceAppWidgetReceiver() { - override val glanceAppWidget: GlanceAppWidget = MangaUpdatesGridGlanceWidget().apply { loadData() } + override val glanceAppWidget: GlanceAppWidget = MangaUpdatesGridGlanceWidget() } diff --git a/presentation-widget/src/main/java/tachiyomi/presentation/widget/entries/manga/MangaUpdatesGridGlanceWidget.kt b/presentation-widget/src/main/java/tachiyomi/presentation/widget/entries/manga/MangaUpdatesGridGlanceWidget.kt index 69d8033cf..d7b78872e 100644 --- a/presentation-widget/src/main/java/tachiyomi/presentation/widget/entries/manga/MangaUpdatesGridGlanceWidget.kt +++ b/presentation-widget/src/main/java/tachiyomi/presentation/widget/entries/manga/MangaUpdatesGridGlanceWidget.kt @@ -1,17 +1,18 @@ package tachiyomi.presentation.widget.entries.manga import android.app.Application +import android.content.Context import android.graphics.Bitmap import android.os.Build -import androidx.compose.runtime.Composable import androidx.core.graphics.drawable.toBitmap +import androidx.glance.GlanceId import androidx.glance.GlanceModifier import androidx.glance.ImageProvider import androidx.glance.appwidget.GlanceAppWidget import androidx.glance.appwidget.GlanceAppWidgetManager import androidx.glance.appwidget.SizeMode import androidx.glance.appwidget.appWidgetBackground -import androidx.glance.appwidget.updateAll +import androidx.glance.appwidget.provideContent import androidx.glance.background import androidx.glance.layout.fillMaxSize import coil.executeBlocking @@ -23,8 +24,7 @@ import coil.size.Scale import coil.transform.RoundedCornersTransformation import eu.kanade.tachiyomi.core.security.SecurityPreferences import eu.kanade.tachiyomi.util.system.dpToPx -import kotlinx.coroutines.MainScope -import tachiyomi.core.util.lang.launchIO +import tachiyomi.core.util.lang.withIOContext import tachiyomi.domain.entries.manga.model.MangaCover import tachiyomi.domain.updates.manga.interactor.GetMangaUpdates import tachiyomi.domain.updates.manga.model.MangaUpdatesWithRelations @@ -46,33 +46,29 @@ class MangaUpdatesGridGlanceWidget : GlanceAppWidget() { private val app: Application by injectLazy() private val preferences: SecurityPreferences by injectLazy() - private val coroutineScope = MainScope() - private var data: List>? = null override val sizeMode = SizeMode.Exact - @Composable - override fun Content() { - // If app lock enabled, don't do anything - if (preferences.useAuthenticator().get()) { - LockedMangaWidget() - return + override suspend fun provideGlance(context: Context, id: GlanceId) { + val locked = preferences.useAuthenticator().get() + if (!locked) loadData() + + provideContent { + // If app lock enabled, don't do anything + if (locked) { + LockedMangaWidget() + return@provideContent + } + UpdatesMangaWidget(data) } - UpdatesMangaWidget(data) } - fun loadData(list: List? = null) { - coroutineScope.launchIO { - // Don't show anything when lock is active - if (preferences.useAuthenticator().get()) { - updateAll(app) - return@launchIO - } - + private suspend fun loadData(list: List? = null) { + withIOContext { val manager = GlanceAppWidgetManager(app) val ids = manager.getGlanceIds(this@MangaUpdatesGridGlanceWidget::class.java) - if (ids.isEmpty()) return@launchIO + if (ids.isEmpty()) return@withIOContext val processList = list ?: Injekt.get().await( @@ -85,7 +81,6 @@ class MangaUpdatesGridGlanceWidget : GlanceAppWidget() { .calculateRowAndColumnCount() data = prepareList(processList, rowCount * columnCount) - ids.forEach { update(app, it) } } } diff --git a/presentation-widget/src/main/java/tachiyomi/presentation/widget/entries/manga/TachiyomiMangaWidgetManager.kt b/presentation-widget/src/main/java/tachiyomi/presentation/widget/entries/manga/TachiyomiMangaWidgetManager.kt index f8b4da56e..77dce84ec 100644 --- a/presentation-widget/src/main/java/tachiyomi/presentation/widget/entries/manga/TachiyomiMangaWidgetManager.kt +++ b/presentation-widget/src/main/java/tachiyomi/presentation/widget/entries/manga/TachiyomiMangaWidgetManager.kt @@ -1,29 +1,36 @@ package tachiyomi.presentation.widget.entries.manga import android.content.Context -import androidx.glance.appwidget.GlanceAppWidgetManager +import androidx.glance.appwidget.updateAll import androidx.lifecycle.LifecycleCoroutineScope +import eu.kanade.tachiyomi.core.security.SecurityPreferences +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import logcat.LogPriority +import tachiyomi.core.util.system.logcat import tachiyomi.domain.updates.manga.interactor.GetMangaUpdates class TachiyomiMangaWidgetManager( private val getUpdates: GetMangaUpdates, + private val securityPreferences: SecurityPreferences, ) { fun Context.init(scope: LifecycleCoroutineScope) { - getUpdates.subscribe( - read = false, - after = MangaUpdatesGridGlanceWidget.DateLimit.timeInMillis, + combine( + getUpdates.subscribe(read = false, after = MangaUpdatesGridGlanceWidget.DateLimit.timeInMillis), + securityPreferences.useAuthenticator().changes(), + transform = { a, _ -> a }, ) .drop(1) .distinctUntilChanged() .onEach { - val manager = GlanceAppWidgetManager(this) - if (manager.getGlanceIds(MangaUpdatesGridGlanceWidget::class.java).isNotEmpty()) { - MangaUpdatesGridGlanceWidget().loadData(it) + try { + MangaUpdatesGridGlanceWidget().updateAll(this) + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) { "Failed to update widget" } } } .launchIn(scope)