feat(android): port cached_quarantine_mine (M8 4.2 slice 4)

Mirrors flutter_client/lib/cache/db.dart's CachedQuarantineMine —
the user's flagged-as-hidden tracks. Server returns the full
denormalized snapshot on /api/me/quarantine; the cache mirrors that
shape so the Quarantine screen renders without a join.

`createdAt` is a server ISO-8601 string (canonical timestamp);
`fetchedAt` is our local sync marker.

DAO covers the three known consumers:
  - observeAll for the Quarantine screen (newest first)
  - observeFlaggedTrackIds for feed-level filtering
  - observeIsHidden(trackId) scalar Flow for per-tile UI

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-22 14:16:11 -04:00
parent 4b38623229
commit c18ad19418
3 changed files with 76 additions and 0 deletions
@@ -8,6 +8,7 @@ import com.fabledsword.minstrel.cache.db.dao.CachedArtistDao
import com.fabledsword.minstrel.cache.db.dao.CachedLikeDao
import com.fabledsword.minstrel.cache.db.dao.CachedPlaylistDao
import com.fabledsword.minstrel.cache.db.dao.CachedPlaylistTrackDao
import com.fabledsword.minstrel.cache.db.dao.CachedQuarantineDao
import com.fabledsword.minstrel.cache.db.dao.CachedTrackDao
import com.fabledsword.minstrel.cache.db.dao.SyncMetadataDao
import com.fabledsword.minstrel.cache.db.entities.CachedAlbumEntity
@@ -15,6 +16,7 @@ import com.fabledsword.minstrel.cache.db.entities.CachedArtistEntity
import com.fabledsword.minstrel.cache.db.entities.CachedLikeEntity
import com.fabledsword.minstrel.cache.db.entities.CachedPlaylistEntity
import com.fabledsword.minstrel.cache.db.entities.CachedPlaylistTrackEntity
import com.fabledsword.minstrel.cache.db.entities.CachedQuarantineEntity
import com.fabledsword.minstrel.cache.db.entities.CachedTrackEntity
import com.fabledsword.minstrel.cache.db.entities.SyncMetadataEntity
@@ -40,6 +42,7 @@ import com.fabledsword.minstrel.cache.db.entities.SyncMetadataEntity
CachedLikeEntity::class,
CachedPlaylistEntity::class,
CachedPlaylistTrackEntity::class,
CachedQuarantineEntity::class,
],
version = 1,
exportSchema = true,
@@ -53,4 +56,5 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun cachedLikeDao(): CachedLikeDao
abstract fun cachedPlaylistDao(): CachedPlaylistDao
abstract fun cachedPlaylistTrackDao(): CachedPlaylistTrackDao
abstract fun cachedQuarantineDao(): CachedQuarantineDao
}
@@ -0,0 +1,36 @@
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.CachedQuarantineEntity
import kotlinx.coroutines.flow.Flow
@Dao
interface CachedQuarantineDao {
/** Newest-first listing for the Quarantine screen. */
@Query("SELECT * FROM cached_quarantine_mine ORDER BY createdAt DESC")
fun observeAll(): Flow<List<CachedQuarantineEntity>>
/** Just the IDs — feed-filtering code reads this to hide tracks. */
@Query("SELECT trackId FROM cached_quarantine_mine")
fun observeFlaggedTrackIds(): Flow<List<String>>
@Query(
"SELECT EXISTS(SELECT 1 FROM cached_quarantine_mine WHERE trackId = :trackId)",
)
fun observeIsHidden(trackId: String): Flow<Boolean>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsert(row: CachedQuarantineEntity)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsertAll(rows: List<CachedQuarantineEntity>)
@Query("DELETE FROM cached_quarantine_mine WHERE trackId = :trackId")
suspend fun deleteByTrackId(trackId: String)
@Query("DELETE FROM cached_quarantine_mine WHERE trackId IN (:trackIds)")
suspend fun deleteByTrackIds(trackIds: List<String>)
}
@@ -0,0 +1,36 @@
package com.fabledsword.minstrel.cache.db.entities
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
/**
* The current user's quarantine flag for one track. Mirrors
* `flutter_client/lib/cache/db.dart`'s `CachedQuarantineMine` Drift
* table.
*
* The flat denormalized track/album/artist columns let the Quarantine
* screen render off this single table without a join — the server
* returns the full snapshot on /api/me/quarantine so we mirror that
* shape locally.
*
* `createdAt` is the server-issued timestamp preserved as an opaque
* ISO-8601 string (it's the canonical "when this was flagged" value);
* `fetchedAt` is our local sync-time marker.
*/
@Entity(tableName = "cached_quarantine_mine")
data class CachedQuarantineEntity(
@PrimaryKey val trackId: String,
val reason: String,
val notes: String? = null,
val createdAt: String,
val trackTitle: String,
val trackDurationMs: Int = 0,
val albumId: String,
val albumTitle: String,
val albumCoverArtPath: String? = null,
val artistId: String,
val artistName: String,
val fetchedAt: Instant = Clock.System.now(),
)