release v2026.05.11.1: Flutter caching, navigation, and player polish #39

Merged
bvandeusen merged 262 commits from dev into main 2026-05-11 13:47:43 -04:00
4 changed files with 63 additions and 3 deletions
Showing only changes of commit f3a0c44460 - Show all commits
@@ -9,6 +9,7 @@ import com.fabledsword.minstrel.cache.db.dao.CachedArtistDao
import com.fabledsword.minstrel.cache.db.dao.CachedLikeDao
import com.fabledsword.minstrel.cache.db.dao.CachedMutationDao
import com.fabledsword.minstrel.cache.db.dao.CachedPlaylistDao
import com.fabledsword.minstrel.cache.db.dao.CachedResumeStateDao
import com.fabledsword.minstrel.cache.db.dao.CachedPlaylistTrackDao
import com.fabledsword.minstrel.cache.db.dao.CachedQuarantineDao
import com.fabledsword.minstrel.cache.db.dao.CachedTrackDao
@@ -19,6 +20,7 @@ import com.fabledsword.minstrel.cache.db.entities.CachedArtistEntity
import com.fabledsword.minstrel.cache.db.entities.CachedLikeEntity
import com.fabledsword.minstrel.cache.db.entities.CachedMutationEntity
import com.fabledsword.minstrel.cache.db.entities.CachedPlaylistEntity
import com.fabledsword.minstrel.cache.db.entities.CachedResumeStateEntity
import com.fabledsword.minstrel.cache.db.entities.CachedPlaylistTrackEntity
import com.fabledsword.minstrel.cache.db.entities.CachedQuarantineEntity
import com.fabledsword.minstrel.cache.db.entities.CachedTrackEntity
@@ -49,6 +51,7 @@ import com.fabledsword.minstrel.cache.db.entities.SyncMetadataEntity
CachedQuarantineEntity::class,
AudioCacheIndexEntity::class,
CachedMutationEntity::class,
CachedResumeStateEntity::class,
],
version = 1,
exportSchema = true,
@@ -65,4 +68,5 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun cachedQuarantineDao(): CachedQuarantineDao
abstract fun audioCacheIndexDao(): AudioCacheIndexDao
abstract fun cachedMutationDao(): CachedMutationDao
abstract fun cachedResumeStateDao(): CachedResumeStateDao
}
@@ -5,13 +5,18 @@ import kotlinx.datetime.Instant
/**
* Source category for a cached audio file. Mirrors the Drift `CacheSource`
* enum used by the Flutter client's `audio_cache_index` table.
* enum used by the Flutter client's `audio_cache_index` table 1:1.
*
* - `MANUAL`: user explicitly pinned (e.g. "save for offline")
* - `INCIDENTAL`: cached as a side effect of playback streaming
* - `AUTO_LIKED`: auto-cached because the track is liked
* - `AUTO_PLAYLIST`: auto-cached because it's in a pinned playlist
* - `AUTO_PREFETCH`: pre-warmed by background prefetch heuristics
* - `INCIDENTAL`: cached as a side effect of playback streaming
*
* Eviction priority order (first to evict): INCIDENTAL → AUTO_PREFETCH
* → AUTO_PLAYLIST → AUTO_LIKED → MANUAL.
*/
enum class CacheSource { MANUAL, INCIDENTAL, AUTO_PREFETCH }
enum class CacheSource { MANUAL, AUTO_LIKED, AUTO_PLAYLIST, AUTO_PREFETCH, INCIDENTAL }
/**
* Cross-cutting Room type converters. Registered on `@Database` via
@@ -0,0 +1,23 @@
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.CachedResumeStateEntity
import kotlinx.coroutines.flow.Flow
@Dao
interface CachedResumeStateDao {
@Query("SELECT * FROM cached_resume_state WHERE id = 1")
suspend fun get(): CachedResumeStateEntity?
@Query("SELECT * FROM cached_resume_state WHERE id = 1")
fun observe(): Flow<CachedResumeStateEntity?>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsert(row: CachedResumeStateEntity)
@Query("DELETE FROM cached_resume_state")
suspend fun clear()
}
@@ -0,0 +1,28 @@
package com.fabledsword.minstrel.cache.db.entities
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
/**
* Single-row snapshot of the last playback session — queue (as JSON),
* current index, position, and source tag. Mirrors
* `flutter_client/lib/cache/db.dart`'s `CachedResumeState` Drift table.
*
* Lets a torn-down session (the player's idle/dismissed teardown)
* resume on next launch; without it the headset / lock-screen play
* button has nothing to act on. The Flutter codebase persists this on
* every queue change and reads on cold start.
*
* The `json` field carries the entire ResumeState shape (queue trackrefs,
* currentIndex, positionMs, sourceTag). Kotlin-side deserialization
* happens in the ResumeController (Phase 6.5) so the schema stays
* stable across resume-shape changes.
*/
@Entity(tableName = "cached_resume_state")
data class CachedResumeStateEntity(
@PrimaryKey val id: Int = 1,
val json: String,
val updatedAt: Instant = Clock.System.now(),
)