M1/#291: wire Postgres migrations + pgxpool #4
@@ -11,6 +11,7 @@ import com.fabledsword.minstrel.cache.sync.SyncController
|
|||||||
import com.fabledsword.minstrel.di.ApplicationScope
|
import com.fabledsword.minstrel.di.ApplicationScope
|
||||||
import com.fabledsword.minstrel.events.EventsStream
|
import com.fabledsword.minstrel.events.EventsStream
|
||||||
import com.fabledsword.minstrel.events.LiveEventsDispatcher
|
import com.fabledsword.minstrel.events.LiveEventsDispatcher
|
||||||
|
import com.fabledsword.minstrel.player.CoverPrefetcher
|
||||||
import com.fabledsword.minstrel.player.PlayEventsReporter
|
import com.fabledsword.minstrel.player.PlayEventsReporter
|
||||||
import com.fabledsword.minstrel.player.ResumeController
|
import com.fabledsword.minstrel.player.ResumeController
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
@@ -70,6 +71,14 @@ class MinstrelApplication :
|
|||||||
*/
|
*/
|
||||||
@Suppress("unused") @Inject lateinit var playEventsReporter: PlayEventsReporter
|
@Suppress("unused") @Inject lateinit var playEventsReporter: PlayEventsReporter
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Same construct-the-singleton trick — CoverPrefetcher's init
|
||||||
|
* block subscribes to PlayerController.uiState and warms Coil's
|
||||||
|
* cache with the next track's cover so track changes don't flash
|
||||||
|
* a loading state.
|
||||||
|
*/
|
||||||
|
@Suppress("unused") @Inject lateinit var coverPrefetcher: CoverPrefetcher
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Same construct-the-singleton trick — EventsStream's init block
|
* Same construct-the-singleton trick — EventsStream's init block
|
||||||
* subscribes to AuthStore.sessionCookie and opens / closes the
|
* subscribes to AuthStore.sessionCookie and opens / closes the
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package com.fabledsword.minstrel.player
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import coil3.SingletonImageLoader
|
||||||
|
import coil3.request.ImageRequest
|
||||||
|
import com.fabledsword.minstrel.di.ApplicationScope
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.mapNotNull
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Warms Coil's memory + disk cache with the *next* queued track's
|
||||||
|
* cover so when the player advances, the on-screen AsyncImage and
|
||||||
|
* the NowPlaying dominant-color extraction both hit a cache instead
|
||||||
|
* of waiting on the network.
|
||||||
|
*
|
||||||
|
* Pulls the next cover URL out of [PlayerController.uiState] every
|
||||||
|
* time the queue or current-index changes, deduplicates, and fires
|
||||||
|
* a Coil enqueue. Fire-and-forget — the request's own Disposable is
|
||||||
|
* discarded; Coil's cache write happens on the loader's background
|
||||||
|
* thread regardless of subscription.
|
||||||
|
*
|
||||||
|
* Constructed at app launch via the construct-the-singleton trick
|
||||||
|
* in [com.fabledsword.minstrel.MinstrelApplication].
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
class CoverPrefetcher @Inject constructor(
|
||||||
|
@ApplicationContext private val context: Context,
|
||||||
|
private val playerController: PlayerController,
|
||||||
|
@ApplicationScope private val scope: CoroutineScope,
|
||||||
|
) {
|
||||||
|
init {
|
||||||
|
scope.launch {
|
||||||
|
playerController.uiState
|
||||||
|
.mapNotNull { it.peekNextCoverUrl() }
|
||||||
|
.filter { it.isNotEmpty() }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.collect(::prefetch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun prefetch(url: String) {
|
||||||
|
val request = ImageRequest.Builder(context)
|
||||||
|
.data(url)
|
||||||
|
.build()
|
||||||
|
SingletonImageLoader.get(context).enqueue(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cover URL for `queueIndex + 1`, or null when the player is at the
|
||||||
|
* end of the queue / has no queue. Empty string is preserved so the
|
||||||
|
* caller can filter it; we don't want to prefetch a placeholder URL.
|
||||||
|
*/
|
||||||
|
private fun PlayerUiState.peekNextCoverUrl(): String? =
|
||||||
|
queue.getOrNull(queueIndex + 1)?.coverUrl
|
||||||
Reference in New Issue
Block a user