Merge pull request #1683 from skoruppa/cmp-rewrite

CW flicker, image cache revalidation, Simkl anime movies watched status
This commit is contained in:
Nayif 2026-08-04 17:32:26 +05:30 committed by GitHub
commit 3902edd7ee
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 45 additions and 15 deletions

View file

@ -434,6 +434,9 @@ kotlin {
implementation("io.coil-kt.coil3:coil-network-ktor3:${libs.versions.coil.get()}") {
exclude(group = "org.jetbrains.skiko", module = "skiko")
}
implementation("io.coil-kt.coil3:coil-network-cache-control:${libs.versions.coil.get()}") {
exclude(group = "org.jetbrains.skiko", module = "skiko")
}
implementation("io.coil-kt.coil3:coil-svg:${libs.versions.coil.get()}") {
exclude(group = "org.jetbrains.skiko", module = "skiko")
}

View file

@ -446,6 +446,11 @@ fun App(
.memoryCachePolicy(CachePolicy.ENABLED)
.components {
add(SvgDecoder.Factory())
add(
coil3.network.ktor3.KtorNetworkFetcherFactory(
cacheStrategy = { coil3.network.cachecontrol.CacheControlCacheStrategy() },
)
)
}
.configurePlatformImageLoader()
.build()

View file

@ -1796,12 +1796,16 @@ private fun ContinueWatchingItem.withFallbackMetadata(
hasPlaceholderHomeTitle() && fallbackTitle != null -> fallbackTitle
else -> title
},
subtitle = subtitle.takeIf { it.isNotBlank() }
?: fallback?.subtitle?.takeIf { it.isNotBlank() }.orEmpty(),
subtitle = when {
subtitle.isBlank() -> fallback?.subtitle?.takeIf { it.isNotBlank() }.orEmpty()
fallback?.subtitle.isNullOrBlank() -> subtitle
else -> fallback.subtitle
},
imageUrl = imageUrl.orNonBlank(fallback?.imageUrl),
logo = logo.orNonBlank(fallback?.logo),
poster = poster.orNonBlank(fallback?.poster),
background = background.orNonBlank(fallback?.background),
videoId = fallback?.videoId?.takeIf { it.isNotBlank() } ?: videoId,
episodeTitle = episodeTitle.orNonBlank(fallback?.episodeTitle),
episodeThumbnail = episodeThumbnail.orNonBlank(fallback?.episodeThumbnail),
pauseDescription = pauseDescription.orNonBlank(fallback?.pauseDescription),

View file

@ -290,7 +290,11 @@ private fun HomeContinueWatchingSectionContent(
key(dataSourceKey) {
val disintegration = remember {
ScopedDisintegrationTracker<WatchProgressSource, String, ContinueWatchingItem>(
itemKey = ContinueWatchingItem::videoId,
itemKey = { item ->
val season = item.seasonNumber ?: -1
val episode = item.episodeNumber ?: -1
"${item.parentMetaId}:$season:$episode"
},
)
}
val displayEntries = disintegration.sync(dataSourceKey, items)

View file

@ -28,7 +28,7 @@ internal fun SimklSyncSnapshot.toSimklWatchedProjection(): SimklWatchedProjectio
entries.forEach { entry ->
val media = entry.media ?: return@forEach
val contentId = media.canonicalContentId() ?: return@forEach
val contentType = if (entry.mediaType == SimklMediaType.MOVIES) "movie" else "series"
val contentType = if (entry.isMovieEntry()) "movie" else "series"
val title = media.title?.takeIf(String::isNotBlank) ?: contentId
val poster = entry.resolvedPosterUrl()
val trackingProviderItemId = media.simklTrackingProviderItemId()
@ -37,7 +37,7 @@ internal fun SimklSyncSnapshot.toSimklWatchedProjection(): SimklWatchedProjectio
?: parseSimklUtcEpochMs(entry.addedToWatchlistAt)
?: 0L
if (entry.mediaType == SimklMediaType.MOVIES) {
if (entry.isMovieEntry()) {
if (entry.lastWatchedAt != null || entry.status == SimklListStatus.COMPLETED) {
watchedItems += WatchedItem(
id = contentId,
@ -115,6 +115,7 @@ internal fun SimklSyncSnapshot.animeAlternateWatchedKeys(): Set<String> {
val extraKeys = linkedSetOf<String>()
entries.forEach { entry ->
if (entry.mediaType != SimklMediaType.ANIME) return@forEach
if (entry.isMovieEntry()) return@forEach
val media = entry.media ?: return@forEach
val contentId = media.canonicalContentId() ?: return@forEach
val contentType = "series"
@ -150,7 +151,7 @@ internal fun SimklSyncSnapshot.animeAlternateWatchedKeys(): Set<String> {
internal fun SimklSyncSnapshot.movieAlternateWatchedKeys(): Set<String> {
val extraKeys = linkedSetOf<String>()
entries.forEach { entry ->
if (entry.mediaType != SimklMediaType.MOVIES) return@forEach
if (!entry.isMovieEntry()) return@forEach
if (entry.lastWatchedAt == null && entry.status != SimklListStatus.COMPLETED) return@forEach
val media = entry.media ?: return@forEach
val contentId = media.canonicalContentId() ?: return@forEach
@ -379,7 +380,8 @@ internal fun parseSimklUtcEpochMs(value: String?): Long? {
internal fun SimklPlaybackSession.toWatchProgressEntry(): WatchProgressEntry? {
val media = media ?: return null
val parentId = media.canonicalContentId() ?: return null
val isMovie = mediaType == SimklMediaType.MOVIES
val isMovie = mediaType == SimklMediaType.MOVIES ||
(mediaType == SimklMediaType.ANIME && episode == null)
val season = episode?.tvdbSeason ?: episode?.season
val episodeNumber = episode?.tvdbNumber ?: episode?.number
if (!isMovie && episodeNumber == null) return null

View file

@ -32,15 +32,17 @@ private fun SimklSyncSnapshot.withPausedScrobble(
.mergeMissing(existingEntry?.media)
.mergeMissing(existingSession?.media)
val mediaType = existingEntry?.mediaType ?: result.mediaType
val isAnimeMovie = mediaType == SimklMediaType.ANIME &&
(existingEntry?.animeType == "movie" || result.episode == null)
val session = SimklPlaybackSession(
id = result.playbackId ?: existingSession?.id,
progress = result.progress,
pausedAt = committedAt,
type = if (mediaType == SimklMediaType.MOVIES) "movie" else "episode",
type = if (mediaType == SimklMediaType.MOVIES || isAnimeMovie) "movie" else "episode",
episode = result.episode?.mergeMissing(existingSession?.episode),
show = media.takeIf { mediaType == SimklMediaType.SHOWS },
anime = media.takeIf { mediaType == SimklMediaType.ANIME },
movie = media.takeIf { mediaType == SimklMediaType.MOVIES },
anime = media.takeIf { mediaType == SimklMediaType.ANIME && !isAnimeMovie },
movie = media.takeIf { mediaType == SimklMediaType.MOVIES || isAnimeMovie },
)
return copy(
playback = playback.filterNot { candidate ->
@ -56,11 +58,13 @@ private fun SimklSyncSnapshot.withCompletedScrobble(
val updatedEntries = entries.toMutableList()
val index = updatedEntries.indexOfMatchingEntry(result)
val existing = updatedEntries.getOrNull(index)
val updated = when (result.mediaType) {
SimklMediaType.MOVIES -> existing
val updated = when {
result.mediaType == SimklMediaType.MOVIES ||
(result.mediaType == SimklMediaType.ANIME &&
(existing?.animeType == "movie" || result.episode == null)) -> existing
?.withWatchedMovie(result, committedAt)
?: result.toWatchedMovieEntry(committedAt)
SimklMediaType.SHOWS, SimklMediaType.ANIME -> existing
else -> existing
?.withWatchedEpisode(result, committedAt)
?: result.toWatchedSeriesEntry(committedAt)
}
@ -81,7 +85,8 @@ private fun SimklLibraryEntry.withWatchedMovie(
result: SimklScrobbleResult,
committedAt: String,
): SimklLibraryEntry = copy(
mediaType = SimklMediaType.MOVIES,
mediaType = result.mediaType,
animeType = if (result.mediaType == SimklMediaType.ANIME) "movie" else animeType,
lastWatchedAt = committedAt,
status = SimklListStatus.COMPLETED,
movie = result.media.mergeMissing(media),
@ -90,7 +95,8 @@ private fun SimklLibraryEntry.withWatchedMovie(
private fun SimklScrobbleResult.toWatchedMovieEntry(committedAt: String): SimklLibraryEntry =
SimklLibraryEntry(
mediaType = SimklMediaType.MOVIES,
mediaType = mediaType,
animeType = if (mediaType == SimklMediaType.ANIME) "movie" else null,
lastWatchedAt = committedAt,
status = SimklListStatus.COMPLETED,
movie = media,

View file

@ -80,6 +80,11 @@ data class SimklLibraryEntry(
fun stableKey(): String? = media?.ids?.stableMediaId()?.let { id ->
"${mediaType.apiValue}:$id"
}
/** True when this entry represents a movie — either a regular movie or an anime movie. */
fun isMovieEntry(): Boolean =
mediaType == SimklMediaType.MOVIES ||
(mediaType == SimklMediaType.ANIME && animeType == "movie")
}
@Serializable

View file

@ -59,6 +59,7 @@ compottie = { module = "io.github.alexzhirkevich:compottie", version.ref = "comp
coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }
coil-gif = { module = "io.coil-kt.coil3:coil-gif", version.ref = "coil" }
coil-network-ktor3 = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil" }
coil-network-cache-control = { module = "io.coil-kt.coil3:coil-network-cache-control", version.ref = "coil" }
coil-svg = { module = "io.coil-kt.coil3:coil-svg", version.ref = "coil" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
kotlinx-atomicfu = { module = "org.jetbrains.kotlinx:atomicfu", version.ref = "atomicfu" }