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..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,7 +171,7 @@ class AiHandler @Inject constructor(
AiSystemPromptType.TAGGING -> 0.4f
AiSystemPromptType.PLAYLIST, AiSystemPromptType.DAILY_MIX -> 0.6f
AiSystemPromptType.PERSONA -> 0.85f
- AiSystemPromptType.GENERAL -> 0.7f
+ AiSystemPromptType.GENERAL, AiSystemPromptType.LYRICS -> 0.7f
}
} else temperature
} else params.temperature
@@ -182,7 +182,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/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/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/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/model/SettingsCategory.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/model/SettingsCategory.kt
index 7d827896f..5a3d1c789 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
@@ -10,6 +10,7 @@ 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.QueueMusic
+import androidx.compose.material.icons.rounded.Tune
import androidx.compose.ui.graphics.vector.ImageVector
import com.theveloper.pixelplay.R
@@ -62,6 +63,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/AiUsageComponents.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/AiUsageComponents.kt
index 750f992a7..97933c5dd 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
@@ -179,6 +179,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/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)
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 bb97e988b..1e3e68a7c 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
@@ -923,12 +923,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(
@@ -1039,6 +1039,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),
@@ -1142,6 +1243,8 @@ fun SettingsCategoryScreen(
)
}
+ Spacer(modifier = Modifier.height(16.dp))
+
// Song Data Configuration Section
SettingsSubsection(title = "Song Data Configuration") {
val aiSampleSize by settingsViewModel.aiSampleSize.collectAsStateWithLifecycle()
@@ -1187,107 +1290,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)
- ) {
- @Suppress("NonObservableLocale")
- 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/java/com/theveloper/pixelplay/presentation/screens/SettingsComponents.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsComponents.kt
index bc5dbb329..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
@@ -551,6 +553,184 @@ fun SearchableModelSelector(
}
}
+@OptIn(ExperimentalMaterial3Api::class)
+@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,
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 292d3448e..2acbf769a 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
@@ -484,6 +484,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)
@@ -498,6 +499,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)
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 8e125a235..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
@@ -295,7 +295,7 @@ class AiStateHolder @Inject constructor(
} else {
"Translate these song lyrics into $targetLanguage."
}
- val prompt = """
+ val xmlPrompt = """
$taskPrefix
@@ -317,8 +317,8 @@ $lyricsText
""".trimIndent()
val response = aiHandler.generateContent(
- prompt = prompt,
- type = AiSystemPromptType.GENERAL,
+ prompt = xmlPrompt,
+ type = AiSystemPromptType.LYRICS,
temperature = 0.1f
)
Result.success(response)
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..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
@@ -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,12 @@ 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()
+ private var artistsFirstBatch = CompletableDeferred()
+
@Volatile
private var needsReloadAfterTrim: Boolean = false
@@ -251,29 +258,43 @@ 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()
+ categoriesJob?.cancel()
+
+ // 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
songsJob = scope?.launch {
_isLoadingLibrary.value = true
musicRepository.getAudioFiles().conflate().collect { songs ->
- // Process heavy list conversions on Default dispatcher to avoid blocking UI
+ songsFirstBatch.complete(Unit)
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' 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 {
- _isLoadingCategories.value = true
+ songsFirstBatch.await()
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
kotlinx.coroutines.flow.combine(
effectiveStorageFilter,
@@ -283,29 +304,30 @@ 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()
}
_albums.value = sortedAlbums
- _isLoadingCategories.value = false
}
}
artistsJob = scope?.launch {
- _isLoadingCategories.value = true
+ 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()
}
_artists.value = sortedArtists
- _isLoadingCategories.value = false
}
}
foldersJob = scope?.launch {
+ artistsFirstBatch.await()
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
effectiveStorageFilter.flatMapLatest { filter ->
musicRepository.getMusicFolders(effectiveFoldersStorageFilter(filter))
@@ -316,6 +338,14 @@ class LibraryStateHolder @Inject constructor(
_musicFolders.value = sortedFolders
}
}
+
+ // Loading categories state: true after songs first batch, false after artists first batch.
+ categoriesJob = scope?.launch {
+ songsFirstBatch.await()
+ _isLoadingCategories.value = true
+ artistsFirstBatch.await()
+ _isLoadingCategories.value = false
+ }
}
// Deprecated imperative loaders - redirected to observer start
@@ -581,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()
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..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
@@ -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()
@@ -1527,15 +1512,16 @@ class PlayerViewModel @Inject constructor(
- viewModelScope.launch {
- userPreferencesRepository.migrateTabOrder()
- }
-
+ // 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()
val legacyFavoriteIds = userPreferencesRepository.favoriteSongIdsFlow.first()
if (legacyFavoriteIds.isNotEmpty()) {
val roomFavoriteIds = musicRepository.getFavoriteSongIdsOnce()
@@ -1677,8 +1663,17 @@ class PlayerViewModel @Inject constructor(
// launchColorSchemeProcessor() - Handled by ThemeStateHolder and on-demand calls
- loadPersistedDailyMix()
- loadSearchHistory()
+ // Load persisted daily mix immediately so checkAndUpdateDailyMixIfNeeded()
+ // in onMainActivityStart() doesn't unnecessarily regenerate the mix.
+ viewModelScope.launch {
+ loadPersistedDailyMix()
+ }
+
+ // Deferred: search history loads after first frame
+ viewModelScope.launch {
+ kotlinx.coroutines.delay(1500)
+ loadSearchHistory()
+ }
viewModelScope.launch {
isSyncingStateFlow.collect { isSyncing ->
@@ -1797,7 +1792,7 @@ class PlayerViewModel @Inject constructor(
aiStatus = status,
aiError = error
)
- }.collect { snapshot ->
+ }.distinctUntilChanged().collect { snapshot ->
_playerUiState.update {
it.copy(
showAiPlaylistSheet = snapshot.showAiPlaylistSheet,
@@ -1820,7 +1815,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 +1836,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,
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..c98723be6
--- /dev/null
+++ b/app/src/main/java/com/theveloper/pixelplay/utils/PlatformUtils.kt
@@ -0,0 +1,62 @@
+package com.theveloper.pixelplay.utils
+
+import android.app.ActivityManager
+import android.content.Context
+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
+ }
+
+ fun isLowRamDevice(context: Context): Boolean = runCatching {
+ val am = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
+ am.isLowRamDevice
+ }.getOrDefault(false)
+
+ 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 afced2d2d..2e16c2669 100644
--- a/app/src/main/res/values-ar/strings_settings.xml
+++ b/app/src/main/res/values-ar/strings_settings.xml
@@ -14,6 +14,8 @@
الإيماءات، التفاعل اللمسي، وسلوك التنقل
دمج الذكاء الاصطناعي (β)
مزودو الذكاء الاصطناعي، مفاتيح الـ 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 147d337ad..499298ea1 100644
--- a/app/src/main/res/values-de/strings_settings.xml
+++ b/app/src/main/res/values-de/strings_settings.xml
@@ -13,6 +13,8 @@
Gesten, Haptik und Navigation
KI-Integration (β)
Anbieter, API Keys und Modelle
+ 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..17e83ca72 100644
--- a/app/src/main/res/values-es/strings_settings.xml
+++ b/app/src/main/res/values-es/strings_settings.xml
@@ -11,6 +11,8 @@
Gestos, vibración y navegación
Integración IA (β)
Proveedores de IA, claves API y modelos
+ 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..97d37134f 100644
--- a/app/src/main/res/values-fr/strings_settings.xml
+++ b/app/src/main/res/values-fr/strings_settings.xml
@@ -11,6 +11,8 @@
Gestes, retour haptique et comportement de navigation
Intégration IA (β)
Fournisseurs d\'IA, clés API et paramètres du modèle
+ 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..ca85cc981 100644
--- a/app/src/main/res/values-in/strings_settings.xml
+++ b/app/src/main/res/values-in/strings_settings.xml
@@ -11,6 +11,8 @@
Gerakan, haptik, dan perilaku navigasi
Integrasi AI (β)
Penyedia AI, kunci API, dan pengaturan model
+ 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..ef2d3b4e1 100644
--- a/app/src/main/res/values-it/strings_settings.xml
+++ b/app/src/main/res/values-it/strings_settings.xml
@@ -11,6 +11,8 @@
Gesti, feedback aptico e comportamento navigazione
Integrazione AI (β)
Provider AI, chiavi API e impostazioni modello
+ 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 da2e36e8d..0245af98b 100644
--- a/app/src/main/res/values-ko/strings_settings.xml
+++ b/app/src/main/res/values-ko/strings_settings.xml
@@ -13,6 +13,8 @@
제스처, 햅틱 및 내비게이션 동작
AI 연동 (β)
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 0295cb20a..94d4646c8 100644
--- a/app/src/main/res/values-nb/strings_settings.xml
+++ b/app/src/main/res/values-nb/strings_settings.xml
@@ -13,6 +13,8 @@
Gester, haptikk og navigasjonsatferd
AI-integrasjon (β)
AI-leverandører, API-nøkler og modellinnstillinger
+ 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 faceb539a..ac8a32c45 100644
--- a/app/src/main/res/values-ru/strings_settings.xml
+++ b/app/src/main/res/values-ru/strings_settings.xml
@@ -13,6 +13,8 @@
Жесты, тактильный отклик и навигация
Интеграция ИИ (β)
Поставщики ИИ, 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 71b05a38e..97e2ccfed 100644
--- a/app/src/main/res/values-tr/strings_settings.xml
+++ b/app/src/main/res/values-tr/strings_settings.xml
@@ -11,6 +11,8 @@
Hareketler, dokunsal geri bildirim ve gezinme davranışı
Yapay Zeka Entegrasyonu (β)
YZ sağlayıcıları, API anahtarları ve model ayarları
+ 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..6f608f006 100644
--- a/app/src/main/res/values-zh-rCN/strings_settings.xml
+++ b/app/src/main/res/values-zh-rCN/strings_settings.xml
@@ -11,6 +11,8 @@
手势、触感反馈和导航行为
AI 集成 (β)
AI 提供商、API 密钥和模型设置
+ Generation Parameters
+ Temperature, sampling, prompts, and song data configuration
备份与恢复
导出和恢复您的个人应用数据
开发者选项
diff --git a/app/src/main/res/values/strings_settings.xml b/app/src/main/res/values/strings_settings.xml
index b7f5cc4f5..feb3fb6a3 100644
--- a/app/src/main/res/values/strings_settings.xml
+++ b/app/src/main/res/values/strings_settings.xml
@@ -12,7 +12,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
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())