fix(android): playlist like state + playlist cover URL
android / Build + lint + test (push) Successful in 3m48s
android / Build + lint + test (push) Successful in 3m48s
Two independent bugs surfaced together: Bug 1: like button on tracks in playlist/album detail screens didn't reflect actual liked state. LikesRepository.observeLikedTracks() does a mapNotNull join against trackDao - a liked track whose row isn't in the local cache yet (e.g. liked via web/notification, cache not sync'd) gets DROPPED from the returned list. PlaylistDetailViewModel + AlbumDetailViewModel both used that as their like-set source, so those rows showed as not-liked. Adds LikesRepository.observeLikedTrackIds(): Flow<Set<String>> that hits the DAO directly via observeLikedIdsOfType - no trackDao join, no drops. The two ViewModels switch to it. LikedTab continues to use observeLikedTracks because it needs the full TrackRef to render. Bug 2: playlist cover art didn't render on the playlist detail header. Server's derivePlaylistView returns CoverURL as the relative path "/api/playlists/<id>/cover". PlaylistsRepository's two domain mappers (CachedPlaylistEntity.toDomain + PlaylistDetailWire .toPlaylistRef) stored it verbatim - Coil's AsyncImage can't fetch a relative URL with no base, so the image silently failed. Wraps the coverPath/coverUrl through resolveServerUrl so the placeholder.invalid host triggers BaseUrlInterceptor's live-server rewrite, same idiom every other cover surface (album / artist / track / playlist track rows) already uses. System-playlist 24h refresh investigation pending - need to know how you verified (server logs, DB state, or client-visible content) before I can dig into the right layer.
This commit is contained in:
+1
-3
@@ -18,7 +18,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
@@ -56,8 +55,7 @@ class AlbumDetailViewModel @Inject constructor(
|
||||
)
|
||||
|
||||
val likedTrackIds: StateFlow<Set<String>> =
|
||||
likes.observeLikedTracks()
|
||||
.map { tracks -> tracks.mapTo(mutableSetOf()) { it.id } }
|
||||
likes.observeLikedTrackIds()
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(SHARE_STOP_TIMEOUT_MS),
|
||||
|
||||
@@ -110,6 +110,20 @@ class LikesRepository @Inject constructor(
|
||||
fun observeIsLiked(entityType: String, entityId: String): Flow<Boolean> =
|
||||
likeDao.observeIsLiked(currentUserId(), entityType, entityId)
|
||||
|
||||
/**
|
||||
* Reactive set of liked track ids. Use this when the caller only
|
||||
* needs "is X in liked set", NOT when it needs to render the liked
|
||||
* tracks themselves — [observeLikedTracks] does a `mapNotNull` join
|
||||
* against `trackDao` and drops any liked id whose track isn't in
|
||||
* local cache yet, so an "is liked" UI built on `observeLikedTracks`
|
||||
* misses cross-device likes whose track row hasn't cached.
|
||||
*
|
||||
* Used by playlist/album detail screens to color a row's like
|
||||
* button independent of whether the track is in the local library.
|
||||
*/
|
||||
fun observeLikedTrackIds(): Flow<Set<String>> =
|
||||
likeDao.observeLikedIdsOfType(currentUserId(), ENTITY_TRACK).map { it.toSet() }
|
||||
|
||||
/** One-shot snapshot of the liked track-id set — for the offline pool filter. */
|
||||
suspend fun likedTrackIds(): Set<String> =
|
||||
likeDao.observeLikedIdsOfType(currentUserId(), ENTITY_TRACK).first().toSet()
|
||||
|
||||
+10
-2
@@ -16,6 +16,7 @@ import com.fabledsword.minstrel.models.TrackRef
|
||||
import com.fabledsword.minstrel.models.wire.PlaylistDetailWire
|
||||
import com.fabledsword.minstrel.models.wire.PlaylistTrackWire
|
||||
import com.fabledsword.minstrel.models.wire.PlaylistWire
|
||||
import com.fabledsword.minstrel.shared.resolveServerUrl
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import retrofit2.Retrofit
|
||||
@@ -241,7 +242,11 @@ private fun CachedPlaylistEntity.toDomain(): PlaylistRef =
|
||||
isPublic = isPublic,
|
||||
systemVariant = systemVariant,
|
||||
trackCount = trackCount,
|
||||
coverUrl = coverPath ?: "",
|
||||
// Server returns a relative cover path like "/api/playlists/<id>/cover";
|
||||
// wrap through the placeholder host so BaseUrlInterceptor + the shared
|
||||
// Coil/OkHttpClient resolve it against the live server. Without this
|
||||
// wrap AsyncImage silently fails on the relative URL.
|
||||
coverUrl = resolveServerUrl(coverPath) ?: "",
|
||||
)
|
||||
|
||||
private fun PlaylistWire.toEntity(): CachedPlaylistEntity =
|
||||
@@ -277,7 +282,10 @@ private fun PlaylistDetailWire.toPlaylistRef(): PlaylistRef =
|
||||
isPublic = isPublic,
|
||||
systemVariant = systemVariant,
|
||||
trackCount = trackCount,
|
||||
coverUrl = coverUrl,
|
||||
// Same relative-path wrap as CachedPlaylistEntity.toDomain — the wire
|
||||
// gives us /api/playlists/<id>/cover and Coil needs the placeholder
|
||||
// host for BaseUrlInterceptor to rewrite to the live server.
|
||||
coverUrl = resolveServerUrl(coverUrl) ?: "",
|
||||
ownerUsername = ownerUsername,
|
||||
)
|
||||
|
||||
|
||||
+1
-3
@@ -87,7 +87,6 @@ import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -141,8 +140,7 @@ class PlaylistDetailViewModel @Inject constructor(
|
||||
val regenerated: Flow<String> = regeneratedChannel.receiveAsFlow()
|
||||
|
||||
val likedTrackIds: StateFlow<Set<String>> =
|
||||
likes.observeLikedTracks()
|
||||
.map { tracks -> tracks.mapTo(mutableSetOf()) { it.id } }
|
||||
likes.observeLikedTrackIds()
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(SHARE_STOP_TIMEOUT_MS),
|
||||
|
||||
Reference in New Issue
Block a user