Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,9 @@ class DailyMixManager @Inject constructor(
suspend fun generateYourMix(
allSongs: List<Song>,
favoriteSongIds: Set<String> = emptySet(),
limit: Int = 60
limit: Int = 60,
favoriteWeightPercent: Int = 30,
coreWeightPercent: Int = 45
): List<Song> {
if (allSongs.isEmpty()) {
return emptyList()
Expand All @@ -423,8 +425,8 @@ class DailyMixManager @Inject constructor(
return allSongs.shuffled(random).take(limit.coerceAtMost(allSongs.size))
}

val favoriteSectionSize = (limit * 0.3).toInt().coerceAtLeast(5).coerceAtMost(limit)
val coreSectionSize = (limit * 0.45).toInt().coerceAtLeast(10).coerceAtMost(limit)
val favoriteSectionSize = (limit * (favoriteWeightPercent / 100.0)).toInt().coerceAtLeast(0).coerceAtMost(limit)
val coreSectionSize = (limit * (coreWeightPercent / 100.0)).toInt().coerceAtLeast(0).coerceAtMost(limit - favoriteSectionSize)
val discoverySectionSize = (limit - favoriteSectionSize - coreSectionSize).coerceAtLeast(0)

val diversityState = DiversityState()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@ class UserPreferencesRepository @Inject constructor(
val LAST_DAILY_MIX_UPDATE = longPreferencesKey("last_daily_mix_update")
val DAILY_MIX_SONG_IDS = stringPreferencesKey("daily_mix_song_ids")
val YOUR_MIX_SONG_IDS = stringPreferencesKey("your_mix_song_ids")
val YOUR_MIX_SIZE = intPreferencesKey("your_mix_size")
val YOUR_MIX_FAVORITE_PERCENT = intPreferencesKey("your_mix_favorite_percent")
val YOUR_MIX_CORE_PERCENT = intPreferencesKey("your_mix_core_percent")
val NAV_BAR_CORNER_RADIUS = intPreferencesKey("nav_bar_corner_radius")
val NAV_BAR_STYLE = stringPreferencesKey("nav_bar_style")
val NAV_BAR_COMPACT_MODE = booleanPreferencesKey("nav_bar_compact_mode")
Expand Down Expand Up @@ -665,6 +668,27 @@ suspend fun markDirectoryRulesVersionApplied(version: Int) {
}
}

val yourMixSizeFlow: Flow<Int> =
dataStore.data.map { preferences -> preferences[PreferencesKeys.YOUR_MIX_SIZE] ?: 60 }

suspend fun setYourMixSize(size: Int) {
dataStore.edit { preferences -> preferences[PreferencesKeys.YOUR_MIX_SIZE] = size.coerceIn(20, 300) }
}

val yourMixFavoritePercentFlow: Flow<Int> =
dataStore.data.map { preferences -> preferences[PreferencesKeys.YOUR_MIX_FAVORITE_PERCENT] ?: 30 }

suspend fun setYourMixFavoritePercent(percent: Int) {
dataStore.edit { preferences -> preferences[PreferencesKeys.YOUR_MIX_FAVORITE_PERCENT] = percent.coerceIn(0, 100) }
}

val yourMixCorePercentFlow: Flow<Int> =
dataStore.data.map { preferences -> preferences[PreferencesKeys.YOUR_MIX_CORE_PERCENT] ?: 45 }

suspend fun setYourMixCorePercent(percent: Int) {
dataStore.edit { preferences -> preferences[PreferencesKeys.YOUR_MIX_CORE_PERCENT] = percent.coerceIn(0, 100) }
}

val isGenreGridViewFlow: Flow<Boolean> =
dataStore.data.map { preferences ->
preferences[PreferencesKeys.IS_GENRE_GRID_VIEW] ?: true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1121,7 +1121,7 @@ class DualPlayerEngine @Inject constructor(
// FLAG_ENABLE_CONSTANT_BITRATE_SEEKING (not _ALWAYS): fallback-only CBR seeking
// so VBR MP3s with proper Xing/VBRI headers still use their seek table and land
// on the exact frame instead of jumping ±30 s on a VBR file.
.setMp3ExtractorFlags(Mp3Extractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING)
.setMp3ExtractorFlags(Mp3Extractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING or Mp3Extractor.FLAG_DISABLE_ID3_METADATA)
.setFlacExtractorFlags(FlacExtractor.FLAG_DISABLE_ID3_METADATA)

val loadControl = buildAdaptiveLoadControl()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ enum class SettingsCategory(
val icon: ImageVector? = null,
val iconRes: Int? = null
) {
YOUR_MIX(
id = "your_mix",
titleRes = R.string.settings_category_yourmix_title,
subtitleRes = R.string.settings_category_yourmix_subtitle,
iconRes = R.drawable.rounded_instant_mix_24
),
LIBRARY(
id = "library",
titleRes = R.string.settings_category_music_management_title,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,52 @@ fun SettingsCategoryScreen(
modifier = Modifier.background(Color.Transparent)
) {
when (category) {
SettingsCategory.YOUR_MIX -> {
SettingsSubsection(title = "Mix size") {
SliderSettingsItem(
label = "Number of songs",
value = uiState.yourMixSize.toFloat(),
valueRange = 20f..300f,
steps = 27,
onValueChange = { settingsViewModel.setYourMixSize(it.toInt()) },
valueText = { value -> "${value.toInt()} songs" }
)
}
SettingsSubsection(title = "Composition") {
SliderSettingsItem(
label = "Favorites",
value = uiState.yourMixFavoritePercent.toFloat(),
valueRange = 0f..100f,
steps = 19,
onValueChange = { settingsViewModel.setYourMixFavoritePercent(it.toInt()) },
valueText = { value -> "${value.toInt()}%" }
)
SliderSettingsItem(
label = "Core (most played)",
value = uiState.yourMixCorePercent.toFloat(),
valueRange = 0f..100f,
steps = 19,
onValueChange = { settingsViewModel.setYourMixCorePercent(it.toInt()) },
valueText = { value -> "${value.toInt()}%" }
)
val discoveryPercent = (100 - uiState.yourMixFavoritePercent - uiState.yourMixCorePercent).coerceAtLeast(0)
Text(
text = "Discovery (rarely played): $discoveryPercent% — fills whatever's left over",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 8.dp)
)
}
SettingsSubsection(title = "Apply changes", addBottomSpace = false) {
ActionSettingsItem(
title = "Regenerate Your Mix now",
subtitle = "Rebuild your home screen mix using the settings above",
icon = { Icon(painterResource(R.drawable.rounded_instant_mix_24), null, tint = MaterialTheme.colorScheme.secondary) },
primaryActionLabel = "Regenerate",
onPrimaryAction = { showRegenerateDailyMixDialog = true }
)
}
}
SettingsCategory.LIBRARY -> {
SettingsSubsection(title = stringResource(R.string.settings_library_structure_section)) {
SettingsItem(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,7 @@ fun ExpressiveSettingsGroup(content: @Composable () -> Unit) {
private fun getCategoryColors(category: SettingsCategory, isDark: Boolean): Pair<Color, Color> {
return if (isDark) {
when (category) {
SettingsCategory.YOUR_MIX -> Color(0xFF4A3B77) to Color(0xFFE2D6FF)
SettingsCategory.LIBRARY -> Color(0xFF004A77) to Color(0xFFC2E7FF)
SettingsCategory.APPEARANCE -> Color(0xFF7D5260) to Color(0xFFFFD8E4)
SettingsCategory.PLAYBACK -> Color(0xFF633B48) to Color(0xFFFFD8EC)
Expand All @@ -491,6 +492,7 @@ private fun getCategoryColors(category: SettingsCategory, isDark: Boolean): Pair
}
} else {
when (category) {
SettingsCategory.YOUR_MIX -> Color(0xFFE2D6FF) to Color(0xFF2C1B5B)
SettingsCategory.LIBRARY -> Color(0xFFD7E3FF) to Color(0xFF005AC1)
SettingsCategory.APPEARANCE -> Color(0xFFFFD8E4) to Color(0xFF631835)
SettingsCategory.PLAYBACK -> Color(0xFFFFD8EC) to Color(0xFF631B4B)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,16 @@ class DailyMixStateHolder @Inject constructor(
userPreferencesRepository.saveDailyMixSongIds(mix.map { it.id })

// Generate your mix
val yourMix = dailyMixManager.generateYourMix(allSongs, favoriteIds)
val yourMixSize = userPreferencesRepository.yourMixSizeFlow.first()
val yourMixFavoritePercent = userPreferencesRepository.yourMixFavoritePercentFlow.first()
val yourMixCorePercent = userPreferencesRepository.yourMixCorePercentFlow.first()
val yourMix = dailyMixManager.generateYourMix(
allSongs,
favoriteIds,
limit = yourMixSize,
favoriteWeightPercent = yourMixFavoritePercent,
coreWeightPercent = yourMixCorePercent
)
_yourMixSongs.value = yourMix.toImmutableList()
userPreferencesRepository.saveYourMixSongIds(yourMix.map { it.id })
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -654,7 +654,7 @@ class PlaybackDispatchStateHolder @Inject constructor(
val action: () -> Unit = {
Timber.d("[TileDebug] action() invoked")
cb.scope.launch {
var songs = musicRepository.getRandomSongs(limit = 500)
var songs = musicRepository.getRandomSongs(limit = 10000)
Timber.d("[TileDebug] Repository returned ${songs.size} random songs immediately")

if (songs.isEmpty()) {
Expand All @@ -664,7 +664,7 @@ class PlaybackDispatchStateHolder @Inject constructor(
songs = withTimeoutOrNull(30_000L) {
var refreshedSongs = emptyList<Song>()
while (refreshedSongs.isEmpty()) {
refreshedSongs = musicRepository.getRandomSongs(limit = 500)
refreshedSongs = musicRepository.getRandomSongs(limit = 10000)
if (refreshedSongs.isEmpty()) {
delay(500L)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class QueueStateHolder @Inject constructor(
) {

companion object {
private const val SHUFFLE_SAMPLE_LIMIT = 500
private const val SHUFFLE_SAMPLE_LIMIT = 10000
private const val ALL_SONGS_SHUFFLED_QUEUE = "All Songs (Shuffled)"
private const val FAVORITES_SHUFFLED_QUEUE = "Liked Songs (Shuffled)"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ data class SettingsUiState(
val albumArtPaletteStyle: AlbumArtPaletteStyle = AlbumArtPaletteStyle.default,
val albumArtColorAccuracy: Int = AlbumArtColorAccuracy.DEFAULT,
val mockGenresEnabled: Boolean = false,
val yourMixSize: Int = 60,
val yourMixFavoritePercent: Int = 30,
val yourMixCorePercent: Int = 45,
val navBarCornerRadius: Int = 32,
val navBarStyle: String = NavBarStyle.DEFAULT,
val navBarCompactMode: Boolean = false,
Expand Down Expand Up @@ -579,6 +582,24 @@ class SettingsViewModel @Inject constructor(
}
}

viewModelScope.launch {
userPreferencesRepository.yourMixSizeFlow.collect { size ->
_uiState.update { it.copy(yourMixSize = size) }
}
}

viewModelScope.launch {
userPreferencesRepository.yourMixFavoritePercentFlow.collect { percent ->
_uiState.update { it.copy(yourMixFavoritePercent = percent) }
}
}

viewModelScope.launch {
userPreferencesRepository.yourMixCorePercentFlow.collect { percent ->
_uiState.update { it.copy(yourMixCorePercent = percent) }
}
}

// One-time device capability check — result is cached inside HiFiCapabilityChecker
_uiState.update {
it.copy(
Expand Down Expand Up @@ -1314,6 +1335,18 @@ class SettingsViewModel @Inject constructor(
fun setNavBarCornerRadius(radius: Int) {
viewModelScope.launch { userPreferencesRepository.setNavBarCornerRadius(radius) }
}

fun setYourMixSize(size: Int) {
viewModelScope.launch { userPreferencesRepository.setYourMixSize(size) }
}

fun setYourMixFavoritePercent(percent: Int) {
viewModelScope.launch { userPreferencesRepository.setYourMixFavoritePercent(percent) }
}

fun setYourMixCorePercent(percent: Int) {
viewModelScope.launch { userPreferencesRepository.setYourMixCorePercent(percent) }
}
/**
* Triggers a test crash to verify the crash handler is working correctly.
* This should only be used for testing in Developer Options.
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values/strings_settings.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<?xml version="1.0" encoding="utf-8" ?>
<resources>
<!-- SettingsCategory -->
<string name="settings_category_yourmix_title">Your Mix</string>
<string name="settings_category_yourmix_subtitle">Tune the size and randomness of your home screen mix</string>
<string name="settings_category_music_management_title">Music Management</string>
<string name="settings_category_music_management_subtitle">Manage folders, refresh library, parsing options</string>
<string name="settings_category_appearance_title">Appearance</string>
Expand Down