Last Commit Merged: 4c65c2311e
This commit is contained in:
LuftVerbot 2023-10-22 14:11:21 +02:00
parent 67d3043533
commit c85576e06b
24 changed files with 604 additions and 103 deletions

View file

@ -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<Episode>,
zonedDateTime: ZonedDateTime = ZonedDateTime.now(),
setCurrentFetchRange: Pair<Long, Long> = 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))
}

View file

@ -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<Chapter>,
zonedDateTime: ZonedDateTime = ZonedDateTime.now(),
setCurrentFetchRange: Pair<Long, Long> = 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))
}

View file

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

View file

@ -139,11 +139,11 @@ class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
setAppCompatDelegateThemeMode(Injekt.get<UiPreferences>().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)
}

View file

@ -19,9 +19,9 @@ class AnimeUpdatesRepositoryImpl(
}
}
override fun subscribeAllAnimeUpdates(after: Long): Flow<List<AnimeUpdatesWithRelations>> {
override fun subscribeAllAnimeUpdates(after: Long, limit: Long): Flow<List<AnimeUpdatesWithRelations>> {
return databaseHandler.subscribeToList {
animeupdatesViewQueries.animeupdates(after, animeUpdateWithRelationMapper)
animeupdatesViewQueries.getRecentAnimeUpdates(after, limit, animeUpdateWithRelationMapper)
}
}

View file

@ -19,9 +19,9 @@ class MangaUpdatesRepositoryImpl(
}
}
override fun subscribeAllMangaUpdates(after: Long): Flow<List<MangaUpdatesWithRelations>> {
override fun subscribeAllMangaUpdates(after: Long, limit: Long): Flow<List<MangaUpdatesWithRelations>> {
return databaseHandler.subscribeToList {
updatesViewQueries.updates(after, mangaUpdateWithRelationMapper)
updatesViewQueries.getRecentUpdates(after, limit, mangaUpdateWithRelationMapper)
}
}

View file

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

View file

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

View file

@ -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<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) {
null
} else { AnimeUpdate(id = anime.id, nextUpdate = nextUpdate, calculateInterval = interval) }
}
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 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, Long>,
): 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<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)
}

View file

@ -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<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) {
null
} else { MangaUpdate(id = manga.id, nextUpdate = nextUpdate, calculateInterval = interval) }
}
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 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, Long>,
): 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<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)
}

View file

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

View file

@ -13,10 +13,8 @@ class GetAnimeUpdates(
return repository.awaitWithSeen(seen, after)
}
fun subscribe(calendar: Calendar): Flow<List<AnimeUpdatesWithRelations>> = subscribe(calendar.time.time)
fun subscribe(after: Long): Flow<List<AnimeUpdatesWithRelations>> {
return repository.subscribeAllAnimeUpdates(after)
fun subscribe(calendar: Calendar): Flow<List<AnimeUpdatesWithRelations>> {
return repository.subscribeAllAnimeUpdates(calendar.time.time, limit = 250)
}
fun subscribe(seen: Boolean, after: Long): Flow<List<AnimeUpdatesWithRelations>> {

View file

@ -7,7 +7,7 @@ interface AnimeUpdatesRepository {
suspend fun awaitWithSeen(seen: Boolean, after: Long): List<AnimeUpdatesWithRelations>
fun subscribeAllAnimeUpdates(after: Long): Flow<List<AnimeUpdatesWithRelations>>
fun subscribeAllAnimeUpdates(after: Long, limit: Long): Flow<List<AnimeUpdatesWithRelations>>
fun subscribeWithSeen(seen: Boolean, after: Long): Flow<List<AnimeUpdatesWithRelations>>
}

View file

@ -13,10 +13,8 @@ class GetMangaUpdates(
return repository.awaitWithRead(read, after)
}
fun subscribe(calendar: Calendar): Flow<List<MangaUpdatesWithRelations>> = subscribe(calendar.time.time)
fun subscribe(after: Long): Flow<List<MangaUpdatesWithRelations>> {
return repository.subscribeAllMangaUpdates(after)
fun subscribe(calendar: Calendar): Flow<List<MangaUpdatesWithRelations>> {
return repository.subscribeAllMangaUpdates(calendar.time.time, limit = 250)
}
fun subscribe(read: Boolean, after: Long): Flow<List<MangaUpdatesWithRelations>> {

View file

@ -7,7 +7,7 @@ interface MangaUpdatesRepository {
suspend fun awaitWithRead(read: Boolean, after: Long): List<MangaUpdatesWithRelations>
fun subscribeAllMangaUpdates(after: Long): Flow<List<MangaUpdatesWithRelations>>
fun subscribeAllMangaUpdates(after: Long, limit: Long): Flow<List<MangaUpdatesWithRelations>>
fun subscribeWithRead(read: Boolean, after: Long): Flow<List<MangaUpdatesWithRelations>>
}

View file

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

View file

@ -320,6 +320,8 @@
<string name="pref_category_hide_hidden">Hide hidden categories from categories screen</string>
<string name="pref_update_anime_release_grace_period">Expected anime release grace period</string>
<!-- TachiyomiSY -->
<string name="data_saver_exclude">Exclude from data saver</string>
<string name="data_saver_stop_exclude">Stop excluding from data saver</string>

View file

@ -42,10 +42,16 @@
<string name="action_settings">Settings</string>
<string name="action_menu">Menu</string>
<string name="action_filter">Filter</string>
<string name="action_set_interval">Set interval</string>
<string name="action_filter_bookmarked">Bookmarked</string>
<string name="action_filter_tracked">Tracked</string>
<string name="action_filter_unread">Unread</string>
<string name="action_filter_started">Started</string>
<string name="action_filter_interval_custom">Customized fetch interval</string>
<string name="action_filter_interval_long">Fetch monthly (28 days)</string>
<string name="action_filter_interval_late">Late 10+ check</string>
<string name="action_filter_interval_dropped">Dropped? Late 20+ and 2 months</string>
<string name="action_filter_interval_passed">Passed check period</string>
<!-- reserved for #4048 -->
<string name="action_filter_empty">Remove filter</string>
<string name="action_sort_alpha">Alphabetically</string>
@ -53,6 +59,7 @@
<string name="action_sort_total">Total chapters</string>
<string name="action_sort_last_read">Last read</string>
<string name="action_sort_unread_count">Unread count</string>
<string name="action_sort_next_updated">Next expected update</string>
<string name="action_sort_latest_chapter">Latest chapter</string>
<string name="action_sort_chapter_fetch_date">Chapter fetch date</string>
<string name="action_sort_date_added">Date added</string>
@ -255,6 +262,18 @@
<string name="pref_library_update_manga_restriction">Skip updating entries</string>
<string name="pref_update_only_non_completed">With \"Completed\" status</string>
<string name="pref_update_only_started">That haven\'t been started</string>
<string name="pref_update_only_in_release_period">Outside expected release period</string>
<string name="pref_update_release_grace_period">Expected manga release grace period</string>
<plurals name="pref_update_release_leading_days">
<item quantity="one">%d day before</item>
<item quantity="other">%d days before</item>
</plurals>
<plurals name="pref_update_release_following_days">
<item quantity="one">%d day after</item>
<item quantity="other">%d days after</item>
</plurals>
<string name="pref_update_release_grace_period_info">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.</string>
<string name="pref_library_update_refresh_metadata">Automatically refresh metadata</string>
<string name="pref_library_update_refresh_metadata_summary">Check for new cover and details when updating library</string>
<string name="pref_library_update_refresh_trackers">Automatically refresh trackers</string>
@ -580,6 +599,7 @@
<string name="updating_category">Updating category</string>
<string name="local_source_badge">Local</string>
<string name="downloaded_chapters">Downloaded chapters</string>
<string name="intervals_header">Intervals</string>
<!-- For badges/buttons on library covers. -->
<string name="overlay_header">Overlay</string>
<string name="tabs_header">Tabs</string>
@ -605,6 +625,10 @@
<string name="local_invalid_format">Invalid chapter format</string>
<string name="local_filter_order_by">Order by</string>
<string name="date">Date</string>
<plurals name="day">
<item quantity="one">1 day</item>
<item quantity="other">%d days</item>
</plurals>
<!-- Item info -->
<plurals name="missing_items">
@ -644,6 +668,10 @@
<!-- Manga chapters / Anime episodes -->
<string name="display_mode_chapter">Chapter %1$s</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_modify_interval_title">Modify 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="show_title">Source title</string>
<string name="show_chapter_number">Chapter number</string>
@ -837,6 +865,7 @@
<string name="skipped_reason_not_caught_up">Skipped because there are unread chapters</string>
<string name="skipped_reason_not_started">Skipped because no chapters are read</string>
<string name="skipped_reason_not_always_update">Skipped because series does not require updates</string>
<string name="skipped_reason_not_in_release_period">Skipped because no release was expected today</string>
<!-- File Picker Titles -->
<string name="file_select_cover">Select cover image</string>

View file

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

View file

@ -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<Pair<Long, Bitmap?>>? = null
override val sizeMode = SizeMode.Exact
@Composable
override fun Content() {
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 (preferences.useAuthenticator().get()) {
if (locked) {
LockedAnimeWidget()
return
return@provideContent
}
UpdatesAnimeWidget(data)
}
fun loadData(list: List<AnimeUpdatesWithRelations>? = 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<AnimeUpdatesWithRelations>? = 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<GetAnimeUpdates>().await(
@ -85,7 +81,6 @@ class AnimeUpdatesGridGlanceWidget : GlanceAppWidget() {
.calculateRowAndColumnCount()
data = prepareList(processList, rowCount * columnCount)
ids.forEach { update(app, it) }
}
}

View file

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

View file

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

View file

@ -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<Pair<Long, Bitmap?>>? = null
override val sizeMode = SizeMode.Exact
@Composable
override fun Content() {
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 (preferences.useAuthenticator().get()) {
if (locked) {
LockedMangaWidget()
return
return@provideContent
}
UpdatesMangaWidget(data)
}
fun loadData(list: List<MangaUpdatesWithRelations>? = 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<MangaUpdatesWithRelations>? = 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<GetMangaUpdates>().await(
@ -85,7 +81,6 @@ class MangaUpdatesGridGlanceWidget : GlanceAppWidget() {
.calculateRowAndColumnCount()
data = prepareList(processList, rowCount * columnCount)
ids.forEach { update(app, it) }
}
}

View file

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