mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 23:21:59 +01:00
One toggle per tab, persisted with the rest of the list query, narrowing to entities that have at least one completed download. It rides the existing filter SQL: every album, artist and playlist paging query takes a flag and the shared filter constant adds an EXISTS over song_downloads, so it composes with the sort, search and starred filter rather than being a separate mode. The filters sheet now renders on every tab, since playlists support Downloaded but not Starred.
146 lines
5.1 KiB
Kotlin
146 lines
5.1 KiB
Kotlin
package com.subtracks.data.prefs
|
|
|
|
import android.content.Context
|
|
import androidx.datastore.core.DataStore
|
|
import androidx.datastore.preferences.core.Preferences
|
|
import androidx.datastore.preferences.core.edit
|
|
import androidx.datastore.preferences.core.intPreferencesKey
|
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
|
import androidx.datastore.preferences.preferencesDataStore
|
|
import com.subtracks.data.net.NetworkMode
|
|
import com.subtracks.data.source.DEFAULT_FETCH_CONCURRENCY
|
|
import kotlinx.coroutines.flow.Flow
|
|
import kotlinx.coroutines.flow.first
|
|
import kotlinx.coroutines.flow.map
|
|
|
|
enum class AlbumSort { Name, Artist, Year, Added, Starred }
|
|
|
|
enum class ArtistSort { Name, AlbumCount, Starred }
|
|
|
|
enum class PlaylistSort { Name, Added, Updated }
|
|
|
|
enum class LibraryListTab(
|
|
val key: String,
|
|
val defaultSort: String,
|
|
val supportsStarred: Boolean,
|
|
) {
|
|
Albums("albums", AlbumSort.Name.name, true),
|
|
Artists("artists", ArtistSort.Name.name, true),
|
|
Playlists("playlists", PlaylistSort.Name.name, false),
|
|
}
|
|
|
|
enum class StarredFilter { Any, Starred, NotStarred }
|
|
|
|
data class ListQuery(
|
|
val sort: String,
|
|
val descending: Boolean = false,
|
|
val starred: StarredFilter = StarredFilter.Any,
|
|
val downloaded: Boolean = false,
|
|
)
|
|
|
|
data class StreamQuality(
|
|
val maxBitrate: Int = 0,
|
|
val format: String? = null,
|
|
) {
|
|
val transcodes: Boolean get() = maxBitrate > 0 || !format.isNullOrEmpty()
|
|
}
|
|
|
|
private val Context.preferences: DataStore<Preferences> by preferencesDataStore("user_prefs")
|
|
|
|
data class ArtworkSeedValue(
|
|
val cacheKey: String,
|
|
val primary: Int,
|
|
val secondary: Int?,
|
|
)
|
|
|
|
class UserPreferences(
|
|
private val store: DataStore<Preferences>,
|
|
) {
|
|
fun listQuery(tab: LibraryListTab): Flow<ListQuery> = store.data.map { prefs -> decode(prefs[listQueryKey(tab)], tab.defaultSort) }
|
|
|
|
suspend fun setListQuery(
|
|
tab: LibraryListTab,
|
|
query: ListQuery,
|
|
) {
|
|
store.edit { prefs -> prefs[listQueryKey(tab)] = encode(query) }
|
|
}
|
|
|
|
fun streamQuality(mode: NetworkMode): Flow<StreamQuality> =
|
|
store.data.map { prefs ->
|
|
prefs[streamQualityKey(mode)]?.let(::decodeStreamQuality) ?: defaultStreamQuality(mode)
|
|
}
|
|
|
|
suspend fun setStreamQuality(
|
|
mode: NetworkMode,
|
|
quality: StreamQuality,
|
|
) {
|
|
store.edit { prefs -> prefs[streamQualityKey(mode)] = encode(quality) }
|
|
}
|
|
|
|
private fun streamQualityKey(mode: NetworkMode) = stringPreferencesKey("stream_quality_${mode.key}")
|
|
|
|
fun syncConcurrency(): Flow<Int> = store.data.map { prefs -> (prefs[syncConcurrencyKey] ?: DEFAULT_FETCH_CONCURRENCY).coerceAtLeast(1) }
|
|
|
|
suspend fun setSyncConcurrency(value: Int) {
|
|
store.edit { prefs -> prefs[syncConcurrencyKey] = value.coerceAtLeast(1) }
|
|
}
|
|
|
|
private val syncConcurrencyKey = intPreferencesKey("sync_concurrency")
|
|
|
|
suspend fun lastSeed(): ArtworkSeedValue? {
|
|
val prefs = store.data.first()
|
|
val cacheKey = prefs[lastSeedKey] ?: return null
|
|
val primary = prefs[lastSeedPrimary] ?: return null
|
|
return ArtworkSeedValue(cacheKey, primary, prefs[lastSeedSecondary])
|
|
}
|
|
|
|
suspend fun setLastSeed(value: ArtworkSeedValue) {
|
|
store.edit { prefs ->
|
|
prefs[lastSeedKey] = value.cacheKey
|
|
prefs[lastSeedPrimary] = value.primary
|
|
if (value.secondary != null) prefs[lastSeedSecondary] = value.secondary else prefs.remove(lastSeedSecondary)
|
|
}
|
|
}
|
|
|
|
private val lastSeedKey = stringPreferencesKey("last_seed_key")
|
|
private val lastSeedPrimary = intPreferencesKey("last_seed_primary")
|
|
private val lastSeedSecondary = intPreferencesKey("last_seed_secondary")
|
|
|
|
private fun listQueryKey(tab: LibraryListTab) = stringPreferencesKey("list_query_${tab.key}")
|
|
|
|
private fun encode(query: ListQuery) =
|
|
"${query.sort}|${if (query.descending) 1 else 0}|${query.starred.ordinal}|${if (query.downloaded) 1 else 0}"
|
|
|
|
private fun encode(quality: StreamQuality) = "${quality.maxBitrate}|${quality.format.orEmpty()}"
|
|
|
|
private fun decodeStreamQuality(stored: String): StreamQuality {
|
|
val parts = stored.split("|")
|
|
return StreamQuality(
|
|
maxBitrate = parts.getOrNull(0)?.toIntOrNull() ?: 0,
|
|
format = parts.getOrNull(1)?.takeIf { it.isNotEmpty() },
|
|
)
|
|
}
|
|
|
|
private fun defaultStreamQuality(mode: NetworkMode): StreamQuality =
|
|
when (mode) {
|
|
NetworkMode.Wifi -> StreamQuality()
|
|
NetworkMode.Mobile -> StreamQuality(maxBitrate = 192, format = "mp3")
|
|
}
|
|
|
|
private fun decode(
|
|
stored: String?,
|
|
defaultSort: String,
|
|
): ListQuery {
|
|
val parts = stored?.split("|")
|
|
if (parts == null || parts.size < 3) return ListQuery(defaultSort)
|
|
return ListQuery(
|
|
sort = parts[0].ifEmpty { defaultSort },
|
|
descending = parts[1] == "1",
|
|
starred = StarredFilter.entries.getOrElse(parts[2].toIntOrNull() ?: 0) { StarredFilter.Any },
|
|
downloaded = parts.getOrNull(3) == "1",
|
|
)
|
|
}
|
|
}
|
|
|
|
fun createUserPreferences(context: Context): UserPreferences = UserPreferences(context.preferences)
|