feat(android): port cached_likes (M8 4.2 slice 2)

Mirrors flutter_client/lib/cache/db.dart's CachedLikes Drift table —
composite PK (userId, entityType, entityId) so the same user can
like a track and its album and its artist independently. entityType
is a plain string ("track" | "album" | "artist") for parity with the
Drift schema and the server wire format.

DAO surface tailored to consumers we know are coming:
  - observeLikedTrackIds(userId): Flow<List<String>> — audio-cache
    eviction reads this set to identify "liked" bucket members
  - observeLikedIdsOfType(userId, entityType): generalized variant
  - observeIsLiked(...): scalar Flow for LikeButton composables
  - upsertAll (sync writes)
  - delete (mutation queue → toggle off)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-22 14:03:42 -04:00
parent 17a3e7dd4f
commit 7b55f586ac
3 changed files with 69 additions and 0 deletions
@@ -5,10 +5,12 @@ import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import com.fabledsword.minstrel.cache.db.dao.CachedAlbumDao
import com.fabledsword.minstrel.cache.db.dao.CachedArtistDao
import com.fabledsword.minstrel.cache.db.dao.CachedLikeDao
import com.fabledsword.minstrel.cache.db.dao.CachedTrackDao
import com.fabledsword.minstrel.cache.db.dao.SyncMetadataDao
import com.fabledsword.minstrel.cache.db.entities.CachedAlbumEntity
import com.fabledsword.minstrel.cache.db.entities.CachedArtistEntity
import com.fabledsword.minstrel.cache.db.entities.CachedLikeEntity
import com.fabledsword.minstrel.cache.db.entities.CachedTrackEntity
import com.fabledsword.minstrel.cache.db.entities.SyncMetadataEntity
@@ -31,6 +33,7 @@ import com.fabledsword.minstrel.cache.db.entities.SyncMetadataEntity
CachedArtistEntity::class,
CachedAlbumEntity::class,
CachedTrackEntity::class,
CachedLikeEntity::class,
],
version = 1,
exportSchema = true,
@@ -41,4 +44,5 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun cachedArtistDao(): CachedArtistDao
abstract fun cachedAlbumDao(): CachedAlbumDao
abstract fun cachedTrackDao(): CachedTrackDao
abstract fun cachedLikeDao(): CachedLikeDao
}
@@ -0,0 +1,39 @@
package com.fabledsword.minstrel.cache.db.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.fabledsword.minstrel.cache.db.entities.CachedLikeEntity
import kotlinx.coroutines.flow.Flow
@Dao
interface CachedLikeDao {
/** All track IDs the user has liked. Audio-cache eviction uses this set. */
@Query(
"SELECT entityId FROM cached_likes " +
"WHERE userId = :userId AND entityType = 'track'",
)
fun observeLikedTrackIds(userId: String): Flow<List<String>>
@Query(
"SELECT entityId FROM cached_likes " +
"WHERE userId = :userId AND entityType = :entityType",
)
fun observeLikedIdsOfType(userId: String, entityType: String): Flow<List<String>>
@Query(
"SELECT EXISTS(SELECT 1 FROM cached_likes " +
"WHERE userId = :userId AND entityType = :entityType AND entityId = :entityId)",
)
fun observeIsLiked(userId: String, entityType: String, entityId: String): Flow<Boolean>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsertAll(rows: List<CachedLikeEntity>)
@Query(
"DELETE FROM cached_likes " +
"WHERE userId = :userId AND entityType = :entityType AND entityId = :entityId",
)
suspend fun delete(userId: String, entityType: String, entityId: String)
}
@@ -0,0 +1,26 @@
package com.fabledsword.minstrel.cache.db.entities
import androidx.room.Entity
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
/**
* Like membership row. Mirrors `flutter_client/lib/cache/db.dart`'s
* `CachedLikes` Drift table. Composite primary key — one user may
* independently like a track AND its album AND its artist; rows are
* disambiguated by the (userId, entityType, entityId) triple.
*
* `entityType` is one of "track" | "album" | "artist". Stored as a
* plain string for parity with the Drift schema and the server's wire
* format; an enum doesn't earn its keep here.
*/
@Entity(
tableName = "cached_likes",
primaryKeys = ["userId", "entityType", "entityId"],
)
data class CachedLikeEntity(
val userId: String,
val entityType: String,
val entityId: String,
val likedAt: Instant = Clock.System.now(),
)