diff --git a/app/src/main/java/com/theveloper/pixelplay/data/DailyMixManager.kt b/app/src/main/java/com/theveloper/pixelplay/data/DailyMixManager.kt index 1d00e69ab5..7bed49928c 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/DailyMixManager.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/DailyMixManager.kt @@ -408,7 +408,9 @@ class DailyMixManager @Inject constructor( suspend fun generateYourMix( allSongs: List, favoriteSongIds: Set = emptySet(), - limit: Int = 60 + limit: Int = 60, + favoriteWeightPercent: Int = 30, + coreWeightPercent: Int = 45 ): List { if (allSongs.isEmpty()) { return emptyList() @@ -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() diff --git a/app/src/main/java/com/theveloper/pixelplay/data/preferences/UserPreferencesRepository.kt b/app/src/main/java/com/theveloper/pixelplay/data/preferences/UserPreferencesRepository.kt index 4598f1a86f..e6766ce968 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/preferences/UserPreferencesRepository.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/preferences/UserPreferencesRepository.kt @@ -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") @@ -665,6 +668,27 @@ suspend fun markDirectoryRulesVersionApplied(version: Int) { } } + val yourMixSizeFlow: Flow = + 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 = + 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 = + 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 = dataStore.data.map { preferences -> preferences[PreferencesKeys.IS_GENRE_GRID_VIEW] ?: true diff --git a/app/src/main/java/com/theveloper/pixelplay/data/service/player/DualPlayerEngine.kt b/app/src/main/java/com/theveloper/pixelplay/data/service/player/DualPlayerEngine.kt index c8f05dc0a2..1721655fdf 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/service/player/DualPlayerEngine.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/service/player/DualPlayerEngine.kt @@ -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() diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/model/SettingsCategory.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/model/SettingsCategory.kt index e59c3d7a8c..d11fa64e51 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/model/SettingsCategory.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/model/SettingsCategory.kt @@ -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, diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsCategoryScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsCategoryScreen.kt index dd1672dbc6..2acbd94316 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsCategoryScreen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsCategoryScreen.kt @@ -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( diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsScreen.kt index fa40ecb265..db70a6d436 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsScreen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsScreen.kt @@ -478,6 +478,7 @@ fun ExpressiveSettingsGroup(content: @Composable () -> Unit) { private fun getCategoryColors(category: SettingsCategory, isDark: Boolean): Pair { 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) @@ -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) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/DailyMixStateHolder.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/DailyMixStateHolder.kt index 92dbee60bd..bbd316bd62 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/DailyMixStateHolder.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/DailyMixStateHolder.kt @@ -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 { diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlaybackDispatchStateHolder.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlaybackDispatchStateHolder.kt index 8e346ebc43..d32229c3ec 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlaybackDispatchStateHolder.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlaybackDispatchStateHolder.kt @@ -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()) { @@ -664,7 +664,7 @@ class PlaybackDispatchStateHolder @Inject constructor( songs = withTimeoutOrNull(30_000L) { var refreshedSongs = emptyList() while (refreshedSongs.isEmpty()) { - refreshedSongs = musicRepository.getRandomSongs(limit = 500) + refreshedSongs = musicRepository.getRandomSongs(limit = 10000) if (refreshedSongs.isEmpty()) { delay(500L) } diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/QueueStateHolder.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/QueueStateHolder.kt index d69d9f52f3..38aafaf1df 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/QueueStateHolder.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/QueueStateHolder.kt @@ -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)" } diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SettingsViewModel.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SettingsViewModel.kt index abba7eaceb..cdfaeae06a 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SettingsViewModel.kt @@ -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, @@ -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( @@ -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. diff --git a/app/src/main/res/values/strings_settings.xml b/app/src/main/res/values/strings_settings.xml index 1c6ae4e284..b335980b79 100644 --- a/app/src/main/res/values/strings_settings.xml +++ b/app/src/main/res/values/strings_settings.xml @@ -1,6 +1,8 @@ + Your Mix + Tune the size and randomness of your home screen mix Music Management Manage folders, refresh library, parsing options Appearance