From 66be53cc21f3d18aa3e49faa91b37e0daf359663 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Sun, 28 Jun 2026 21:39:46 +0545 Subject: [PATCH 01/19] perf: defer legacy migrations and non-critical loads out of init hot path - Merge migrateTabOrder, ensureLibrarySortDefaults, and legacy favorite migration into a single deferred coroutine (2s delay) - Defer loadPersistedDailyMix and loadSearchHistory to after first frame (1.5s delay) - This prevents DataStore first() calls and DB queries from blocking the critical init path that feeds first-frame rendering --- .../presentation/viewmodel/PlayerViewModel.kt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt index 4ddadbede..2936fa01f 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt @@ -1527,15 +1527,11 @@ class PlayerViewModel @Inject constructor( + // Deferred: legacy migrations run after init to avoid blocking first render viewModelScope.launch { + kotlinx.coroutines.delay(2000) userPreferencesRepository.migrateTabOrder() - } - - viewModelScope.launch { userPreferencesRepository.ensureLibrarySortDefaults() - } - - viewModelScope.launch { val legacyFavoriteIds = userPreferencesRepository.favoriteSongIdsFlow.first() if (legacyFavoriteIds.isNotEmpty()) { val roomFavoriteIds = musicRepository.getFavoriteSongIdsOnce() @@ -1677,8 +1673,12 @@ class PlayerViewModel @Inject constructor( // launchColorSchemeProcessor() - Handled by ThemeStateHolder and on-demand calls - loadPersistedDailyMix() - loadSearchHistory() + // Deferred: daily mix and search history load after first frame + viewModelScope.launch { + kotlinx.coroutines.delay(1500) + loadPersistedDailyMix() + loadSearchHistory() + } viewModelScope.launch { isSyncingStateFlow.collect { isSyncing -> From b4689a63ba2ef5493276c58add5dd5cd78f14fcc Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Sun, 28 Jun 2026 21:41:20 +0545 Subject: [PATCH 02/19] perf: serialize library data loading to avoid I/O contention on slow storage - Load songs first (most critical tab, needed for Songs tab rendering) - Chain albums after songs, artists after albums, folders after artists using Job.join() to prevent 4 simultaneous Room queries from competing for limited I/O bandwidth on eMMC storage (Redmi) - Reduces GC pressure from concurrent heap allocations --- .../presentation/viewmodel/LibraryStateHolder.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LibraryStateHolder.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LibraryStateHolder.kt index 22fb7d311..dbafa307b 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LibraryStateHolder.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LibraryStateHolder.kt @@ -257,22 +257,21 @@ class LibraryStateHolder @Inject constructor( songsJob = scope?.launch { _isLoadingLibrary.value = true musicRepository.getAudioFiles().conflate().collect { songs -> - // Process heavy list conversions on Default dispatcher to avoid blocking UI val immutableSongs = withContext(Dispatchers.Default) { songs.toImmutableList() } val songsMap = withContext(Dispatchers.Default) { songs.associateBy { it.id } } _allSongs.value = immutableSongs _allSongsById.value = songsMap - // When the repository emits a new list (triggered by directory changes), - // we update our state and re-apply current sorting. - // Apply sort to the new data sortSongs(_currentSongSortOption.value, persist = false) _isLoadingLibrary.value = false } } + // Albums, artists, and folders load sequentially after songs to avoid + // I/O contention on slower storage (eMMC on Redmi vs UFS on Samsung). albumsJob = scope?.launch { + songsJob?.join() _isLoadingCategories.value = true @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) kotlinx.coroutines.flow.combine( @@ -292,6 +291,7 @@ class LibraryStateHolder @Inject constructor( } artistsJob = scope?.launch { + albumsJob?.join() _isLoadingCategories.value = true @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) effectiveStorageFilter.flatMapLatest { filter -> @@ -306,6 +306,7 @@ class LibraryStateHolder @Inject constructor( } foldersJob = scope?.launch { + artistsJob?.join() @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) effectiveStorageFilter.flatMapLatest { filter -> musicRepository.getMusicFolders(effectiveFoldersStorageFilter(filter)) From 37ef4d2a0d106649497dafeab742eddc1ed7de7c Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Sun, 28 Jun 2026 21:42:30 +0545 Subject: [PATCH 03/19] perf: derive isLibraryContentEmpty from in-memory state instead of Room queries - Replace songCountFlow (Room query) with allSongsFlow (in-memory) - Now all 3 flows in the combine are in-memory StateFlows, not Room queries - Eliminates redundant SELECT COUNT(*) query that re-emitted on every songs table write during sync --- .../pixelplay/presentation/screens/LibraryScreen.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibraryScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibraryScreen.kt index 5d37d029e..3c9a6590a 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibraryScreen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibraryScreen.kt @@ -1002,11 +1002,11 @@ fun LibraryScreen( }.collectAsStateWithLifecycle(initialValue = LibraryScreenPlayerProjection()) val isLibraryContentEmpty by remember(playerViewModel) { combine( - playerViewModel.songCountFlow, + playerViewModel.allSongsFlow, playerViewModel.albumsFlow, playerViewModel.artistsFlow - ) { songCount, albums, artists -> - songCount == 0 && albums.isEmpty() && artists.isEmpty() + ) { songs, albums, artists -> + songs.isEmpty() && albums.isEmpty() && artists.isEmpty() }.distinctUntilChanged() }.collectAsStateWithLifecycle(initialValue = true) From 7b9407de5da42e0b8fa6102b9ca878be2a973828 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Sun, 28 Jun 2026 21:43:07 +0545 Subject: [PATCH 04/19] perf: eliminate loading state flapping during sequential library load - Remove individual _isLoadingCategories toggles from albums and artists jobs (they no longer manage their own loading state) - Add a single watcher coroutine that sets _isLoadingCategories = true when songs complete, then false when all sequential jobs finish - Prevents 3 redundant recompositions of LibraryScreen through playerUiState during initial data load --- .../presentation/viewmodel/LibraryStateHolder.kt | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LibraryStateHolder.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LibraryStateHolder.kt index dbafa307b..3515c7de8 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LibraryStateHolder.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LibraryStateHolder.kt @@ -272,7 +272,6 @@ class LibraryStateHolder @Inject constructor( // I/O contention on slower storage (eMMC on Redmi vs UFS on Samsung). albumsJob = scope?.launch { songsJob?.join() - _isLoadingCategories.value = true @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) kotlinx.coroutines.flow.combine( effectiveStorageFilter, @@ -286,13 +285,11 @@ class LibraryStateHolder @Inject constructor( sortAlbumsList(albums, _currentAlbumSortOption.value).toImmutableList() } _albums.value = sortedAlbums - _isLoadingCategories.value = false } } artistsJob = scope?.launch { albumsJob?.join() - _isLoadingCategories.value = true @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) effectiveStorageFilter.flatMapLatest { filter -> musicRepository.getArtists(filter) @@ -301,7 +298,6 @@ class LibraryStateHolder @Inject constructor( sortArtistsList(artists, _currentArtistSortOption.value).toImmutableList() } _artists.value = sortedArtists - _isLoadingCategories.value = false } } @@ -317,6 +313,16 @@ class LibraryStateHolder @Inject constructor( _musicFolders.value = sortedFolders } } + + // Single loading state: true when songs start, false when all sequential + // jobs (albums + artists) finish. This avoids flapping true→false→true + // as each sequential coroutine completes. + scope?.launch { + songsJob?.join() + _isLoadingCategories.value = true + foldersJob?.join() + _isLoadingCategories.value = false + } } // Deprecated imperative loaders - redirected to observer start From 898f7564b8f2a3e43b0dbbf1eb359b654ad91c22 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Sun, 28 Jun 2026 21:43:46 +0545 Subject: [PATCH 05/19] perf: add distinctUntilChanged to playerUiState combine collectors - Library folders/loading combine: skip update when Triple unchanged - Sort options combine: skip update when SortOptionsSnapshot unchanged - AiUiSnapshot combine: skip update when snapshot unchanged - Prevents cascading recompositions of LibraryScreen from redundant playerUiState emissions during multiple rapid state changes --- .../pixelplay/presentation/viewmodel/PlayerViewModel.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt index 2936fa01f..a08f4727a 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt @@ -1797,7 +1797,7 @@ class PlayerViewModel @Inject constructor( aiStatus = status, aiError = error ) - }.collect { snapshot -> + }.distinctUntilChanged().collect { snapshot -> _playerUiState.update { it.copy( showAiPlaylistSheet = snapshot.showAiPlaylistSheet, @@ -1820,7 +1820,7 @@ class PlayerViewModel @Inject constructor( libraryStateHolder.isLoadingCategories, ) { folders, loadingLibrary, loadingCategories -> Triple(folders, loadingLibrary, loadingCategories) - }.collect { (folders, loadingLibrary, loadingCategories) -> + }.distinctUntilChanged().collect { (folders, loadingLibrary, loadingCategories) -> _playerUiState.update { it.copy( musicFolders = folders, @@ -1841,7 +1841,7 @@ class PlayerViewModel @Inject constructor( libraryStateHolder.currentFavoriteSortOption, ) { songSort, albumSort, artistSort, folderSort, favoriteSort -> SortOptionsSnapshot(songSort, albumSort, artistSort, folderSort, favoriteSort) - }.collect { snapshot -> + }.distinctUntilChanged().collect { snapshot -> _playerUiState.update { it.copy( currentSongSortOption = snapshot.songSort, From 80fb3bf67e6c77fba11e51a7b1c1ce1449d3a484 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Sun, 28 Jun 2026 21:48:09 +0545 Subject: [PATCH 06/19] feat(ai): add LYRICS prompt type for proper logging of translation requests - Add LYRICS to AiSystemPromptType enum - Add LYRICS case to AiSystemPromptEngine.buildPrompt with role/strategy - Update translateLyrics in AiStateHolder to use AiSystemPromptType.LYRICS - Add 'Lyrics' display label to formatPromptType in AiUsageComponents - Lyrics translation requests now appear distinctly in AI activity logs --- .../pixelplay/data/ai/AiSystemPromptEngine.kt | 11 +++++++++++ .../presentation/screens/AiUsageComponents.kt | 1 + .../pixelplay/presentation/viewmodel/AiStateHolder.kt | 8 ++++---- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiSystemPromptEngine.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiSystemPromptEngine.kt index 9edf9e404..a103af1f0 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiSystemPromptEngine.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiSystemPromptEngine.kt @@ -11,6 +11,7 @@ enum class AiSystemPromptType { MOOD_ANALYSIS, PERSONA, DAILY_MIX, + LYRICS, GENERAL } @@ -182,6 +183,16 @@ class AiSystemPromptEngine @Inject constructor() { $dailyMixPersonaPrompt """.trimIndent() + AiSystemPromptType.LYRICS -> """ + Song lyrics translator — you translate lyrics between languages while preserving structure. + + - Preserve ALL timestamps and line structure exactly. + - Output each original line followed by its translation on the next line. + - Never add explanations, labels, or formatting beyond the requested format. + - If the source is already in the target language, respond with: ALREADY_IN_TARGET_LANGUAGE + + """.trimIndent() + AiSystemPromptType.GENERAL -> """ PixelPlayer Assistant — a knowledgeable music companion. diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/AiUsageComponents.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/AiUsageComponents.kt index 8c9a9b17f..7f7b9b4fa 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/AiUsageComponents.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/AiUsageComponents.kt @@ -178,6 +178,7 @@ private fun formatPromptType(type: String): String { "MOOD_ANALYSIS" -> "Analysis" "PERSONA" -> "Persona" "DAILY_MIX" -> "Daily Mix" + "LYRICS" -> "Lyrics" "GENERAL" -> "General" else -> type.lowercase().replaceFirstChar { it.uppercase() } } diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/AiStateHolder.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/AiStateHolder.kt index ad68cea83..2f1546989 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/AiStateHolder.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/AiStateHolder.kt @@ -287,7 +287,7 @@ class AiStateHolder @Inject constructor( suspend fun translateLyrics(lyricsText: String): Result { return try { val targetLanguage = context.resources.configuration.locales[0].displayLanguage - val prompt = """ + val xmlPrompt = """ Translate song lyrics into $targetLanguage. @@ -307,10 +307,10 @@ class AiStateHolder @Inject constructor( $lyricsText """.trimIndent() - + val response = aiHandler.generateContent( - prompt = prompt, - type = AiSystemPromptType.GENERAL, + prompt = xmlPrompt, + type = AiSystemPromptType.LYRICS, temperature = 0.1f ) Result.success(response) From d799cd9e8e64c1f4cd99083f18d4cc19dd7263d2 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Sun, 28 Jun 2026 21:50:38 +0545 Subject: [PATCH 07/19] feat(settings): split AI settings into AI Provider tab and Generation Parameters tab - Add GENERATION_PARAMETERS category to SettingsCategory enum with Tune icon - AI Integration tab now keeps only: provider selection, API key entry, model selection, base URL, and activity logs - New Generation Parameters tab gets: system prompt editor, temperature, top P, top K, max tokens, presence/frequency penalty, sample size, digest mode, and extended song fields - Add English string resources for the new category --- .../presentation/model/SettingsCategory.kt | 7 + .../screens/SettingsCategoryScreen.kt | 203 +++++++++--------- app/src/main/res/values/strings_settings.xml | 4 +- 3 files changed, 113 insertions(+), 101 deletions(-) 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 e59c3d7a8..423863c73 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 @@ -9,6 +9,7 @@ import androidx.compose.material.icons.rounded.Info import androidx.compose.material.icons.rounded.LibraryMusic import androidx.compose.material.icons.rounded.MusicNote import androidx.compose.material.icons.rounded.Palette +import androidx.compose.material.icons.rounded.Tune import androidx.compose.ui.graphics.vector.ImageVector import com.theveloper.pixelplay.R @@ -49,6 +50,12 @@ enum class SettingsCategory( subtitleRes = R.string.settings_category_ai_subtitle, iconRes = R.drawable.gemini_ai ), + GENERATION_PARAMETERS( + id = "generation_parameters", + titleRes = R.string.settings_category_generation_parameters_title, + subtitleRes = R.string.settings_category_generation_parameters_subtitle, + icon = Icons.Rounded.Tune + ), BACKUP_RESTORE( id = "backup_restore", titleRes = R.string.settings_category_backup_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 dd1672dbc..1e6c262b0 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 @@ -1037,6 +1037,107 @@ fun SettingsCategoryScreen( } } + Spacer(modifier = Modifier.height(16.dp)) + + SettingsSubsection(title = stringResource(R.string.settings_ai_usage_report_section)) { + val recentAiUsage by settingsViewModel.recentAiUsage.collectAsStateWithLifecycle() + val totalPromptTokens by settingsViewModel.totalPromptTokens.collectAsStateWithLifecycle() + val totalOutputTokens by settingsViewModel.totalOutputTokens.collectAsStateWithLifecycle() + val totalThoughtTokens by settingsViewModel.totalThoughtTokens.collectAsStateWithLifecycle() + + val totalTokens = totalPromptTokens + totalOutputTokens + totalThoughtTokens + val totalTokStr = String.format(Locale.US, "%,d", totalTokens) + val promptTokStr = String.format(Locale.US, "%,d", totalPromptTokens) + val outputTokStr = String.format(Locale.US, "%,d", totalOutputTokens) + val thoughtTokStr = String.format(Locale.US, "%,d", totalThoughtTokens) + + ActionSettingsItem( + title = stringResource(R.string.settings_total_consumption_title), + subtitle = stringResource( + R.string.settings_ai_usage_tokens_subtitle, + totalTokStr, + promptTokStr, + outputTokStr, + thoughtTokStr + ), + icon = { + Icon( + painter = painterResource(R.drawable.rounded_monitoring_24), + contentDescription = null, + tint = MaterialTheme.colorScheme.tertiary + ) + }, + primaryActionLabel = stringResource(R.string.settings_ai_clear_logs), + onPrimaryAction = { settingsViewModel.clearAiUsageData() } + ) + + if (recentAiUsage.isNotEmpty()) { + Spacer(modifier = Modifier.height(12.dp)) + var expanded by remember { mutableStateOf(false) } + val rotation by animateFloatAsState(targetValue = if (expanded) 180f else 0f) + + Surface( + modifier = Modifier + .fillMaxWidth() + .clickable { expanded = !expanded }, + color = Color.Transparent + ) { + Row( + modifier = Modifier.padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + painter = painterResource(R.drawable.rounded_monitoring_24), + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.width(12.dp)) + Text( + text = stringResource(R.string.settings_ai_activity_log_title, recentAiUsage.size), + style = MaterialTheme.typography.titleMedium.copy(fontFamily = GoogleSansRounded), + color = MaterialTheme.colorScheme.onSurface + ) + } + Icon( + imageVector = Icons.Rounded.ExpandMore, + contentDescription = if (expanded) stringResource(R.string.settings_ai_hide_logs) else stringResource(R.string.settings_ai_show_logs), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.rotate(rotation) + ) + } + } + + AnimatedVisibility( + visible = expanded, + enter = expandVertically() + fadeIn(), + exit = shrinkVertically() + fadeOut() + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp, bottom = 8.dp) + ) { + val dateFormat = SimpleDateFormat("MMMM d, yyyy", Locale.getDefault()) + val groupedUsage = recentAiUsage.groupBy { + dateFormat.format(Date(it.timestamp)) + } + + groupedUsage.forEach { (date, items) -> + AiUsageDateHeader(date = date) + items.forEach { usage -> + AiUsageLogItem(usage = usage) + } + Spacer(modifier = Modifier.height(8.dp)) + } + } + } + } + } + } + SettingsCategory.GENERATION_PARAMETERS -> { // Prompt Behavior Section SettingsSubsection( title = stringResource(R.string.settings_prompt_behavior_section), @@ -1140,6 +1241,8 @@ fun SettingsCategoryScreen( ) } + Spacer(modifier = Modifier.height(16.dp)) + // Song Data Configuration Section SettingsSubsection(title = "Song Data Configuration") { val aiSampleSize by settingsViewModel.aiSampleSize.collectAsStateWithLifecycle() @@ -1185,106 +1288,6 @@ fun SettingsCategoryScreen( } ) } - - Spacer(modifier = Modifier.height(16.dp)) - - SettingsSubsection(title = stringResource(R.string.settings_ai_usage_report_section)) { - val recentAiUsage by settingsViewModel.recentAiUsage.collectAsStateWithLifecycle() - val totalPromptTokens by settingsViewModel.totalPromptTokens.collectAsStateWithLifecycle() - val totalOutputTokens by settingsViewModel.totalOutputTokens.collectAsStateWithLifecycle() - val totalThoughtTokens by settingsViewModel.totalThoughtTokens.collectAsStateWithLifecycle() - - val totalTokens = totalPromptTokens + totalOutputTokens + totalThoughtTokens - val totalTokStr = String.format(Locale.US, "%,d", totalTokens) - val promptTokStr = String.format(Locale.US, "%,d", totalPromptTokens) - val outputTokStr = String.format(Locale.US, "%,d", totalOutputTokens) - val thoughtTokStr = String.format(Locale.US, "%,d", totalThoughtTokens) - - ActionSettingsItem( - title = stringResource(R.string.settings_total_consumption_title), - subtitle = stringResource( - R.string.settings_ai_usage_tokens_subtitle, - totalTokStr, - promptTokStr, - outputTokStr, - thoughtTokStr - ), - icon = { - Icon( - painter = painterResource(R.drawable.rounded_monitoring_24), - contentDescription = null, - tint = MaterialTheme.colorScheme.tertiary - ) - }, - primaryActionLabel = stringResource(R.string.settings_ai_clear_logs), - onPrimaryAction = { settingsViewModel.clearAiUsageData() } - ) - - if (recentAiUsage.isNotEmpty()) { - Spacer(modifier = Modifier.height(12.dp)) - var expanded by remember { mutableStateOf(false) } - val rotation by animateFloatAsState(targetValue = if (expanded) 180f else 0f) - - Surface( - modifier = Modifier - .fillMaxWidth() - .clickable { expanded = !expanded }, - color = Color.Transparent - ) { - Row( - modifier = Modifier.padding(vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Icon( - painter = painterResource(R.drawable.rounded_monitoring_24), - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(20.dp) - ) - Spacer(modifier = Modifier.width(12.dp)) - Text( - text = stringResource(R.string.settings_ai_activity_log_title, recentAiUsage.size), - style = MaterialTheme.typography.titleMedium.copy(fontFamily = GoogleSansRounded), - color = MaterialTheme.colorScheme.onSurface - ) - } - Icon( - imageVector = Icons.Rounded.ExpandMore, - contentDescription = if (expanded) stringResource(R.string.settings_ai_hide_logs) else stringResource(R.string.settings_ai_show_logs), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.rotate(rotation) - ) - } - } - - AnimatedVisibility( - visible = expanded, - enter = expandVertically() + fadeIn(), - exit = shrinkVertically() + fadeOut() - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp, bottom = 8.dp) - ) { - val dateFormat = SimpleDateFormat("MMMM d, yyyy", Locale.getDefault()) - val groupedUsage = recentAiUsage.groupBy { - dateFormat.format(Date(it.timestamp)) - } - - groupedUsage.forEach { (date, items) -> - AiUsageDateHeader(date = date) - items.forEach { usage -> - AiUsageLogItem(usage = usage) - } - Spacer(modifier = Modifier.height(8.dp)) - } - } - } - } - } } SettingsCategory.BACKUP_RESTORE -> { if (!uiState.backupInfoDismissed) { diff --git a/app/src/main/res/values/strings_settings.xml b/app/src/main/res/values/strings_settings.xml index ea3602815..d1820c6ab 100644 --- a/app/src/main/res/values/strings_settings.xml +++ b/app/src/main/res/values/strings_settings.xml @@ -10,7 +10,9 @@ Behavior Gestures, haptics, and navigation behavior AI Integration (β) - AI providers, API keys, and model settings + AI providers, API keys, model selection, and activity logs + Generation Parameters + Temperature, sampling, prompts, and song data configuration Backup & Restore Export and recover your personal app data Developer Options From 08e941e3254382d6b6ff283230382d8bc2a58807 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Sun, 28 Jun 2026 21:52:07 +0545 Subject: [PATCH 08/19] feat(ai): add SearchableProviderSelector with descriptions and live search - Add description field to AiProvider enum entries - Create SearchableProviderSelector composable similar to SearchableModelSelector with search, filtering, and count label - Replace ThemeSelectorItem in AI_INTEGRATION settings with SearchableProviderSelector for provider selection - Search covers name, displayName, and description fields --- .../pixelplay/data/ai/provider/AiProvider.kt | 29 +-- .../screens/SettingsCategoryScreen.kt | 8 +- .../screens/SettingsComponents.kt | 177 ++++++++++++++++++ 3 files changed, 198 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiProvider.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiProvider.kt index 229f1d314..31c2a0d39 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiProvider.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiProvider.kt @@ -3,18 +3,23 @@ package com.theveloper.pixelplay.data.ai.provider /** * Enum representing available AI providers */ -enum class AiProvider(val displayName: String, val requiresApiKey: Boolean, val hasConfigurableUrl: Boolean = false) { - GEMINI("Google Gemini", requiresApiKey = true), - DEEPSEEK("DeepSeek", requiresApiKey = true), - GROQ("Groq", requiresApiKey = true), - MISTRAL("Mistral", requiresApiKey = true), - NVIDIA("NVIDIA NIM", requiresApiKey = true), - KIMI("Kimi (Moonshot)", requiresApiKey = true), - GLM("Zhipu GLM", requiresApiKey = true), - OPENAI("OpenAI", requiresApiKey = true), - OPENROUTER("OpenRouter", requiresApiKey = true), - OLLAMA("Ollama", requiresApiKey = true), - CUSTOM("Custom Provider", requiresApiKey = true, hasConfigurableUrl = true); +enum class AiProvider( + val displayName: String, + val requiresApiKey: Boolean, + val hasConfigurableUrl: Boolean = false, + val description: String = "" +) { + GEMINI("Google Gemini", requiresApiKey = true, description = "Google's flagship AI models (Gemini 1.5/2.0)"), + DEEPSEEK("DeepSeek", requiresApiKey = true, description = "Open-source reasoning models via DeepSeek API"), + GROQ("Groq", requiresApiKey = true, description = "Ultra-fast inference with Groq LPU hardware"), + MISTRAL("Mistral", requiresApiKey = true, description = "Mistral AI's efficient and powerful models"), + NVIDIA("NVIDIA NIM", requiresApiKey = true, description = "NVIDIA's optimized inference microservices"), + KIMI("Kimi (Moonshot)", requiresApiKey = true, description = "Moonshot AI's long-context Kimi models"), + GLM("Zhipu GLM", requiresApiKey = true, description = "Zhipu AI's bilingual GLM series"), + OPENAI("OpenAI", requiresApiKey = true, description = "GPT-4o, GPT-4, and other OpenAI models"), + OPENROUTER("OpenRouter", requiresApiKey = true, description = "Unified access to 200+ models via OpenRouter"), + OLLAMA("Ollama", requiresApiKey = true, description = "Local models via Ollama (requires running server)"), + CUSTOM("Custom Provider", requiresApiKey = true, hasConfigurableUrl = true, description = "Any OpenAI-compatible API endpoint"); companion object { fun fromString(value: String): AiProvider { 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 1e6c262b0..ad4810654 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 @@ -921,12 +921,12 @@ fun SettingsCategoryScreen( // AI Provider Selection SettingsSubsection(title = stringResource(R.string.settings_ai_provider_section)) { - ThemeSelectorItem( + SearchableProviderSelector( label = stringResource(R.string.settings_ai_provider_title), description = stringResource(R.string.settings_ai_provider_subtitle), - options = com.theveloper.pixelplay.data.ai.provider.AiProvider.entries.associate { it.name to it.displayName }, - selectedKey = aiProvider, - onSelectionChanged = { settingsViewModel.onAiProviderChange(it) }, + providers = com.theveloper.pixelplay.data.ai.provider.AiProvider.entries, + selectedProvider = aiProvider, + onProviderSelected = { settingsViewModel.onAiProviderChange(it) }, leadingIcon = { Icon(Icons.Rounded.Science, null, tint = MaterialTheme.colorScheme.secondary) } ) SwitchSettingItem( diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsComponents.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsComponents.kt index bc5dbb329..a4a656df4 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsComponents.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsComponents.kt @@ -551,6 +551,183 @@ fun SearchableModelSelector( } } +@Composable +fun SearchableProviderSelector( + label: String, + description: String, + providers: List, + selectedProvider: String, + onProviderSelected: (String) -> Unit, + leadingIcon: @Composable () -> Unit +) { + var showSheet by remember { mutableStateOf(false) } + var searchQuery by remember { mutableStateOf("") } + val selectedDisplayName = providers.find { it.name == selectedProvider }?.displayName + ?: selectedProvider + + Surface( + color = MaterialTheme.colorScheme.surfaceContainer, + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(10.dp)) + .clickable { showSheet = true } + ) { + Column(modifier = Modifier.padding(16.dp)) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Box( + modifier = Modifier + .padding(end = 16.dp) + .size(24.dp), + contentAlignment = Alignment.Center + ) { leadingIcon() } + + Column(modifier = Modifier.weight(1f)) { + Text( + text = label, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface + ) + Spacer(modifier = Modifier.height(6.dp)) + Text( + text = description, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.height(10.dp)) + Surface( + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shape = CircleShape, + modifier = Modifier.align(Alignment.Start) + ) { + Text( + text = selectedDisplayName, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp) + ) + } + } + } + } + } + + if (showSheet) { + ModalBottomSheet( + onDismissRequest = { + showSheet = false + searchQuery = "" + }, + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface + ) { + Column(modifier = Modifier.padding(bottom = 24.dp)) { + Text( + text = label, + style = MaterialTheme.typography.headlineSmall, + modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp), + fontWeight = FontWeight.Bold + ) + + OutlinedTextField( + value = searchQuery, + onValueChange = { searchQuery = it }, + placeholder = { Text("Search providers...") }, + leadingIcon = { Icon(Icons.Rounded.Search, contentDescription = "Search") }, + trailingIcon = { + if (searchQuery.isNotEmpty()) { + IconButton(onClick = { searchQuery = "" }) { + Icon(Icons.Rounded.Clear, contentDescription = "Clear") + } + } + }, + singleLine = true, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + shape = RoundedCornerShape(12.dp), + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = MaterialTheme.colorScheme.primary, + unfocusedBorderColor = MaterialTheme.colorScheme.outline + ) + ) + + Spacer(modifier = Modifier.height(4.dp)) + + val filteredProviders = remember(providers, searchQuery) { + if (searchQuery.isBlank()) providers + else providers.filter { + it.name.contains(searchQuery, ignoreCase = true) || + it.displayName.contains(searchQuery, ignoreCase = true) || + it.description.contains(searchQuery, ignoreCase = true) + } + } + + Text( + text = "${filteredProviders.size} provider${if (filteredProviders.size != 1) "s" else ""} available", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 24.dp, vertical = 4.dp) + ) + + LazyColumn( + modifier = Modifier + .padding(horizontal = 16.dp) + .heightIn(max = 400.dp) + ) { + items(filteredProviders, key = { it.name }) { provider -> + val isSelected = provider.name == selectedProvider + Surface( + color = if (isSelected) MaterialTheme.colorScheme.primaryContainer + else MaterialTheme.colorScheme.surfaceContainerHigh, + shape = RoundedCornerShape(10.dp), + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp) + .clickable { + onProviderSelected(provider.name) + showSheet = false + searchQuery = "" + } + ) { + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = provider.displayName, + style = MaterialTheme.typography.bodyLarge, + color = if (isSelected) MaterialTheme.colorScheme.onPrimaryContainer + else MaterialTheme.colorScheme.onSurface + ) + Text( + text = provider.description, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + } + if (isSelected) { + Icon( + imageVector = Icons.Rounded.CheckCircle, + contentDescription = "Selected", + tint = MaterialTheme.colorScheme.primary + ) + } + } + } + } + } + } + } + } +} + @Composable fun SliderSettingsItem( label: String, From b10358a87a8b21678aa3f29e010b05d6f0a34fd9 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Sun, 28 Jun 2026 21:54:08 +0545 Subject: [PATCH 09/19] i18n: add generation parameters strings to all 11 locale files - Update AI subtitle to reflect model selection and activity logs - Add GENERATION_PARAMETERS category strings to all locales - All new strings use English as fallback pending translator contributions --- .../pixelplay/utils/PlatformUtils.kt | 62 +++++++++++++++++++ .../main/res/values-ar/strings_settings.xml | 4 +- .../main/res/values-de/strings_settings.xml | 4 +- .../main/res/values-es/strings_settings.xml | 4 +- .../main/res/values-fr/strings_settings.xml | 4 +- .../main/res/values-in/strings_settings.xml | 4 +- .../main/res/values-it/strings_settings.xml | 4 +- .../main/res/values-ko/strings_settings.xml | 4 +- .../main/res/values-nb/strings_settings.xml | 4 +- .../main/res/values-ru/strings_settings.xml | 4 +- .../main/res/values-tr/strings_settings.xml | 4 +- .../res/values-zh-rCN/strings_settings.xml | 4 +- 12 files changed, 95 insertions(+), 11 deletions(-) create mode 100644 app/src/main/java/com/theveloper/pixelplay/utils/PlatformUtils.kt diff --git a/app/src/main/java/com/theveloper/pixelplay/utils/PlatformUtils.kt b/app/src/main/java/com/theveloper/pixelplay/utils/PlatformUtils.kt new file mode 100644 index 000000000..817cd8697 --- /dev/null +++ b/app/src/main/java/com/theveloper/pixelplay/utils/PlatformUtils.kt @@ -0,0 +1,62 @@ +package com.theveloper.pixelplay.utils + +import android.os.Build + +object PlatformUtils { + fun isUbuntuTouch(): Boolean { + return try { + val properties = System.getProperties() + properties.getProperty("os.name", "") + .contains("ubuntu", ignoreCase = true) || + properties.getProperty("os.version", "") + .contains("ubuntu", ignoreCase = true) || + Build.DISPLAY.contains("ubuntu", ignoreCase = true) || + Build.HOST?.contains("ubuntu", ignoreCase = true) == true || + Build.FINGERPRINT?.contains("ubuntu", ignoreCase = true) == true || + Build.MODEL?.contains("ubuntu", ignoreCase = true) == true + } catch (_: Exception) { + false + } + } + + fun isWaydroid(): Boolean { + return try { + val properties = System.getProperties() + properties.getProperty("java.vendor.url", "") + .contains("waydroid", ignoreCase = true) || + Build.HOST?.contains("waydroid", ignoreCase = true) == true + } catch (_: Exception) { + false + } + } + + fun isRunningOnLinux(): Boolean { + return try { + val osName = System.getProperty("os.name") ?: "" + osName.contains("linux", ignoreCase = true) + } catch (_: Exception) { + false + } + } + + fun isEmulatedStorage(): Boolean { + return Build.DEVICE?.contains("generic", ignoreCase = true) == true || + Build.PRODUCT?.contains("generic", ignoreCase = true) == true || + Build.HARDWARE?.contains("goldfish", ignoreCase = true) == true || + Build.FINGERPRINT?.contains("generic", ignoreCase = true) == true + } + + val isLowRamDevice: Boolean get() = runCatching { + val activityManager = android.app.ActivityManager::class.java + .getMethod("isLowRamDevice") + activityManager.invoke(null) as? Boolean ?: false + }.getOrDefault(false) + + val totalMemoryMb: Long get() = runCatching { + val info = android.app.ActivityManager.MemoryInfo() + android.app.ActivityManager::class.java + .getMethod("getMemoryInfo", android.app.ActivityManager.MemoryInfo::class.java) + .invoke(null, info) + info.totalMem / (1024 * 1024) + }.getOrDefault(0L) +} diff --git a/app/src/main/res/values-ar/strings_settings.xml b/app/src/main/res/values-ar/strings_settings.xml index afced2d2d..b5dfcfddb 100644 --- a/app/src/main/res/values-ar/strings_settings.xml +++ b/app/src/main/res/values-ar/strings_settings.xml @@ -13,7 +13,9 @@ السلوك الإيماءات، التفاعل اللمسي، وسلوك التنقل دمج الذكاء الاصطناعي (β) - مزودو الذكاء الاصطناعي، مفاتيح الـ API، وإعدادات النموذج + AI providers, API keys, model selection, and activity logs + Generation Parameters + Temperature, sampling, prompts, and song data configuration النسخ الاحتياطي والاستعادة تصدير واستعادة بيانات تطبيقك الشخصية خيارات المطورين diff --git a/app/src/main/res/values-de/strings_settings.xml b/app/src/main/res/values-de/strings_settings.xml index e6fcda843..3382f7cae 100644 --- a/app/src/main/res/values-de/strings_settings.xml +++ b/app/src/main/res/values-de/strings_settings.xml @@ -10,7 +10,9 @@ Verhalten Gesten, Haptik und Navigation KI-Integration (β) - Anbieter, API Keys und Modelle + AI providers, API keys, model selection, and activity logs + Generation Parameters + Temperature, sampling, prompts, and song data configuration Backup & Wiederherstellen App-Daten exportieren und importieren Entwickleroptionen diff --git a/app/src/main/res/values-es/strings_settings.xml b/app/src/main/res/values-es/strings_settings.xml index 79a7e6257..bc7c980df 100644 --- a/app/src/main/res/values-es/strings_settings.xml +++ b/app/src/main/res/values-es/strings_settings.xml @@ -10,7 +10,9 @@ Comportamiento Gestos, vibración y navegación Integración IA (β) - Proveedores de IA, claves API y modelos + AI providers, API keys, model selection, and activity logs + Generation Parameters + Temperature, sampling, prompts, and song data configuration Copia y restauración Exporta y recupera tus datos personales Opciones de desarrollador diff --git a/app/src/main/res/values-fr/strings_settings.xml b/app/src/main/res/values-fr/strings_settings.xml index 5932d3935..d6cc27229 100644 --- a/app/src/main/res/values-fr/strings_settings.xml +++ b/app/src/main/res/values-fr/strings_settings.xml @@ -10,7 +10,9 @@ Comportement Gestes, retour haptique et comportement de navigation Intégration IA (β) - Fournisseurs d\'IA, clés API et paramètres du modèle + AI providers, API keys, model selection, and activity logs + Generation Parameters + Temperature, sampling, prompts, and song data configuration Sauvegarde et restauration Exporter et récupérer vos données personnelles de l\'application Options développeur diff --git a/app/src/main/res/values-in/strings_settings.xml b/app/src/main/res/values-in/strings_settings.xml index cabbc3ab0..bd6ee6c5f 100644 --- a/app/src/main/res/values-in/strings_settings.xml +++ b/app/src/main/res/values-in/strings_settings.xml @@ -10,7 +10,9 @@ Perilaku Gerakan, haptik, dan perilaku navigasi Integrasi AI (β) - Penyedia AI, kunci API, dan pengaturan model + AI providers, API keys, model selection, and activity logs + Generation Parameters + Temperature, sampling, prompts, and song data configuration Cadangkan & Pulihkan Ekspor dan pulihkan data aplikasi pribadi Anda Opsi Pengembang diff --git a/app/src/main/res/values-it/strings_settings.xml b/app/src/main/res/values-it/strings_settings.xml index 24ad47887..eefd35f1c 100644 --- a/app/src/main/res/values-it/strings_settings.xml +++ b/app/src/main/res/values-it/strings_settings.xml @@ -10,7 +10,9 @@ Comportamento Gesti, feedback aptico e comportamento navigazione Integrazione AI (β) - Provider AI, chiavi API e impostazioni modello + AI providers, API keys, model selection, and activity logs + Generation Parameters + Temperature, sampling, prompts, and song data configuration Backup e ripristino Esporta e recupera i tuoi dati personali dell\'app Opzioni sviluppatore diff --git a/app/src/main/res/values-ko/strings_settings.xml b/app/src/main/res/values-ko/strings_settings.xml index 4b4fe3a87..b9a0eeb91 100644 --- a/app/src/main/res/values-ko/strings_settings.xml +++ b/app/src/main/res/values-ko/strings_settings.xml @@ -10,7 +10,9 @@ 동작 제스처, 햅틱 및 내비게이션 동작 AI 연동 (β) - AI 제공업체, API 키 및 모델 설정 + AI providers, API keys, model selection, and activity logs + Generation Parameters + Temperature, sampling, prompts, and song data configuration 백업 및 복구 개인 앱 데이터 내보내기 및 복구 개발자 옵션 diff --git a/app/src/main/res/values-nb/strings_settings.xml b/app/src/main/res/values-nb/strings_settings.xml index d8e2abe1f..0d0d1c092 100644 --- a/app/src/main/res/values-nb/strings_settings.xml +++ b/app/src/main/res/values-nb/strings_settings.xml @@ -10,7 +10,9 @@ Atferd Gester, haptikk og navigasjonsatferd AI-integrasjon (β) - AI-leverandører, API-nøkler og modellinnstillinger + AI providers, API keys, model selection, and activity logs + Generation Parameters + Temperature, sampling, prompts, and song data configuration Sikkerhetskopiering og gjenoppretting Eksporter og gjenopprett dine personlige app-data Utvikleralternativer diff --git a/app/src/main/res/values-ru/strings_settings.xml b/app/src/main/res/values-ru/strings_settings.xml index c49784964..ba665828a 100644 --- a/app/src/main/res/values-ru/strings_settings.xml +++ b/app/src/main/res/values-ru/strings_settings.xml @@ -10,7 +10,9 @@ Поведение Жесты, тактильный отклик и навигация Интеграция ИИ (β) - Поставщики ИИ, API-ключи и настройки моделей + AI providers, API keys, model selection, and activity logs + Generation Parameters + Temperature, sampling, prompts, and song data configuration Резервное копирование и восстановление Экспорт и восстановление персональных данных приложения Параметры разработчика diff --git a/app/src/main/res/values-tr/strings_settings.xml b/app/src/main/res/values-tr/strings_settings.xml index 71b05a38e..31fba8ef4 100644 --- a/app/src/main/res/values-tr/strings_settings.xml +++ b/app/src/main/res/values-tr/strings_settings.xml @@ -10,7 +10,9 @@ Davranış Hareketler, dokunsal geri bildirim ve gezinme davranışı Yapay Zeka Entegrasyonu (β) - YZ sağlayıcıları, API anahtarları ve model ayarları + AI providers, API keys, model selection, and activity logs + Generation Parameters + Temperature, sampling, prompts, and song data configuration Yedekle ve Geri Yükle Kişisel uygulama verilerinizi dışa aktarın ve kurtarın Geliştirici Seçenekleri diff --git a/app/src/main/res/values-zh-rCN/strings_settings.xml b/app/src/main/res/values-zh-rCN/strings_settings.xml index daf7d4eac..07496a312 100644 --- a/app/src/main/res/values-zh-rCN/strings_settings.xml +++ b/app/src/main/res/values-zh-rCN/strings_settings.xml @@ -10,7 +10,9 @@ 行为 手势、触感反馈和导航行为 AI 集成 (β) - AI 提供商、API 密钥和模型设置 + AI providers, API keys, model selection, and activity logs + Generation Parameters + Temperature, sampling, prompts, and song data configuration 备份与恢复 导出和恢复您的个人应用数据 开发者选项 From 5b0444cd2f0c0c68d23a8c42c3bf1e6f9e6e87f1 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Sun, 28 Jun 2026 21:57:27 +0545 Subject: [PATCH 10/19] cleanup: remove unused hasGeminiApiKey, songCountFlow, and repository method - Remove dead hasGeminiApiKey StateFlow in PlayerViewModel - Remove dead songCountFlow StateFlow in PlayerViewModel - Remove unused getSongCountFlow() from MusicRepository interface and impl - Remove corresponding mock from PlayerViewModelTest --- .../pixelplay/data/repository/MusicRepository.kt | 6 ------ .../data/repository/MusicRepositoryImpl.kt | 4 ---- .../presentation/viewmodel/PlayerViewModel.kt | 15 --------------- .../presentation/viewmodel/PlayerViewModelTest.kt | 1 - 4 files changed, 26 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/data/repository/MusicRepository.kt b/app/src/main/java/com/theveloper/pixelplay/data/repository/MusicRepository.kt index 36d9081b9..d7bcb635f 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/repository/MusicRepository.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/repository/MusicRepository.kt @@ -77,12 +77,6 @@ interface MusicRepository { storageFilter: com.theveloper.pixelplay.data.model.StorageFilter = com.theveloper.pixelplay.data.model.StorageFilter.ALL ): Flow - /** - * Returns the count of songs in the library. - * @return Flow emitting the current song count. - */ - fun getSongCountFlow(): Flow - /** * Returns the count of cloud songs in the library. */ diff --git a/app/src/main/java/com/theveloper/pixelplay/data/repository/MusicRepositoryImpl.kt b/app/src/main/java/com/theveloper/pixelplay/data/repository/MusicRepositoryImpl.kt index 5f026af8d..11a16bd1e 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/repository/MusicRepositoryImpl.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/repository/MusicRepositoryImpl.kt @@ -310,10 +310,6 @@ class MusicRepositoryImpl @Inject constructor( return songRepository.getFavoriteSongCountFlow(storageFilter) } - override fun getSongCountFlow(): Flow { - return musicDao.getSongCount().distinctUntilChanged() - } - override fun getCloudSongCountFlow(): Flow { return musicDao.getCloudSongCount().distinctUntilChanged() } diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt index a08f4727a..1c0100c96 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt @@ -536,14 +536,6 @@ class PlayerViewModel @Inject constructor( initialValue = false ) - val hasGeminiApiKey: StateFlow = aiPreferencesRepository.geminiApiKey - .map { it.isNotBlank() } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5000), - initialValue = false - ) - val fullPlayerLoadingTweaks: StateFlow = userPreferencesRepository.fullPlayerLoadingTweaksFlow .stateIn( scope = viewModelScope, @@ -1140,13 +1132,6 @@ class PlayerViewModel @Inject constructor( initialValue = persistentListOf() ) - val songCountFlow: StateFlow = musicRepository.getSongCountFlow() - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5000), - initialValue = 0 - ) - val hasCloudSongsFlow: StateFlow = musicRepository.getCloudSongCountFlow() .map { it > 0 } .distinctUntilChanged() diff --git a/app/src/test/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModelTest.kt b/app/src/test/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModelTest.kt index 7996811c6..5ff639014 100644 --- a/app/src/test/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModelTest.kt +++ b/app/src/test/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModelTest.kt @@ -205,7 +205,6 @@ class PlayerViewModelTest { every { mockMusicRepository.getAudioFiles() } returns flowOf(emptyList()) every { mockMusicRepository.getDistinctAlbumArtSongs() } returns flowOf(emptyList()) every { mockMusicRepository.getHomeMixPreviewSongs(any()) } returns flowOf(emptyList()) - every { mockMusicRepository.getSongCountFlow() } returns flowOf(0) every { mockMusicRepository.getCloudSongCountFlow() } returns flowOf(0) every { mockMusicRepository.searchSongs(any(), any()) } returns flowOf(emptyList()) every { mockMusicRepository.getMusicByGenre(any()) } returns flowOf(emptyList()) From 273f6d95f9aecfdde75e47fd69653360f7c30141 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Sun, 28 Jun 2026 22:20:38 +0545 Subject: [PATCH 11/19] fix: add missing LYRICS and GENERATION_PARAMETERS branches in when expressions - Add LYRICS case to temperature selection in AiHandler.kt - Add GENERATION_PARAMETERS case to category colors in SettingsScreen.kt (both dark and light color schemes) --- app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt | 1 + .../theveloper/pixelplay/presentation/screens/SettingsScreen.kt | 2 ++ 2 files changed, 3 insertions(+) diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt index d9da7840b..f21b6992e 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt @@ -171,6 +171,7 @@ class AiHandler @Inject constructor( AiSystemPromptType.TAGGING -> 0.4f AiSystemPromptType.PLAYLIST, AiSystemPromptType.DAILY_MIX -> 0.6f AiSystemPromptType.PERSONA -> 0.85f + AiSystemPromptType.LYRICS -> 0.7f AiSystemPromptType.GENERAL -> 0.7f } } else temperature 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 fa40ecb26..1bff62daf 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 @@ -483,6 +483,7 @@ private fun getCategoryColors(category: SettingsCategory, isDark: Boolean): Pair SettingsCategory.PLAYBACK -> Color(0xFF633B48) to Color(0xFFFFD8EC) SettingsCategory.BEHAVIOR -> Color(0xFF3E4C63) to Color(0xFFD7E3FF) SettingsCategory.AI_INTEGRATION -> Color(0xFF004F58) to Color(0xFF88FAFF) + SettingsCategory.GENERATION_PARAMETERS -> Color(0xFF4A256B) to Color(0xFFE8B0FF) SettingsCategory.BACKUP_RESTORE -> Color(0xFF3B4869) to Color(0xFFD9E2FF) SettingsCategory.DEVELOPER -> Color(0xFF324F34) to Color(0xFFCBEFD0) SettingsCategory.EQUALIZER -> Color(0xFF6E4E13) to Color(0xFFFFDEAC) @@ -496,6 +497,7 @@ private fun getCategoryColors(category: SettingsCategory, isDark: Boolean): Pair SettingsCategory.PLAYBACK -> Color(0xFFFFD8EC) to Color(0xFF631B4B) SettingsCategory.BEHAVIOR -> Color(0xFFD7E3FF) to Color(0xFF253347) SettingsCategory.AI_INTEGRATION -> Color(0xFFCCE8EA) to Color(0xFF004F58) + SettingsCategory.GENERATION_PARAMETERS -> Color(0xFFE8B0FF) to Color(0xFF3B1A53) SettingsCategory.BACKUP_RESTORE -> Color(0xFFD9E2FF) to Color(0xFF27304E) SettingsCategory.DEVELOPER -> Color(0xFFCBEFD0) to Color(0xFF042106) SettingsCategory.EQUALIZER -> Color(0xFFFFDEAC) to Color(0xFF281900) From 61702e2e0e14508b108a46af1184dde1a4fc7991 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Sun, 28 Jun 2026 22:47:16 +0545 Subject: [PATCH 12/19] fix: add missing @OptIn for ExperimentalMaterial3Api on SearchableProviderSelector --- .../pixelplay/presentation/screens/SettingsComponents.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsComponents.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsComponents.kt index a4a656df4..420fcd422 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsComponents.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsComponents.kt @@ -551,6 +551,7 @@ fun SearchableModelSelector( } } +@OptIn(ExperimentalMaterial3Api::class) @Composable fun SearchableProviderSelector( label: String, From 51209dc3d4a33c0f3144238eab05052a693824d6 Mon Sep 17 00:00:00 2001 From: VoidX3D Date: Sun, 28 Jun 2026 22:54:46 +0545 Subject: [PATCH 13/19] fix: use file-level @OptIn for ExperimentalMaterial3Api in SettingsComponents --- .../pixelplay/presentation/screens/SettingsComponents.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsComponents.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsComponents.kt index 420fcd422..0f9ca6572 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsComponents.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsComponents.kt @@ -1,3 +1,5 @@ +@file:OptIn(ExperimentalMaterial3Api::class) + package com.theveloper.pixelplay.presentation.screens import androidx.compose.animation.AnimatedContent From 39c088718cbcb3de2e2e5c5e2e299b5d00d62c2e Mon Sep 17 00:00:00 2001 From: Dae Euhwa Date: Sun, 28 Jun 2026 14:57:58 -0500 Subject: [PATCH 14/19] fix: address PR review issues - Replace Job.join() with CompletableDeferred for infinite flow sequencing - Fix PlatformUtils reflection calls to use proper instance methods - Remove dead code detectLyricsLanguage() - Include prompt type in cache key hash - Fix loading state coordination in LibraryStateHolder - Move ensureLibrarySortDefaults() to immediate launch - Load persisted daily mix before checkAndUpdateDailyMixIfNeeded() - Restore localized AI subtitle in all 11 locale files --- .../theveloper/pixelplay/data/ai/AiHandler.kt | 2 +- .../presentation/viewmodel/AiStateHolder.kt | 29 ------------------- .../viewmodel/LibraryStateHolder.kt | 28 +++++++++++------- .../presentation/viewmodel/PlayerViewModel.kt | 16 ++++++++-- .../pixelplay/utils/PlatformUtils.kt | 18 ++++++------ .../main/res/values-ar/strings_settings.xml | 2 +- .../main/res/values-de/strings_settings.xml | 2 +- .../main/res/values-es/strings_settings.xml | 2 +- .../main/res/values-fr/strings_settings.xml | 2 +- .../main/res/values-in/strings_settings.xml | 2 +- .../main/res/values-it/strings_settings.xml | 2 +- .../main/res/values-ko/strings_settings.xml | 2 +- .../main/res/values-nb/strings_settings.xml | 2 +- .../main/res/values-ru/strings_settings.xml | 2 +- .../main/res/values-tr/strings_settings.xml | 2 +- .../res/values-zh-rCN/strings_settings.xml | 2 +- 16 files changed, 52 insertions(+), 63 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt index f21b6992e..e7c735740 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt @@ -183,7 +183,7 @@ class AiHandler @Inject constructor( val basePersona = getBasePersona(userProvider) val combinedSystemPrompt = promptEngine.buildPrompt(basePersona, type, context) - val hash = (userProvider.name + combinedSystemPrompt + prompt).sha256() + val hash = (type.name + userProvider.name + combinedSystemPrompt + prompt).sha256() cacheDao.getCache(hash)?.let { cached -> val age = System.currentTimeMillis() - cached.timestamp diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/AiStateHolder.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/AiStateHolder.kt index 0123c537a..0eaffe3ef 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/AiStateHolder.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/AiStateHolder.kt @@ -10,7 +10,6 @@ import com.theveloper.pixelplay.data.ai.AiSystemPromptType import com.theveloper.pixelplay.data.ai.provider.AiProviderException import com.theveloper.pixelplay.data.preferences.PlaylistPreferencesRepository import com.theveloper.pixelplay.data.model.Song -import com.theveloper.pixelplay.utils.MultiLangRomanizer import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow @@ -321,34 +320,6 @@ $lyricsText } } - private val timestampRegex = Regex("\\[\\d{1,2}:\\d{2}(?:[.:]\\d{1,3})?]") - - private fun detectLyricsLanguage(lyricsText: String): String? { - val sample = timestampRegex.replace(lyricsText, "").trim().take(200) - if (sample.isBlank()) return null - - return when { - MultiLangRomanizer.isJapanese(sample) -> "Japanese" - MultiLangRomanizer.isKorean(sample) -> "Korean" - MultiLangRomanizer.isChinese(sample) -> "Chinese" - MultiLangRomanizer.isThai(sample) -> "Thai" - MultiLangRomanizer.isArabic(sample) -> "Arabic" - MultiLangRomanizer.isGreek(sample) -> "Greek" - MultiLangRomanizer.isHebrew(sample) -> "Hebrew" - MultiLangRomanizer.isHindi(sample) -> "Hindi" - MultiLangRomanizer.isPunjabi(sample) -> "Punjabi" - MultiLangRomanizer.isCyrillic(sample) -> { - when { - MultiLangRomanizer.isRussian(sample) -> "Russian" - MultiLangRomanizer.isUkrainian(sample) -> "Ukrainian" - else -> null - } - } - MultiLangRomanizer.isVietnamese(sample) -> "Vietnamese" - else -> null - } - } - fun onCleared() { scope = null allSongsProvider = null diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LibraryStateHolder.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LibraryStateHolder.kt index 3515c7de8..cdbd04537 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LibraryStateHolder.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LibraryStateHolder.kt @@ -22,6 +22,7 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.first @@ -238,6 +239,11 @@ class LibraryStateHolder @Inject constructor( private var albumsJob: Job? = null private var artistsJob: Job? = null private var foldersJob: Job? = null + + private val songsFirstBatch = CompletableDeferred() + private val albumsFirstBatch = CompletableDeferred() + private val artistsFirstBatch = CompletableDeferred() + @Volatile private var needsReloadAfterTrim: Boolean = false @@ -257,6 +263,7 @@ class LibraryStateHolder @Inject constructor( songsJob = scope?.launch { _isLoadingLibrary.value = true musicRepository.getAudioFiles().conflate().collect { songs -> + songsFirstBatch.complete(Unit) val immutableSongs = withContext(Dispatchers.Default) { songs.toImmutableList() } val songsMap = withContext(Dispatchers.Default) { songs.associateBy { it.id } } @@ -268,10 +275,11 @@ class LibraryStateHolder @Inject constructor( } } - // Albums, artists, and folders load sequentially after songs to avoid - // I/O contention on slower storage (eMMC on Redmi vs UFS on Samsung). + // Albums, artists, and folders load sequentially after songs' first emission + // to avoid I/O contention on slower storage. Uses CompletableDeferred instead of + // Job.join() because the underlying flows never complete (they're infinite reactive streams). albumsJob = scope?.launch { - songsJob?.join() + songsFirstBatch.await() @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) kotlinx.coroutines.flow.combine( effectiveStorageFilter, @@ -281,6 +289,7 @@ class LibraryStateHolder @Inject constructor( }.flatMapLatest { (filter, minTracks) -> musicRepository.getAlbums(filter, minTracks) }.conflate().collect { albums -> + albumsFirstBatch.complete(Unit) val sortedAlbums = withContext(Dispatchers.Default) { sortAlbumsList(albums, _currentAlbumSortOption.value).toImmutableList() } @@ -289,11 +298,12 @@ class LibraryStateHolder @Inject constructor( } artistsJob = scope?.launch { - albumsJob?.join() + albumsFirstBatch.await() @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) effectiveStorageFilter.flatMapLatest { filter -> musicRepository.getArtists(filter) }.conflate().collect { artists -> + artistsFirstBatch.complete(Unit) val sortedArtists = withContext(Dispatchers.Default) { sortArtistsList(artists, _currentArtistSortOption.value).toImmutableList() } @@ -302,7 +312,7 @@ class LibraryStateHolder @Inject constructor( } foldersJob = scope?.launch { - artistsJob?.join() + artistsFirstBatch.await() @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) effectiveStorageFilter.flatMapLatest { filter -> musicRepository.getMusicFolders(effectiveFoldersStorageFilter(filter)) @@ -314,13 +324,11 @@ class LibraryStateHolder @Inject constructor( } } - // Single loading state: true when songs start, false when all sequential - // jobs (albums + artists) finish. This avoids flapping true→false→true - // as each sequential coroutine completes. + // Loading categories state: true after songs first batch, false after folders first batch. scope?.launch { - songsJob?.join() + songsFirstBatch.await() _isLoadingCategories.value = true - foldersJob?.join() + artistsFirstBatch.await() _isLoadingCategories.value = false } } diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt index 1c0100c96..8fff62bb4 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt @@ -1512,11 +1512,16 @@ class PlayerViewModel @Inject constructor( + // Sort defaults must be set immediately so LibraryStateHolder.initialize() + // reads the correct sort options rather than pre-migration values. + viewModelScope.launch { + userPreferencesRepository.ensureLibrarySortDefaults() + } + // Deferred: legacy migrations run after init to avoid blocking first render viewModelScope.launch { kotlinx.coroutines.delay(2000) userPreferencesRepository.migrateTabOrder() - userPreferencesRepository.ensureLibrarySortDefaults() val legacyFavoriteIds = userPreferencesRepository.favoriteSongIdsFlow.first() if (legacyFavoriteIds.isNotEmpty()) { val roomFavoriteIds = musicRepository.getFavoriteSongIdsOnce() @@ -1658,10 +1663,15 @@ class PlayerViewModel @Inject constructor( // launchColorSchemeProcessor() - Handled by ThemeStateHolder and on-demand calls - // Deferred: daily mix and search history load after first frame + // Load persisted daily mix immediately so checkAndUpdateDailyMixIfNeeded() + // in onMainActivityStart() doesn't unnecessarily regenerate the mix. viewModelScope.launch { - kotlinx.coroutines.delay(1500) loadPersistedDailyMix() + } + + // Deferred: search history loads after first frame + viewModelScope.launch { + kotlinx.coroutines.delay(1500) loadSearchHistory() } diff --git a/app/src/main/java/com/theveloper/pixelplay/utils/PlatformUtils.kt b/app/src/main/java/com/theveloper/pixelplay/utils/PlatformUtils.kt index 817cd8697..c98723be6 100644 --- a/app/src/main/java/com/theveloper/pixelplay/utils/PlatformUtils.kt +++ b/app/src/main/java/com/theveloper/pixelplay/utils/PlatformUtils.kt @@ -1,5 +1,7 @@ package com.theveloper.pixelplay.utils +import android.app.ActivityManager +import android.content.Context import android.os.Build object PlatformUtils { @@ -46,17 +48,15 @@ object PlatformUtils { Build.FINGERPRINT?.contains("generic", ignoreCase = true) == true } - val isLowRamDevice: Boolean get() = runCatching { - val activityManager = android.app.ActivityManager::class.java - .getMethod("isLowRamDevice") - activityManager.invoke(null) as? Boolean ?: false + fun isLowRamDevice(context: Context): Boolean = runCatching { + val am = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + am.isLowRamDevice }.getOrDefault(false) - val totalMemoryMb: Long get() = runCatching { - val info = android.app.ActivityManager.MemoryInfo() - android.app.ActivityManager::class.java - .getMethod("getMemoryInfo", android.app.ActivityManager.MemoryInfo::class.java) - .invoke(null, info) + fun totalMemoryMb(context: Context): Long = runCatching { + val am = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + val info = ActivityManager.MemoryInfo() + am.getMemoryInfo(info) info.totalMem / (1024 * 1024) }.getOrDefault(0L) } diff --git a/app/src/main/res/values-ar/strings_settings.xml b/app/src/main/res/values-ar/strings_settings.xml index b5dfcfddb..2e16c2669 100644 --- a/app/src/main/res/values-ar/strings_settings.xml +++ b/app/src/main/res/values-ar/strings_settings.xml @@ -13,7 +13,7 @@ السلوك الإيماءات، التفاعل اللمسي، وسلوك التنقل دمج الذكاء الاصطناعي (β) - AI providers, API keys, model selection, and activity logs + مزودو الذكاء الاصطناعي، مفاتيح الـ API، وإعدادات النموذج Generation Parameters Temperature, sampling, prompts, and song data configuration النسخ الاحتياطي والاستعادة diff --git a/app/src/main/res/values-de/strings_settings.xml b/app/src/main/res/values-de/strings_settings.xml index db1a0dd22..499298ea1 100644 --- a/app/src/main/res/values-de/strings_settings.xml +++ b/app/src/main/res/values-de/strings_settings.xml @@ -12,7 +12,7 @@ Verhalten Gesten, Haptik und Navigation KI-Integration (β) - AI providers, API keys, model selection, and activity logs + Anbieter, API Keys und Modelle Generation Parameters Temperature, sampling, prompts, and song data configuration Backup & Wiederherstellen diff --git a/app/src/main/res/values-es/strings_settings.xml b/app/src/main/res/values-es/strings_settings.xml index bc7c980df..17e83ca72 100644 --- a/app/src/main/res/values-es/strings_settings.xml +++ b/app/src/main/res/values-es/strings_settings.xml @@ -10,7 +10,7 @@ Comportamiento Gestos, vibración y navegación Integración IA (β) - AI providers, API keys, model selection, and activity logs + Proveedores de IA, claves API y modelos Generation Parameters Temperature, sampling, prompts, and song data configuration Copia y restauración diff --git a/app/src/main/res/values-fr/strings_settings.xml b/app/src/main/res/values-fr/strings_settings.xml index d6cc27229..97d37134f 100644 --- a/app/src/main/res/values-fr/strings_settings.xml +++ b/app/src/main/res/values-fr/strings_settings.xml @@ -10,7 +10,7 @@ Comportement Gestes, retour haptique et comportement de navigation Intégration IA (β) - AI providers, API keys, model selection, and activity logs + Fournisseurs d\'IA, clés API et paramètres du modèle Generation Parameters Temperature, sampling, prompts, and song data configuration Sauvegarde et restauration diff --git a/app/src/main/res/values-in/strings_settings.xml b/app/src/main/res/values-in/strings_settings.xml index bd6ee6c5f..ca85cc981 100644 --- a/app/src/main/res/values-in/strings_settings.xml +++ b/app/src/main/res/values-in/strings_settings.xml @@ -10,7 +10,7 @@ Perilaku Gerakan, haptik, dan perilaku navigasi Integrasi AI (β) - AI providers, API keys, model selection, and activity logs + Penyedia AI, kunci API, dan pengaturan model Generation Parameters Temperature, sampling, prompts, and song data configuration Cadangkan & Pulihkan diff --git a/app/src/main/res/values-it/strings_settings.xml b/app/src/main/res/values-it/strings_settings.xml index eefd35f1c..ef2d3b4e1 100644 --- a/app/src/main/res/values-it/strings_settings.xml +++ b/app/src/main/res/values-it/strings_settings.xml @@ -10,7 +10,7 @@ Comportamento Gesti, feedback aptico e comportamento navigazione Integrazione AI (β) - AI providers, API keys, model selection, and activity logs + Provider AI, chiavi API e impostazioni modello Generation Parameters Temperature, sampling, prompts, and song data configuration Backup e ripristino diff --git a/app/src/main/res/values-ko/strings_settings.xml b/app/src/main/res/values-ko/strings_settings.xml index 1d1957f05..0245af98b 100644 --- a/app/src/main/res/values-ko/strings_settings.xml +++ b/app/src/main/res/values-ko/strings_settings.xml @@ -12,7 +12,7 @@ 동작 제스처, 햅틱 및 내비게이션 동작 AI 연동 (β) - AI providers, API keys, model selection, and activity logs + AI 제공업체, API 키 및 모델 설정 Generation Parameters Temperature, sampling, prompts, and song data configuration 백업 및 복구 diff --git a/app/src/main/res/values-nb/strings_settings.xml b/app/src/main/res/values-nb/strings_settings.xml index 39af2eb94..94d4646c8 100644 --- a/app/src/main/res/values-nb/strings_settings.xml +++ b/app/src/main/res/values-nb/strings_settings.xml @@ -12,7 +12,7 @@ Atferd Gester, haptikk og navigasjonsatferd AI-integrasjon (β) - AI providers, API keys, model selection, and activity logs + AI-leverandører, API-nøkler og modellinnstillinger Generation Parameters Temperature, sampling, prompts, and song data configuration Sikkerhetskopiering og gjenoppretting diff --git a/app/src/main/res/values-ru/strings_settings.xml b/app/src/main/res/values-ru/strings_settings.xml index c99d6d76c..ac8a32c45 100644 --- a/app/src/main/res/values-ru/strings_settings.xml +++ b/app/src/main/res/values-ru/strings_settings.xml @@ -12,7 +12,7 @@ Поведение Жесты, тактильный отклик и навигация Интеграция ИИ (β) - AI providers, API keys, model selection, and activity logs + Поставщики ИИ, API-ключи и настройки моделей Generation Parameters Temperature, sampling, prompts, and song data configuration Резервное копирование и восстановление diff --git a/app/src/main/res/values-tr/strings_settings.xml b/app/src/main/res/values-tr/strings_settings.xml index 31fba8ef4..97e2ccfed 100644 --- a/app/src/main/res/values-tr/strings_settings.xml +++ b/app/src/main/res/values-tr/strings_settings.xml @@ -10,7 +10,7 @@ Davranış Hareketler, dokunsal geri bildirim ve gezinme davranışı Yapay Zeka Entegrasyonu (β) - AI providers, API keys, model selection, and activity logs + YZ sağlayıcıları, API anahtarları ve model ayarları Generation Parameters Temperature, sampling, prompts, and song data configuration Yedekle ve Geri Yükle diff --git a/app/src/main/res/values-zh-rCN/strings_settings.xml b/app/src/main/res/values-zh-rCN/strings_settings.xml index 07496a312..6f608f006 100644 --- a/app/src/main/res/values-zh-rCN/strings_settings.xml +++ b/app/src/main/res/values-zh-rCN/strings_settings.xml @@ -10,7 +10,7 @@ 行为 手势、触感反馈和导航行为 AI 集成 (β) - AI providers, API keys, model selection, and activity logs + AI 提供商、API 密钥和模型设置 Generation Parameters Temperature, sampling, prompts, and song data configuration 备份与恢复 From b19efc3aa9ad4d5afce76beba0ec68b2d889c27a Mon Sep 17 00:00:00 2001 From: Dae Euhwa Date: Sun, 28 Jun 2026 15:08:59 -0500 Subject: [PATCH 15/19] fix: reset CompletableDeferred gates after trim; consolidate dead LYRICS temp branch --- .../com/theveloper/pixelplay/data/ai/AiHandler.kt | 3 +-- .../presentation/viewmodel/LibraryStateHolder.kt | 12 +++++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt index e7c735740..2995b4d78 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt @@ -171,8 +171,7 @@ class AiHandler @Inject constructor( AiSystemPromptType.TAGGING -> 0.4f AiSystemPromptType.PLAYLIST, AiSystemPromptType.DAILY_MIX -> 0.6f AiSystemPromptType.PERSONA -> 0.85f - AiSystemPromptType.LYRICS -> 0.7f - AiSystemPromptType.GENERAL -> 0.7f + AiSystemPromptType.GENERAL, AiSystemPromptType.LYRICS -> 0.7f } } else temperature } else params.temperature diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LibraryStateHolder.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LibraryStateHolder.kt index cdbd04537..b14477e9a 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LibraryStateHolder.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LibraryStateHolder.kt @@ -240,9 +240,9 @@ class LibraryStateHolder @Inject constructor( private var artistsJob: Job? = null private var foldersJob: Job? = null - private val songsFirstBatch = CompletableDeferred() - private val albumsFirstBatch = CompletableDeferred() - private val artistsFirstBatch = CompletableDeferred() + private var songsFirstBatch = CompletableDeferred() + private var albumsFirstBatch = CompletableDeferred() + private var artistsFirstBatch = CompletableDeferred() @Volatile private var needsReloadAfterTrim: Boolean = false @@ -257,6 +257,12 @@ class LibraryStateHolder @Inject constructor( return } + // Reset one-shot completion gates so that after trimMemory + restart + // the sequential loading order is preserved (await() will suspend again). + songsFirstBatch = CompletableDeferred() + albumsFirstBatch = CompletableDeferred() + artistsFirstBatch = CompletableDeferred() + Log.d("LibraryStateHolder", "startObservingLibraryData called.") needsReloadAfterTrim = false From 901b49120b71ea44bac24591ee8871a981801dfb Mon Sep 17 00:00:00 2001 From: Dae Euhwa Date: Sun, 28 Jun 2026 15:19:21 -0500 Subject: [PATCH 16/19] docs: fix comment - categories state tracks artists, not folders --- .../pixelplay/presentation/viewmodel/LibraryStateHolder.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LibraryStateHolder.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LibraryStateHolder.kt index b14477e9a..4e26eefe5 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LibraryStateHolder.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LibraryStateHolder.kt @@ -330,7 +330,7 @@ class LibraryStateHolder @Inject constructor( } } - // Loading categories state: true after songs first batch, false after folders first batch. + // Loading categories state: true after songs first batch, false after artists first batch. scope?.launch { songsFirstBatch.await() _isLoadingCategories.value = true From fb768bf1045b4b730ae89ca6bb9d194118d1773a Mon Sep 17 00:00:00 2001 From: Dae Euhwa Date: Sun, 28 Jun 2026 15:29:05 -0500 Subject: [PATCH 17/19] fix: cancel old jobs before recreating CompletableDeferred gates --- .../pixelplay/presentation/viewmodel/LibraryStateHolder.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LibraryStateHolder.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LibraryStateHolder.kt index 4e26eefe5..38c1e5d4c 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LibraryStateHolder.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LibraryStateHolder.kt @@ -257,6 +257,13 @@ class LibraryStateHolder @Inject constructor( return } + // Cancel any still-active jobs before resetting gates and launching new ones, + // otherwise old collectors will complete the new deferreds prematurely. + songsJob?.cancel() + albumsJob?.cancel() + artistsJob?.cancel() + foldersJob?.cancel() + // Reset one-shot completion gates so that after trimMemory + restart // the sequential loading order is preserved (await() will suspend again). songsFirstBatch = CompletableDeferred() From 8bd6a2e9fb91c958c855f581c891f83ea402b35e Mon Sep 17 00:00:00 2001 From: Dae Euhwa Date: Sun, 28 Jun 2026 15:44:58 -0500 Subject: [PATCH 18/19] fix: restore source language detection in translateLyrics prompt Korean/Hangul lyrics were incorrectly flagged as already in English by the AI because the prompt lacked source language context. Restore detectLyricsLanguage() to generate a typed task hint (e.g. 'Translate these Korean song lyrics...'). --- .../presentation/viewmodel/AiStateHolder.kt | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/AiStateHolder.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/AiStateHolder.kt index 0eaffe3ef..20595b9be 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/AiStateHolder.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/AiStateHolder.kt @@ -10,6 +10,7 @@ import com.theveloper.pixelplay.data.ai.AiSystemPromptType import com.theveloper.pixelplay.data.ai.provider.AiProviderException import com.theveloper.pixelplay.data.preferences.PlaylistPreferencesRepository import com.theveloper.pixelplay.data.model.Song +import com.theveloper.pixelplay.utils.MultiLangRomanizer import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow @@ -288,8 +289,14 @@ class AiStateHolder @Inject constructor( suspend fun translateLyrics(lyricsText: String): Result { return try { val targetLanguage = context.resources.configuration.locales[0].displayLanguage + val sourceLanguage = detectLyricsLanguage(lyricsText) + val taskPrefix = if (sourceLanguage != null) { + "Translate these $sourceLanguage song lyrics into $targetLanguage." + } else { + "Translate these song lyrics into $targetLanguage." + } val xmlPrompt = """ -Translate song lyrics into $targetLanguage. +$taskPrefix - Preserve ALL timestamps [mm:ss.xx] exactly — never modify, merge, or drop them. @@ -320,6 +327,34 @@ $lyricsText } } + private val timestampRegex = Regex("\\[\\d{1,2}:\\d{2}(?:[.:]\\d{1,3})?]") + + private fun detectLyricsLanguage(lyricsText: String): String? { + val sample = timestampRegex.replace(lyricsText, "").trim().take(200) + if (sample.isBlank()) return null + + return when { + MultiLangRomanizer.isJapanese(sample) -> "Japanese" + MultiLangRomanizer.isKorean(sample) -> "Korean" + MultiLangRomanizer.isChinese(sample) -> "Chinese" + MultiLangRomanizer.isThai(sample) -> "Thai" + MultiLangRomanizer.isArabic(sample) -> "Arabic" + MultiLangRomanizer.isGreek(sample) -> "Greek" + MultiLangRomanizer.isHebrew(sample) -> "Hebrew" + MultiLangRomanizer.isHindi(sample) -> "Hindi" + MultiLangRomanizer.isPunjabi(sample) -> "Punjabi" + MultiLangRomanizer.isCyrillic(sample) -> { + when { + MultiLangRomanizer.isRussian(sample) -> "Russian" + MultiLangRomanizer.isUkrainian(sample) -> "Ukrainian" + else -> null + } + } + MultiLangRomanizer.isVietnamese(sample) -> "Vietnamese" + else -> null + } + } + fun onCleared() { scope = null allSongsProvider = null From 450b2b0200ed6a436c4bcd563c7e23c033b74f1c Mon Sep 17 00:00:00 2001 From: Dae Euhwa Date: Sun, 28 Jun 2026 15:46:20 -0500 Subject: [PATCH 19/19] fix: track and cancel categories-loading coroutine on re-entry --- .../presentation/viewmodel/LibraryStateHolder.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LibraryStateHolder.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LibraryStateHolder.kt index 38c1e5d4c..b3417eb27 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LibraryStateHolder.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LibraryStateHolder.kt @@ -239,6 +239,7 @@ class LibraryStateHolder @Inject constructor( private var albumsJob: Job? = null private var artistsJob: Job? = null private var foldersJob: Job? = null + private var categoriesJob: Job? = null private var songsFirstBatch = CompletableDeferred() private var albumsFirstBatch = CompletableDeferred() @@ -263,6 +264,7 @@ class LibraryStateHolder @Inject constructor( albumsJob?.cancel() artistsJob?.cancel() foldersJob?.cancel() + categoriesJob?.cancel() // Reset one-shot completion gates so that after trimMemory + restart // the sequential loading order is preserved (await() will suspend again). @@ -338,7 +340,7 @@ class LibraryStateHolder @Inject constructor( } // Loading categories state: true after songs first batch, false after artists first batch. - scope?.launch { + categoriesJob = scope?.launch { songsFirstBatch.await() _isLoadingCategories.value = true artistsFirstBatch.await() @@ -609,17 +611,20 @@ class LibraryStateHolder @Inject constructor( songsJob?.isActive == true || albumsJob?.isActive == true || artistsJob?.isActive == true || - foldersJob?.isActive == true + foldersJob?.isActive == true || + categoriesJob?.isActive == true if (!hasLoadedData && !hasActiveCollectors) return songsJob?.cancel() albumsJob?.cancel() artistsJob?.cancel() foldersJob?.cancel() + categoriesJob?.cancel() songsJob = null albumsJob = null artistsJob = null foldersJob = null + categoriesJob = null _allSongs.value = persistentListOf() _allSongsById.value = emptyMap()