mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-29 23:59:32 +00:00
Merge remote-tracking branch 'origin/cmp-rewrite' into feat/extract-hardcoded-strings-pass4
# Conflicts: # composeApp/src/commonMain/composeResources/values-fr/strings.xml # composeApp/src/commonMain/composeResources/values/strings.xml
This commit is contained in:
commit
fa9528d3a7
25 changed files with 544 additions and 26 deletions
|
|
@ -1242,4 +1242,6 @@
|
|||
<string name="unit_bytes_kb">KB</string>
|
||||
<string name="unit_bytes_mb">MB</string>
|
||||
<string name="unit_bytes_gb">GB</string>
|
||||
<string name="cw_new_episode">Nový díl</string>
|
||||
<string name="cw_new_season">Nová sezóna</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1196,4 +1196,20 @@
|
|||
<string name="unit_bytes_kb">KB</string>
|
||||
<string name="unit_bytes_mb">MB</string>
|
||||
<string name="unit_bytes_gb">GB</string>
|
||||
<string name="cw_airs_date">Ausstrahlung am %1$s</string>
|
||||
<string name="cw_airs_today">Ausstrahlung heute</string>
|
||||
<string name="cw_airs_tomorrow">Ausstrahlung morgen</string>
|
||||
<plurals name="cw_airs_in_days">
|
||||
<item quantity="one">Ausstrahlung in %1$d Tag</item>
|
||||
<item quantity="other">Ausstrahlung in %1$d Tagen</item>
|
||||
</plurals>
|
||||
<string name="cw_airs_date_short">%1$s</string>
|
||||
<string name="cw_airs_today_short">Heute</string>
|
||||
<string name="cw_airs_tomorrow_short">Morgen</string>
|
||||
<plurals name="cw_airs_in_days_short">
|
||||
<item quantity="one">In %1$d Tag</item>
|
||||
<item quantity="other">In %1$d Tagen</item>
|
||||
</plurals>
|
||||
<string name="cw_new_episode">Neue Folge</string>
|
||||
<string name="cw_new_season">Neue Staffel</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1040,4 +1040,20 @@
|
|||
<string name="unit_bytes_kb">KB</string>
|
||||
<string name="unit_bytes_mb">MB</string>
|
||||
<string name="unit_bytes_gb">GB</string>
|
||||
<string name="cw_airs_date">Πρεμιέρα %1$s</string>
|
||||
<string name="cw_airs_today">Πρεμιέρα σήμερα</string>
|
||||
<string name="cw_airs_tomorrow">Πρεμιέρα αύριο</string>
|
||||
<plurals name="cw_airs_in_days">
|
||||
<item quantity="one">Πρεμιέρα σε %1$d ημέρα</item>
|
||||
<item quantity="other">Πρεμιέρα σε %1$d ημέρες</item>
|
||||
</plurals>
|
||||
<string name="cw_airs_date_short">%1$s</string>
|
||||
<string name="cw_airs_today_short">Σήμερα</string>
|
||||
<string name="cw_airs_tomorrow_short">Αύριο</string>
|
||||
<plurals name="cw_airs_in_days_short">
|
||||
<item quantity="one">Σε %1$d ημέρα</item>
|
||||
<item quantity="other">Σε %1$d ημέρες</item>
|
||||
</plurals>
|
||||
<string name="cw_new_episode">Νέο επεισόδιο</string>
|
||||
<string name="cw_new_season">Νέα σεζόν</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1158,4 +1158,22 @@
|
|||
<string name="unit_bytes_kb">KB</string>
|
||||
<string name="unit_bytes_mb">MB</string>
|
||||
<string name="unit_bytes_gb">GB</string>
|
||||
<string name="cw_airs_date">Se transmite %1$s</string>
|
||||
<string name="cw_airs_today">Se transmite hoy</string>
|
||||
<string name="cw_airs_tomorrow">Se transmite mañana</string>
|
||||
<plurals name="cw_airs_in_days">
|
||||
<item quantity="one">Se transmite dentro de %1$d día</item>
|
||||
<item quantity="many">Se transmite dentro de %1$d días</item>
|
||||
<item quantity="other">Se transmite dentro de %1$d días</item>
|
||||
</plurals>
|
||||
<string name="cw_airs_date_short">%1$s</string>
|
||||
<string name="cw_airs_today_short">Hoy</string>
|
||||
<string name="cw_airs_tomorrow_short">Mañana</string>
|
||||
<plurals name="cw_airs_in_days_short">
|
||||
<item quantity="one">En %1$d día</item>
|
||||
<item quantity="many">En %1$d días</item>
|
||||
<item quantity="other">En %1$d días</item>
|
||||
</plurals>
|
||||
<string name="cw_new_episode">Nuevo episodio</string>
|
||||
<string name="cw_new_season">Nueva temporada</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1737,4 +1737,23 @@
|
|||
<string name="updates_github_api_error">Erreur API releases GitHub : %1$d</string>
|
||||
<string name="updates_no_channel_release">Aucune mise à jour n’a encore été publiée.</string>
|
||||
<string name="updates_release_missing_title">La release n’a pas de tag ni de nom</string>
|
||||
<!-- Continue Watching air date display -->
|
||||
<string name="cw_airs_date">Diffusion le %1$s</string>
|
||||
<string name="cw_airs_today">Diffusion aujourd’hui</string>
|
||||
<string name="cw_airs_tomorrow">Diffusion demain</string>
|
||||
<plurals name="cw_airs_in_days">
|
||||
<item quantity="one">Diffusion dans %1$d jour</item>
|
||||
<item quantity="many">Diffusion dans %1$d jours</item>
|
||||
<item quantity="other">Diffusion dans %1$d jours</item>
|
||||
</plurals>
|
||||
<string name="cw_airs_date_short">%1$s</string>
|
||||
<string name="cw_airs_today_short">Aujourd’hui</string>
|
||||
<string name="cw_airs_tomorrow_short">Demain</string>
|
||||
<plurals name="cw_airs_in_days_short">
|
||||
<item quantity="one">Dans %1$d jour</item>
|
||||
<item quantity="many">Dans %1$d jours</item>
|
||||
<item quantity="other">Dans %1$d jours</item>
|
||||
</plurals>
|
||||
<string name="cw_new_episode">Nouvel épisode</string>
|
||||
<string name="cw_new_season">Nouvelle saison</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1274,4 +1274,18 @@
|
|||
<string name="unit_bytes_kb">KB</string>
|
||||
<string name="unit_bytes_mb">MB</string>
|
||||
<string name="unit_bytes_gb">GB</string>
|
||||
<string name="cw_new_episode">Episode Baru</string>
|
||||
<string name="cw_new_season">Musim Baru</string>
|
||||
<string name="cw_airs_date">Tayang %1$s</string>
|
||||
<string name="cw_airs_today">Tayang hari ini</string>
|
||||
<string name="cw_airs_tomorrow">Tayang besok</string>
|
||||
<plurals name="cw_airs_in_days">
|
||||
<item quantity="other">Tayang dalam %d hari</item>
|
||||
</plurals>
|
||||
<string name="cw_airs_date_short">%1$s</string>
|
||||
<string name="cw_airs_today_short">Hari ini</string>
|
||||
<string name="cw_airs_tomorrow_short">Besok</string>
|
||||
<plurals name="cw_airs_in_days_short">
|
||||
<item quantity="other">Dalam %d hari</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1170,4 +1170,22 @@
|
|||
<string name="collections_editor_tmdb_subtitle_person">Persona</string>
|
||||
<string name="collections_editor_tmdb_subtitle_director">Regista</string>
|
||||
<string name="collections_editor_tmdb_subtitle_discover">TMDB Discover</string>
|
||||
<string name="cw_airs_date">In onda il %1$s</string>
|
||||
<string name="cw_airs_today">In onda oggi</string>
|
||||
<string name="cw_airs_tomorrow">In onda domani</string>
|
||||
<plurals name="cw_airs_in_days">
|
||||
<item quantity="one">In onda tra %1$d giorno</item>
|
||||
<item quantity="many">In onda tra %1$d giorni</item>
|
||||
<item quantity="other">In onda tra %1$d giorni</item>
|
||||
</plurals>
|
||||
<string name="cw_airs_date_short">%1$s</string>
|
||||
<string name="cw_airs_today_short">Oggi</string>
|
||||
<string name="cw_airs_tomorrow_short">Domani</string>
|
||||
<plurals name="cw_airs_in_days_short">
|
||||
<item quantity="one">Tra %1$d giorno</item>
|
||||
<item quantity="many">Tra %1$d giorni</item>
|
||||
<item quantity="other">Tra %1$d giorni</item>
|
||||
</plurals>
|
||||
<string name="cw_new_episode">Nuovo episodio</string>
|
||||
<string name="cw_new_season">Nuova stagione</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1352,4 +1352,20 @@
|
|||
<string name="unit_bytes_kb">KB</string>
|
||||
<string name="unit_bytes_mb">MB</string>
|
||||
<string name="unit_bytes_gb">GB</string>
|
||||
<string name="cw_new_episode">Ny episode</string>
|
||||
<string name="cw_new_season">Ny sesong</string>
|
||||
<string name="cw_airs_date">Sendes %1$s</string>
|
||||
<string name="cw_airs_today">Sendes i dag</string>
|
||||
<string name="cw_airs_tomorrow">Sendes i morgen</string>
|
||||
<plurals name="cw_airs_in_days">
|
||||
<item quantity="one">Sendes om %d dag</item>
|
||||
<item quantity="other">Sendes om %d dager</item>
|
||||
</plurals>
|
||||
<string name="cw_airs_date_short">%1$s</string>
|
||||
<string name="cw_airs_today_short">I dag</string>
|
||||
<string name="cw_airs_tomorrow_short">I morgen</string>
|
||||
<plurals name="cw_airs_in_days_short">
|
||||
<item quantity="one">Om %d dag</item>
|
||||
<item quantity="other">Om %d dager</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1341,4 +1341,24 @@
|
|||
<string name="unit_bytes_kb">KB</string>
|
||||
<string name="unit_bytes_mb">MB</string>
|
||||
<string name="unit_bytes_gb">GB</string>
|
||||
<string name="cw_airs_date">Emisja %1$s</string>
|
||||
<string name="cw_airs_today">Emisja dzisiaj</string>
|
||||
<string name="cw_airs_tomorrow">Emisja jutro</string>
|
||||
<plurals name="cw_airs_in_days">
|
||||
<item quantity="one">Emisja za %1$d dzień</item>
|
||||
<item quantity="few">Emisja za %1$d dni</item>
|
||||
<item quantity="many">Emisja za %1$d dni</item>
|
||||
<item quantity="other">Emisja za %1$d dni</item>
|
||||
</plurals>
|
||||
<string name="cw_airs_date_short">%1$s</string>
|
||||
<string name="cw_airs_today_short">Dzisiaj</string>
|
||||
<string name="cw_airs_tomorrow_short">Jutro</string>
|
||||
<plurals name="cw_airs_in_days_short">
|
||||
<item quantity="one">Za %1$d dzień</item>
|
||||
<item quantity="few">Za %1$d dni</item>
|
||||
<item quantity="many">Za %1$d dni</item>
|
||||
<item quantity="other">Za %1$d dni</item>
|
||||
</plurals>
|
||||
<string name="cw_new_episode">Nowy odcinek</string>
|
||||
<string name="cw_new_season">Nowy sezon</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1170,4 +1170,22 @@
|
|||
<string name="unit_bytes_kb">KB</string>
|
||||
<string name="unit_bytes_mb">MB</string>
|
||||
<string name="unit_bytes_gb">GB</string>
|
||||
<string name="cw_airs_date">Disponível em %1$s</string>
|
||||
<string name="cw_airs_today">Disponível hoje</string>
|
||||
<string name="cw_airs_tomorrow">Disponível amanhã</string>
|
||||
<plurals name="cw_airs_in_days">
|
||||
<item quantity="one">Disponível em %1$d dia</item>
|
||||
<item quantity="many">Disponível em %1$d dias</item>
|
||||
<item quantity="other">Disponível em %1$d dias</item>
|
||||
</plurals>
|
||||
<string name="cw_airs_date_short">%1$s</string>
|
||||
<string name="cw_airs_today_short">Hoje</string>
|
||||
<string name="cw_airs_tomorrow_short">Amanhã</string>
|
||||
<plurals name="cw_airs_in_days_short">
|
||||
<item quantity="one">Em %1$d dia</item>
|
||||
<item quantity="many">Em %1$d dias</item>
|
||||
<item quantity="other">Em %1$d dias</item>
|
||||
</plurals>
|
||||
<string name="cw_new_episode">Novo Episódio</string>
|
||||
<string name="cw_new_season">Nova Temporada</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1040,4 +1040,20 @@
|
|||
<string name="unit_bytes_kb">KB</string>
|
||||
<string name="unit_bytes_mb">MB</string>
|
||||
<string name="unit_bytes_gb">GB</string>
|
||||
<string name="cw_airs_date">Yayın tarihi: %1$s</string>
|
||||
<string name="cw_airs_today">Bugün yayında</string>
|
||||
<string name="cw_airs_tomorrow">Yarın yayında</string>
|
||||
<plurals name="cw_airs_in_days">
|
||||
<item quantity="one">Yayın tarihi: %1$d gün sonra</item>
|
||||
<item quantity="other">Yayın tarihi: %1$d gün sonra</item>
|
||||
</plurals>
|
||||
<string name="cw_airs_date_short">%1$s</string>
|
||||
<string name="cw_airs_today_short">Bugün</string>
|
||||
<string name="cw_airs_tomorrow_short">Yarın</string>
|
||||
<plurals name="cw_airs_in_days_short">
|
||||
<item quantity="one">%1$d gün sonra</item>
|
||||
<item quantity="other">%1$d gün sonra</item>
|
||||
</plurals>
|
||||
<string name="cw_new_episode">Yeni Bölüm</string>
|
||||
<string name="cw_new_season">Yeni Sezon</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1735,4 +1735,21 @@
|
|||
<string name="updates_github_api_error">GitHub releases API error: %1$d</string>
|
||||
<string name="updates_no_channel_release">No update has been published yet.</string>
|
||||
<string name="updates_release_missing_title">Release has no tag or name</string>
|
||||
<!-- Continue Watching air date display -->
|
||||
<string name="cw_airs_date">Airs %1$s</string>
|
||||
<string name="cw_airs_today">Airs today</string>
|
||||
<string name="cw_airs_tomorrow">Airs tomorrow</string>
|
||||
<plurals name="cw_airs_in_days">
|
||||
<item quantity="one">Airs in %1$d day</item>
|
||||
<item quantity="other">Airs in %1$d days</item>
|
||||
</plurals>
|
||||
<string name="cw_airs_date_short">%1$s</string>
|
||||
<string name="cw_airs_today_short">Today</string>
|
||||
<string name="cw_airs_tomorrow_short">Tomorrow</string>
|
||||
<plurals name="cw_airs_in_days_short">
|
||||
<item quantity="one">In %1$d day</item>
|
||||
<item quantity="other">In %1$d days</item>
|
||||
</plurals>
|
||||
<string name="cw_new_episode">New Episode</string>
|
||||
<string name="cw_new_season">New Season</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,17 @@ fun formatReleaseDateForDisplay(raw: String): String {
|
|||
return "$year ${localizedMonthName(month)} $day"
|
||||
}
|
||||
|
||||
fun formatReleaseDateWithoutYear(raw: String): String {
|
||||
val trimmed = raw.trim()
|
||||
if (trimmed.isEmpty()) return raw
|
||||
val datePart = trimmed.substringBefore('T').trim()
|
||||
val parts = datePart.split('-')
|
||||
if (parts.size != 3) return raw
|
||||
val month = parts[1].toIntOrNull()?.takeIf { it in 1..12 } ?: return raw
|
||||
val day = parts[2].toIntOrNull()?.takeIf { it in 1..31 } ?: return raw
|
||||
return "${localizedMonthName(month)} $day"
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a release/air string (ISO date, year-only, or timestamp prefix) for compact UI (e.g. year chips).
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -7,18 +7,14 @@ import nuvio.composeapp.generated.resources.*
|
|||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@Composable
|
||||
fun localizedContinueWatchingSubtitle(item: ContinueWatchingItem): String {
|
||||
fun localizedContinueWatchingSubtitle(item: ContinueWatchingItem, compact: Boolean = false): String {
|
||||
val seasonNumber = item.seasonNumber
|
||||
val episodeNumber = item.episodeNumber
|
||||
val episodeTitle = item.episodeTitle?.takeIf { it.isNotBlank() }
|
||||
|
||||
val base = when {
|
||||
seasonNumber != null && episodeNumber != null && item.isNextUp ->
|
||||
stringResource(Res.string.continue_watching_up_next_episode, seasonNumber, episodeNumber)
|
||||
seasonNumber != null && episodeNumber != null ->
|
||||
stringResource(Res.string.compose_player_episode_code_full, seasonNumber, episodeNumber)
|
||||
item.isNextUp ->
|
||||
stringResource(Res.string.continue_watching_up_next)
|
||||
item.parentMetaType.equals(CloudLibraryContentType, ignoreCase = true) ->
|
||||
stringResource(Res.string.library_source_cloud)
|
||||
else ->
|
||||
|
|
|
|||
|
|
@ -172,8 +172,13 @@ fun HomeScreen(
|
|||
isTraktProgressActive,
|
||||
traktSettingsUiState.continueWatchingDaysCap,
|
||||
) {
|
||||
val filtered = if (isTraktProgressActive) {
|
||||
watchProgressUiState.entries.filter { !WatchProgressRepository.isDroppedShow(it.parentMetaId) }
|
||||
} else {
|
||||
watchProgressUiState.entries
|
||||
}
|
||||
filterEntriesForTraktContinueWatchingWindow(
|
||||
entries = watchProgressUiState.entries,
|
||||
entries = filtered,
|
||||
isTraktProgressActive = isTraktProgressActive,
|
||||
daysCap = traktSettingsUiState.continueWatchingDaysCap,
|
||||
nowEpochMs = WatchProgressClock.nowEpochMs(),
|
||||
|
|
@ -186,9 +191,19 @@ fun HomeScreen(
|
|||
isTraktProgressActive,
|
||||
continueWatchingPreferences.upNextFromFurthestEpisode,
|
||||
) {
|
||||
val filteredEntries = if (isTraktProgressActive) {
|
||||
watchProgressUiState.entries.filter { !WatchProgressRepository.isDroppedShow(it.parentMetaId) }
|
||||
} else {
|
||||
watchProgressUiState.entries
|
||||
}
|
||||
val filteredWatchedItems = if (isTraktProgressActive) {
|
||||
watchedUiState.items.filter { !WatchProgressRepository.isDroppedShow(it.id) }
|
||||
} else {
|
||||
watchedUiState.items
|
||||
}
|
||||
buildHomeNextUpSeedCandidates(
|
||||
progressEntries = watchProgressUiState.entries,
|
||||
watchedItems = watchedUiState.items,
|
||||
progressEntries = filteredEntries,
|
||||
watchedItems = filteredWatchedItems,
|
||||
isTraktProgressActive = isTraktProgressActive,
|
||||
preferFurthestEpisode = continueWatchingPreferences.upNextFromFurthestEpisode,
|
||||
nowEpochMs = WatchProgressClock.nowEpochMs(),
|
||||
|
|
@ -312,14 +327,25 @@ fun HomeScreen(
|
|||
if (!cached.hasAired && !continueWatchingPreferences.showUnairedNextUp) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
if (isTraktProgressActive && WatchProgressRepository.isDroppedShow(cached.contentId)) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
val item = cached.toContinueWatchingItem() ?: return@mapNotNull null
|
||||
cached.contentId to (cached.sortTimestamp to item)
|
||||
val sortTimestamp = if (item.isReleaseAlert) {
|
||||
com.nuvio.app.features.watchprogress.parseReleaseDateToEpochMs(item.released) ?: cached.lastWatched
|
||||
} else {
|
||||
cached.lastWatched
|
||||
}
|
||||
cached.contentId to (sortTimestamp to item)
|
||||
}.toMap()
|
||||
}
|
||||
val cachedInProgressItems = remember(cachedSnapshots.second) {
|
||||
cachedSnapshots.second.associate { cached ->
|
||||
val cachedInProgressItems = remember(cachedSnapshots.second, isTraktProgressActive) {
|
||||
cachedSnapshots.second.mapNotNull { cached ->
|
||||
if (isTraktProgressActive && WatchProgressRepository.isDroppedShow(cached.contentId)) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
cached.videoId to cached.toContinueWatchingItem()
|
||||
}
|
||||
}.toMap()
|
||||
}
|
||||
|
||||
val effectivNextUpItems = remember(
|
||||
|
|
@ -413,7 +439,6 @@ fun HomeScreen(
|
|||
|
||||
LaunchedEffect(
|
||||
completedSeriesCandidates,
|
||||
cachedNextUpItems,
|
||||
visibleContinueWatchingEntries,
|
||||
metaProviderKey,
|
||||
continueWatchingPreferences.showUnairedNextUp,
|
||||
|
|
@ -452,6 +477,7 @@ fun HomeScreen(
|
|||
candidate.content.id !in cachedResolvedNextUpItems
|
||||
}
|
||||
val resolutionCandidates = candidatesToResolve.take(NEXT_UP_INITIAL_RESOLUTION_LIMIT)
|
||||
val seedLastWatchedMap = completedSeriesCandidates.associate { it.content.id to it.markedAtEpochMs }
|
||||
if (candidatesToResolve.isEmpty()) {
|
||||
nextUpItemsBySeries = cachedResolvedNextUpItems
|
||||
processedNextUpContentIds = completedSeriesCandidates.mapTo(mutableSetOf()) { candidate ->
|
||||
|
|
@ -461,6 +487,7 @@ fun HomeScreen(
|
|||
nextUpItemsBySeries = cachedResolvedNextUpItems,
|
||||
visibleContinueWatchingEntries = visibleContinueWatchingEntries,
|
||||
todayIsoDate = CurrentDateProvider.todayIsoDate(),
|
||||
seedLastWatchedMap = seedLastWatchedMap,
|
||||
)
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
|
@ -523,6 +550,7 @@ fun HomeScreen(
|
|||
nextUpItemsBySeries = results,
|
||||
visibleContinueWatchingEntries = visibleContinueWatchingEntries,
|
||||
todayIsoDate = todayIsoDate,
|
||||
seedLastWatchedMap = seedLastWatchedMap,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -891,7 +919,12 @@ private suspend fun resolveHomeNextUpCandidate(
|
|||
return null
|
||||
}
|
||||
|
||||
return contentId to (completedEntry.markedAtEpochMs to item)
|
||||
val sortTimestamp = if (item.isReleaseAlert) {
|
||||
com.nuvio.app.features.watchprogress.parseReleaseDateToEpochMs(item.released) ?: completedEntry.markedAtEpochMs
|
||||
} else {
|
||||
completedEntry.markedAtEpochMs
|
||||
}
|
||||
return contentId to (sortTimestamp to item)
|
||||
}
|
||||
|
||||
private fun MetaDetails.videoForSeriesAction(action: SeriesPrimaryAction): MetaVideo? {
|
||||
|
|
@ -1065,6 +1098,7 @@ private fun saveContinueWatchingSnapshots(
|
|||
nextUpItemsBySeries: Map<String, Pair<Long, ContinueWatchingItem>>,
|
||||
visibleContinueWatchingEntries: List<WatchProgressEntry>,
|
||||
todayIsoDate: String,
|
||||
seedLastWatchedMap: Map<String, Long>,
|
||||
) {
|
||||
val nextUpCache = nextUpItemsBySeries.mapNotNull { (contentId, pair) ->
|
||||
val item = pair.second
|
||||
|
|
@ -1085,10 +1119,12 @@ private fun saveContinueWatchingSnapshots(
|
|||
hasAired = item.released?.let { released ->
|
||||
isReleasedBy(todayIsoDate = todayIsoDate, releasedDate = released)
|
||||
} ?: true,
|
||||
lastWatched = pair.first,
|
||||
lastWatched = seedLastWatchedMap[contentId] ?: pair.first,
|
||||
sortTimestamp = pair.first,
|
||||
seedSeason = item.nextUpSeedSeasonNumber,
|
||||
seedEpisode = item.nextUpSeedEpisodeNumber,
|
||||
isReleaseAlert = item.isReleaseAlert,
|
||||
isNewSeasonRelease = item.isNewSeasonRelease,
|
||||
)
|
||||
}
|
||||
val inProgressCache = visibleContinueWatchingEntries.map { entry ->
|
||||
|
|
@ -1139,6 +1175,12 @@ private fun ContinueWatchingItem.shouldDisplayInContinueWatching(): Boolean =
|
|||
isNextUp || progressFraction < 0.995f
|
||||
|
||||
private fun CachedNextUpItem.toContinueWatchingItem(): ContinueWatchingItem? {
|
||||
val alertState = com.nuvio.app.features.watchprogress.calculateReleaseAlertState(
|
||||
seedLastUpdatedEpochMs = lastWatched,
|
||||
seedSeasonNumber = seedSeason,
|
||||
nextSeasonNumber = season,
|
||||
releasedIso = released,
|
||||
)
|
||||
return ContinueWatchingItem(
|
||||
parentMetaId = contentId,
|
||||
parentMetaType = contentType,
|
||||
|
|
@ -1166,6 +1208,8 @@ private fun CachedNextUpItem.toContinueWatchingItem(): ContinueWatchingItem? {
|
|||
resumeProgressFraction = null,
|
||||
durationMs = 0L,
|
||||
progressFraction = 0f,
|
||||
isReleaseAlert = alertState.isReleaseAlert,
|
||||
isNewSeasonRelease = alertState.isNewSeasonRelease,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,8 @@ import com.nuvio.app.features.cloud.cloudLibraryDisplayArtworkUrl
|
|||
import com.nuvio.app.features.home.HomeCatalogSettingsRepository
|
||||
import com.nuvio.app.features.watchprogress.ContinueWatchingItem
|
||||
import com.nuvio.app.features.watchprogress.ContinueWatchingSectionStyle
|
||||
import com.nuvio.app.features.watchprogress.CurrentDateProvider
|
||||
import com.nuvio.app.features.watchprogress.computeAirDateBadgeText
|
||||
import kotlin.math.roundToInt
|
||||
import nuvio.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
|
@ -60,12 +62,8 @@ private fun continueWatchingProgressPercent(progressFraction: Float): Int =
|
|||
@Composable
|
||||
private fun localizedContinueWatchingMetaLine(item: ContinueWatchingItem): String =
|
||||
when {
|
||||
item.seasonNumber != null && item.episodeNumber != null && item.isNextUp ->
|
||||
stringResource(Res.string.continue_watching_up_next_episode, item.seasonNumber, item.episodeNumber)
|
||||
item.seasonNumber != null && item.episodeNumber != null ->
|
||||
stringResource(Res.string.compose_player_episode_code_full, item.seasonNumber, item.episodeNumber)
|
||||
item.isNextUp ->
|
||||
stringResource(Res.string.continue_watching_up_next)
|
||||
item.isCloudLibraryItem() ->
|
||||
stringResource(Res.string.library_source_cloud)
|
||||
else ->
|
||||
|
|
@ -430,6 +428,7 @@ private fun ContinueWatchingWideCard(
|
|||
.padding(layout.wideContentPadding),
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
val isCompact = layout.wideCardWidth < 350.dp
|
||||
val wideMetaLine = localizedContinueWatchingMetaLine(item)
|
||||
val episodeTitle = item.episodeTitle?.trim()?.takeIf { it.isNotBlank() }
|
||||
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
|
|
@ -450,7 +449,18 @@ private fun ContinueWatchingWideCard(
|
|||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
if (item.progressFraction <= 0f && item.seasonNumber != null && item.episodeNumber != null) {
|
||||
UpNextBadge(compact = false, textSize = layout.wideBadgeTextSize)
|
||||
val todayIsoDate = CurrentDateProvider.todayIsoDate()
|
||||
val badgeText = when {
|
||||
item.isReleaseAlert -> {
|
||||
if (item.isNewSeasonRelease) stringResource(Res.string.cw_new_season)
|
||||
else stringResource(Res.string.cw_new_episode)
|
||||
}
|
||||
else -> {
|
||||
computeAirDateBadgeText(item.released, todayIsoDate, compact = isCompact)
|
||||
?: stringResource(Res.string.home_continue_watching_up_next)
|
||||
}
|
||||
}
|
||||
UpNextBadge(text = badgeText, compact = isCompact, textSize = layout.wideBadgeTextSize)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
|
|
@ -550,7 +560,18 @@ private fun ContinueWatchingPosterCard(
|
|||
.align(Alignment.TopEnd)
|
||||
.padding(8.dp),
|
||||
) {
|
||||
UpNextBadge(compact = true, textSize = layout.posterBadgeTextSize)
|
||||
val todayIsoDate = CurrentDateProvider.todayIsoDate()
|
||||
val badgeText = when {
|
||||
item.isReleaseAlert -> {
|
||||
if (item.isNewSeasonRelease) stringResource(Res.string.cw_new_season)
|
||||
else stringResource(Res.string.cw_new_episode)
|
||||
}
|
||||
else -> {
|
||||
computeAirDateBadgeText(item.released, todayIsoDate, compact = true)
|
||||
?: stringResource(Res.string.home_continue_watching_up_next)
|
||||
}
|
||||
}
|
||||
UpNextBadge(text = badgeText, compact = true, textSize = layout.posterBadgeTextSize)
|
||||
}
|
||||
}
|
||||
if (item.progressFraction > 0f) {
|
||||
|
|
@ -644,6 +665,7 @@ private fun ArtworkPanel(
|
|||
|
||||
@Composable
|
||||
private fun UpNextBadge(
|
||||
text: String,
|
||||
compact: Boolean,
|
||||
textSize: androidx.compose.ui.unit.TextUnit,
|
||||
) {
|
||||
|
|
@ -660,7 +682,7 @@ private fun UpNextBadge(
|
|||
),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(Res.string.home_continue_watching_up_next),
|
||||
text = text,
|
||||
style = MaterialTheme.typography.labelSmall.copy(
|
||||
fontSize = textSize,
|
||||
fontWeight = FontWeight.Bold,
|
||||
|
|
|
|||
|
|
@ -58,6 +58,8 @@ object TraktProgressRepository {
|
|||
private val _uiState = MutableStateFlow(TraktProgressUiState())
|
||||
val uiState: StateFlow<TraktProgressUiState> = _uiState.asStateFlow()
|
||||
|
||||
private val hiddenProgressShowIds = MutableStateFlow<Set<String>>(emptySet())
|
||||
|
||||
private var hasLoaded = false
|
||||
private var refreshRequestId: Long = 0L
|
||||
private val refreshJobMutex = Mutex()
|
||||
|
|
@ -68,9 +70,23 @@ object TraktProgressRepository {
|
|||
hasLoaded = true
|
||||
}
|
||||
|
||||
fun isShowHiddenFromProgress(contentId: String): Boolean {
|
||||
val ids = hiddenProgressShowIds.value
|
||||
if (ids.isEmpty()) return false
|
||||
val parsed = parseTraktContentIds(contentId)
|
||||
val keys = buildList {
|
||||
add(contentId)
|
||||
parsed.imdb?.takeIf { it.isNotBlank() }?.let { add(it) }
|
||||
parsed.tmdb?.let { add("tmdb:$it") }
|
||||
parsed.trakt?.let { add("trakt:$it") }
|
||||
}
|
||||
return keys.any { ids.contains(it) }
|
||||
}
|
||||
|
||||
fun onProfileChanged() {
|
||||
invalidateInFlightRefreshes()
|
||||
hasLoaded = false
|
||||
hiddenProgressShowIds.value = emptySet()
|
||||
_uiState.value = TraktProgressUiState()
|
||||
ensureLoaded()
|
||||
}
|
||||
|
|
@ -78,6 +94,7 @@ object TraktProgressRepository {
|
|||
fun clearLocalState() {
|
||||
invalidateInFlightRefreshes()
|
||||
hasLoaded = false
|
||||
hiddenProgressShowIds.value = emptySet()
|
||||
_uiState.value = TraktProgressUiState()
|
||||
}
|
||||
|
||||
|
|
@ -147,7 +164,11 @@ object TraktProgressRepository {
|
|||
coroutineScope {
|
||||
val history = async { fetchHistoryEntries(headers) }
|
||||
val watchedShowSeeds = async { fetchWatchedShowSeedEntries(headers) }
|
||||
history.await() + watchedShowSeeds.await()
|
||||
val hiddenShows = async { fetchHiddenShowIds(headers) }
|
||||
|
||||
val list = history.await() + watchedShowSeeds.await()
|
||||
hiddenProgressShowIds.value = hiddenShows.await()
|
||||
list
|
||||
}
|
||||
}.onFailure { error ->
|
||||
if (error is CancellationException) throw error
|
||||
|
|
@ -321,6 +342,35 @@ object TraktProgressRepository {
|
|||
refreshNow()
|
||||
}
|
||||
|
||||
private suspend fun fetchHiddenShowIds(headers: Map<String, String>): Set<String> {
|
||||
val allIds = mutableSetOf<String>()
|
||||
var page = 1
|
||||
val limit = 1000
|
||||
while (true) {
|
||||
val payload = runCatching {
|
||||
httpGetTextWithHeaders(
|
||||
url = "$BASE_URL/users/hidden/dropped?type=show&page=$page&limit=$limit",
|
||||
headers = headers,
|
||||
)
|
||||
}.getOrNull() ?: break
|
||||
|
||||
val items = runCatching {
|
||||
json.decodeFromString<List<TraktHiddenItem>>(payload)
|
||||
}.getOrNull() ?: break
|
||||
|
||||
if (items.isEmpty()) break
|
||||
for (item in items) {
|
||||
val ids = item.show?.ids ?: continue
|
||||
ids.imdb?.takeIf { it.isNotBlank() }?.let { allIds.add(it) }
|
||||
ids.tmdb?.let { allIds.add("tmdb:$it") }
|
||||
ids.trakt?.let { allIds.add("trakt:$it") }
|
||||
}
|
||||
if (items.size < limit) break
|
||||
page++
|
||||
}
|
||||
return allIds
|
||||
}
|
||||
|
||||
private suspend fun fetchPlaybackEntries(headers: Map<String, String>): List<WatchProgressEntry> = withContext(Dispatchers.Default) {
|
||||
val payloads = coroutineScope {
|
||||
val moviesPayload = async {
|
||||
|
|
@ -862,3 +912,11 @@ private data class TraktEpisode(
|
|||
@SerialName("number") val number: Int? = null,
|
||||
@SerialName("ids") val ids: TraktExternalIds? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
private data class TraktHiddenItem(
|
||||
@SerialName("hidden_at") val hiddenAt: String? = null,
|
||||
@SerialName("type") val type: String? = null,
|
||||
@SerialName("show") val show: TraktMedia? = null,
|
||||
@SerialName("movie") val movie: TraktMedia? = null,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ private fun isExplicitlyReleasedBy(
|
|||
return isoDate <= todayIsoDate
|
||||
}
|
||||
|
||||
private fun daysUntilExplicitRelease(
|
||||
internal fun daysUntilExplicitRelease(
|
||||
todayIsoDate: String,
|
||||
releasedDate: String?,
|
||||
): Int? {
|
||||
|
|
@ -82,7 +82,7 @@ private fun daysUntilExplicitRelease(
|
|||
return (isoEpochDay(targetDate) - isoEpochDay(startDate)).toInt()
|
||||
}
|
||||
|
||||
private fun isoCalendarDateOrNull(value: String?): String? {
|
||||
internal fun isoCalendarDateOrNull(value: String?): String? {
|
||||
val datePart = value
|
||||
?.trim()
|
||||
?.substringBefore('T')
|
||||
|
|
@ -99,7 +99,7 @@ private fun isoCalendarDateOrNull(value: String?): String? {
|
|||
return "$normalizedYear-$normalizedMonth-$normalizedDay"
|
||||
}
|
||||
|
||||
private fun isoEpochDay(date: String): Long {
|
||||
internal fun isoEpochDay(date: String): Long {
|
||||
val year = date.substring(0, 4).toLong()
|
||||
val month = date.substring(5, 7).toLong()
|
||||
val day = date.substring(8, 10).toLong()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,112 @@
|
|||
package com.nuvio.app.features.watchprogress
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.nuvio.app.core.format.formatReleaseDateWithoutYear
|
||||
import com.nuvio.app.features.watching.domain.daysUntilExplicitRelease
|
||||
import com.nuvio.app.features.watching.domain.isoCalendarDateOrNull
|
||||
import com.nuvio.app.features.trakt.parseTraktIsoDateTimeToEpochMs
|
||||
import nuvio.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.pluralStringResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import co.touchlab.kermit.Logger
|
||||
|
||||
@Composable
|
||||
fun computeAirDateBadgeText(
|
||||
releasedIso: String?,
|
||||
todayIsoDate: String,
|
||||
compact: Boolean
|
||||
): String? {
|
||||
if (releasedIso.isNullOrBlank() || todayIsoDate.isBlank()) {
|
||||
return null
|
||||
}
|
||||
|
||||
val releaseEpoch = parseTraktIsoDateTimeToEpochMs(releasedIso)
|
||||
if (releaseEpoch != null && WatchProgressClock.nowEpochMs() >= releaseEpoch) {
|
||||
return null
|
||||
}
|
||||
|
||||
val daysUntil = daysUntilExplicitRelease(
|
||||
todayIsoDate = todayIsoDate,
|
||||
releasedDate = releasedIso,
|
||||
) ?: return null
|
||||
|
||||
return when {
|
||||
daysUntil < 0 -> null
|
||||
daysUntil == 0 -> {
|
||||
if (compact) stringResource(Res.string.cw_airs_today_short)
|
||||
else stringResource(Res.string.cw_airs_today)
|
||||
}
|
||||
daysUntil == 1 -> {
|
||||
if (compact) stringResource(Res.string.cw_airs_tomorrow_short)
|
||||
else stringResource(Res.string.cw_airs_tomorrow)
|
||||
}
|
||||
daysUntil in 2..7 -> {
|
||||
if (compact) pluralStringResource(Res.plurals.cw_airs_in_days_short, daysUntil, daysUntil)
|
||||
else pluralStringResource(Res.plurals.cw_airs_in_days, daysUntil, daysUntil)
|
||||
}
|
||||
else -> {
|
||||
val formattedDate = formatReleaseDateWithoutYear(releasedIso)
|
||||
if (compact) stringResource(Res.string.cw_airs_date_short, formattedDate)
|
||||
else stringResource(Res.string.cw_airs_date, formattedDate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun parseReleaseDateToEpochMs(raw: String?): Long? {
|
||||
if (raw.isNullOrBlank()) return null
|
||||
val trimmed = raw.trim()
|
||||
val epochMs = parseTraktIsoDateTimeToEpochMs(trimmed)
|
||||
if (epochMs != null) return epochMs
|
||||
|
||||
val datePart = isoCalendarDateOrNull(trimmed) ?: return null
|
||||
return parseTraktIsoDateTimeToEpochMs("${datePart}T00:00:00Z")
|
||||
}
|
||||
|
||||
class ReleaseAlertState(
|
||||
val isReleaseAlert: Boolean,
|
||||
val isNewSeasonRelease: Boolean,
|
||||
)
|
||||
|
||||
fun calculateReleaseAlertState(
|
||||
seedLastUpdatedEpochMs: Long,
|
||||
seedSeasonNumber: Int?,
|
||||
nextSeasonNumber: Int?,
|
||||
releasedIso: String?,
|
||||
): ReleaseAlertState {
|
||||
val releaseEpoch = parseReleaseDateToEpochMs(releasedIso)
|
||||
val nowMs = WatchProgressClock.nowEpochMs()
|
||||
|
||||
val log = Logger.withTag("ReleaseAlert")
|
||||
log.d {
|
||||
"calculateReleaseAlertState inputs: releasedIso=$releasedIso, " +
|
||||
"releaseEpoch=$releaseEpoch, seedLastUpdatedEpochMs=$seedLastUpdatedEpochMs, " +
|
||||
"seedSeasonNumber=$seedSeasonNumber, nextSeasonNumber=$nextSeasonNumber, nowMs=$nowMs"
|
||||
}
|
||||
|
||||
if (releaseEpoch == null) {
|
||||
log.d { "calculateReleaseAlertState failed: releaseEpoch is null" }
|
||||
return ReleaseAlertState(false, false)
|
||||
}
|
||||
|
||||
val hasAired = nowMs >= releaseEpoch
|
||||
val sixtyDaysMs = 60L * 24 * 60 * 60 * 1000
|
||||
val isReleaseAlert = hasAired &&
|
||||
releaseEpoch > seedLastUpdatedEpochMs &&
|
||||
(nowMs - releaseEpoch) < sixtyDaysMs
|
||||
|
||||
val isNewSeasonRelease = isReleaseAlert &&
|
||||
seedSeasonNumber != null &&
|
||||
nextSeasonNumber != null &&
|
||||
nextSeasonNumber != seedSeasonNumber
|
||||
|
||||
log.d {
|
||||
"calculateReleaseAlertState result: isReleaseAlert=$isReleaseAlert (hasAired=$hasAired, " +
|
||||
"epoch>seed=${releaseEpoch > seedLastUpdatedEpochMs}, ageMs=${nowMs - releaseEpoch}), " +
|
||||
"isNewSeasonRelease=$isNewSeasonRelease"
|
||||
}
|
||||
|
||||
return ReleaseAlertState(
|
||||
isReleaseAlert = isReleaseAlert,
|
||||
isNewSeasonRelease = isNewSeasonRelease
|
||||
)
|
||||
}
|
||||
|
|
@ -25,6 +25,8 @@ data class CachedNextUpItem(
|
|||
val sortTimestamp: Long,
|
||||
val seedSeason: Int? = null,
|
||||
val seedEpisode: Int? = null,
|
||||
val isReleaseAlert: Boolean = false,
|
||||
val isNewSeasonRelease: Boolean = false,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
|
|
|||
|
|
@ -173,6 +173,8 @@ data class ContinueWatchingItem(
|
|||
val resumeProgressFraction: Float? = null,
|
||||
val durationMs: Long,
|
||||
val progressFraction: Float,
|
||||
val isReleaseAlert: Boolean = false,
|
||||
val isNewSeasonRelease: Boolean = false,
|
||||
)
|
||||
|
||||
data class ContinueWatchingPreferencesUiState(
|
||||
|
|
@ -233,6 +235,8 @@ internal fun WatchProgressEntry.toContinueWatchingItem(): ContinueWatchingItem {
|
|||
resumeProgressFraction = explicitResumeProgressFraction,
|
||||
durationMs = normalizedEntry.durationMs,
|
||||
progressFraction = normalizedEntry.progressFraction,
|
||||
isReleaseAlert = false,
|
||||
isNewSeasonRelease = false,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -249,6 +253,12 @@ private fun WatchProgressEntry.cloudLibraryPosterFallbackUrl(): String? {
|
|||
internal fun WatchProgressEntry.toUpNextContinueWatchingItem(
|
||||
nextEpisode: MetaVideo,
|
||||
): ContinueWatchingItem {
|
||||
val alertState = calculateReleaseAlertState(
|
||||
seedLastUpdatedEpochMs = lastUpdatedEpochMs,
|
||||
seedSeasonNumber = seasonNumber,
|
||||
nextSeasonNumber = nextEpisode.season,
|
||||
releasedIso = nextEpisode.released,
|
||||
)
|
||||
return ContinueWatchingItem(
|
||||
parentMetaId = parentMetaId,
|
||||
parentMetaType = parentMetaType,
|
||||
|
|
@ -281,6 +291,8 @@ internal fun WatchProgressEntry.toUpNextContinueWatchingItem(
|
|||
resumeProgressFraction = null,
|
||||
durationMs = 0L,
|
||||
progressFraction = 0f,
|
||||
isReleaseAlert = alertState.isReleaseAlert,
|
||||
isNewSeasonRelease = alertState.isNewSeasonRelease,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -585,4 +585,8 @@ object WatchProgressRepository {
|
|||
}
|
||||
}
|
||||
|
||||
fun isDroppedShow(contentId: String): Boolean {
|
||||
return shouldUseTraktProgress() && TraktProgressRepository.isShowHiddenFromProgress(contentId)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,4 +33,24 @@ class ReleaseDateDisplayTest {
|
|||
fun extractsYearFromYearOnly() {
|
||||
assertEquals(2024, extractReleaseYearForDisplay("2024"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun formatsIsoDateWithoutYear() {
|
||||
assertEquals("February 1", formatReleaseDateWithoutYear("2025-02-01"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun formatReleaseDateWithoutYearStripsTimePortion() {
|
||||
assertEquals("January 15", formatReleaseDateWithoutYear("2024-01-15T12:30:00Z"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun formatReleaseDateWithoutYearLeavesYearOnlyUnchanged() {
|
||||
assertEquals("2024", formatReleaseDateWithoutYear("2024"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun formatReleaseDateWithoutYearLeavesNonIsoUnchanged() {
|
||||
assertEquals("TBA", formatReleaseDateWithoutYear("TBA"))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -143,4 +143,38 @@ class WatchingPoliciesTest {
|
|||
assertEquals(2, latestCompleted.seasonNumber)
|
||||
assertEquals(3, latestCompleted.episodeNumber)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isoCalendarDateOrNull parses various date formats`() {
|
||||
assertEquals("2026-05-23", isoCalendarDateOrNull("2026-05-23T12:00:00Z"))
|
||||
assertEquals("2026-05-23", isoCalendarDateOrNull(" 2026-05-23 "))
|
||||
assertEquals(null, isoCalendarDateOrNull("2026-5-23"))
|
||||
assertEquals(null, isoCalendarDateOrNull("2026-5-9"))
|
||||
assertEquals(null, isoCalendarDateOrNull("2026-05"))
|
||||
assertEquals(null, isoCalendarDateOrNull("invalid"))
|
||||
assertEquals(null, isoCalendarDateOrNull(null))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `daysUntilExplicitRelease calculates correct offset`() {
|
||||
assertEquals(0, daysUntilExplicitRelease("2026-05-23", "2026-05-23"))
|
||||
assertEquals(1, daysUntilExplicitRelease("2026-05-23", "2026-05-24"))
|
||||
assertEquals(7, daysUntilExplicitRelease("2026-05-23", "2026-05-30"))
|
||||
assertEquals(-3, daysUntilExplicitRelease("2026-05-23", "2026-05-20"))
|
||||
assertEquals(null, daysUntilExplicitRelease("invalid", "2026-05-23"))
|
||||
assertEquals(null, daysUntilExplicitRelease("2026-05-23", null))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isoEpochDay handles leap years and standard calculations`() {
|
||||
// Epoch reference (1970-01-01 is epoch day 0)
|
||||
assertEquals(0L, isoEpochDay("1970-01-01"))
|
||||
assertEquals(20596L, isoEpochDay("2026-05-23"))
|
||||
// Leap year: 2024-02-29
|
||||
assertEquals(19782L, isoEpochDay("2024-02-29"))
|
||||
assertEquals(19783L, isoEpochDay("2024-03-01"))
|
||||
// Non-leap year: 2023-02-28
|
||||
assertEquals(19416L, isoEpochDay("2023-02-28"))
|
||||
assertEquals(19417L, isoEpochDay("2023-03-01"))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -265,6 +265,19 @@ class WatchProgressRulesTest {
|
|||
assertEquals("kitsu:244:2", item.videoId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parseReleaseDateToEpochMs handles ISO and date-only formats`() {
|
||||
val t1 = parseReleaseDateToEpochMs("2026-05-24T15:00:00Z")
|
||||
assertEquals(1779634800000L, t1)
|
||||
|
||||
val t2 = parseReleaseDateToEpochMs("2026-05-24")
|
||||
assertEquals(1779580800000L, t2) // 2026-05-24T00:00:00Z is 1779580800 seconds
|
||||
|
||||
assertNull(parseReleaseDateToEpochMs(null))
|
||||
assertNull(parseReleaseDateToEpochMs(" "))
|
||||
assertNull(parseReleaseDateToEpochMs("invalid-date"))
|
||||
}
|
||||
|
||||
private fun entry(
|
||||
videoId: String,
|
||||
parentMetaId: String = videoId.substringBefore(':'),
|
||||
|
|
|
|||
Loading…
Reference in a new issue