diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index b2793cfd8..115355c14 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -21,7 +21,7 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v6
+ uses: actions/checkout@v7
- name: Set up JDK 21
uses: actions/setup-java@v5
diff --git a/.github/workflows/nightly-apk.yml b/.github/workflows/nightly-apk.yml
index 2c6f7728f..7d7831616 100644
--- a/.github/workflows/nightly-apk.yml
+++ b/.github/workflows/nightly-apk.yml
@@ -85,7 +85,7 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v6
+ uses: actions/checkout@v7
- name: Set up JDK 21
uses: actions/setup-java@v5
diff --git a/.github/workflows/phone-debug.yml b/.github/workflows/phone-debug.yml
index 3f7d70fb2..ddf07be71 100644
--- a/.github/workflows/phone-debug.yml
+++ b/.github/workflows/phone-debug.yml
@@ -18,7 +18,7 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v6
+ uses: actions/checkout@v7
- name: Set up JDK 21
uses: actions/setup-java@v5
diff --git a/.github/workflows/phone-release.yml b/.github/workflows/phone-release.yml
index 1c33965a3..87b2f6a0a 100644
--- a/.github/workflows/phone-release.yml
+++ b/.github/workflows/phone-release.yml
@@ -18,7 +18,7 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v6
+ uses: actions/checkout@v7
- name: Set up JDK 21
uses: actions/setup-java@v5
diff --git a/.github/workflows/wearos-apk.yml b/.github/workflows/wearos-apk.yml
index 6e7b53b6d..afc488b21 100644
--- a/.github/workflows/wearos-apk.yml
+++ b/.github/workflows/wearos-apk.yml
@@ -18,7 +18,7 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v6
+ uses: actions/checkout@v7
- name: Set up JDK 21
uses: actions/setup-java@v5
diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
deleted file mode 100644
index d8b7d40b0..000000000
--- a/.idea/deploymentTargetSelector.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/theveloper/pixelplay/data/repository/PlaylistSongCountTest.kt b/app/src/androidTest/java/com/theveloper/pixelplay/data/repository/PlaylistSongCountTest.kt
new file mode 100644
index 000000000..ed7e1eebc
--- /dev/null
+++ b/app/src/androidTest/java/com/theveloper/pixelplay/data/repository/PlaylistSongCountTest.kt
@@ -0,0 +1,136 @@
+package com.theveloper.pixelplay.data.repository
+
+import android.content.Context
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.core.PreferenceDataStoreFactory
+import androidx.datastore.preferences.preferencesDataStoreFile
+import androidx.room.Room
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.theveloper.pixelplay.data.database.LocalPlaylistDao
+import com.theveloper.pixelplay.data.database.PixelPlayDatabase
+import com.theveloper.pixelplay.data.preferences.PlaylistPreferencesRepository
+import com.theveloper.pixelplay.data.preferences.UserPreferencesRepository
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
+import kotlinx.serialization.json.Json
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Regression test for issue #2391:
+ * "Playlist song count doesn't update when removing songs — only when adding."
+ *
+ * Exercises the real PlaylistPreferencesRepository against an in-memory Room DB to
+ * verify that the song count exposed by userPlaylistsFlow (used by the Playlists menu)
+ * reflects removals as well as additions.
+ */
+@RunWith(AndroidJUnit4::class)
+class PlaylistSongCountTest {
+
+ private lateinit var db: PixelPlayDatabase
+ private lateinit var dao: LocalPlaylistDao
+ private lateinit var dataStore: DataStore
+ private lateinit var repo: PlaylistPreferencesRepository
+
+ @Before
+ fun setup() {
+ val context = ApplicationProvider.getApplicationContext()
+ db = Room.inMemoryDatabaseBuilder(context, PixelPlayDatabase::class.java)
+ .addCallback(PixelPlayDatabase.createRuntimeArtifactsCallback())
+ .allowMainThreadQueries()
+ .build()
+ dao = db.localPlaylistDao()
+ dataStore = PreferenceDataStoreFactory.create {
+ context.preferencesDataStoreFile("test_settings_${System.nanoTime()}")
+ }
+ val userPrefs = UserPreferencesRepository(dataStore, Json { ignoreUnknownKeys = true })
+ repo = PlaylistPreferencesRepository(dao, userPrefs)
+ }
+
+ @After
+ fun teardown() {
+ db.close()
+ }
+
+ private suspend fun countFor(playlistId: String): Int =
+ repo.userPlaylistsFlow.first().first { it.id == playlistId }.songIds.size
+
+ @Test
+ fun menuSongCount_reflectsAddAndRemove() = runTest {
+ val playlist = repo.createPlaylist(name = "J-Pop", songIds = listOf("10", "20", "30"))
+ assertEquals("initial count", 3, countFor(playlist.id))
+
+ // Remove a song — the bug report says this does NOT update the count.
+ repo.removeSongFromPlaylist(playlist.id, "20")
+ assertEquals("after removing one song", 2, countFor(playlist.id))
+
+ // Remove another.
+ repo.removeSongFromPlaylist(playlist.id, "30")
+ assertEquals("after removing a second song", 1, countFor(playlist.id))
+
+ // Adding works per the report — verify it still does.
+ repo.addSongsToPlaylist(playlist.id, listOf("40"))
+ assertEquals("after adding one song", 2, countFor(playlist.id))
+ }
+
+ /**
+ * Reproduces the real-world trigger for issue #2391: removing several songs in
+ * quick succession. Each edit does an unsynchronized read-modify-write
+ * (userPlaylistsFlow.first() -> modify -> updatePlaylist), so concurrent removals
+ * all read the same original list and the last writer wins, silently dropping the
+ * other removals. The Playlists-menu count (songIds.size) then stays stuck high.
+ */
+ @Test
+ fun concurrentRemovals_doNotLoseUpdates() = runBlocking {
+ val playlist = repo.createPlaylist(
+ name = "Race",
+ songIds = listOf("1", "2", "3", "4", "5")
+ )
+ assertEquals(5, countFor(playlist.id))
+
+ // Remove four songs concurrently — "remove one or two of them", fast.
+ coroutineScope {
+ listOf("1", "2", "3", "4").forEach { id ->
+ launch(Dispatchers.IO) { repo.removeSongFromPlaylist(playlist.id, id) }
+ }
+ }
+
+ assertEquals("All concurrent removals must persist", 1, countFor(playlist.id))
+ }
+
+ /**
+ * Walks the exact reproduction from issue #2391, asserting the fixed behaviour:
+ * the song count stays accurate after a quick removal of "one or two" songs, and
+ * a later addition does not preserve a phantom difference.
+ */
+ @Test
+ fun issue2391_quickRemoveThenAdd_keepsCountAccurate() = runBlocking {
+ // Steps 2-3: create a playlist and add a few songs.
+ val playlist = repo.createPlaylist(
+ name = "J-Pop",
+ songIds = listOf("1", "2", "3", "4", "5", "6")
+ )
+ assertEquals(6, countFor(playlist.id))
+
+ // Step 4: remove one or two of them — quickly, as fast taps do.
+ coroutineScope {
+ launch(Dispatchers.IO) { repo.removeSongFromPlaylist(playlist.id, "2") }
+ launch(Dispatchers.IO) { repo.removeSongFromPlaylist(playlist.id, "4") }
+ }
+ // Step 5: the menu count must reflect BOTH removals (the bug left it stuck high).
+ assertEquals("count after removing two songs", 4, countFor(playlist.id))
+
+ // Steps 6-7: adding more must not carry over a phantom difference.
+ repo.addSongsToPlaylist(playlist.id, listOf("7", "8"))
+ assertEquals("count after adding two songs", 6, countFor(playlist.id))
+ }
+}
diff --git a/app/src/main/java/com/theveloper/pixelplay/MainActivity.kt b/app/src/main/java/com/theveloper/pixelplay/MainActivity.kt
index 503ac7a21..ec33ffa8d 100644
--- a/app/src/main/java/com/theveloper/pixelplay/MainActivity.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/MainActivity.kt
@@ -1,4 +1,4 @@
-package com.theveloper.pixelplay
+package com.theveloper.pixelplay
import com.theveloper.pixelplay.presentation.navigation.navigateSafely
@@ -46,7 +46,9 @@ import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize
@@ -54,6 +56,8 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.calculateStartPadding
+import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.material3.CircularWavyProgressIndicator
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
@@ -80,16 +84,20 @@ import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.layout.widthIn
import com.theveloper.pixelplay.presentation.viewmodel.PlayerSheetState
import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.lerp
import androidx.core.net.toUri
@@ -122,6 +130,7 @@ import com.theveloper.pixelplay.presentation.components.DrawerDestination
import com.theveloper.pixelplay.presentation.components.MiniPlayerBottomSpacer
import com.theveloper.pixelplay.presentation.components.MiniPlayerHeight
import com.theveloper.pixelplay.presentation.components.PlayerInternalNavigationBar
+import com.theveloper.pixelplay.presentation.components.PlayerInternalNavigationRail
import com.theveloper.pixelplay.presentation.components.PlayStoreAnnouncementDefaults
import com.theveloper.pixelplay.presentation.components.PlayStoreAnnouncementDialog
import com.theveloper.pixelplay.presentation.components.PlayStoreAnnouncementUiModel
@@ -608,6 +617,11 @@ class MainActivity : ComponentActivity() {
private fun MainUI(playerViewModel: PlayerViewModel, navController: NavHostController) {
Trace.beginSection("MainActivity.MainUI")
+ val configuration = LocalConfiguration.current
+ val useNavigationRail = remember(configuration) {
+ configuration.screenWidthDp > 600
+ }
+
val commonNavItems = remember {
persistentListOf(
BottomNavItem("Home", R.string.nav_bar_home, R.drawable.rounded_home_24, R.drawable.home_24_rounded_filled, Screen.Home),
@@ -703,8 +717,14 @@ class MainActivity : ComponentActivity() {
)
val bottomBarPadding = animatedBottomBarPadding
val navBarHeight = resolveNavBarSurfaceHeight(navBarStyle, systemNavBarInset, navBarCompactMode)
- val navBarOccupiedHeight by remember(systemNavBarInset, navBarCompactMode) {
- derivedStateOf { resolveNavBarOccupiedHeight(systemNavBarInset, navBarCompactMode) }
+ val navBarOccupiedHeight by remember(systemNavBarInset, navBarCompactMode, useNavigationRail) {
+ derivedStateOf {
+ if (useNavigationRail) {
+ systemNavBarInset
+ } else {
+ resolveNavBarOccupiedHeight(systemNavBarInset, navBarCompactMode)
+ }
+ }
}
val navBarVisibilityProgressState = animateFloatAsState(
targetValue = if (shouldHideNavigationBar) 0f else 1f,
@@ -798,7 +818,7 @@ class MainActivity : ComponentActivity() {
Scaffold(
modifier = Modifier.fillMaxSize(),
bottomBar = {
- if (shouldRenderNavigationBar) {
+ if (shouldRenderNavigationBar && !useNavigationRail) {
val currentSongId by remember {
playerViewModel.stablePlayerState
.map { it.currentSong?.id }
@@ -839,6 +859,7 @@ class MainActivity : ComponentActivity() {
Box(
modifier = Modifier
.fillMaxWidth()
+ .widthIn(max = 540.dp)
.height(navBarOccupiedHeight)
.clipToBounds()
) {
@@ -850,6 +871,7 @@ class MainActivity : ComponentActivity() {
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth()
+ .widthIn(max = 540.dp)
.padding(bottom = bottomBarPadding)
.onSizeChanged { componentHeightPx = it.height }
.graphicsLayer {
@@ -870,7 +892,7 @@ class MainActivity : ComponentActivity() {
.padding(horizontal = horizontalPadding)
.graphicsLayer {
// Animated corner shape resolved in the draw phase:
- // animating the radius re-clips this layer only — no
+ // animating the radius re-clips this layer only 閳?no
// recomposition and no layout pass for the bar.
val fraction = playerViewModel.playerContentExpansionFraction.value
val safeFraction = fraction.coerceIn(0f, 1f)
@@ -902,20 +924,56 @@ class MainActivity : ComponentActivity() {
compactMode = navBarCompactMode,
bottomBarPadding = bottomBarPadding,
onSearchIconDoubleTap = onSearchIconDoubleTap,
- modifier = Modifier.fillMaxSize()
+ modifier = Modifier.fillMaxSize().widthIn(max = 540.dp)
)
}
}
}
}
) { innerPadding ->
- BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
- val density = LocalDensity.current
- val containerHeight = this.maxHeight
- val screenHeightPx = remember(containerHeight, density) {
- with(density) { containerHeight.toPx() }
+ val appNavigationPadding = remember(innerPadding, useNavigationRail, shouldRenderNavigationBar) {
+ if (useNavigationRail && shouldRenderNavigationBar) {
+ androidx.compose.foundation.layout.PaddingValues(
+ start = innerPadding.calculateStartPadding(LayoutDirection.Ltr) + 80.dp,
+ top = innerPadding.calculateTopPadding(),
+ end = innerPadding.calculateEndPadding(LayoutDirection.Ltr),
+ bottom = innerPadding.calculateBottomPadding()
+ )
+ } else {
+ innerPadding
}
+ }
+ Row(modifier = Modifier.fillMaxSize()) {
+ if (useNavigationRail && shouldRenderNavigationBar) {
+ PlayerInternalNavigationRail(
+ navController = navController,
+ navItems = commonNavItems,
+ currentRoute = currentRoute,
+ onSearchIconDoubleTap = { playerViewModel.onSearchNavIconDoubleTapped() },
+ onOpenSidebar = { scope.launch { drawerState.open() } },
+ modifier = Modifier
+ .layout { measurable, constraints ->
+ val expansionHide = playerViewModel.playerContentExpansionFraction.value.coerceIn(0f, 1f)
+ val routeHide = (1f - navBarVisibilityProgressState.value).coerceIn(0f, 1f)
+ val hideFraction = maxOf(expansionHide, routeHide)
+ val placeable = measurable.measure(constraints)
+ val shrinkBy = (placeable.width * hideFraction).roundToInt()
+ layout(placeable.width - shrinkBy, placeable.height) {
+ placeable.placeRelative(0, 0)
+ }
+ }
+ .graphicsLayer {
+ // reading state here is fine: graphicsLayer runs per frame, not per layout
+ val expansionHide = playerViewModel.playerContentExpansionFraction.value.coerceIn(0f, 1f)
+ val routeHide = (1f - navBarVisibilityProgressState.value).coerceIn(0f, 1f)
+ val hideFraction = maxOf(expansionHide, routeHide)
+ // size.width is already reduced by the layout modifier above
+ translationX = -size.width * hideFraction
+ alpha = 1f - hideFraction
+ }
+ )
+ }
val showPlayerContentInitially by remember {
playerViewModel.stablePlayerState
.map { it.currentSong?.id != null }
@@ -925,7 +983,30 @@ class MainActivity : ComponentActivity() {
val shouldHideMiniPlayer by remember(currentRoute) {
derivedStateOf { currentRoute in routesWithHiddenMiniPlayer }
}
-
+ val density = LocalDensity.current
+ val collapsedMaxWidthDp by remember {
+ derivedStateOf {
+ val expansionHide = playerViewModel.playerContentExpansionFraction.value.coerceIn(0f, 1f)
+ val routeHide = (1f - navBarVisibilityProgressState.value).coerceIn(0f, 1f)
+ val hideFraction = maxOf(expansionHide, routeHide)
+ lerp(450.dp, 2000.dp, hideFraction)
+ }
+ }
+ BoxWithConstraints(
+ modifier = Modifier
+ .fillMaxSize()
+ .then(
+ if (useNavigationRail) {
+ Modifier.widthIn(max = if (showPlayerContentInitially && !shouldHideMiniPlayer) collapsedMaxWidthDp else Dp.Infinity)
+ } else {
+ Modifier.widthIn(max = 540.dp)
+ }
+ )
+ ) {
+ val containerHeight = this.maxHeight
+ val screenHeightPx = remember(containerHeight, density) {
+ with(density) { containerHeight.toPx() }
+ }
val miniPlayerH = with(density) { MiniPlayerHeight.toPx() }
val totalSheetHeightWhenContentCollapsedPx = if (showPlayerContentInitially && !shouldHideMiniPlayer) miniPlayerH else 0f
@@ -967,7 +1048,7 @@ class MainActivity : ComponentActivity() {
AppNavigation(
playerViewModel = playerViewModel,
navController = navController,
- paddingValues = innerPadding,
+ paddingValues = appNavigationPadding,
userPreferencesRepository = userPreferencesRepository,
onSearchBarActiveChange = { isSearchBarActive = it },
onOpenSidebar = { scope.launch { drawerState.open() } }
@@ -979,7 +1060,7 @@ class MainActivity : ComponentActivity() {
playerViewModel.playerContentExpansionFraction.value > 0.01f
}
}
- AnimatedVisibility(
+ androidx.compose.animation.AnimatedVisibility(
visible = isExpandedOrExpanding,
enter = fadeIn(animationSpec = tween(durationMillis = 350)),
exit = fadeOut(animationSpec = tween(durationMillis = 350)),
@@ -1008,7 +1089,8 @@ class MainActivity : ComponentActivity() {
hideMiniPlayer = shouldHideMiniPlayer,
containerHeight = containerHeight,
navController = navController,
- isNavBarHidden = isNavBarEffectivelyHidden
+ isNavBarHidden = isNavBarEffectivelyHidden,
+ isNavRailHidden = useNavigationRail
)
val dismissUndoBarSlice by remember {
@@ -1028,7 +1110,7 @@ class MainActivity : ComponentActivity() {
{ playerViewModel.hideDismissUndoBar() }
}
- AnimatedVisibility(
+ androidx.compose.animation.AnimatedVisibility(
visible = dismissUndoBarSlice.isVisible,
enter = slideInVertically(initialOffsetY = { it }) + fadeIn(),
exit = slideOutVertically(targetOffsetY = { -it }) + fadeOut(),
@@ -1061,6 +1143,7 @@ class MainActivity : ComponentActivity() {
}
}
}
+ }
}
Trace.endSection()
diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiOrchestrator.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt
similarity index 68%
rename from app/src/main/java/com/theveloper/pixelplay/data/ai/AiOrchestrator.kt
rename to app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt
index b3109247b..d9da7840b 100644
--- a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiOrchestrator.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiHandler.kt
@@ -19,7 +19,7 @@ import javax.inject.Inject
import javax.inject.Singleton
@Singleton
-class AiOrchestrator @Inject constructor(
+class AiHandler @Inject constructor(
private val preferencesRepo: AiPreferencesRepository,
private val clientFactory: AiClientFactory,
private val cacheDao: AiCacheDao,
@@ -60,58 +60,79 @@ class AiOrchestrator @Inject constructor(
preferencesRepo.setModel(provider, model)
}
+ private data class GenerationParams(
+ val temperature: Float,
+ val topP: Float,
+ val topK: Int,
+ val maxTokens: Int,
+ val presencePenalty: Float,
+ val frequencyPenalty: Float,
+ )
+
+ private data class GenerationResult(
+ val response: String,
+ val modelUsed: String,
+ )
+
+ private suspend fun getGenerationParams(): GenerationParams {
+ return GenerationParams(
+ temperature = preferencesRepo.aiTemperature.first(),
+ topP = preferencesRepo.aiTopP.first(),
+ topK = preferencesRepo.aiTopK.first(),
+ maxTokens = preferencesRepo.aiMaxTokens.first(),
+ presencePenalty = preferencesRepo.aiPresencePenalty.first(),
+ frequencyPenalty = preferencesRepo.aiFrequencyPenalty.first(),
+ )
+ }
+
private suspend fun generateWithRecovery(
provider: AiProvider,
apiKey: String,
systemPrompt: String,
prompt: String,
- temperature: Float
- ): String {
+ temperature: Float,
+ topP: Float,
+ topK: Int,
+ maxTokens: Int,
+ presencePenalty: Float,
+ frequencyPenalty: Float,
+ ): GenerationResult {
val client = clientFactory.createClient(provider, apiKey)
val requestedModel = getModel(provider).ifBlank { client.getDefaultModel() }
- return try {
- // Wrap in timeout to prevent hanging requests
- withTimeout(REQUEST_TIMEOUT_MS) {
- client.generateContent(
- requestedModel,
- systemPrompt,
- prompt,
- temperature
+ suspend fun callWithModel(model: String): String {
+ return try {
+ withTimeout(REQUEST_TIMEOUT_MS) {
+ client.generateContent(
+ model, systemPrompt, prompt, temperature,
+ topP, topK, maxTokens, presencePenalty, frequencyPenalty,
+ )
+ }
+ } catch (e: kotlinx.coroutines.TimeoutCancellationException) {
+ throw com.theveloper.pixelplay.data.ai.provider.AiProviderSupport.createException(
+ providerName = provider.displayName,
+ statusCode = null,
+ transportMessage = "Request timed out after ${REQUEST_TIMEOUT_MS / 1000}s. The model may be overloaded.",
+ responseBody = null,
+ requestedModel = model
)
}
- } catch (e: kotlinx.coroutines.TimeoutCancellationException) {
- throw com.theveloper.pixelplay.data.ai.provider.AiProviderSupport.createException(
- providerName = provider.displayName,
- statusCode = null,
- transportMessage = "Request timed out after ${REQUEST_TIMEOUT_MS / 1000}s. The model may be overloaded.",
- responseBody = null,
- requestedModel = requestedModel
- )
+ }
+
+ return try {
+ val response = callWithModel(requestedModel)
+ GenerationResult(response, requestedModel)
} catch (e: Exception) {
val failure = com.theveloper.pixelplay.data.ai.provider.AiProviderSupport.wrapThrowable(
- provider.displayName,
- e,
- requestedModel
+ provider.displayName, e, requestedModel
)
val recoveredModel = recoverModelIfNeeded(
- provider = provider,
- apiKey = apiKey,
- requestedModel = requestedModel,
- client = client,
- failure = failure
+ provider, apiKey, requestedModel, client, failure
) ?: throw failure
- // Retry with recovered model (also with timeout)
- withTimeout(REQUEST_TIMEOUT_MS) {
- client.generateContent(
- recoveredModel,
- systemPrompt,
- prompt,
- temperature
- )
- }
+ val response = callWithModel(recoveredModel)
+ GenerationResult(response, recoveredModel)
}
}
@@ -141,48 +162,40 @@ class AiOrchestrator @Inject constructor(
temperature: Float = 0.7f,
context: String = ""
): String {
- // Dynamic temperature adjustment if default value is used
- val resolvedTemperature = if (temperature == 0.7f) {
- when (type) {
- // AI Optimization: Use low temperature for high-precision metadata to prevent hallucinations
- AiSystemPromptType.METADATA -> 0.1f
- AiSystemPromptType.MOOD_ANALYSIS -> 0.2f
- // AI Optimization: Moderate temperature for tags to allow creative yet relevant descriptors
- AiSystemPromptType.TAGGING -> 0.4f
- // AI Optimization: Balanced temperature for playlists to ensure variety without losing cohesion
- AiSystemPromptType.PLAYLIST, AiSystemPromptType.DAILY_MIX -> 0.6f
- // AI Optimization: High temperature for persona-based responses to increase flair and engagement
- AiSystemPromptType.PERSONA -> 0.85f
- AiSystemPromptType.GENERAL -> 0.7f
- }
- } else temperature
+ val params = getGenerationParams()
+ val effectiveTemperature = if (params.temperature == 0.7f) {
+ if (temperature == 0.7f) {
+ when (type) {
+ AiSystemPromptType.METADATA -> 0.1f
+ AiSystemPromptType.MOOD_ANALYSIS -> 0.2f
+ AiSystemPromptType.TAGGING -> 0.4f
+ AiSystemPromptType.PLAYLIST, AiSystemPromptType.DAILY_MIX -> 0.6f
+ AiSystemPromptType.PERSONA -> 0.85f
+ AiSystemPromptType.GENERAL -> 0.7f
+ }
+ } else temperature
+ } else params.temperature
- // Determine chain based on user preference
val userProviderStr = preferencesRepo.aiProvider.first()
val userProvider = AiProvider.fromString(userProviderStr)
- // Generate combined prompt for hashing and execution
val basePersona = getBasePersona(userProvider)
val combinedSystemPrompt = promptEngine.buildPrompt(basePersona, type, context)
-
- // Cache entry is valid for a specific prompt + system instruction + provider
+
val hash = (userProvider.name + combinedSystemPrompt + prompt).sha256()
- // Check cache with TTL — don't serve stale results
cacheDao.getCache(hash)?.let { cached ->
val age = System.currentTimeMillis() - cached.timestamp
if (age < CACHE_TTL_MS) {
return cached.responseJson
}
- // Cache expired — proceed with fresh generation
}
val providersToTry = com.theveloper.pixelplay.data.ai.provider.AiProviderSupport.buildProviderChain(userProvider)
val failedProviders = mutableListOf()
val now = System.currentTimeMillis()
-
+
for (provider in providersToTry) {
- // Skip if in cooldown
val cooldownExpiry = providerCooldowns[provider] ?: 0L
if (now < cooldownExpiry) {
failedProviders.add("${provider.name}: on cooldown (${((cooldownExpiry - now) / 1000)}s remaining)")
@@ -196,29 +209,30 @@ class AiOrchestrator @Inject constructor(
continue
}
- // Use the shared base persona but specialized type rules for each provider in the chain
val providerPersona = getBasePersona(provider)
val finalSystemPrompt = promptEngine.buildPrompt(providerPersona, type, context)
- val response = generateWithRecovery(
+ val result = generateWithRecovery(
provider = provider,
apiKey = apiKey,
systemPrompt = finalSystemPrompt,
prompt = prompt,
- temperature = resolvedTemperature
+ temperature = effectiveTemperature,
+ topP = params.topP,
+ topK = params.topK,
+ maxTokens = params.maxTokens,
+ presencePenalty = params.presencePenalty,
+ frequencyPenalty = params.frequencyPenalty,
)
- // Validate response is not empty
- if (response.isBlank()) {
+ if (result.response.isBlank()) {
failedProviders.add("${provider.name}: returned empty response")
continue
}
- // Low-maintenance usage tracking using highly accurate proportional estimation bounds (4 chars ~ 1 token)
- // Models with "thinking" or "reasoning" generally output 2-3x internal tokens for complex generation
val isThinkingModel = finalSystemPrompt.contains("think", true) || provider.name.contains("reasoning", true)
val estimatedPromptTokens = (finalSystemPrompt.length + prompt.length) / 4
- val estimatedOutputTokens = response.length / 4
+ val estimatedOutputTokens = result.response.length / 4
val estimatedThoughtTokens = if (isThinkingModel) (estimatedOutputTokens * 1.5).toInt() else 0
appScope.launch {
@@ -227,7 +241,7 @@ class AiOrchestrator @Inject constructor(
AiUsageEntity(
timestamp = now,
provider = provider.displayName,
- model = provider.name,
+ model = result.modelUsed,
promptType = type.name,
promptTokens = estimatedPromptTokens,
outputTokens = estimatedOutputTokens,
@@ -235,16 +249,16 @@ class AiOrchestrator @Inject constructor(
)
)
}.onFailure { error ->
- Timber.tag("AiOrchestrator").e(error, "Failed to persist AI usage")
+ Timber.tag("AiHandler").e(error, "Failed to persist AI usage")
}
}
- cacheDao.insert(AiCacheEntity(promptHash = hash, responseJson = response, timestamp = System.currentTimeMillis()))
- return response
+ cacheDao.insert(AiCacheEntity(promptHash = hash, responseJson = result.response, timestamp = System.currentTimeMillis()))
+ return result.response
} catch (e: Exception) {
// AI Optimization: Robust failover logic—if one provider fails, we log and try the next in the chain
val failure = com.theveloper.pixelplay.data.ai.provider.AiProviderSupport.wrapThrowable(provider.displayName, e)
- Timber.tag("AiOrchestrator").w(e, "Provider ${provider.name} failed: ${failure.message}")
+ Timber.tag("AiHandler").w(e, "Provider ${provider.name} failed: ${failure.message}")
failedProviders.add("${provider.name}: ${failure.message ?: "Unknown error"}")
// Trigger cooldown only on provider-level outages and account problems.
if (failure.shouldCooldown()) {
@@ -268,7 +282,7 @@ class AiOrchestrator @Inject constructor(
"AI generation failed after trying ${failedProviders.size} providers:\n${failedProviders.joinToString("\n• ", prefix = "• ")}"
}
- Timber.tag("AiOrchestrator").e("All providers failed. Details: %s", failedProviders.joinToString(" | "))
+ Timber.tag("AiHandler").e("All providers failed. Details: %s", failedProviders.joinToString(" | "))
throw Exception(errorMessage)
}
}
diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiMetadataGenerator.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiMetadataGenerator.kt
deleted file mode 100644
index f67ccb324..000000000
--- a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiMetadataGenerator.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-package com.theveloper.pixelplay.data.ai
-
-
-import com.theveloper.pixelplay.data.model.Song
-import kotlinx.serialization.SerializationException
-import kotlinx.serialization.Serializable
-import kotlinx.serialization.json.Json
-import timber.log.Timber
-import javax.inject.Inject
-
-@Serializable
-data class SongMetadata(
- val title: String? = null,
- val artist: String? = null,
- val album: String? = null,
- val genre: String? = null
-)
-
-class AiMetadataGenerator @Inject constructor(
- private val aiOrchestrator: AiOrchestrator,
- private val json: Json
-) {
- private fun cleanJson(jsonString: String): String {
- return jsonString.replace("```json", "").replace("```", "").trim()
- }
-
- suspend fun generate(
- song: Song,
- fieldsToComplete: List
- ): Result {
- return try {
- val fieldsJson = fieldsToComplete.joinToString(separator = ", ") { "\"$it\"" }
-
- val albumInfo = if (song.album.isNotBlank()) "${song.album}" else ""
-
- val fullPrompt = """
-
- ${song.title}
- ${song.displayArtist}
- $albumInfo
-
-
- Complete the following fields using your music knowledge:
- [$fieldsJson]
-
- """.trimIndent()
-
- val responseText = aiOrchestrator.generateContent(fullPrompt, AiSystemPromptType.METADATA)
- if (responseText.isBlank()) {
- Timber.e("AI returned an empty or null response.")
- return Result.failure(Exception("AI returned an empty response."))
- }
-
- Timber.d("AI Response: $responseText")
- val cleanedJson = cleanJson(responseText)
- val metadata = json.decodeFromString(cleanedJson)
-
- Result.success(metadata)
- } catch (e: SerializationException) {
- Timber.e(e, "Error deserializing AI response.")
- Result.failure(Exception("Failed to parse AI response: ${e.message}", e))
- } catch (e: Exception) {
- Timber.e(e, "Generic error in AiMetadataGenerator.")
- Result.failure(Exception("AI Error: ${e.message}", e))
- }
- }
-}
diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiPlaylistGenerator.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiPlaylistGenerator.kt
index 7017ec6a6..06b91dce4 100644
--- a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiPlaylistGenerator.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiPlaylistGenerator.kt
@@ -11,7 +11,7 @@ import kotlin.math.max
class AiPlaylistGenerator @Inject constructor(
private val dailyMixManager: DailyMixManager,
- private val aiOrchestrator: AiOrchestrator,
+ private val aiHandler: AiHandler,
private val digestGenerator: UserProfileDigestGenerator,
private val preferencesRepo: AiPreferencesRepository,
private val json: Json
@@ -40,13 +40,12 @@ class AiPlaylistGenerator @Inject constructor(
}
}
- // Token Optimization: Reduce sample size based on safe mode
val isSafe = preferencesRepo.isSafeTokenLimitEnabled.first()
- val sampleCap = if (isSafe) 40 else 80
- val sampleSize = max(minLength, sampleCap).coerceAtMost(sampleCap)
- val songSample = samplingPool.take(sampleSize)
-
- // Token Optimization: Compact JSON format — only essential fields
+ val prefSampleSize = preferencesRepo.aiSampleSize.first()
+ val useExtendedFields = preferencesRepo.aiIncludeExtendedFields.first()
+ val sampleCap = if (isSafe) prefSampleSize else prefSampleSize * 2
+ val songSample = samplingPool.take(sampleCap)
+
val availableSongsJson = buildString {
songSample.forEachIndexed { index, song ->
val score = dailyMixManager.getScore(song.id)
@@ -54,7 +53,14 @@ class AiPlaylistGenerator @Inject constructor(
val artist = song.displayArtist.replace("\"", "'").take(25)
val genre = song.genre?.replace("\"", "'")?.take(15) ?: "?"
if (index > 0) append(",\n")
- append("""{"id":"${song.id}","t":"$title","a":"$artist","g":"$genre","s":$score}""")
+ if (useExtendedFields) {
+ val album = song.album?.replace("\"", "'")?.take(25) ?: "?"
+ val dur = song.duration
+ val fav = if (song.isFavorite) "1" else "0"
+ append("""{"id":"${song.id}","t":"$title","a":"$artist","g":"$genre","al":"$album","d":$dur,"f":$fav,"s":$score}""")
+ } else {
+ append("""{"id":"${song.id}","t":"$title","a":"$artist","g":"$genre","s":$score}""")
+ }
}
}
@@ -73,7 +79,7 @@ class AiPlaylistGenerator @Inject constructor(
""".trimIndent()
- val responseText = aiOrchestrator.generateContent(fullPrompt, type)
+ val responseText = aiHandler.generateContent(fullPrompt, type)
val songIds = extractPlaylistSongIds(responseText)
@@ -148,55 +154,18 @@ class AiPlaylistGenerator @Inject constructor(
}
private fun extractPlaylistSongIds(rawResponse: String): List {
- val sanitized = rawResponse
- .replace("```json", "")
- .replace("```", "")
- .trim()
-
- for (startIndex in sanitized.indices) {
- if (sanitized[startIndex] != '[') continue
-
- var depth = 0
- var inString = false
- var isEscaped = false
-
- for (index in startIndex until sanitized.length) {
- val character = sanitized[index]
-
- if (inString) {
- if (isEscaped) {
- isEscaped = false
- continue
- }
-
- when (character) {
- '\\' -> isEscaped = true
- '"' -> inString = false
- }
- continue
- }
-
- when (character) {
- '"' -> inString = true
- '[' -> depth++
- ']' -> {
- depth--
- if (depth == 0) {
- val candidate = sanitized.substring(startIndex, index + 1)
- val decoded = runCatching { json.decodeFromString>(candidate) }
- if (decoded.isSuccess) {
- return decoded.getOrThrow()
- }
- break
- }
- }
- }
+ val cleaned = AiResponseCleaner.cleanJsonResponse(rawResponse)
+ val jsonArray = AiResponseCleaner.extractJsonArray(cleaned)
+ ?: throw IllegalArgumentException(
+ "AI returned an invalid response format. Expected a JSON array of song IDs but got something else. " +
+ "This usually happens with smaller models. Try selecting a more capable model in AI Settings."
+ )
+
+ return runCatching { json.decodeFromString>(jsonArray) }
+ .getOrElse {
+ throw IllegalArgumentException(
+ "AI returned malformed JSON. Expected a string array but got: ${jsonArray.take(100)}"
+ )
}
- }
-
- throw IllegalArgumentException(
- "AI returned an invalid response format. Expected a JSON array of song IDs but got something else. " +
- "This usually happens with smaller models. Try selecting a more capable model in AI Settings."
- )
}
}
diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/AiResponseCleaner.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiResponseCleaner.kt
new file mode 100644
index 000000000..92d6e27de
--- /dev/null
+++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/AiResponseCleaner.kt
@@ -0,0 +1,93 @@
+package com.theveloper.pixelplay.data.ai
+
+object AiResponseCleaner {
+
+ fun cleanJsonResponse(raw: String): String {
+ var cleaned = raw
+ .replace("```json", "")
+ .replace("```kotlin", "")
+ .replace("```", "")
+ .trim()
+
+ if (cleaned.startsWith("[")) {
+ val end = findMatchingBracket(cleaned, 0)
+ if (end > 0) cleaned = cleaned.substring(0, end + 1)
+ } else if (cleaned.startsWith("{")) {
+ val end = findMatchingBrace(cleaned, 0)
+ if (end > 0) cleaned = cleaned.substring(0, end + 1)
+ }
+
+ return cleaned
+ }
+
+ fun cleanTextResponse(raw: String): String {
+ return raw
+ .replace("```text", "")
+ .replace("```", "")
+ .trim()
+ }
+
+ fun extractJsonArray(text: String): String? {
+ for (i in text.indices) {
+ if (text[i] == '[') {
+ val end = findMatchingBracket(text, i)
+ if (end > i) return text.substring(i, end + 1)
+ }
+ }
+ return null
+ }
+
+ fun extractJsonObject(text: String): String? {
+ for (i in text.indices) {
+ if (text[i] == '{') {
+ val end = findMatchingBrace(text, i)
+ if (end > i) return text.substring(i, end + 1)
+ }
+ }
+ return null
+ }
+
+ private fun findMatchingBracket(text: String, start: Int): Int {
+ var depth = 0
+ var inString = false
+ var escaped = false
+ for (i in start until text.length) {
+ val c = text[i]
+ if (escaped) { escaped = false; continue }
+ if (inString) {
+ if (c == '\\') escaped = true
+ else if (c == '"') inString = false
+ continue
+ }
+ when (c) {
+ '\\' -> escaped = true
+ '"' -> inString = true
+ '[' -> depth++
+ ']' -> { depth--; if (depth == 0) return i }
+ }
+ }
+ return -1
+ }
+
+ private fun findMatchingBrace(text: String, start: Int): Int {
+ var depth = 0
+ var inString = false
+ var escaped = false
+ for (i in start until text.length) {
+ val c = text[i]
+ if (escaped) { escaped = false; continue }
+ if (inString) {
+ if (c == '\\') escaped = true
+ else if (c == '"') inString = false
+ continue
+ }
+ when (c) {
+ '\\' -> escaped = true
+ '"' -> inString = true
+ '{' -> depth++
+ '}' -> { depth--; if (depth == 0) return i }
+ }
+ }
+ return -1
+ }
+}
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 6759713d0..9edf9e404 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
@@ -17,85 +17,177 @@ enum class AiSystemPromptType {
@Singleton
class AiSystemPromptEngine @Inject constructor() {
- // Advanced prompt engineering: Enforcing structured output boundaries
private val UNIVERSAL_CONSTRAINTS = """
-
+
- You are communicating with a programmatic parser, not a human.
- - Output ONLY the expected structure.
- - NO markdown formatting (e.g., do not wrap in ```json).
- - NO conversational filler, greetings, or explanations.
- - Any deviation will crash the application.
-
+ - Output ONLY the expected structure — nothing else.
+ - NO markdown fences, NO code blocks, NO conversational framing.
+ - Any deviation will cause an application crash.
+ - If uncertain, make your best reasoned guess rather than refusing.
+ - Verify your output matches the required schema before responding.
+
+ """.trimIndent()
+
+ private val playlistFewShot = """
+
+ GOOD: ["a1b2c3","d4e5f6","g7h8i9"]
+ BAD: Here is a playlist for you: ["a1b2c3","d4e5f6"]
+ GOOD IDs are exactly 6 alphanumeric characters from the pool.
+ Every ID in your output MUST exist in the candidate_pool.
+
+ """.trimIndent()
+
+ private val metadataFewShot = """
+
+ Input: title="Thriller (2008 Remaster)", artist="Micheal Jakson", album="THRILLER 25", genre="Pop"
+ Output: {"title":"Thriller (2008 Remaster)","artist":"Michael Jackson","album":"Thriller 25","genre":"Pop"}
+
+ Input: title="untitled", artist="unknown", album="", genre="Electronic"
+ Output: {"title":"Untitled","artist":"Unknown Artist","album":"","genre":"Synthwave"}
+
+ Input: title="Bohemian Rhapsody", artist="Queen", album="A Night at the Opera", genre="Rock"
+ Output: {"title":"Bohemian Rhapsody","artist":"Queen","album":"A Night at the Opera","genre":"Progressive Rock"}
+
+ """.trimIndent()
+
+ private val taggingFewShot = """
+
+ Input: synth-heavy track with driving bass and ethereal female vocals
+ Output: electronic, synth-driven, ethereal-vocals, driving-bass, atmospheric, hypnotic
+
+ Input: acoustic guitar ballad with soft percussion and strings
+ Output: acoustic, fingerstyle-guitar, soft-percussion, string-arrangement, intimate, folk-tinged
+
+ """.trimIndent()
+
+ private val moodAnalysisFewShot = """
+
+ Input: Fast tempo (140 BPM), heavy distortion, aggressive drums, minor key
+ Output: Aggressive | Energy:0.95 | Valence:0.2 | Danceability:0.6 | Acousticness:0.0
+
+ Input: Slow tempo (70 BPM), acoustic piano, soft strings, major key
+ Output: Calm | Energy:0.2 | Valence:0.8 | Danceability:0.3 | Acousticness:0.9
+
+ """.trimIndent()
+
+ private val dailyMixPersonaPrompt = """
+
+ - Open with a thematic hook that frames the mix (e.g., "This set leans into your late-night exploratory side.")
+ - Reference 1-2 specific listening patterns from the user's data to show curation intent.
+ - Describe the emotional arc of the mix in 2-3 sentences.
+ - Close with a subtle invitation to explore further.
+ - Tone: warm, insightful, never overly familiar or robotic.
+ - Length: 4-6 sentences maximum.
+
""".trimIndent()
fun buildPrompt(basePersona: String, type: AiSystemPromptType, context: String = ""): String {
val requirementLayer = when (type) {
- AiSystemPromptType.PLAYLIST, AiSystemPromptType.DAILY_MIX -> """
- Music curation engine mapping user requests to a strict candidate pool.
+ AiSystemPromptType.PLAYLIST -> """
+ Expert music curator — you select songs from the provided pool to build cohesive, emotionally intelligent playlists.
+
+
+ 1. Parse the user's request for desired mood, energy, genre, era, or activity.
+ 2. Review the candidate pool — note available genres, tempos, and artists.
+ 3. Select songs that form a coherent arc: opening, build, peak, cool-down.
+ 4. Ensure variety — avoid repeating the same artist or genre consecutively.
+ 5. Prefer higher-scored songs (score field) but prioritize diversity and fit.
+
+ - If request implies discovery/novelty, favor the [DISCOVERY_POOL] entries.
+ - If request implies familiarity/favorites, weight the [LISTENED] pool.
+ - For mixed/blended requests, interleave both pools for surprise + comfort.
+ - Target length is specified in the request — respect it within ±2 tracks.
+
+
+ Return ONLY a raw JSON array of song IDs.
+ Format: ["id_1","id_2","id_3",...,"id_N"]
+
+ $playlistFewShot
+ """.trimIndent()
+
+ AiSystemPromptType.DAILY_MIX -> """
+ Daily Mix curator — you build themed mini-sets from the user's library for daily listening.
- - If request implies "discovery/new", prioritize the [DISCOVERY_POOL].
- - If request implies "favorites/familiar", heavily weight the [LISTENED] pool.
- - Otherwise, blend pools intelligently based on requested tempo, genre, or mood.
- - Guarantee a cohesive listening journey with natural transitions.
+
+ 1. Identify the dominant mood or genre from the user's recent listening profile.
+ 2. Select 8-15 tracks that form a single coherent mood/genre pocket.
+ 3. Lead with a familiar track, introduce 1-2 discoveries mid-set, close on a strong note.
+
+ - Seamless transitions: adjacent tracks should share tempo (±20 BPM) or complementary keys.
+ - These mixes are for daily refreshes — avoid repeating the same tracks across mixes.
- Return ONLY a raw JSON array of song IDs representing the playlist sequence.
- Format: ["id_1","id_2","id_3"]
+ Return ONLY a raw JSON array of song IDs.
+ Format: ["id_1","id_2","id_3",...,"id_N"]
""".trimIndent()
AiSystemPromptType.METADATA -> """
- Precision music metadata specialist.
+ Precision music metadata specialist — you clean and enrich song metadata.
- - Fix spelling errors and standardizations in song titles and artists.
- - Replace generic genres ("Music", "Electronic") with highly specific subgenres ("Synthwave", "Nu-Disco").
+ - Fix spelling errors (e.g., "Micheal" → "Michael", "Thriler" → "Thriller").
+ - Capitalize properly: title case for titles and artists, proper casing for albums.
+ - Replace generic genres ("Music", "Electronic", "Other") with specific subgenres calibrated to the track's sound.
+ - If a field is empty or "unknown", leave it as empty string — do not fabricate data.
+ - Preserve any edition/remaster/year annotations in parentheses.
- Return ONLY a raw JSON object string.
- Format: {"title":"Clean Title", "artist":"Primary Artist", "album":"Album Name", "genre":"Specific Genre"}
+ Return ONLY a raw JSON object with EXACTLY these keys:
+ {"title":"...", "artist":"...", "album":"...", "genre":"..."}
+ $metadataFewShot
""".trimIndent()
AiSystemPromptType.TAGGING -> """
- Atmospheric audio tagging engine.
+ Atmospheric audio tagging engine — you generate perceptive acoustic tags for music discovery.
- - Generate exactly 6-10 highly descriptive, hyphenated acoustic tags.
- - Focus on mood, instrumentation, pace, and sonic texture.
- - All tags must be strictly lowercase.
+ - Generate 6-10 hyphenated tags that capture: mood, instrumentation, tempo feel, sonic texture, and energy.
+ - All tags must be lowercase, hyphenated, and ordered by prominence.
+ - Be specific: prefer "lush-orchestral" over "orchestral", "glitchy-beats" over "beats".
+ - Tags should be useful for content-based recommendation — focus on audible characteristics.
- Return ONLY a raw comma-separated text list.
- Format: cinematic, atmospheric-build, dark-synth, driving-beat
+ Return ONLY a comma-separated list — no JSON, no formatting.
+ Format: tag1, tag2, tag3, tag4, tag5, tag6
+ $taggingFewShot
""".trimIndent()
AiSystemPromptType.MOOD_ANALYSIS -> """
- Algorithmic audio sentiment analyzer.
+ Algorithmic audio sentiment analyzer — you infer emotional and structural attributes from track metadata.
- - Deduce structural properties from the given metadata.
- - Map confidence values from 0.0 to 1.0.
- - Primary moods: Joyful, Aggressive, Calm, Melancholic, Radiant, Intense, Somber.
+ - Infer mood from: title keywords, genre, artist style, and any available context.
+ - Choose the single best PrimaryMood from: Joyful, Aggressive, Calm, Melancholic, Radiant, Intense, Somber, Euphoric, Brooding, Playful.
+ - Map confidence values 0.0-1.0 for each attribute based on how strongly the metadata supports it.
+ - Energy: driven by tempo indicators (fast/hard = high, slow/soft = low).
+ - Valence: positive/happy feel vs. negative/sad feel.
+ - Danceability: rhythmic groove suitability.
+ - Acousticness: likelihood of organic/non-electronic instrumentation.
- Return ONLY the exact structured text format.
- Format: PrimaryMood | Energy:0.9 | Valence:0.1 | Danceability:0.4 | Acousticness:0.0
+ Return ONLY one line in this exact format:
+ PrimaryMood | Energy:0.X | Valence:0.X | Danceability:0.X | Acousticness:0.X
+ $moodAnalysisFewShot
""".trimIndent()
AiSystemPromptType.PERSONA -> """
- Daily Mix professional curator. You represent the persona: "$basePersona"
+ Daily Mix professional curator. You embody the persona: "$basePersona"
- - Speak directly to the listener's tastes using their data.
+ - Speak directly to the listener using "you" and their data as evidence of your curation.
- Maintain an enigmatic, sophisticated, and deeply empathetic tone.
- - Keep responses reasonably concise but beautifully written.
- - Do NOT use the universal programmatic constraints for persona responses; you are allowed to be conversational.
+ - Do NOT mention that you are an AI, a model, or that the data comes from a profile.
+ - Be concise but evocative — 4-6 sentences that feel hand-crafted.
+ $dailyMixPersonaPrompt
""".trimIndent()
AiSystemPromptType.GENERAL -> """
- PixelPlayer Assistant
+ PixelPlayer Assistant — a knowledgeable music companion.
- Assist the user with any complex queries or actions inside their music ecosystem.
+ - Answer questions about music, artists, genres, and playback features.
+ - Be concise and accurate. If you don't know something, say so directly.
+ - Provide actionable answers that help the user enjoy their music library.
""".trimIndent()
}
@@ -106,8 +198,9 @@ class AiSystemPromptEngine @Inject constructor() {
$context
""".trimIndent()
} else ""
@@ -119,8 +212,7 @@ class AiSystemPromptEngine @Inject constructor() {
""".trimIndent()
- // Persona generation bypasses the strict JSON/raw constraints since it is meant to read as prose to the user
- return if (type == AiSystemPromptType.PERSONA || type == AiSystemPromptType.GENERAL) {
+ return if (type == AiSystemPromptType.PERSONA) {
listOf(systemBlock, contextLayer).filter { it.isNotBlank() }.joinToString("\n\n")
} else {
listOf(systemBlock, UNIVERSAL_CONSTRAINTS, contextLayer).filter { it.isNotBlank() }.joinToString("\n\n")
diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/GeminiModelService.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/GeminiModelService.kt
index b6d943af8..c97959f5c 100644
--- a/app/src/main/java/com/theveloper/pixelplay/data/ai/GeminiModelService.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/GeminiModelService.kt
@@ -15,7 +15,7 @@ data class GeminiModel(
@Singleton
class GeminiModelService @Inject constructor(
- private val orchestrator: AiOrchestrator,
+ private val handler: AiHandler,
private val digestGenerator: UserProfileDigestGenerator,
private val musicRepository: MusicRepository,
private val workerManager: AiWorkerManager
@@ -122,7 +122,7 @@ class GeminiModelService @Inject constructor(
digestGenerator.generateDigest(allSongs)
} else ""
- return orchestrator.generateContent(
+ return handler.generateContent(
prompt = prompt,
type = type,
temperature = temperature,
diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/UserProfileDigestGenerator.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/UserProfileDigestGenerator.kt
index 99c2fdb3b..e7f9bef52 100644
--- a/app/src/main/java/com/theveloper/pixelplay/data/ai/UserProfileDigestGenerator.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/UserProfileDigestGenerator.kt
@@ -12,43 +12,36 @@ import javax.inject.Singleton
@Singleton
class UserProfileDigestGenerator @Inject constructor(
private val statsRepository: PlaybackStatsRepository,
- private val playlistDao: LocalPlaylistDao
+ private val playlistDao: LocalPlaylistDao,
+ private val preferencesRepo: com.theveloper.pixelplay.data.preferences.AiPreferencesRepository,
) {
- // Token Budget Tiers:
- // SAFE: ~1000 tokens (4000 chars) — fast, cheap, still gives good results
- // FULL: ~8000 tokens (32000 chars) — deep context for maximum personalization
private val SAFE_TARGET_CHAR_LIMIT = 4000
private val MAX_TARGET_CHAR_LIMIT = 32000
- // Track limits per tier — prevents runaway context size
private val SAFE_LISTENED_LIMIT = 15
private val SAFE_DISCOVERY_LIMIT = 30
private val FULL_LISTENED_LIMIT = 60
private val FULL_DISCOVERY_LIMIT = 120
- /**
- * Computes a highly condensed representation of the user's listening profile.
- * Uses a compact key-value format to minimize token consumption while maximizing signal.
- *
- * Safe mode aggressively caps all sections to stay under ~1000 tokens.
- * Full mode provides deep context for maximum personalization quality.
- */
suspend fun generateDigest(allSongs: List, isSafeLimit: Boolean = true): String {
- val targetLimit = if (isSafeLimit) SAFE_TARGET_CHAR_LIMIT else MAX_TARGET_CHAR_LIMIT
- val listenedLimit = if (isSafeLimit) SAFE_LISTENED_LIMIT else FULL_LISTENED_LIMIT
- val discoveryLimit = if (isSafeLimit) SAFE_DISCOVERY_LIMIT else FULL_DISCOVERY_LIMIT
+ val digestMode = preferencesRepo.aiDigestMode.first()
+ val useExtendedFields = preferencesRepo.aiIncludeExtendedFields.first()
+ val isSafe = if (digestMode == "full") false else isSafeLimit
+
+ val targetLimit = if (isSafe) SAFE_TARGET_CHAR_LIMIT else MAX_TARGET_CHAR_LIMIT
+ val listenedLimit = if (isSafe) SAFE_LISTENED_LIMIT else FULL_LISTENED_LIMIT
+ val discoveryLimit = if (isSafe) SAFE_DISCOVERY_LIMIT else FULL_DISCOVERY_LIMIT
val summary = statsRepository.loadSummary(StatsTimeRange.ALL, allSongs)
val playlists = playlistDao.observePlaylistsWithSongs().first()
-
+
val sb = StringBuilder()
sb.append("USER_PROFILE\n")
-
- // --- 1. Behavioral & Pattern Metrics (compact) ---
+
sb.append("STATS: plays=${summary.totalPlayCount}, uniq=${summary.uniqueSongs}\n")
sb.append("GENRES: ${summary.topGenres.take(3).joinToString(",") { it.genre }}\n")
sb.append("ARTISTS: ${summary.topArtists.take(5).joinToString(",") { it.artist }}\n")
-
+
summary.dayListeningDistribution?.let { dist ->
val phases = dist.buckets.groupBy { bucket ->
val hour = bucket.startMinute / 60
@@ -61,50 +54,56 @@ class UserProfileDigestGenerator @Inject constructor(
}.mapValues { it.value.sumOf { b -> b.totalDurationMs } }
sb.append("PHASE: ${phases.maxByOrNull { it.value }?.key ?: "Unknown"}\n")
}
-
+
val variety = if (summary.totalPlayCount > 0) summary.uniqueSongs.toDouble() / summary.totalPlayCount else 0.0
sb.append("VAR: ${"%.2f".format(variety)}\n")
-
- val playlistLimit = if (isSafeLimit) 5 else 20
+
+ val playlistLimit = if (isSafe) 5 else 20
if (playlists.isNotEmpty()) {
sb.append("PL: ${playlists.take(playlistLimit).joinToString(",") { it.playlist.name }}\n")
}
-
- // --- 2. Listened Tracks (capped) ---
- // Compact format: ID|plays|mins|fav|title-artist
+
sb.append("\nLISTENED: id|p|d|f|meta\n")
-
+
val songMap = allSongs.associateBy { it.id }
val playedSongs = summary.songs.take(listenedLimit)
-
+
playedSongs.forEach { s ->
if (sb.length >= (targetLimit * 0.6).toInt()) return@forEach
val song = songMap[s.songId]
val fav = if (song?.isFavorite == true) "1" else "0"
val mins = s.totalDurationMs / 60000
- // Truncate long titles to save tokens
val title = s.title.take(30)
val artist = s.artist.take(20)
- sb.append("${s.songId}|${s.playCount}|$mins|$fav|$title-$artist\n")
+ if (useExtendedFields) {
+ val album = song?.album?.take(20) ?: "?"
+ val year = song?.year?.toString()?.take(4) ?: "?"
+ sb.append("${s.songId}|${s.playCount}|$mins|$fav|$title-$artist|$album|$year\n")
+ } else {
+ sb.append("${s.songId}|${s.playCount}|$mins|$fav|$title-$artist\n")
+ }
}
-
- // --- 3. Discovery Pool (strictly capped) ---
- // AI needs to know what's available but unplayed
+
val playedIds = summary.songs.map { it.songId }.toSet()
val unplayed = allSongs.filter { it.id !in playedIds }
.shuffled()
.take(discoveryLimit)
-
+
if (unplayed.isNotEmpty()) {
sb.append("\nDISCOVERY_POOL:\n")
unplayed.forEach { s ->
if (sb.length >= targetLimit) return@forEach
val title = s.title.take(30)
val artist = s.displayArtist.take(20)
- sb.append("${s.id}|$title-$artist\n")
+ if (useExtendedFields) {
+ val genre = s.genre?.take(15) ?: "?"
+ sb.append("${s.id}|$title-$artist|$genre\n")
+ } else {
+ sb.append("${s.id}|$title-$artist\n")
+ }
}
}
-
+
return sb.toString()
}
}
diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiClient.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiClient.kt
index 413a0fb2f..348547ee6 100644
--- a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiClient.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiClient.kt
@@ -5,20 +5,17 @@ package com.theveloper.pixelplay.data.ai.provider
* Defines common operations for text generation and metadata completion
*/
interface AiClient {
-
- /**
- * Generate text content based on a prompt
- * @param model The model identifier to use
- * @param systemPrompt The system prompt instructions
- * @param prompt The input prompt
- * @param temperature Creativity control (0.0 to 1.0)
- * @return Generated text response
- */
+
suspend fun generateContent(
model: String,
systemPrompt: String,
prompt: String,
- temperature: Float = 0.7f
+ temperature: Float = 0.7f,
+ topP: Float = 0.95f,
+ topK: Int = 64,
+ maxTokens: Int = 4096,
+ presencePenalty: Float = 0.0f,
+ frequencyPenalty: Float = 0.0f
): String
/**
diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiClientFactory.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiClientFactory.kt
index a1c29211e..4322ac24e 100644
--- a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiClientFactory.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiClientFactory.kt
@@ -22,9 +22,24 @@ class AiClientFactory @Inject constructor() {
return when (provider) {
AiProvider.GEMINI -> GeminiAiClient(apiKey)
- AiProvider.DEEPSEEK -> DeepSeekAiClient(apiKey)
- AiProvider.GROQ -> GroqAiClient(apiKey)
- AiProvider.MISTRAL -> MistralAiClient(apiKey)
+ AiProvider.DEEPSEEK -> GenericOpenAiClient(
+ apiKey = apiKey,
+ baseUrl = "https://api.deepseek.com",
+ defaultModelId = "deepseek-chat",
+ providerName = "DeepSeek"
+ )
+ AiProvider.GROQ -> GenericOpenAiClient(
+ apiKey = apiKey,
+ baseUrl = "https://api.groq.com/openai/v1",
+ defaultModelId = "llama-3.1-8b-instant",
+ providerName = "Groq"
+ )
+ AiProvider.MISTRAL -> GenericOpenAiClient(
+ apiKey = apiKey,
+ baseUrl = "https://api.mistral.ai/v1",
+ defaultModelId = "mistral-large-latest",
+ providerName = "Mistral"
+ )
AiProvider.NVIDIA -> GenericOpenAiClient(
apiKey = apiKey,
baseUrl = "https://integrate.api.nvidia.com/v1",
@@ -55,6 +70,23 @@ class AiClientFactory @Inject constructor() {
defaultModelId = "google/gemini-2.0-flash-lite-preview-02-05:free",
providerName = "OpenRouter"
)
+ AiProvider.OLLAMA -> GenericOpenAiClient(
+ apiKey = apiKey,
+ baseUrl = "https://api.ollama.ai/v1",
+ defaultModelId = "llama3",
+ providerName = "Ollama"
+ )
+ AiProvider.CUSTOM -> GenericOpenAiClient(
+ apiKey = apiKey,
+ baseUrl = "",
+ defaultModelId = "",
+ providerName = "Custom Provider"
+ )
}
}
+
+ fun createClientWithUrl(provider: AiProvider, apiKey: String, baseUrl: String): AiClient {
+ val displayName = provider.displayName
+ return GenericOpenAiClient(apiKey, baseUrl.trimEnd('/'), "", displayName)
+ }
}
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 f0f7b91dd..229f1d314 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,7 +3,7 @@ package com.theveloper.pixelplay.data.ai.provider
/**
* Enum representing available AI providers
*/
-enum class AiProvider(val displayName: String, val requiresApiKey: Boolean) {
+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),
@@ -12,7 +12,9 @@ enum class AiProvider(val displayName: String, val requiresApiKey: Boolean) {
KIMI("Kimi (Moonshot)", requiresApiKey = true),
GLM("Zhipu GLM", requiresApiKey = true),
OPENAI("OpenAI", requiresApiKey = true),
- OPENROUTER("OpenRouter", requiresApiKey = true);
+ OPENROUTER("OpenRouter", requiresApiKey = true),
+ OLLAMA("Ollama", requiresApiKey = true),
+ CUSTOM("Custom Provider", requiresApiKey = true, hasConfigurableUrl = true);
companion object {
fun fromString(value: String): AiProvider {
diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiProviderSupport.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiProviderSupport.kt
index 386758356..82c61f6a9 100644
--- a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiProviderSupport.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/AiProviderSupport.kt
@@ -88,7 +88,9 @@ internal object AiProviderSupport {
AiProvider.OPENROUTER,
AiProvider.NVIDIA,
AiProvider.KIMI,
- AiProvider.GLM
+ AiProvider.GLM,
+ AiProvider.OLLAMA,
+ AiProvider.CUSTOM
)
return buildList {
diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/DeepSeekAiClient.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/DeepSeekAiClient.kt
deleted file mode 100644
index afb84b3ea..000000000
--- a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/DeepSeekAiClient.kt
+++ /dev/null
@@ -1,171 +0,0 @@
-package com.theveloper.pixelplay.data.ai.provider
-
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-import kotlinx.serialization.Serializable
-import kotlinx.serialization.json.Json
-import okhttp3.MediaType.Companion.toMediaType
-import okhttp3.OkHttpClient
-import okhttp3.Request
-import okhttp3.RequestBody.Companion.toRequestBody
-import java.util.concurrent.TimeUnit
-
-/**
- * DeepSeek AI provider implementation
- * Uses OpenAI-compatible API
- */
-class DeepSeekAiClient(private val apiKey: String) : AiClient {
-
- companion object {
- private const val DEFAULT_DEEPSEEK_MODEL = "deepseek-chat"
- private const val BASE_URL = "https://api.deepseek.com"
- }
-
- @Serializable
- data class ChatMessage(val role: String, val content: String)
-
- @Serializable
- data class ChatRequest(
- val model: String,
- val messages: List,
- val temperature: Double = 0.7
- )
-
- @Serializable
- data class ChatChoice(val message: ChatMessage)
-
- @Serializable
- data class ChatResponse(val choices: List)
-
- @Serializable
- data class ModelItem(val id: String)
-
- @Serializable
- data class ModelsResponse(val data: List)
-
- private val client = OkHttpClient.Builder()
- .connectTimeout(30, TimeUnit.SECONDS)
- .readTimeout(60, TimeUnit.SECONDS)
- .writeTimeout(30, TimeUnit.SECONDS)
- .build()
-
- private val json = Json {
- ignoreUnknownKeys = true
- isLenient = true
- }
-
- override suspend fun generateContent(
- model: String,
- systemPrompt: String,
- prompt: String,
- temperature: Float
- ): String {
- return withContext(Dispatchers.IO) {
- val resolvedModel = model.ifBlank { DEFAULT_DEEPSEEK_MODEL }
- val messagesList = mutableListOf()
- if (systemPrompt.isNotBlank()) {
- messagesList.add(ChatMessage(role = "system", content = systemPrompt))
- }
- messagesList.add(ChatMessage(role = "user", content = prompt))
-
- val requestBody = ChatRequest(
- model = resolvedModel,
- messages = messagesList,
- temperature = temperature.toDouble()
- )
-
- val jsonBody = json.encodeToString(ChatRequest.serializer(), requestBody)
- val body = jsonBody.toRequestBody("application/json".toMediaType())
-
- val request = Request.Builder()
- .url("$BASE_URL/chat/completions")
- .addHeader("Authorization", "Bearer $apiKey")
- .addHeader("Content-Type", "application/json")
- .post(body)
- .build()
-
- try {
- client.newCall(request).execute().use { response ->
- val responseBody = response.body.string()
-
- if (!response.isSuccessful) {
- throw AiProviderSupport.createException(
- providerName = "DeepSeek",
- statusCode = response.code,
- transportMessage = response.message,
- responseBody = responseBody,
- requestedModel = resolvedModel
- )
- }
-
- val chatResponse = json.decodeFromString(responseBody)
- chatResponse.choices.firstOrNull()?.message?.content
- ?: throw AiProviderSupport.createException(
- providerName = "DeepSeek",
- statusCode = response.code,
- transportMessage = "Response had no content",
- responseBody = responseBody,
- requestedModel = resolvedModel
- )
- }
- } catch (e: Exception) {
- throw AiProviderSupport.wrapThrowable("DeepSeek", e, resolvedModel)
- }
- }
- }
-
- override suspend fun countTokens(model: String, systemPrompt: String, prompt: String): Int {
- // DeepSeek estimation
- return (systemPrompt.length + prompt.length) / 4
- }
-
- override suspend fun getAvailableModels(apiKey: String): List {
- return withContext(Dispatchers.IO) {
- try {
- val request = Request.Builder()
- .url("$BASE_URL/models")
- .addHeader("Authorization", "Bearer $apiKey")
- .get()
- .build()
-
- val response = client.newCall(request).execute()
-
- if (!response.isSuccessful) {
- return@withContext getDefaultModels()
- }
-
- val responseBody = response.body.string()
- val modelsResponse = json.decodeFromString(responseBody)
- modelsResponse.data.map { it.id }
- } catch (e: Exception) {
- getDefaultModels()
- }
- }
- }
-
- override suspend fun validateApiKey(apiKey: String): Boolean {
- return withContext(Dispatchers.IO) {
- try {
- val request = Request.Builder()
- .url("$BASE_URL/models")
- .addHeader("Authorization", "Bearer $apiKey")
- .get()
- .build()
-
- val response = client.newCall(request).execute()
- response.isSuccessful
- } catch (e: Exception) {
- false
- }
- }
- }
-
- override fun getDefaultModel(): String = DEFAULT_DEEPSEEK_MODEL
-
- private fun getDefaultModels(): List {
- return listOf(
- "deepseek-chat",
- "deepseek-reasoner"
- )
- }
-}
diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/GeminiAiClient.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/GeminiAiClient.kt
index b730603cb..cee7e23a4 100644
--- a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/GeminiAiClient.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/GeminiAiClient.kt
@@ -25,12 +25,7 @@ class GeminiAiClient(private val apiKey: String) : AiClient {
private const val DEFAULT_GEMINI_MODEL = "gemini-3.1-flash-lite"
private const val BASE_URL = "https://generativelanguage.googleapis.com/v1beta"
- // Markers for models that cannot perform text chat generation. These are the
- // only things we filter out — every other model the API returns is selectable.
- private val NON_CHAT_MARKERS = listOf(
- "embedding", "aqa", "imagen", "image-generation",
- "tts", "audio", "veo", "vision-only", "learnlm-embedding"
- )
+
}
private val httpClient = OkHttpClient.Builder()
@@ -42,7 +37,6 @@ class GeminiAiClient(private val apiKey: String) : AiClient {
private val json = Json {
ignoreUnknownKeys = true
isLenient = true
- encodeDefaults = true
}
@Serializable
@@ -55,7 +49,10 @@ class GeminiAiClient(private val apiKey: String) : AiClient {
private data class GenerationConfig(
val temperature: Double,
val topK: Int = 64,
- val topP: Double = 0.95
+ val topP: Double = 0.95,
+ @SerialName("maxOutputTokens") val maxOutputTokens: Int = 8192,
+ @SerialName("presencePenalty") val presencePenalty: Double? = null,
+ @SerialName("frequencyPenalty") val frequencyPenalty: Double? = null
)
@Serializable
@@ -86,7 +83,12 @@ class GeminiAiClient(private val apiKey: String) : AiClient {
model: String,
systemPrompt: String,
prompt: String,
- temperature: Float
+ temperature: Float,
+ topP: Float,
+ topK: Int,
+ maxTokens: Int,
+ presencePenalty: Float,
+ frequencyPenalty: Float
): String {
return withContext(Dispatchers.IO) {
val resolvedModel = model.ifBlank { DEFAULT_GEMINI_MODEL }
@@ -96,7 +98,14 @@ class GeminiAiClient(private val apiKey: String) : AiClient {
systemInstruction = systemPrompt
.takeIf { it.isNotBlank() }
?.let { Content(parts = listOf(Part(it))) },
- generationConfig = GenerationConfig(temperature = temperature.toDouble())
+ generationConfig = GenerationConfig(
+ temperature = temperature.toDouble(),
+ topK = topK,
+ topP = topP.toDouble(),
+ maxOutputTokens = maxTokens,
+ presencePenalty = presencePenalty.toDouble().takeIf { it != 0.0 },
+ frequencyPenalty = frequencyPenalty.toDouble().takeIf { it != 0.0 }
+ )
)
val jsonBody = json.encodeToString(GenerateRequest.serializer(), requestBody)
@@ -260,8 +269,7 @@ class GeminiAiClient(private val apiKey: String) : AiClient {
}
private fun isNonChatModel(modelName: String): Boolean {
- val lower = modelName.lowercase()
- return NON_CHAT_MARKERS.any { lower.contains(it) }
+ return !UnifiedModelFilter.isModelUsableForChat(modelName)
}
private fun getDefaultModels(): List {
diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/GenericOpenAiClient.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/GenericOpenAiClient.kt
index 658906dd2..fd0fb1c1d 100644
--- a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/GenericOpenAiClient.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/GenericOpenAiClient.kt
@@ -2,6 +2,7 @@ package com.theveloper.pixelplay.data.ai.provider
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
+import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
@@ -27,7 +28,11 @@ class GenericOpenAiClient(
private data class ChatRequest(
val model: String,
val messages: List,
- val temperature: Double = 0.7
+ val temperature: Double = 0.7,
+ @SerialName("top_p") val topP: Double? = null,
+ @SerialName("max_tokens") val maxTokens: Int? = null,
+ @SerialName("presence_penalty") val presencePenalty: Double? = null,
+ @SerialName("frequency_penalty") val frequencyPenalty: Double? = null
)
@Serializable
@@ -57,7 +62,12 @@ class GenericOpenAiClient(
model: String,
systemPrompt: String,
prompt: String,
- temperature: Float
+ temperature: Float,
+ topP: Float,
+ topK: Int,
+ maxTokens: Int,
+ presencePenalty: Float,
+ frequencyPenalty: Float
): String {
return withContext(Dispatchers.IO) {
val resolvedModel = model.ifBlank { defaultModelId }
@@ -70,7 +80,11 @@ class GenericOpenAiClient(
val requestBody = ChatRequest(
model = resolvedModel,
messages = messagesList,
- temperature = temperature.toDouble()
+ temperature = temperature.toDouble(),
+ topP = topP.toDouble(),
+ maxTokens = maxTokens.takeIf { it > 0 },
+ presencePenalty = presencePenalty.toDouble(),
+ frequencyPenalty = frequencyPenalty.toDouble()
)
val jsonBody = json.encodeToString(ChatRequest.serializer(), requestBody)
@@ -140,9 +154,7 @@ class GenericOpenAiClient(
val responseBody = response.body.string()
val modelsResponse = json.decodeFromString(responseBody)
- modelsResponse.data.map { it.id }.filter {
- !it.contains("whisper") && !it.contains("embed") && !it.contains("tts")
- }
+ modelsResponse.data.map { it.id }.let { UnifiedModelFilter.filterChatModels(it) }
} catch (e: Exception) {
listOf(defaultModelId)
}
diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/GroqAiClient.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/GroqAiClient.kt
deleted file mode 100644
index 0adf6cf70..000000000
--- a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/GroqAiClient.kt
+++ /dev/null
@@ -1,170 +0,0 @@
-package com.theveloper.pixelplay.data.ai.provider
-
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-import kotlinx.serialization.Serializable
-import kotlinx.serialization.json.Json
-import okhttp3.MediaType.Companion.toMediaType
-import okhttp3.OkHttpClient
-import okhttp3.Request
-import okhttp3.RequestBody.Companion.toRequestBody
-import java.util.concurrent.TimeUnit
-
-class GroqAiClient(private val apiKey: String) : AiClient {
-
- companion object {
- private const val DEFAULT_MODEL = "llama-3.1-8b-instant"
- private const val BASE_URL = "https://api.groq.com/openai/v1"
- }
-
- @Serializable
- private data class ChatMessage(val role: String, val content: String)
-
- @Serializable
- private data class ChatRequest(
- val model: String,
- val messages: List,
- val temperature: Double = 0.7
- )
-
- @Serializable
- private data class ChatChoice(val message: ChatMessage)
-
- @Serializable
- private data class ChatResponse(val choices: List)
-
- @Serializable
- private data class ModelItem(val id: String)
-
- @Serializable
- private data class ModelsResponse(val data: List)
-
- private val client = OkHttpClient.Builder()
- .connectTimeout(30, TimeUnit.SECONDS)
- .readTimeout(60, TimeUnit.SECONDS)
- .writeTimeout(30, TimeUnit.SECONDS)
- .build()
-
- private val json = Json {
- ignoreUnknownKeys = true
- isLenient = true
- }
-
- override suspend fun generateContent(
- model: String,
- systemPrompt: String,
- prompt: String,
- temperature: Float
- ): String {
- return withContext(Dispatchers.IO) {
- val resolvedModel = model.ifBlank { DEFAULT_MODEL }
- val messagesList = mutableListOf()
- if (systemPrompt.isNotBlank()) {
- messagesList.add(ChatMessage(role = "system", content = systemPrompt))
- }
- messagesList.add(ChatMessage(role = "user", content = prompt))
-
- val requestBody = ChatRequest(
- model = resolvedModel,
- messages = messagesList,
- temperature = temperature.toDouble()
- )
-
- val jsonBody = json.encodeToString(ChatRequest.serializer(), requestBody)
- val body = jsonBody.toRequestBody("application/json".toMediaType())
-
- val request = Request.Builder()
- .url("$BASE_URL/chat/completions")
- .addHeader("Authorization", "Bearer $apiKey")
- .addHeader("Content-Type", "application/json")
- .post(body)
- .build()
-
- try {
- client.newCall(request).execute().use { response ->
- val responseBody = response.body.string()
-
- if (!response.isSuccessful) {
- throw AiProviderSupport.createException(
- providerName = "Groq",
- statusCode = response.code,
- transportMessage = response.message,
- responseBody = responseBody,
- requestedModel = resolvedModel
- )
- }
-
- val chatResponse = json.decodeFromString(responseBody)
- chatResponse.choices.firstOrNull()?.message?.content
- ?: throw AiProviderSupport.createException(
- providerName = "Groq",
- statusCode = response.code,
- transportMessage = "Response had no content",
- responseBody = responseBody,
- requestedModel = resolvedModel
- )
- }
- } catch (e: Exception) {
- throw AiProviderSupport.wrapThrowable("Groq", e, resolvedModel)
- }
- }
- }
-
- override suspend fun countTokens(model: String, systemPrompt: String, prompt: String): Int {
- // Groq doesn't provide a native token counting endpoint, so we estimate.
- // Rule of thumb: 1 token ≈ 4 characters for English text.
- return (systemPrompt.length + prompt.length) / 4
- }
-
- override suspend fun getAvailableModels(apiKey: String): List {
- return withContext(Dispatchers.IO) {
- try {
- val request = Request.Builder()
- .url("$BASE_URL/models")
- .addHeader("Authorization", "Bearer $apiKey")
- .get()
- .build()
-
- val response = client.newCall(request).execute()
-
- if (!response.isSuccessful) {
- return@withContext getDefaultModels()
- }
-
- val responseBody = response.body.string()
- val modelsResponse = json.decodeFromString(responseBody)
- modelsResponse.data.map { it.id }.filter { !it.contains("whisper") }
- } catch (e: Exception) {
- getDefaultModels()
- }
- }
- }
-
- override suspend fun validateApiKey(apiKey: String): Boolean {
- return withContext(Dispatchers.IO) {
- try {
- val request = Request.Builder()
- .url("$BASE_URL/models")
- .addHeader("Authorization", "Bearer $apiKey")
- .get()
- .build()
-
- val response = client.newCall(request).execute()
- response.isSuccessful
- } catch (e: Exception) {
- false
- }
- }
- }
-
- override fun getDefaultModel(): String = DEFAULT_MODEL
-
- private fun getDefaultModels(): List {
- return listOf(
- "llama-3.1-8b-instant",
- "llama-3.3-70b-versatile",
- "mixtral-8x7b-32768",
- "gemma2-9b-it"
- )
- }
-}
diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/MistralAiClient.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/MistralAiClient.kt
deleted file mode 100644
index a4d166e2a..000000000
--- a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/MistralAiClient.kt
+++ /dev/null
@@ -1,169 +0,0 @@
-package com.theveloper.pixelplay.data.ai.provider
-
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-import kotlinx.serialization.Serializable
-import kotlinx.serialization.json.Json
-import okhttp3.MediaType.Companion.toMediaType
-import okhttp3.OkHttpClient
-import okhttp3.Request
-import okhttp3.RequestBody.Companion.toRequestBody
-import java.util.concurrent.TimeUnit
-
-class MistralAiClient(private val apiKey: String) : AiClient {
-
- companion object {
- private const val DEFAULT_MODEL = "mistral-large-latest"
- private const val BASE_URL = "https://api.mistral.ai/v1"
- }
-
- @Serializable
- private data class ChatMessage(val role: String, val content: String)
-
- @Serializable
- private data class ChatRequest(
- val model: String,
- val messages: List,
- val temperature: Double = 0.7
- )
-
- @Serializable
- private data class ChatChoice(val message: ChatMessage)
-
- @Serializable
- private data class ChatResponse(val choices: List)
-
- @Serializable
- private data class ModelItem(val id: String)
-
- @Serializable
- private data class ModelsResponse(val data: List)
-
- private val client = OkHttpClient.Builder()
- .connectTimeout(30, TimeUnit.SECONDS)
- .readTimeout(60, TimeUnit.SECONDS)
- .writeTimeout(30, TimeUnit.SECONDS)
- .build()
-
- private val json = Json {
- ignoreUnknownKeys = true
- isLenient = true
- }
-
- override suspend fun generateContent(
- model: String,
- systemPrompt: String,
- prompt: String,
- temperature: Float
- ): String {
- return withContext(Dispatchers.IO) {
- val resolvedModel = model.ifBlank { DEFAULT_MODEL }
- val messagesList = mutableListOf()
- if (systemPrompt.isNotBlank()) {
- messagesList.add(ChatMessage(role = "system", content = systemPrompt))
- }
- messagesList.add(ChatMessage(role = "user", content = prompt))
-
- val requestBody = ChatRequest(
- model = resolvedModel,
- messages = messagesList,
- temperature = temperature.toDouble()
- )
-
- val jsonBody = json.encodeToString(ChatRequest.serializer(), requestBody)
- val body = jsonBody.toRequestBody("application/json".toMediaType())
-
- val request = Request.Builder()
- .url("$BASE_URL/chat/completions")
- .addHeader("Authorization", "Bearer $apiKey")
- .addHeader("Content-Type", "application/json")
- .post(body)
- .build()
-
- try {
- client.newCall(request).execute().use { response ->
- val responseBody = response.body.string()
-
- if (!response.isSuccessful) {
- throw AiProviderSupport.createException(
- providerName = "Mistral",
- statusCode = response.code,
- transportMessage = response.message,
- responseBody = responseBody,
- requestedModel = resolvedModel
- )
- }
-
- val chatResponse = json.decodeFromString(responseBody)
- chatResponse.choices.firstOrNull()?.message?.content
- ?: throw AiProviderSupport.createException(
- providerName = "Mistral",
- statusCode = response.code,
- transportMessage = "Response had no content",
- responseBody = responseBody,
- requestedModel = resolvedModel
- )
- }
- } catch (e: Exception) {
- throw AiProviderSupport.wrapThrowable("Mistral", e, resolvedModel)
- }
- }
- }
-
- override suspend fun countTokens(model: String, systemPrompt: String, prompt: String): Int {
- // Mistral estimation
- return (systemPrompt.length + prompt.length) / 4
- }
-
- override suspend fun getAvailableModels(apiKey: String): List {
- return withContext(Dispatchers.IO) {
- try {
- val request = Request.Builder()
- .url("$BASE_URL/models")
- .addHeader("Authorization", "Bearer $apiKey")
- .get()
- .build()
-
- val response = client.newCall(request).execute()
-
- if (!response.isSuccessful) {
- return@withContext getDefaultModels()
- }
-
- val responseBody = response.body.string()
- val modelsResponse = json.decodeFromString(responseBody)
- modelsResponse.data.map { it.id }
- } catch (e: Exception) {
- getDefaultModels()
- }
- }
- }
-
- override suspend fun validateApiKey(apiKey: String): Boolean {
- return withContext(Dispatchers.IO) {
- try {
- val request = Request.Builder()
- .url("$BASE_URL/models")
- .addHeader("Authorization", "Bearer $apiKey")
- .get()
- .build()
-
- val response = client.newCall(request).execute()
- response.isSuccessful
- } catch (e: Exception) {
- false
- }
- }
- }
-
- override fun getDefaultModel(): String = DEFAULT_MODEL
-
- private fun getDefaultModels(): List {
- return listOf(
- "mistral-large-latest",
- "mistral-small-latest",
- "open-mixtral-8x22b",
- "open-mixtral-8x7b"
- )
- }
-}
diff --git a/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/UnifiedModelFilter.kt b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/UnifiedModelFilter.kt
new file mode 100644
index 000000000..d72d1786f
--- /dev/null
+++ b/app/src/main/java/com/theveloper/pixelplay/data/ai/provider/UnifiedModelFilter.kt
@@ -0,0 +1,30 @@
+package com.theveloper.pixelplay.data.ai.provider
+
+object UnifiedModelFilter {
+ private val UNSUITABLE_PATTERNS = listOf(
+ "embedding", "embed", "aqa", "imagen", "image-generation",
+ "tts", "text-to-speech", "speech", "audio", "whisper",
+ "veo", "vision-only", "learnlm-embedding", "moderation",
+ "dall-e", "stable-diffusion", "sdxl", "kandinsky",
+ "upscale", "background", "remove-background",
+ "segment", "detect", "classify", "object-detection"
+ )
+
+ fun isModelUsableForChat(modelName: String): Boolean {
+ val lower = modelName.lowercase()
+ return UNSUITABLE_PATTERNS.none { lower.contains(it) }
+ }
+
+ fun filterChatModels(models: List): List {
+ return models.filter { isModelUsableForChat(it) }
+ }
+
+ fun filterChatModelsWithDefaults(
+ apiModels: List,
+ defaultModels: List
+ ): List {
+ return (apiModels.filter { isModelUsableForChat(it) } + defaultModels)
+ .distinct()
+ .sorted()
+ }
+}
diff --git a/app/src/main/java/com/theveloper/pixelplay/data/backup/BackupManager.kt b/app/src/main/java/com/theveloper/pixelplay/data/backup/BackupManager.kt
index 6472ab11a..27afc146b 100644
--- a/app/src/main/java/com/theveloper/pixelplay/data/backup/BackupManager.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/data/backup/BackupManager.kt
@@ -3,6 +3,7 @@ package com.theveloper.pixelplay.data.backup
import android.content.Context
import android.net.Uri
import android.os.Build
+import timber.log.Timber
import com.theveloper.pixelplay.data.backup.format.BackupReader
import com.theveloper.pixelplay.data.backup.format.BackupWriter
import com.theveloper.pixelplay.data.backup.history.BackupHistoryRepository
@@ -66,7 +67,7 @@ class BackupManager @Inject constructor(
// Build manifest
val packageInfo = try {
context.packageManager.getPackageInfo(context.packageName, 0)
- } catch (_: Exception) { null }
+ } catch (e: Exception) { Timber.w(e, "Failed to get package info"); null }
val manifest = BackupManifest(
schemaVersion = BackupManifest.CURRENT_SCHEMA_VERSION,
@@ -183,8 +184,8 @@ class BackupManager @Inject constructor(
appVersion = plan.manifest.appVersion
)
)
- } catch (_: Exception) {
- // Non-critical; don't fail restore because of history persistence
+ } catch (e: Exception) {
+ Timber.w(e, "Failed to persist restore history entry")
}
}
diff --git a/app/src/main/java/com/theveloper/pixelplay/data/backup/format/BackupWriter.kt b/app/src/main/java/com/theveloper/pixelplay/data/backup/format/BackupWriter.kt
index 92b01734f..c9d5a36f1 100644
--- a/app/src/main/java/com/theveloper/pixelplay/data/backup/format/BackupWriter.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/data/backup/format/BackupWriter.kt
@@ -5,6 +5,7 @@ import android.net.Uri
import com.google.gson.Gson
import com.theveloper.pixelplay.data.backup.model.BackupManifest
import com.theveloper.pixelplay.data.backup.model.BackupModuleInfo
+import timber.log.Timber
import com.theveloper.pixelplay.di.BackupGson
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
@@ -85,7 +86,8 @@ class BackupWriter @Inject constructor(
} else {
1
}
- } catch (_: Exception) {
+ } catch (e: Exception) {
+ Timber.w(e, "Failed to count items in backup module JSON")
0
}
}
diff --git a/app/src/main/java/com/theveloper/pixelplay/data/backup/format/LegacyPayloadAdapter.kt b/app/src/main/java/com/theveloper/pixelplay/data/backup/format/LegacyPayloadAdapter.kt
index 486462f7c..96fd3771c 100644
--- a/app/src/main/java/com/theveloper/pixelplay/data/backup/format/LegacyPayloadAdapter.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/data/backup/format/LegacyPayloadAdapter.kt
@@ -4,6 +4,7 @@ import com.google.gson.Gson
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import com.google.gson.JsonParser
+import timber.log.Timber
import com.theveloper.pixelplay.data.backup.model.BackupManifest
import com.theveloper.pixelplay.data.backup.model.BackupModuleInfo
import com.theveloper.pixelplay.data.backup.model.DeviceInfo
@@ -120,7 +121,7 @@ class LegacyPayloadAdapter @Inject constructor() {
} else {
1
}
- } catch (_: Exception) { 0 }
+ } catch (e: Exception) { Timber.w(e, "Failed to count legacy backup entries"); 0 }
}
private fun sha256(data: ByteArray): String {
diff --git a/app/src/main/java/com/theveloper/pixelplay/data/backup/history/BackupHistoryRepository.kt b/app/src/main/java/com/theveloper/pixelplay/data/backup/history/BackupHistoryRepository.kt
index 2d1b03374..4deec0c1d 100644
--- a/app/src/main/java/com/theveloper/pixelplay/data/backup/history/BackupHistoryRepository.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/data/backup/history/BackupHistoryRepository.kt
@@ -8,6 +8,7 @@ import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.theveloper.pixelplay.data.backup.model.BackupHistoryEntry
import com.theveloper.pixelplay.di.BackupGson
+import timber.log.Timber
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
@@ -30,7 +31,8 @@ class BackupHistoryRepository @Inject constructor(
if (json != null) {
try {
gson.fromJson>(json, listType)
- } catch (_: Exception) {
+ } catch (e: Exception) {
+ Timber.w(e, "Failed to parse backup history")
emptyList()
}
} else {
@@ -68,7 +70,8 @@ class BackupHistoryRepository @Inject constructor(
val json = preferences[BACKUP_HISTORY_KEY] ?: return emptyList()
return try {
gson.fromJson(json, listType)
- } catch (_: Exception) {
+ } catch (e: Exception) {
+ Timber.w(e, "Failed to read backup history")
emptyList()
}
}
diff --git a/app/src/main/java/com/theveloper/pixelplay/data/database/PixelPlayDatabase.kt b/app/src/main/java/com/theveloper/pixelplay/data/database/PixelPlayDatabase.kt
index 094929dd9..947f17208 100644
--- a/app/src/main/java/com/theveloper/pixelplay/data/database/PixelPlayDatabase.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/data/database/PixelPlayDatabase.kt
@@ -3,6 +3,7 @@ package com.theveloper.pixelplay.data.database
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.migration.Migration
+import timber.log.Timber
import androidx.sqlite.db.SupportSQLiteDatabase
@Database(
@@ -759,9 +760,8 @@ abstract class PixelPlayDatabase : RoomDatabase() {
try {
db.execSQL("ALTER TABLE songs ADD COLUMN date_added INTEGER NOT NULL DEFAULT 0")
- } catch (_: Exception) {
- // Some restored databases report the right version but still carry
- // a drifted songs table. If ALTER TABLE did not stick, rebuild it.
+ } catch (e: Exception) {
+ Timber.w(e, "ALTER TABLE songs ADD date_added failed; will recreate table")
}
if ("date_added" !in getTableColumns(db, "songs")) {
@@ -1133,8 +1133,8 @@ abstract class PixelPlayDatabase : RoomDatabase() {
if ("disc_number" !in columns) {
try {
db.execSQL("ALTER TABLE songs ADD COLUMN disc_number INTEGER DEFAULT null")
- } catch (_: Exception) {
- // Restored/drifted databases may already contain a partially applied column.
+ } catch (e: Exception) {
+ Timber.w(e, "ALTER TABLE songs ADD disc_number failed; may already exist")
}
}
diff --git a/app/src/main/java/com/theveloper/pixelplay/data/database/SongEntity.kt b/app/src/main/java/com/theveloper/pixelplay/data/database/SongEntity.kt
index 19f948b79..4368e0f1d 100644
--- a/app/src/main/java/com/theveloper/pixelplay/data/database/SongEntity.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/data/database/SongEntity.kt
@@ -3,6 +3,7 @@ package com.theveloper.pixelplay.data.database
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
+import timber.log.Timber
import androidx.room.Index
import androidx.room.PrimaryKey
import com.theveloper.pixelplay.data.model.ArtistRef
@@ -57,15 +58,13 @@ object SourceType {
entity = AlbumEntity::class,
parentColumns = ["id"],
childColumns = ["album_id"],
- onDelete = ForeignKey.CASCADE // Si un álbum se borra, sus canciones también
+ onDelete = ForeignKey.CASCADE // Deleting an album cascades to its songs
),
ForeignKey(
entity = ArtistEntity::class,
parentColumns = ["id"],
childColumns = ["artist_id"],
- onDelete = ForeignKey.SET_NULL // Si un artista se borra, el artist_id de la canción se pone a null
- // o podrías elegir CASCADE si las canciones no deben existir sin artista.
- // SET_NULL es más flexible si las canciones pueden ser de "Artista Desconocido".
+ onDelete = ForeignKey.SET_NULL // Nullify artist_id when artist is deleted (keeps song as "Unknown Artist")
)
]
)
@@ -76,7 +75,7 @@ data class SongEntity(
@ColumnInfo(name = "artist_id") val artistId: Long, // Primary artist ID for backward compatibility
@ColumnInfo(name = "album_artist") val albumArtist: String? = null, // Album artist from metadata
@ColumnInfo(name = "album_name") val albumName: String,
- @ColumnInfo(name = "album_id") val albumId: Long, // index = true eliminado
+ @ColumnInfo(name = "album_id") val albumId: Long,
@ColumnInfo(name = "content_uri_string") val contentUriString: String,
@ColumnInfo(name = "album_art_uri_string") val albumArtUriString: String?,
@ColumnInfo(name = "duration") val duration: Long,
@@ -171,7 +170,8 @@ private fun parseArtistsJson(json: String?): List {
isPrimary = obj.optBoolean("primary", false)
)
}
- } catch (_: Exception) {
+ } catch (e: Exception) {
+ Timber.w(e, "Failed to parse artist refs JSON")
emptyList()
}
}
@@ -212,9 +212,8 @@ fun List.toSongs(): List {
return this.map { it.toSong() }
}
-// El modelo Song usa id como String, pero la entidad lo necesita como Long (de MediaStore)
-// El modelo Song no tiene filePath, así que no se puede mapear desde ahí directamente.
-// filePath y parentDirectoryPath se poblarán desde MediaStore en el SyncWorker.
+// Song model uses String id but the entity needs Long (from MediaStore).
+// filePath and parentDirectoryPath are populated from MediaStore in SyncWorker.
fun Song.toEntity(filePathFromMediaStore: String, parentDirFromMediaStore: String): SongEntity {
return SongEntity(
id = this.id.toLong(),
@@ -252,8 +251,7 @@ data class SongSummary(
val duration: Long
)
-// Sobrecarga o alternativa si los paths no están disponibles o no son necesarios al convertir de Modelo a Entidad
-// (menos probable que se use si la entidad siempre requiere los paths)
+// Fallback when file paths are unavailable during Song-to-Entity conversion.
fun Song.toEntityWithoutPaths(): SongEntity {
return SongEntity(
id = this.id.toLong(),
diff --git a/app/src/main/java/com/theveloper/pixelplay/data/gdrive/GDriveApiService.kt b/app/src/main/java/com/theveloper/pixelplay/data/gdrive/GDriveApiService.kt
index e6ffdf81c..dd64dad4a 100644
--- a/app/src/main/java/com/theveloper/pixelplay/data/gdrive/GDriveApiService.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/data/gdrive/GDriveApiService.kt
@@ -6,6 +6,8 @@ import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
+import org.json.JSONArray
+import org.json.JSONObject
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@@ -31,7 +33,11 @@ class GDriveApiService @Inject constructor(
fun hasToken(): Boolean = !accessToken.isNullOrBlank()
- fun getAuthHeader(): String = "Bearer ${accessToken ?: ""}"
+ fun getAuthHeader(): String {
+ val token = accessToken
+ require(!token.isNullOrBlank()) { "GDrive access token not set" }
+ return "Bearer $token"
+ }
fun getStreamUrl(fileId: String): String {
return "${GDriveConstants.DRIVE_API_BASE}/files/$fileId?alt=media"
@@ -94,8 +100,12 @@ class GDriveApiService @Inject constructor(
*/
suspend fun createFolder(name: String, parentId: String = "root"): String {
return withContext(Dispatchers.IO) {
- val json = """{"name":"$name","mimeType":"application/vnd.google-apps.folder","parents":["$parentId"]}"""
- val body = json.toRequestBody("application/json".toMediaType())
+ val jsonBody = JSONObject().apply {
+ put("name", name)
+ put("mimeType", "application/vnd.google-apps.folder")
+ put("parents", JSONArray().put(parentId))
+ }.toString()
+ val body = jsonBody.toRequestBody("application/json".toMediaType())
val request = Request.Builder()
.url("${GDriveConstants.DRIVE_API_BASE}/files")
@@ -103,14 +113,15 @@ class GDriveApiService @Inject constructor(
.post(body)
.build()
- val response = okHttpClient.newCall(request).execute()
- val responseBody = response.body.string()
- Timber.d("GDriveApi createFolder: code=${response.code}, body=${responseBody.take(200)}")
+ okHttpClient.newCall(request).execute().use { response ->
+ val responseBody = response.body.string()
+ Timber.d("GDriveApi createFolder: code=${response.code}, body=${responseBody.take(200)}")
- if (!response.isSuccessful) {
- throw Exception("Drive API error ${response.code}: $responseBody")
+ if (!response.isSuccessful) {
+ throw Exception("Drive API error ${response.code}: $responseBody")
+ }
+ responseBody
}
- responseBody
}
}
@@ -136,14 +147,15 @@ class GDriveApiService @Inject constructor(
.post(formBody)
.build()
- val response = okHttpClient.newCall(request).execute()
- val responseBody = response.body.string()
- Timber.d("GDriveApi exchangeAuthCode: code=${response.code}")
+ okHttpClient.newCall(request).execute().use { response ->
+ val responseBody = response.body.string()
+ Timber.d("GDriveApi exchangeAuthCode: code=${response.code}")
- if (!response.isSuccessful) {
- throw Exception("Token exchange failed ${response.code}: $responseBody")
+ if (!response.isSuccessful) {
+ throw Exception("Token exchange failed ${response.code}: $responseBody")
+ }
+ responseBody
}
- responseBody
}
}
@@ -168,14 +180,15 @@ class GDriveApiService @Inject constructor(
.post(formBody)
.build()
- val response = okHttpClient.newCall(request).execute()
- val responseBody = response.body.string()
- Timber.d("GDriveApi refreshToken: code=${response.code}")
+ okHttpClient.newCall(request).execute().use { response ->
+ val responseBody = response.body.string()
+ Timber.d("GDriveApi refreshToken: code=${response.code}")
- if (!response.isSuccessful) {
- throw Exception("Token refresh failed ${response.code}: $responseBody")
+ if (!response.isSuccessful) {
+ throw Exception("Token refresh failed ${response.code}: $responseBody")
+ }
+ responseBody
}
- responseBody
}
}
@@ -194,14 +207,15 @@ class GDriveApiService @Inject constructor(
.get()
.build()
- val response = okHttpClient.newCall(request).execute()
- val responseBody = response.body.string()
- Timber.d("GDriveApi GET ${url.take(80)}: code=${response.code}")
+ okHttpClient.newCall(request).execute().use { response ->
+ val responseBody = response.body.string()
+ Timber.d("GDriveApi GET ${url.take(80)}: code=${response.code}")
- if (!response.isSuccessful) {
- throw Exception("Drive API error ${response.code}: $responseBody")
+ if (!response.isSuccessful) {
+ throw Exception("Drive API error ${response.code}: $responseBody")
+ }
+ responseBody
}
- responseBody
}
}
}
diff --git a/app/src/main/java/com/theveloper/pixelplay/data/gdrive/GDriveStreamProxy.kt b/app/src/main/java/com/theveloper/pixelplay/data/gdrive/GDriveStreamProxy.kt
index 7014ddd09..9ad245b58 100644
--- a/app/src/main/java/com/theveloper/pixelplay/data/gdrive/GDriveStreamProxy.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/data/gdrive/GDriveStreamProxy.kt
@@ -51,10 +51,10 @@ class GDriveStreamProxy @Inject constructor(
)
}
- private var server: EmbeddedServer? = null
- private var actualPort: Int = 0
+ @Volatile private var server: EmbeddedServer? = null
+ @Volatile private var actualPort: Int = 0
private val proxyScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
- private var startJob: Job? = null
+ @Volatile private var startJob: Job? = null
fun isReady(): Boolean = actualPort > 0
diff --git a/app/src/main/java/com/theveloper/pixelplay/data/jellyfin/model/JellyfinCredentials.kt b/app/src/main/java/com/theveloper/pixelplay/data/jellyfin/model/JellyfinCredentials.kt
index 84514af91..bf472a68a 100644
--- a/app/src/main/java/com/theveloper/pixelplay/data/jellyfin/model/JellyfinCredentials.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/data/jellyfin/model/JellyfinCredentials.kt
@@ -1,8 +1,7 @@
package com.theveloper.pixelplay.data.jellyfin.model
-import com.theveloper.pixelplay.data.stream.CloudStreamSecurity
+import com.theveloper.pixelplay.utils.ServerUrlUtils
import okhttp3.HttpUrl
-import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
data class JellyfinCredentials(
val serverUrl: String,
@@ -29,35 +28,11 @@ data class JellyfinCredentials(
get() = !accessToken.isNullOrBlank() && !userId.isNullOrBlank()
val normalizedHttpUrlOrNull: HttpUrl?
- get() {
- val trimmed = serverUrl.trim().trimEnd('/')
- // Auto-prepend https:// if no scheme is provided
- val withScheme = if (!trimmed.startsWith("http://", ignoreCase = true) &&
- !trimmed.startsWith("https://", ignoreCase = true)
- ) {
- "https://$trimmed"
- } else {
- trimmed
- }
- return withScheme.toHttpUrlOrNull()
- }
+ get() = ServerUrlUtils.normalizeHttpUrl(serverUrl)
val normalizedServerUrl: String
- get() = normalizedHttpUrlOrNull?.toString()?.trimEnd('/') ?: serverUrl.trim().trimEnd('/')
+ get() = ServerUrlUtils.normalizeServerUrl(serverUrl)
- fun connectionValidationError(): String? {
- val parsed = normalizedHttpUrlOrNull
- ?: return "Invalid server URL format"
-
- if (parsed.username.isNotEmpty() || parsed.password.isNotEmpty()) {
- return "Server URL must not contain embedded credentials"
- }
-
- // Warn about cleartext HTTP on public hosts
- if (!parsed.isHttps && !CloudStreamSecurity.isLocalOrPrivateHost(parsed.host)) {
- return "Use https:// for remote Jellyfin servers. HTTP is only allowed for local network addresses."
- }
-
- return null
- }
+ fun connectionValidationError(): String? =
+ ServerUrlUtils.connectionValidationError(serverUrl, "Jellyfin")
}
diff --git a/app/src/main/java/com/theveloper/pixelplay/data/model/LibraryTabId.kt b/app/src/main/java/com/theveloper/pixelplay/data/model/LibraryTabId.kt
index 76efcc3ae..16b96ee56 100644
--- a/app/src/main/java/com/theveloper/pixelplay/data/model/LibraryTabId.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/data/model/LibraryTabId.kt
@@ -19,6 +19,7 @@ enum class LibraryTabId(
LIKED("LIKED", "LIKED", R.string.library_tab_liked, SortOption.LikedSongDateLiked);
companion object {
+ val defaultOrder: List = entries.toList()
fun fromStorageKey(key: String): LibraryTabId =
entries.firstOrNull { it.storageKey == key } ?: SONGS
}
diff --git a/app/src/main/java/com/theveloper/pixelplay/data/navidrome/model/NavidromeCredentials.kt b/app/src/main/java/com/theveloper/pixelplay/data/navidrome/model/NavidromeCredentials.kt
index 2742fd5bd..8a4db443a 100644
--- a/app/src/main/java/com/theveloper/pixelplay/data/navidrome/model/NavidromeCredentials.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/data/navidrome/model/NavidromeCredentials.kt
@@ -1,8 +1,7 @@
package com.theveloper.pixelplay.data.navidrome.model
+import com.theveloper.pixelplay.utils.ServerUrlUtils
import okhttp3.HttpUrl
-import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
-import com.theveloper.pixelplay.data.stream.CloudStreamSecurity
/**
* Represents authentication credentials for a Navidrome/Subsonic server.
@@ -49,38 +48,26 @@ data class NavidromeCredentials(
* Returns the parsed and normalized server URL, or null if it is invalid.
*/
val normalizedHttpUrlOrNull: HttpUrl?
- get() {
- val trimmed = serverUrl.trim().trimEnd('/')
- // Auto-prepend https:// if no scheme is provided
- val withScheme = if (!trimmed.startsWith("http://", ignoreCase = true) &&
- !trimmed.startsWith("https://", ignoreCase = true)
- ) {
- "https://$trimmed"
- } else {
- trimmed
- }
- return withScheme.toHttpUrlOrNull()
- }
+ get() = ServerUrlUtils.normalizeHttpUrl(serverUrl)
/**
* Returns the normalized server URL (without trailing slash).
*/
val normalizedServerUrl: String
- get() = normalizedHttpUrlOrNull?.toString()?.trimEnd('/') ?: serverUrl.trim().trimEnd('/')
+ get() = ServerUrlUtils.normalizeServerUrl(serverUrl)
/**
* Returns a validation error for connection setup, or null when the URL is acceptable.
*/
fun connectionValidationError(requireHttps: Boolean = true): String? {
- val httpUrl = normalizedHttpUrlOrNull ?: return "Enter a valid server URL."
- if (httpUrl.username.isNotEmpty() || httpUrl.password.isNotEmpty()) {
- return "Server URL must not include embedded credentials."
- }
- if (requireHttps && !httpUrl.isHttps &&
- !CloudStreamSecurity.isLocalOrPrivateHost(httpUrl.host)
- ) {
- return "Use an https:// server URL for remote Navidrome/Subsonic servers. HTTP is only allowed for local network addresses."
+ if (!requireHttps) {
+ val httpUrl = normalizedHttpUrlOrNull ?: return "Enter a valid server URL."
+ if (httpUrl.username.isNotEmpty() || httpUrl.password.isNotEmpty()) {
+ return "Server URL must not include embedded credentials."
+ }
+ return null
}
- return null
+ return ServerUrlUtils.connectionValidationError(serverUrl, "Navidrome/Subsonic")
+ ?.let { if (it == "Invalid server URL format") "Enter a valid server URL." else it }
}
}
diff --git a/app/src/main/java/com/theveloper/pixelplay/data/network/netease/NeteaseApiService.kt b/app/src/main/java/com/theveloper/pixelplay/data/network/netease/NeteaseApiService.kt
index 5c5b4da22..26370d178 100644
--- a/app/src/main/java/com/theveloper/pixelplay/data/network/netease/NeteaseApiService.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/data/network/netease/NeteaseApiService.kt
@@ -313,7 +313,8 @@ class NeteaseApiService @Inject constructor() {
resp = call()
}
resp
- } catch (_: Exception) {
+ } catch (e: Exception) {
+ Timber.w(e, "$TAG: retry after session warm-up failed")
resp
}
}
diff --git a/app/src/main/java/com/theveloper/pixelplay/data/preferences/AiPreferencesRepository.kt b/app/src/main/java/com/theveloper/pixelplay/data/preferences/AiPreferencesRepository.kt
index d339efbba..964875d04 100644
--- a/app/src/main/java/com/theveloper/pixelplay/data/preferences/AiPreferencesRepository.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/data/preferences/AiPreferencesRepository.kt
@@ -4,6 +4,8 @@ import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.floatPreferencesKey
+import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
@@ -35,13 +37,23 @@ class AiPreferencesRepository @Inject constructor(
private object Keys {
val AI_PROVIDER = stringPreferencesKey("ai_provider")
val SAFE_TOKEN_LIMIT = booleanPreferencesKey("safe_token_limit")
+ val AI_TEMPERATURE = floatPreferencesKey("ai_temperature")
+ val AI_TOP_P = floatPreferencesKey("ai_top_p")
+ val AI_TOP_K = intPreferencesKey("ai_top_k")
+ val AI_MAX_TOKENS = intPreferencesKey("ai_max_tokens")
+ val AI_PRESENCE_PENALTY = floatPreferencesKey("ai_presence_penalty")
+ val AI_FREQUENCY_PENALTY = floatPreferencesKey("ai_frequency_penalty")
+ val AI_SAMPLE_SIZE = intPreferencesKey("ai_sample_size")
+ val AI_DIGEST_MODE = stringPreferencesKey("ai_digest_mode")
+ val AI_INCLUDE_EXTENDED_FIELDS = booleanPreferencesKey("ai_include_extended_fields")
fun getApiKey(provider: AiProvider) = stringPreferencesKey("${provider.name.lowercase()}_api_key")
fun getModel(provider: AiProvider) = stringPreferencesKey("${provider.name.lowercase()}_model")
fun getSystemPrompt(provider: AiProvider) = stringPreferencesKey("${provider.name.lowercase()}_system_prompt")
+ fun getBaseUrl(provider: AiProvider) = stringPreferencesKey("${provider.name.lowercase()}_base_url")
}
- // Generic accessors for AiOrchestrator
+ // Generic accessors for AiHandler
fun getApiKey(provider: AiProvider): Flow =
dataStore.data.map { preferences -> preferences[Keys.getApiKey(provider)]?.trim() ?: "" }
@@ -53,6 +65,9 @@ class AiPreferencesRepository @Inject constructor(
preferences[Keys.getSystemPrompt(provider)] ?: DEFAULT_SYSTEM_PROMPT
}
+ fun getBaseUrl(provider: AiProvider): Flow =
+ dataStore.data.map { preferences -> preferences[Keys.getBaseUrl(provider)] ?: "" }
+
suspend fun setApiKey(provider: AiProvider, apiKey: String) {
dataStore.edit { preferences -> preferences[Keys.getApiKey(provider)] = apiKey.trim() }
}
@@ -71,6 +86,10 @@ class AiPreferencesRepository @Inject constructor(
}
}
+ suspend fun setBaseUrl(provider: AiProvider, url: String) {
+ dataStore.edit { preferences -> preferences[Keys.getBaseUrl(provider)] = url.trim() }
+ }
+
// Convenience properties for legacy compatibility (e.g. PlayerViewModel)
val geminiApiKey: Flow = getApiKey(AiProvider.GEMINI)
val geminiModel: Flow = getModel(AiProvider.GEMINI)
@@ -108,12 +127,48 @@ class AiPreferencesRepository @Inject constructor(
val openrouterModel: Flow = getModel(AiProvider.OPENROUTER)
val openrouterSystemPrompt: Flow = getSystemPrompt(AiProvider.OPENROUTER)
+ val ollamaApiKey: Flow = getApiKey(AiProvider.OLLAMA)
+ val ollamaModel: Flow = getModel(AiProvider.OLLAMA)
+ val ollamaSystemPrompt: Flow = getSystemPrompt(AiProvider.OLLAMA)
+
+ val customApiKey: Flow = getApiKey(AiProvider.CUSTOM)
+ val customModel: Flow = getModel(AiProvider.CUSTOM)
+ val customSystemPrompt: Flow = getSystemPrompt(AiProvider.CUSTOM)
+ val customBaseUrl: Flow = getBaseUrl(AiProvider.CUSTOM)
+
val aiProvider: Flow =
dataStore.data.map { preferences -> preferences[Keys.AI_PROVIDER] ?: "GEMINI" }
val isSafeTokenLimitEnabled: Flow =
dataStore.data.map { preferences -> preferences[Keys.SAFE_TOKEN_LIMIT] ?: true }
+ val aiTemperature: Flow =
+ dataStore.data.map { preferences -> preferences[Keys.AI_TEMPERATURE] ?: 0.7f }
+
+ val aiTopP: Flow =
+ dataStore.data.map { preferences -> preferences[Keys.AI_TOP_P] ?: 0.95f }
+
+ val aiTopK: Flow =
+ dataStore.data.map { preferences -> preferences[Keys.AI_TOP_K] ?: 64 }
+
+ val aiMaxTokens: Flow =
+ dataStore.data.map { preferences -> preferences[Keys.AI_MAX_TOKENS] ?: 4096 }
+
+ val aiPresencePenalty: Flow =
+ dataStore.data.map { preferences -> preferences[Keys.AI_PRESENCE_PENALTY] ?: 0.0f }
+
+ val aiFrequencyPenalty: Flow =
+ dataStore.data.map { preferences -> preferences[Keys.AI_FREQUENCY_PENALTY] ?: 0.0f }
+
+ val aiSampleSize: Flow =
+ dataStore.data.map { preferences -> preferences[Keys.AI_SAMPLE_SIZE] ?: 40 }
+
+ val aiDigestMode: Flow =
+ dataStore.data.map { preferences -> preferences[Keys.AI_DIGEST_MODE] ?: "safe" }
+
+ val aiIncludeExtendedFields: Flow =
+ dataStore.data.map { preferences -> preferences[Keys.AI_INCLUDE_EXTENDED_FIELDS] ?: false }
+
suspend fun setAiProvider(provider: String) {
dataStore.edit { preferences -> preferences[Keys.AI_PROVIDER] = provider }
}
@@ -121,4 +176,40 @@ class AiPreferencesRepository @Inject constructor(
suspend fun setSafeTokenLimitEnabled(enabled: Boolean) {
dataStore.edit { preferences -> preferences[Keys.SAFE_TOKEN_LIMIT] = enabled }
}
+
+ suspend fun setAiTemperature(value: Float) {
+ dataStore.edit { preferences -> preferences[Keys.AI_TEMPERATURE] = value }
+ }
+
+ suspend fun setAiTopP(value: Float) {
+ dataStore.edit { preferences -> preferences[Keys.AI_TOP_P] = value }
+ }
+
+ suspend fun setAiTopK(value: Int) {
+ dataStore.edit { preferences -> preferences[Keys.AI_TOP_K] = value }
+ }
+
+ suspend fun setAiMaxTokens(value: Int) {
+ dataStore.edit { preferences -> preferences[Keys.AI_MAX_TOKENS] = value }
+ }
+
+ suspend fun setAiPresencePenalty(value: Float) {
+ dataStore.edit { preferences -> preferences[Keys.AI_PRESENCE_PENALTY] = value }
+ }
+
+ suspend fun setAiFrequencyPenalty(value: Float) {
+ dataStore.edit { preferences -> preferences[Keys.AI_FREQUENCY_PENALTY] = value }
+ }
+
+ suspend fun setAiSampleSize(value: Int) {
+ dataStore.edit { preferences -> preferences[Keys.AI_SAMPLE_SIZE] = value }
+ }
+
+ suspend fun setAiDigestMode(mode: String) {
+ dataStore.edit { preferences -> preferences[Keys.AI_DIGEST_MODE] = mode }
+ }
+
+ suspend fun setAiIncludeExtendedFields(enabled: Boolean) {
+ dataStore.edit { preferences -> preferences[Keys.AI_INCLUDE_EXTENDED_FIELDS] = enabled }
+ }
}
diff --git a/app/src/main/java/com/theveloper/pixelplay/data/preferences/AppLanguage.kt b/app/src/main/java/com/theveloper/pixelplay/data/preferences/AppLanguage.kt
index 5630700f9..d1727d0ef 100644
--- a/app/src/main/java/com/theveloper/pixelplay/data/preferences/AppLanguage.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/data/preferences/AppLanguage.kt
@@ -4,28 +4,30 @@ import android.content.Context
import androidx.annotation.StringRes
import com.theveloper.pixelplay.R
-enum class AppLanguage(val tag: String, @StringRes val labelRes: Int) {
- SYSTEM("", R.string.settings_language_system),
- ENGLISH("en", R.string.settings_language_english),
- GERMAN("de", R.string.settings_language_german),
- SPANISH("es", R.string.settings_language_spanish),
- FRENCH("fr", R.string.settings_language_french),
- INDONESIAN("in", R.string.settings_language_indonesian),
- ITALIAN("it", R.string.settings_language_italian),
- KOREAN("ko", R.string.settings_language_korean),
- NORWEGIAN_BOKMAL("nb", R.string.settings_language_norwegian_bokmal),
- RUSSIAN("ru", R.string.settings_language_russian),
- SIMPLIFIED_CHINESE("zh-CN", R.string.settings_language_chinese),
- TURKISH("tr", R.string.settings_language_turkish);
+enum class AppLanguage(val tag: String, val nativeName: String, @StringRes val labelRes: Int?) {
+ SYSTEM("", "", R.string.settings_language_system),
+ ENGLISH("en", "English", null),
+ GERMAN("de", "Deutsch", null),
+ SPANISH("es", "Español", null),
+ FRENCH("fr", "Français", null),
+ INDONESIAN("in", "Bahasa Indonesia", null),
+ ITALIAN("it", "Italiano", null),
+ JAPANESE("ja", "日本語", null),
+ KOREAN("ko", "한국어", null),
+ NORWEGIAN_BOKMAL("nb", "Norsk bokmål", null),
+ RUSSIAN("ru", "Русский", null),
+ SIMPLIFIED_CHINESE("zh-CN", "简体中文", null),
+ TURKISH("tr", "Türkçe", null),
+ ARABIC("ar", "العربية", null);
companion object {
val supportedLanguageTags: Set = values().map { it.tag }.toSet()
fun getLanguageOptions(context: Context): Map {
- val systemOption = SYSTEM.tag to context.getString(SYSTEM.labelRes)
+ val systemOption = SYSTEM.tag to (SYSTEM.labelRes?.let { context.getString(it) } ?: "")
val otherOptions = values()
.filter { it != SYSTEM }
- .map { it.tag to context.getString(it.labelRes) }
+ .map { it.tag to it.nativeName }
.sortedBy { it.second.lowercase() }
val result = LinkedHashMap()
diff --git a/app/src/main/java/com/theveloper/pixelplay/data/preferences/EqualizerPreferencesRepository.kt b/app/src/main/java/com/theveloper/pixelplay/data/preferences/EqualizerPreferencesRepository.kt
index 9fbe6a162..f4f1a89b1 100644
--- a/app/src/main/java/com/theveloper/pixelplay/data/preferences/EqualizerPreferencesRepository.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/data/preferences/EqualizerPreferencesRepository.kt
@@ -13,6 +13,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
+import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@@ -44,7 +45,8 @@ class EqualizerPreferencesRepository @Inject constructor(
if (modeString != null) {
try {
EqualizerViewMode.valueOf(modeString)
- } catch (_: Exception) {
+ } catch (e: Exception) {
+ Timber.w(e, "Failed to parse equalizer view mode")
EqualizerViewMode.SLIDERS
}
} else {
@@ -71,7 +73,8 @@ class EqualizerPreferencesRepository @Inject constructor(
decoded.isEmpty() -> List(10) { 0 }
else -> decoded + List(10 - decoded.size) { 0 }
}
- } catch (_: Exception) {
+ } catch (e: Exception) {
+ Timber.w(e, "Failed to parse equalizer custom bands")
List(10) { 0 }
}
} else {
@@ -120,7 +123,8 @@ class EqualizerPreferencesRepository @Inject constructor(
if (jsonString != null) {
try {
json.decodeFromString>(jsonString)
- } catch (_: Exception) {
+ } catch (e: Exception) {
+ Timber.w(e, "Failed to parse custom presets")
emptyList()
}
} else {
@@ -133,7 +137,8 @@ class EqualizerPreferencesRepository @Inject constructor(
if (jsonString != null) {
try {
json.decodeFromString>(jsonString)
- } catch (_: Exception) {
+ } catch (e: Exception) {
+ Timber.w(e, "Failed to parse pinned presets")
EqualizerPreset.ALL_PRESETS.map { it.name }
}
} else {
diff --git a/app/src/main/java/com/theveloper/pixelplay/data/preferences/PlaylistPreferencesRepository.kt b/app/src/main/java/com/theveloper/pixelplay/data/preferences/PlaylistPreferencesRepository.kt
index 647a68163..eb072ce1a 100644
--- a/app/src/main/java/com/theveloper/pixelplay/data/preferences/PlaylistPreferencesRepository.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/data/preferences/PlaylistPreferencesRepository.kt
@@ -21,6 +21,11 @@ class PlaylistPreferencesRepository @Inject constructor(
private val userPreferencesRepository: UserPreferencesRepository
) {
private val migrationMutex = Mutex()
+ // Serializes read-modify-write edits to playlists. Without this, concurrent edits
+ // (e.g. removing several songs in quick succession) each read the same snapshot via
+ // userPlaylistsFlow.first() and the last writer wins, silently dropping the other
+ // edits — which left the Playlists-menu song count stuck high. See issue #2391.
+ private val editMutex = Mutex()
@Volatile
private var migrationChecked = false
@@ -92,16 +97,26 @@ class PlaylistPreferencesRepository @Inject constructor(
}
suspend fun renamePlaylist(playlistId: String, newName: String) {
- ensureMigratedIfNeeded()
- val existing = userPlaylistsFlow.first().find { it.id == playlistId } ?: return
- val updated = existing.copy(
- name = newName,
- lastModified = System.currentTimeMillis()
- )
- localPlaylistDao.upsertPlaylist(updated.toEntity())
+ editMutex.withLock {
+ ensureMigratedIfNeeded()
+ val existing = userPlaylistsFlow.first().find { it.id == playlistId } ?: return
+ val updated = existing.copy(
+ name = newName,
+ lastModified = System.currentTimeMillis()
+ )
+ localPlaylistDao.upsertPlaylist(updated.toEntity())
+ }
}
suspend fun updatePlaylist(playlist: Playlist) {
+ editMutex.withLock {
+ updatePlaylistLocked(playlist)
+ }
+ }
+
+ // Persists a playlist and its songs. Caller must hold [editMutex] so the
+ // surrounding read-modify-write stays atomic.
+ private suspend fun updatePlaylistLocked(playlist: Playlist) {
ensureMigratedIfNeeded()
val updated = playlist.copy(lastModified = System.currentTimeMillis())
localPlaylistDao.upsertPlaylist(updated.toEntity())
@@ -109,10 +124,12 @@ class PlaylistPreferencesRepository @Inject constructor(
}
suspend fun addSongsToPlaylist(playlistId: String, songIdsToAdd: List) {
- ensureMigratedIfNeeded()
- val existing = userPlaylistsFlow.first().find { it.id == playlistId } ?: return
- val merged = (existing.songIds + songIdsToAdd).distinct()
- updatePlaylist(existing.copy(songIds = merged))
+ editMutex.withLock {
+ ensureMigratedIfNeeded()
+ val existing = userPlaylistsFlow.first().find { it.id == playlistId } ?: return
+ val merged = (existing.songIds + songIdsToAdd).distinct()
+ updatePlaylistLocked(existing.copy(songIds = merged))
+ }
}
suspend fun addOrRemoveSongFromPlaylists(songId: String, playlistIds: List): MutableList {
@@ -137,15 +154,19 @@ class PlaylistPreferencesRepository @Inject constructor(
}
suspend fun removeSongFromPlaylist(playlistId: String, songIdToRemove: String) {
- ensureMigratedIfNeeded()
- val existing = userPlaylistsFlow.first().find { it.id == playlistId } ?: return
- updatePlaylist(existing.copy(songIds = existing.songIds.filterNot { it == songIdToRemove }))
+ editMutex.withLock {
+ ensureMigratedIfNeeded()
+ val existing = userPlaylistsFlow.first().find { it.id == playlistId } ?: return
+ updatePlaylistLocked(existing.copy(songIds = existing.songIds.filterNot { it == songIdToRemove }))
+ }
}
suspend fun reorderSongsInPlaylist(playlistId: String, newSongOrderIds: List) {
- ensureMigratedIfNeeded()
- val existing = userPlaylistsFlow.first().find { it.id == playlistId } ?: return
- updatePlaylist(existing.copy(songIds = newSongOrderIds))
+ editMutex.withLock {
+ ensureMigratedIfNeeded()
+ val existing = userPlaylistsFlow.first().find { it.id == playlistId } ?: return
+ updatePlaylistLocked(existing.copy(songIds = newSongOrderIds))
+ }
}
suspend fun setPlaylistSongOrderMode(playlistId: String, modeValue: String) =
@@ -177,15 +198,17 @@ class PlaylistPreferencesRepository @Inject constructor(
}
suspend fun removeSongFromAllPlaylists(songId: String) {
- ensureMigratedIfNeeded()
- val playlists = userPlaylistsFlow.first()
- playlists.forEach { playlist ->
- if (songId in playlist.songIds) {
- updatePlaylist(
- playlist.copy(
- songIds = playlist.songIds.filterNot { it == songId }
+ editMutex.withLock {
+ ensureMigratedIfNeeded()
+ val playlists = userPlaylistsFlow.first()
+ playlists.forEach { playlist ->
+ if (songId in playlist.songIds) {
+ updatePlaylistLocked(
+ playlist.copy(
+ songIds = playlist.songIds.filterNot { it == songId }
+ )
)
- )
+ }
}
}
}
diff --git a/app/src/main/java/com/theveloper/pixelplay/data/preferences/UserPreferencesRepository.kt b/app/src/main/java/com/theveloper/pixelplay/data/preferences/UserPreferencesRepository.kt
index 9efca552a..0a796a8ba 100644
--- a/app/src/main/java/com/theveloper/pixelplay/data/preferences/UserPreferencesRepository.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/data/preferences/UserPreferencesRepository.kt
@@ -129,6 +129,7 @@ class UserPreferencesRepository @Inject constructor(
// Transition
val GLOBAL_TRANSITION_SETTINGS = stringPreferencesKey("global_transition_settings_json")
val LIBRARY_TABS_ORDER = stringPreferencesKey("library_tabs_order")
+ val LIBRARY_HIDDEN_TABS = stringSetPreferencesKey("library_hidden_tabs")
val IS_FOLDER_FILTER_ACTIVE = booleanPreferencesKey("is_folder_filter_active")
val IS_FOLDERS_PLAYLIST_VIEW = booleanPreferencesKey("is_folders_playlist_view")
val SHOW_TELEGRAM_CLOUD_PLAYLISTS = booleanPreferencesKey("show_telegram_cloud_playlists")
@@ -873,8 +874,18 @@ suspend fun markDirectoryRulesVersionApplied(version: Int) {
dataStore.edit { it[PreferencesKeys.LIBRARY_TABS_ORDER] = order }
}
+ val libraryHiddenTabsFlow: Flow> =
+ pref { it[PreferencesKeys.LIBRARY_HIDDEN_TABS] ?: emptySet() }
+
+ suspend fun setLibraryHiddenTabs(hiddenTabs: Set) {
+ dataStore.edit { it[PreferencesKeys.LIBRARY_HIDDEN_TABS] = hiddenTabs }
+ }
+
suspend fun resetLibraryTabsOrder() {
- dataStore.edit { it.remove(PreferencesKeys.LIBRARY_TABS_ORDER) }
+ dataStore.edit {
+ it.remove(PreferencesKeys.LIBRARY_TABS_ORDER)
+ it.remove(PreferencesKeys.LIBRARY_HIDDEN_TABS)
+ }
}
suspend fun migrateTabOrder() {
diff --git a/app/src/main/java/com/theveloper/pixelplay/data/qqmusic/QqMusicRepository.kt b/app/src/main/java/com/theveloper/pixelplay/data/qqmusic/QqMusicRepository.kt
index 32487f396..697489281 100644
--- a/app/src/main/java/com/theveloper/pixelplay/data/qqmusic/QqMusicRepository.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/data/qqmusic/QqMusicRepository.kt
@@ -613,7 +613,8 @@ class QqMusicRepository @Inject constructor(
val result = String(decoded, Charsets.UTF_8)
// Verify the decoded result contains actual readable text
if (result.isNotBlank() && !result.contains('\u0000')) result else input
- } catch (_: Exception) {
+ } catch (e: Exception) {
+ Timber.w(e, "Failed to decode base64 artist name")
input
}
}
diff --git a/app/src/main/java/com/theveloper/pixelplay/data/remote/qqmusic/QQSignGenerator.kt b/app/src/main/java/com/theveloper/pixelplay/data/remote/qqmusic/QQSignGenerator.kt
index 3a1995d00..7563c0619 100644
--- a/app/src/main/java/com/theveloper/pixelplay/data/remote/qqmusic/QQSignGenerator.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/data/remote/qqmusic/QQSignGenerator.kt
@@ -118,7 +118,8 @@ class QQSignGenerator(private val context: Context) {
if (raw == null || raw == "null" || raw.isBlank()) return null
return try {
if (raw.startsWith('"')) JSONArray("[$raw]").getString(0) else raw
- } catch (_: Exception) {
+ } catch (e: Exception) {
+ Timber.w(e, "Failed to decode evaluate result")
raw
}
}
diff --git a/app/src/main/java/com/theveloper/pixelplay/data/repository/LyricsRepositoryImpl.kt b/app/src/main/java/com/theveloper/pixelplay/data/repository/LyricsRepositoryImpl.kt
index 05e357d05..b1941fdef 100644
--- a/app/src/main/java/com/theveloper/pixelplay/data/repository/LyricsRepositoryImpl.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/data/repository/LyricsRepositoryImpl.kt
@@ -136,6 +136,12 @@ class LyricsRepositoryImpl @Inject constructor(
private val BRACKETED_QUALIFIER_REGEX = Regex("""[\(\[\{\uFF08\uFF3B\uFF5B\u3010\u300E\u300C\u3014\u3008\u300A]([^)\]\}\uFF09\uFF3D\uFF5D\u3011\u300F\u300D\u3015\u3009\u300B]*)[\)\]\}\uFF09\uFF3D\uFF5D\u3011\u300F\u300D\u3015\u3009\u300B]""")
private val FEATURE_QUALIFIER_REGEX = Regex("""\b(feat(?:uring)?|ft)\.?\b""", RegexOption.IGNORE_CASE)
private val TITLE_SEPARATOR_REGEX = Regex("""\s*[-\u2013\u2014:\uFF0D\u00B7\u30FB]\s*""")
+ private val MASH_UP_REGEX = Regex("""\bmash\s+up\b""")
+ private val DIACRITICS_REGEX = Regex("""\p{Mn}+""")
+ private val APOSTROPHE_REGEX = Regex("""[\u2019'`]""")
+ private val NON_ALNUM_REGEX = Regex("""[^\p{L}\p{N}]+""")
+ private val WHITESPACE_COLLAPSE_REGEX = Regex("""\s+""")
+ private val LONG_LATIN_RUN_REGEX = Regex("""[A-Za-z]{10,}""")
private val TIMING_VARIANT_KEYWORDS = setOf(
"remix",
"mix",
@@ -799,7 +805,7 @@ class LyricsRepositoryImpl @Inject constructor(
.filter { it in TIMING_VARIANT_KEYWORDS }
.toMutableSet()
- if (Regex("""\bmash\s+up\b""").containsMatchIn(normalized)) {
+ if (MASH_UP_REGEX.containsMatchIn(normalized)) {
variants += "mashup"
}
if ("versus" in tokens || "vs" in tokens) {
@@ -854,14 +860,14 @@ class LyricsRepositoryImpl @Inject constructor(
private fun normalizeForMatch(value: String): String {
val withoutDiacritics = Normalizer.normalize(value.lowercase(Locale.ROOT), Normalizer.Form.NFD)
- .replace(Regex("""\p{Mn}+"""), "")
+ .replace(DIACRITICS_REGEX, "")
return withoutDiacritics
.replace("&", " and ")
- .replace(Regex("""[\u2019'`]"""), "")
- .replace(Regex("""[^\p{L}\p{N}]+"""), " ")
+ .replace(APOSTROPHE_REGEX, "")
+ .replace(NON_ALNUM_REGEX, " ")
.trim()
- .replace(Regex("""\s+"""), " ")
+ .replace(WHITESPACE_COLLAPSE_REGEX, " ")
}
private fun isUnknownArtist(value: String): Boolean =
@@ -1142,7 +1148,7 @@ class LyricsRepositoryImpl @Inject constructor(
val text = line.line
if (text.isBlank() || text.any { it.isWhitespace() }) continue
- val hasLongLatinRun = Regex("[A-Za-z]{10,}").containsMatchIn(text)
+ val hasLongLatinRun = LONG_LATIN_RUN_REGEX.containsMatchIn(text)
if (hasLongLatinRun) {
suspiciousLines += 1
if (suspiciousLines >= 2) return true
diff --git a/app/src/main/java/com/theveloper/pixelplay/data/service/wear/PhoneDirectWatchTransferCoordinator.kt b/app/src/main/java/com/theveloper/pixelplay/data/service/wear/PhoneDirectWatchTransferCoordinator.kt
index 7f935a080..35182504f 100644
--- a/app/src/main/java/com/theveloper/pixelplay/data/service/wear/PhoneDirectWatchTransferCoordinator.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/data/service/wear/PhoneDirectWatchTransferCoordinator.kt
@@ -309,7 +309,8 @@ class PhoneDirectWatchTransferCoordinator @Inject constructor(
contentResolver.openAssetFileDescriptor(uri, "r")?.use { afd ->
afd.length != 0L
} ?: false
- } catch (_: Exception) {
+ } catch (e: Exception) {
+ Timber.w(e, "Failed to verify content URI accessibility")
false
}
}
diff --git a/app/src/main/java/com/theveloper/pixelplay/data/worker/AiWorker.kt b/app/src/main/java/com/theveloper/pixelplay/data/worker/AiWorker.kt
index a19c57615..0b1441d97 100644
--- a/app/src/main/java/com/theveloper/pixelplay/data/worker/AiWorker.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/data/worker/AiWorker.kt
@@ -7,7 +7,7 @@ import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import androidx.work.workDataOf
import com.theveloper.pixelplay.data.ai.AiNotificationManager
-import com.theveloper.pixelplay.data.ai.AiOrchestrator
+import com.theveloper.pixelplay.data.ai.AiHandler
import com.theveloper.pixelplay.data.ai.AiSystemPromptType
import com.theveloper.pixelplay.data.ai.UserProfileDigestGenerator
import com.theveloper.pixelplay.data.model.Song
@@ -24,7 +24,7 @@ import timber.log.Timber
class AiWorker @AssistedInject constructor(
@Assisted appContext: Context,
@Assisted workerParams: WorkerParameters,
- private val orchestrator: AiOrchestrator,
+ private val handler: AiHandler,
private val notificationManager: AiNotificationManager,
private val musicRepository: MusicRepository,
private val digestGenerator: UserProfileDigestGenerator,
@@ -75,7 +75,7 @@ class AiWorker @AssistedInject constructor(
digestGenerator.generateDigest(allSongs, isSafe)
} else ""
- val result = orchestrator.generateContent(
+ val result = handler.generateContent(
prompt = prompt,
type = type,
temperature = temp,
diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/BackupModuleSelectionDialog.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/BackupModuleSelectionDialog.kt
index 1584be7e5..8b7b7af7d 100644
--- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/BackupModuleSelectionDialog.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/BackupModuleSelectionDialog.kt
@@ -76,6 +76,7 @@ import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import androidx.compose.animation.animateColorAsState
+import androidx.compose.foundation.layout.widthIn
import androidx.compose.ui.res.stringResource
@OptIn(
@@ -135,7 +136,7 @@ fun BackupModuleSelectionDialog(
label = "import_module_selection_dialog"
) {
Surface(
- modifier = Modifier.fillMaxSize(),
+ modifier = Modifier.fillMaxSize().widthIn(max = 540.dp),
color = MaterialTheme.colorScheme.surfaceContainerLowest
) {
Scaffold(
diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/BetaInfoBottomSheet.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/BetaInfoBottomSheet.kt
index 0370ac54d..8e46e4a5a 100644
--- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/BetaInfoBottomSheet.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/BetaInfoBottomSheet.kt
@@ -127,7 +127,7 @@ fun BetaInfoBottomSheet(modifier: Modifier = Modifier) {
alpha = 0.95f,
strokeWidth = 4.dp,
amplitude = 4.dp,
- waves = 7.6f,
+ wavesDensity = 7.6f,
phase = 0f
)
}
diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/ChangelogBottomSheet.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/ChangelogBottomSheet.kt
index 5d8a3860d..2312ef977 100644
--- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/ChangelogBottomSheet.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/ChangelogBottomSheet.kt
@@ -165,7 +165,7 @@ fun ChangelogBottomSheet(
alpha = 0.95f,
strokeWidth = 4.dp,
amplitude = 4.dp,
- waves = 7.6f,
+ wavesDensity = 7.6f,
phase = 0f
)
@@ -346,7 +346,7 @@ fun VersionBadge(
}
private fun openUrl(context: Context, url: String) {
- val uri = try { url.toUri() } catch (_: Throwable) { url.toUri() }
+ val uri = url.toUri()
val intent = Intent(Intent.ACTION_VIEW, uri)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
try {
diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/DailyMixSection.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/DailyMixSection.kt
index 235d7ab0b..98eaa91b3 100644
--- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/DailyMixSection.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/DailyMixSection.kt
@@ -159,9 +159,6 @@ fun DailyMixSection(
coverArtUpdate
)
},
- generateAiMetadata = { fields ->
- playerViewModel.generateAiMetadata(song, fields)
- },
removeFromListTrigger = {}
)
diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/FileExplorerBottomSheet.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/FileExplorerBottomSheet.kt
index e1aed1b4c..43d85a34d 100644
--- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/FileExplorerBottomSheet.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/FileExplorerBottomSheet.kt
@@ -28,6 +28,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.systemBars
+import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
@@ -141,7 +142,7 @@ fun FileExplorerDialog(
label = "file_explorer_dialog"
) {
Surface(
- modifier = Modifier.fillMaxSize(),
+ modifier = Modifier.fillMaxSize().widthIn(max = 540.dp),
color = MaterialTheme.colorScheme.surfaceContainerLow
) {
FileExplorerContent(
diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlayerInternalNavigationBar.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlayerInternalNavigationBar.kt
index 651c5f26e..86b1a3dbe 100644
--- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlayerInternalNavigationBar.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlayerInternalNavigationBar.kt
@@ -3,17 +3,46 @@ package com.theveloper.pixelplay.presentation.components
import com.theveloper.pixelplay.presentation.navigation.navigateToTopLevelSafely
import android.os.SystemClock
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.core.CubicBezierEasing
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.spring
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.scaleIn
+import androidx.compose.animation.scaleOut
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.NavigationBarDefaults
+import androidx.compose.material3.ProvideTextStyle
+import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -22,12 +51,21 @@ import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.clearAndSetSemantics
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
import androidx.navigation.NavHostController
import com.theveloper.pixelplay.BottomNavItem
+import com.theveloper.pixelplay.R
import com.theveloper.pixelplay.data.preferences.NavBarStyle
import com.theveloper.pixelplay.presentation.components.scoped.CustomNavigationBarItem
import com.theveloper.pixelplay.presentation.navigation.Screen
@@ -245,3 +283,271 @@ fun PlayerInternalNavigationBar(
modifier = modifier
)
}
+
+@Composable
+fun ColumnScope.CustomNavigationRailItem(
+ selected: Boolean,
+ onClick: () -> Unit,
+ icon: @Composable () -> Unit,
+ selectedIcon: @Composable () -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ label: @Composable (() -> Unit)? = null,
+ contentDescription: String? = null,
+ alwaysShowLabel: Boolean = true,
+ selectedIconColor: androidx.compose.ui.graphics.Color = MaterialTheme.colorScheme.onSecondaryContainer,
+ unselectedIconColor: androidx.compose.ui.graphics.Color = MaterialTheme.colorScheme.onSurfaceVariant,
+ selectedTextColor: androidx.compose.ui.graphics.Color = MaterialTheme.colorScheme.onSurface,
+ unselectedTextColor: androidx.compose.ui.graphics.Color = MaterialTheme.colorScheme.onSurfaceVariant,
+ indicatorColor: androidx.compose.ui.graphics.Color = MaterialTheme.colorScheme.secondaryContainer,
+ interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
+) {
+ val iconColor by animateColorAsState(
+ targetValue = if (selected) selectedIconColor else unselectedIconColor,
+ animationSpec = tween(durationMillis = 150),
+ label = "iconColor"
+ )
+
+ val textColor by animateColorAsState(
+ targetValue = if (selected) selectedTextColor else unselectedTextColor,
+ animationSpec = tween(durationMillis = 150),
+ label = "textColor"
+ )
+
+ val iconScale by animateFloatAsState(
+ targetValue = if (selected) 1.1f else 1f,
+ animationSpec = spring(
+ dampingRatio = Spring.DampingRatioMediumBouncy,
+ stiffness = Spring.StiffnessMedium
+ ),
+ label = "iconScale"
+ )
+
+ val showLabel = label != null && (alwaysShowLabel || selected)
+ val indicatorWidth = 64.dp
+ val indicatorHeight = 32.dp
+ val iconWidth = 48.dp
+ val iconHeight = 24.dp
+ val indicatorPadding = 4.dp
+ val indicatorShape = RoundedCornerShape(16.dp)
+ val iconShape = RoundedCornerShape(12.dp)
+
+ Column(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(vertical = 12.dp)
+ .clickable(
+ onClick = onClick,
+ enabled = enabled,
+ role = Role.Tab,
+ interactionSource = interactionSource,
+ indication = null
+ )
+ .semantics {
+ if (contentDescription != null) {
+ this.contentDescription = contentDescription
+ }
+ },
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier.size(indicatorWidth, indicatorHeight)
+ ) {
+ androidx.compose.animation.AnimatedVisibility(
+ visible = selected,
+ enter = fadeIn(animationSpec = tween(100)) + scaleIn(animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessLow)),
+ exit = fadeOut(animationSpec = tween(100)) + scaleOut(animationSpec = tween(100, easing = CubicBezierEasing(0.5f, 0f, 0.75f, 0f)))
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(horizontal = indicatorPadding)
+ .background(
+ color = indicatorColor,
+ shape = indicatorShape
+ )
+ )
+ }
+
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier
+ .size(iconWidth, iconHeight)
+ .clip(iconShape)
+ .graphicsLayer {
+ scaleX = iconScale
+ scaleY = iconScale
+ }
+ ) {
+ CompositionLocalProvider(LocalContentColor provides iconColor) {
+ Box(
+ modifier = Modifier.clearAndSetSemantics {
+ if (showLabel) { }
+ }
+ ) {
+ if (selected) selectedIcon() else icon()
+ }
+ }
+ }
+ }
+
+ androidx.compose.animation.AnimatedVisibility(
+ visible = showLabel,
+ enter = fadeIn(animationSpec = tween(200, delayMillis = 50)),
+ exit = fadeOut(animationSpec = tween(100))
+ ) {
+ Spacer(modifier = Modifier.height(4.dp))
+ Box(
+ modifier = Modifier.padding(top = 4.dp)
+ ) {
+ ProvideTextStyle(
+ value = MaterialTheme.typography.labelMedium.copy(
+ color = textColor,
+ fontSize = 13.sp,
+ fontWeight = if (selected) FontWeight.Medium else FontWeight.Normal
+ )
+ ) {
+ label?.invoke()
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun PlayerInternalNavigationRail(
+ navController: NavHostController,
+ navItems: ImmutableList,
+ currentRoute: String?,
+ modifier: Modifier = Modifier,
+ onSearchIconDoubleTap: () -> Unit = {},
+ onOpenSidebar: () -> Unit = {}
+) {
+ val latestCurrentRoute by rememberUpdatedState(currentRoute)
+ val latestOnSearchIconDoubleTap by rememberUpdatedState(onSearchIconDoubleTap)
+ val latestNavigationEnabled by rememberUpdatedState(currentRoute != null)
+
+ Surface(
+ modifier = modifier
+ .fillMaxHeight()
+ .width(80.dp),
+ color = NavigationBarDefaults.containerColor,
+ tonalElevation = 3.dp
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxHeight()
+ .padding(vertical = 24.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ IconButton(
+ onClick = onOpenSidebar,
+ modifier = Modifier.padding(bottom = 16.dp)
+ ) {
+ Icon(
+ painter = painterResource(id = R.drawable.rounded_menu_24),
+ contentDescription = "Open Drawer",
+ tint = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+
+ val scope = rememberCoroutineScope()
+ var lastSearchTapTimestamp by remember { mutableStateOf(0L) }
+
+ navItems.forEach { item ->
+ val isSelected = currentRoute != null && currentRoute == item.screen.route
+ val selectedColor = MaterialTheme.colorScheme.primary
+ val unselectedColor = MaterialTheme.colorScheme.onSurfaceVariant
+ val indicatorColorFromTheme = MaterialTheme.colorScheme.secondaryContainer
+
+ val iconPainterResId = if (isSelected && item.selectedIconResId != null && item.selectedIconResId != 0) {
+ item.selectedIconResId
+ } else {
+ item.iconResId
+ }
+ val localizedLabel = stringResource(id = item.labelResId)
+ val iconLambda: @Composable () -> Unit = remember(iconPainterResId, localizedLabel) {
+ {
+ Icon(
+ painter = painterResource(id = iconPainterResId),
+ contentDescription = localizedLabel
+ )
+ }
+ }
+ val selectedIconLambda: @Composable () -> Unit = remember(iconPainterResId, localizedLabel) {
+ {
+ Icon(
+ painter = painterResource(id = iconPainterResId),
+ contentDescription = localizedLabel
+ )
+ }
+ }
+ val labelLambda: @Composable () -> Unit = remember(localizedLabel) {
+ { Text(localizedLabel) }
+ }
+
+ val onClickLambda: () -> Unit = remember(item.screen.route, navController, scope) {
+ click@{
+ if (!latestNavigationEnabled) {
+ lastSearchTapTimestamp = 0L
+ return@click
+ }
+
+ val itemRoute = item.screen.route
+ val isSearchTab = itemRoute == Screen.Search.route
+ val isAlreadySelected = latestCurrentRoute == itemRoute
+
+ if (isSearchTab) {
+ val now = SystemClock.elapsedRealtime()
+ val isDoubleTap = now - lastSearchTapTimestamp <= 350L
+ lastSearchTapTimestamp = now
+
+ if (!isAlreadySelected) {
+ if (!navController.navigateToTopLevelSafely(itemRoute)) {
+ lastSearchTapTimestamp = 0L
+ return@click
+ }
+ }
+
+ if (isDoubleTap) {
+ lastSearchTapTimestamp = 0L
+ if (isAlreadySelected) {
+ latestOnSearchIconDoubleTap()
+ } else {
+ scope.launch {
+ delay(160L)
+ latestOnSearchIconDoubleTap()
+ }
+ }
+ }
+ } else if (!isAlreadySelected) {
+ lastSearchTapTimestamp = 0L
+ navController.navigateToTopLevelSafely(itemRoute)
+ } else {
+ lastSearchTapTimestamp = 0L
+ }
+ }
+ }
+
+ CustomNavigationRailItem(
+ selected = isSelected,
+ onClick = onClickLambda,
+ enabled = currentRoute != null,
+ icon = iconLambda,
+ selectedIcon = selectedIconLambda,
+ label = labelLambda,
+ contentDescription = localizedLabel,
+ alwaysShowLabel = true,
+ selectedIconColor = selectedColor,
+ unselectedIconColor = unselectedColor,
+ selectedTextColor = selectedColor,
+ unselectedTextColor = unselectedColor,
+ indicatorColor = indicatorColorFromTheme
+ )
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlaylistBottomSheet.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlaylistBottomSheet.kt
index 2ceed9d9f..6a3dc19f3 100644
--- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlaylistBottomSheet.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlaylistBottomSheet.kt
@@ -63,7 +63,7 @@ fun PlaylistBottomSheet(
currentPlaylistId: String? = null
) {
val playlistCreatedAndSongsAddedMessage = stringResource(R.string.playlist_sheet_created_and_songs_added)
- val setGeminiApiKeyFirstMessage = stringResource(R.string.library_toast_set_gemini_api_key_first)
+ val setAiProviderApiKeyFirstMessage = stringResource(R.string.library_toast_set_ai_provider_api_key_first)
val songAddedToPlaylistsMessage = stringResource(R.string.playlist_sheet_song_added_to_playlists)
val commonSavedMessage = stringResource(R.string.common_saved)
val saveActionText = stringResource(R.string.common_save)
@@ -214,7 +214,7 @@ fun PlaylistBottomSheet(
if (hasActiveAiProviderApiKey) {
playerViewModel.showAiPlaylistSheet()
} else {
- playerViewModel.sendToast(setGeminiApiKeyFirstMessage)
+ playerViewModel.sendToast(setAiProviderApiKeyFirstMessage)
}
}
)
diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlaylistContainer.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlaylistContainer.kt
index ca39bb451..cff5d286c 100644
--- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlaylistContainer.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlaylistContainer.kt
@@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
@@ -577,6 +578,7 @@ fun CreatePlaylistDialogRedesigned(
) {
Column(
modifier = Modifier
+ .widthIn(max = 540.dp)
.padding(24.dp)
.fillMaxWidth()
) {
diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/ReorderTabsSheet.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/ReorderTabsSheet.kt
index 641d2f567..91bb960fb 100644
--- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/ReorderTabsSheet.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/ReorderTabsSheet.kt
@@ -13,13 +13,16 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.Check
+import androidx.compose.material.icons.rounded.Clear
import androidx.compose.material.icons.rounded.DragIndicator
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ContainedLoadingIndicator
@@ -53,7 +56,7 @@ import androidx.compose.ui.unit.dp
import androidx.core.view.ViewCompat
import android.view.HapticFeedbackConstants
import com.theveloper.pixelplay.R
-import com.theveloper.pixelplay.presentation.library.LibraryTabId
+import com.theveloper.pixelplay.data.model.LibraryTabId
import com.theveloper.pixelplay.presentation.utils.LocalAppHapticsConfig
import com.theveloper.pixelplay.presentation.utils.performAppCompatHapticFeedback
import com.theveloper.pixelplay.ui.theme.GoogleSansRounded
@@ -63,21 +66,25 @@ import racra.compose.smooth_corner_rect_library.AbsoluteSmoothCornerShape
import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyListState
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun ReorderTabsSheet(
- tabs: List,
- onReorder: (List) -> Unit,
+ visibleTabs: List,
+ hiddenTabs: List,
+ onSave: (visible: List, hidden: Set) -> Unit,
onReset: () -> Unit,
onDismiss: () -> Unit
) {
var showResetDialog by remember { mutableStateOf(false) }
- var localTabs by remember { mutableStateOf(tabs) }
+ var localVisibleTabs by remember { mutableStateOf(visibleTabs) }
+ var localHiddenTabs by remember { mutableStateOf(hiddenTabs) }
- LaunchedEffect(tabs) {
- localTabs = tabs
+ LaunchedEffect(visibleTabs, hiddenTabs) {
+ localVisibleTabs = visibleTabs
+ localHiddenTabs = hiddenTabs
}
if (showResetDialog) {
@@ -89,7 +96,7 @@ fun ReorderTabsSheet(
TextButton(
onClick = {
onReset()
- localTabs = tabs
+ // Local state will be updated by the LaunchedEffect when visibleTabs/hiddenTabs change via VM
showResetDialog = false
}
) {
@@ -114,15 +121,30 @@ fun ReorderTabsSheet(
val reorderableState = rememberReorderableLazyListState(
onMove = { from, to ->
- localTabs = localTabs.toMutableList().apply {
- add(to.index, removeAt(from.index))
+ val fromKey = from.key as? String ?: return@rememberReorderableLazyListState
+ val toKey = to.key as? String ?: return@rememberReorderableLazyListState
+
+ // Only move if both items are in the visible section
+ if (fromKey.startsWith("v_") && toKey.startsWith("v_")) {
+ val fromId = fromKey.removePrefix("v_")
+ val toId = toKey.removePrefix("v_")
+
+ val fromIdx = localVisibleTabs.indexOf(fromId)
+ val toIdx = localVisibleTabs.indexOf(toId)
+
+ if (fromIdx != -1 && toIdx != -1) {
+ localVisibleTabs = localVisibleTabs.toMutableList().apply {
+ add(toIdx, removeAt(fromIdx))
+ }
+ // Haptic feedback on reorder
+ performAppCompatHapticFeedback(
+ view,
+ appHapticsConfig,
+ HapticFeedbackConstants.CLOCK_TICK,
+ HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING
+ )
+ }
}
- // Haptic feedback on reorder
- performAppCompatHapticFeedback(
- view,
- appHapticsConfig,
- HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING
- )
},
lazyListState = listState
)
@@ -152,13 +174,13 @@ fun ReorderTabsSheet(
floatingActionButton = {
FloatingToolBar(
modifier = Modifier,
- onReset = { showResetDialog = true }, // This will now trigger the dialog
+ onReset = { showResetDialog = true },
onDismiss = onDismiss,
onClick = {
scope.launch {
isLoading = true
- delay(700) // Simulate network/db operation
- onReorder(localTabs)
+ delay(400) // Visual confirmation
+ onSave(localVisibleTabs, localHiddenTabs.toSet())
isLoading = false
onDismiss()
}
@@ -183,11 +205,12 @@ fun ReorderTabsSheet(
LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize().padding(horizontal = 14.dp),
- contentPadding = PaddingValues(bottom = 100.dp, top = 8.dp),
+ contentPadding = PaddingValues(bottom = 150.dp, top = 8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
- items(localTabs, key = { it }) { tab ->
- ReorderableItem(reorderableState, key = tab) { isDragging ->
+
+ items(localVisibleTabs, key = { "v_$it" }) { tab ->
+ ReorderableItem(reorderableState, key = "v_$tab") { isDragging ->
LaunchedEffect(isDragging) {
if (isDragging) {
performAppCompatHapticFeedback(
@@ -201,12 +224,13 @@ fun ReorderTabsSheet(
Surface(
modifier = Modifier
.fillMaxWidth()
+ .height(60.dp)
.clip(CircleShape),
shadowElevation = if (isDragging) 4.dp else 0.dp,
color = MaterialTheme.colorScheme.surfaceContainerLowest
) {
Row(
- modifier = Modifier.padding(horizontal = 16.dp, vertical = 18.dp),
+ modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
@@ -216,11 +240,111 @@ fun ReorderTabsSheet(
)
Spacer(modifier = Modifier.width(16.dp))
Text(
- text = LibraryTabId.fromStableKey(tab)
- ?.let { stringResource(it.labelRes) }
- ?: tab,
- style = MaterialTheme.typography.bodyLarge
+ text = LibraryTabId.fromStorageKey(tab)
+ .let { stringResource(it.titleRes) },
+ style = MaterialTheme.typography.bodyLarge,
+ modifier = Modifier.weight(1f)
+ )
+
+ if (localVisibleTabs.size > 2) {
+ Surface(
+ onClick = {
+ performAppCompatHapticFeedback(
+ view,
+ appHapticsConfig,
+ HapticFeedbackConstants.CLOCK_TICK,
+ HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING
+ )
+ localVisibleTabs = localVisibleTabs.filter { it != tab }
+ localHiddenTabs = localHiddenTabs + tab
+ },
+ modifier = Modifier.size(36.dp),
+ shape = AbsoluteSmoothCornerShape(12.dp, 60),
+ color = MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.4f)
+ ) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ imageVector = Icons.Rounded.Clear,
+ contentDescription = stringResource(R.string.reorder_tabs_cd_remove_tab),
+ tint = MaterialTheme.colorScheme.error,
+ modifier = Modifier.size(20.dp)
+ )
+ }
+ }
+ } else {
+ Spacer(modifier = Modifier.width(36.dp))
+ }
+ }
+ }
+ }
+ }
+
+ if (localHiddenTabs.isNotEmpty()) {
+ item(key = "h_hidden") {
+ Text(
+ text = stringResource(R.string.reorder_tabs_hidden_section),
+ style = MaterialTheme.typography.titleMedium,
+ fontFamily = GoogleSansRounded,
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier.padding(start = 8.dp, top = 16.dp, bottom = 4.dp),
+ color = MaterialTheme.colorScheme.secondary
+ )
+ }
+
+ items(localHiddenTabs, key = { "h_$it" }) { tab ->
+ Surface(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(60.dp)
+ .clip(CircleShape),
+ color = MaterialTheme.colorScheme.surfaceContainerLowest
+ ) {
+ Row(
+ modifier = Modifier.padding(horizontal = 16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ imageVector = Icons.Rounded.DragIndicator,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f)
+ )
+ Spacer(modifier = Modifier.width(16.dp))
+ Text(
+ text = LibraryTabId.fromStorageKey(tab)
+ .let { stringResource(it.titleRes) },
+ style = MaterialTheme.typography.bodyLarge,
+ modifier = Modifier.weight(1f)
)
+ Surface(
+ onClick = {
+ performAppCompatHapticFeedback(
+ view,
+ appHapticsConfig,
+ HapticFeedbackConstants.CLOCK_TICK,
+ HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING
+ )
+ localHiddenTabs = localHiddenTabs.filter { it != tab }
+ localVisibleTabs = localVisibleTabs + tab
+ },
+ modifier = Modifier.size(36.dp),
+ shape = AbsoluteSmoothCornerShape(12.dp, 60),
+ color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.4f)
+ ) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ imageVector = Icons.Rounded.Add,
+ contentDescription = stringResource(R.string.reorder_tabs_cd_add_tab),
+ tint = MaterialTheme.colorScheme.primary,
+ modifier = Modifier.size(20.dp)
+ )
+ }
+ }
}
}
}
@@ -266,7 +390,7 @@ fun FloatingToolBar(
) {
IconButton(
modifier = Modifier.align(Alignment.CenterVertically),
- onClick = onReset // This now calls the lambda from the parent
+ onClick = onReset
) {
Icon(
painter = painterResource(R.drawable.outline_restart_alt_24),
@@ -285,4 +409,4 @@ fun FloatingToolBar(
)
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/SongInfoBottomSheet.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/SongInfoBottomSheet.kt
index 01b934507..5dc12aa89 100644
--- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/SongInfoBottomSheet.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/SongInfoBottomSheet.kt
@@ -90,7 +90,6 @@ import com.theveloper.pixelplay.utils.shapes.RoundedStarShape
import racra.compose.smooth_corner_rect_library.AbsoluteSmoothCornerShape
import androidx.core.net.toUri
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
-import com.theveloper.pixelplay.data.ai.SongMetadata
import com.theveloper.pixelplay.data.media.CoverArtUpdate
import com.theveloper.pixelplay.ui.theme.MontserratFamily
import com.theveloper.pixelplay.presentation.viewmodel.SongInfoBottomSheetViewModel
@@ -142,12 +141,7 @@ fun SongInfoBottomSheet(
replayGainAlbumGainDb: String,
coverArtUpdate: CoverArtUpdate?
) -> Unit,
- generateAiMetadata: suspend (List) -> Result,
removeFromListTrigger: () -> Unit,
- isGeneratingMetadata: Boolean = false,
- aiMetadataSuccess: Boolean = false,
- aiError: String? = null,
- onRetryMetadata: () -> Unit = {},
songInfoViewModel: SongInfoBottomSheetViewModel = hiltViewModel()
) {
val context = LocalContext.current
diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerOverlaysLayer.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerOverlaysLayer.kt
index 5a7fba4fd..2cb218cce 100644
--- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerOverlaysLayer.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerOverlaysLayer.kt
@@ -256,9 +256,6 @@ internal fun UnifiedPlayerSongInfoLayer(
)
onDismissSongInfo()
},
- generateAiMetadata = { fields ->
- playerViewModel.generateAiMetadata(liveSong, fields)
- },
removeFromListTrigger = {
playerViewModel.removeSongFromQueue(liveSong.id)
onDismissSongInfo()
diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerSheetLayers.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerSheetLayers.kt
index 2aed35222..5f6915fc4 100644
--- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerSheetLayers.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerSheetLayers.kt
@@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.material3.ColorScheme
import androidx.compose.runtime.Composable
@@ -69,7 +70,8 @@ internal fun BoxScope.UnifiedPlayerMiniAndFullLayers(
onQueueDragStart: () -> Unit,
onQueueDrag: (Float) -> Unit,
onQueueRelease: (Float, Float) -> Unit,
- onShowCastClicked: () -> Unit
+ onShowCastClicked: () -> Unit,
+ isNavRailHidden: Boolean
) {
currentSong?.let { currentSongNonNull ->
miniPlayerScheme?.let { readyScheme ->
@@ -116,7 +118,7 @@ internal fun BoxScope.UnifiedPlayerMiniAndFullLayers(
.zIndex(miniPlayerZIndex)
) {
val isMiniPlayerVisible by remember {
- derivedStateOf { playerContentExpansionFraction.value < 0.01f }
+ derivedStateOf { playerContentExpansionFraction.value < 0.000001f } //0.01f is really huge for it
}
MiniPlayerContentInternal(
song = currentSongNonNull,
@@ -127,7 +129,9 @@ internal fun BoxScope.UnifiedPlayerMiniAndFullLayers(
onPrevious = { playerViewModel.previousSong() },
onNext = { playerViewModel.nextSong() },
canScroll = isMiniPlayerVisible && infrequentPlayerState.isPlaying,
- modifier = Modifier.fillMaxSize()
+ modifier = Modifier.fillMaxSize().then(
+ if (isNavRailHidden && !isMiniPlayerVisible) Modifier.padding(end = 80.dp) else Modifier
+ )
)
}
}
diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerSheetShared.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerSheetShared.kt
index 59ae7c316..71f733653 100644
--- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerSheetShared.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerSheetShared.kt
@@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Pause
@@ -83,6 +84,7 @@ internal fun MiniPlayerContentInternal(
Row(
modifier = modifier
.fillMaxWidth()
+ .widthIn(max = 450.dp)
.height(MiniPlayerHeight)
.padding(start = 10.dp, end = 12.dp),
verticalAlignment = Alignment.CenterVertically
diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerSheetV2.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerSheetV2.kt
index d692ed9fd..21718a40d 100644
--- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerSheetV2.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerSheetV2.kt
@@ -1,4 +1,4 @@
-package com.theveloper.pixelplay.presentation.components
+package com.theveloper.pixelplay.presentation.components
import android.widget.Toast
import com.theveloper.pixelplay.presentation.components.ExpressiveOfflineDialog
@@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.ui.layout.layout
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.widthIn
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.MotionScheme
@@ -115,7 +116,8 @@ fun UnifiedPlayerSheetV2(
collapsedStateHorizontalPadding: Dp = 12.dp,
navController: NavHostController,
hideMiniPlayer: Boolean = false,
- isNavBarHidden: Boolean = false
+ isNavBarHidden: Boolean = false,
+ isNavRailHidden: Boolean = false
) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
@@ -620,6 +622,7 @@ fun UnifiedPlayerSheetV2(
Surface(
modifier = Modifier
.fillMaxWidth()
+ .widthIn(max = 450.dp)
.layout { measurable, constraints ->
val translationY = visualSheetTranslationYProvider().roundToInt()
val overshoot = if (currentSheetContentState == PlayerSheetState.EXPANDED && !isDragging) {
@@ -772,7 +775,8 @@ fun UnifiedPlayerSheetV2(
onQueueDragStart = sheetActionHandlers.beginQueueDrag,
onQueueDrag = sheetActionHandlers.dragQueueBy,
onQueueRelease = sheetActionHandlers.endQueueDrag,
- onShowCastClicked = castSheetState.openCastSheet
+ onShowCastClicked = castSheetState.openCastSheet,
+ isNavRailHidden = isNavRailHidden,
)
}
}
@@ -815,6 +819,7 @@ fun UnifiedPlayerSheetV2(
)
queuePredictiveBackSwipeEdge = null
}
+
}
} catch (_: kotlin.coroutines.cancellation.CancellationException) {
scope.launch {
@@ -832,6 +837,7 @@ fun UnifiedPlayerSheetV2(
}
}
+
val queuePredictiveBackSwipeEdgeState = rememberUpdatedState(queuePredictiveBackSwipeEdge)
UnifiedPlayerQueueAndSongInfoHost(
diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/scoped/PlayerSheetPredictiveBackHandler.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/scoped/PlayerSheetPredictiveBackHandler.kt
index fd533e570..f763b7174 100644
--- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/scoped/PlayerSheetPredictiveBackHandler.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/scoped/PlayerSheetPredictiveBackHandler.kt
@@ -1,4 +1,4 @@
-package com.theveloper.pixelplay.presentation.components.scoped
+package com.theveloper.pixelplay.presentation.components.scoped
import android.os.Build
import androidx.activity.compose.BackHandler
@@ -34,9 +34,12 @@ internal fun PlayerSheetPredictiveBackHandler(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
PredictiveBackHandler(enabled = enabled) { progressFlow ->
try {
+ val startingExpansionFraction = playerViewModel.playerContentExpansionFraction.value
progressFlow.collect { backEvent ->
onSwipeEdgeChanged(backEvent.swipeEdge)
playerViewModel.updatePredictiveBackCollapseFraction(backEvent.progress)
+ val contractedFraction = ((1f - backEvent.progress) * startingExpansionFraction).coerceIn(0f, 1f)
+ playerViewModel.playerContentExpansionFraction.snapTo(contractedFraction)
}
scope.launch {
val progressAtRelease = playerViewModel.predictiveBackCollapseFraction.value
@@ -48,6 +51,7 @@ internal fun PlayerSheetPredictiveBackHandler(
)
playerViewModel.updatePredictiveBackCollapseFraction(1f)
playerViewModel.collapsePlayerSheet()
+ playerViewModel.playerContentExpansionFraction.snapTo(0f)
playerViewModel.updatePredictiveBackCollapseFraction(0f)
onSwipeEdgeChanged(null)
}
@@ -62,8 +66,13 @@ internal fun PlayerSheetPredictiveBackHandler(
if (playerViewModel.sheetState.value == PlayerSheetState.EXPANDED) {
playerViewModel.expandPlayerSheet()
+ playerViewModel.playerContentExpansionFraction.animateTo(
+ targetValue = 1f,
+ animationSpec = tween(animationDurationMs)
+ )
} else {
playerViewModel.collapsePlayerSheet()
+ playerViewModel.playerContentExpansionFraction.snapTo(0f)
}
onSwipeEdgeChanged(null)
diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/subcomps/SineWaveLine.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/subcomps/SineWaveLine.kt
index ea703dad3..55e24441c 100644
--- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/subcomps/SineWaveLine.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/subcomps/SineWaveLine.kt
@@ -22,7 +22,7 @@ import kotlin.math.sin
* @param alpha Opacidad (0f..1f).
* @param strokeWidth Grosor de la línea (Dp).
* @param amplitude Amplitud de la onda (Dp) — la altura máxima desde el centro.
- * @param waves Número de ondas completas a lo largo del ancho (ej: 1f = una onda).
+ * @param wavesDensity Density of wave (float) - as the number in standard screen width 380dp
* @param phase Desplazamiento de fase estático (radianes). Se usa solo si animate = false.
* @param animate Si es true, activa una animación de desplazamiento infinita.
* @param animationDurationMillis Duración en milisegundos de un ciclo completo de animación.
@@ -36,7 +36,7 @@ fun SineWaveLine(
alpha: Float = 1f,
strokeWidth: Dp = 2.dp,
amplitude: Dp = 8.dp,
- waves: Float = 2f,
+ wavesDensity: Float = 7.6f,
phase: Float = 0f,
animate: Boolean? = false,
animationDurationMillis: Int = 2000,
@@ -80,8 +80,8 @@ fun SineWaveLine(
moveTo(0f, centerY + (ampPx * sin(currentPhase)))
for (i in 1 until samples) {
val x = i * step
- // theta recorre 0..(2π * waves)
- val theta = (x / w) * (2f * PI.toFloat() * waves) + currentPhase
+ // theta recorre 0..(2π * wavesDensity)
+ val theta = (x / w) * (2f * PI.toFloat() * (wavesDensity) * size.width / 380.dp.toPx()) + currentPhase
val y = centerY + ampPx * sin(theta)
lineTo(x, y)
}
diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/AboutScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/AboutScreen.kt
index fdb1b7b19..459e68aaf 100644
--- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/AboutScreen.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/AboutScreen.kt
@@ -83,6 +83,9 @@ import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.clearAndSetSemantics
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.role
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
@@ -130,7 +133,6 @@ private val CoreMaintainer = Contributor(
avatarUrl = "https://avatars.githubusercontent.com/u/26845343?v=4",
iconRes = R.drawable.round_developer_board_24,
githubUrl = "https://github.com/theovilardo",
- telegramUrl = "https://t.me/thevelopersupport",
)
private val PinnedCommunityMembers = listOf(
@@ -497,6 +499,7 @@ private fun AboutHeroCard(
) {
val heroShape = AbsoluteSmoothCornerShape(30.dp, 60)
val haptic = LocalHapticFeedback.current
+ val context = LocalContext.current
Surface(
modifier = modifier,
@@ -578,6 +581,82 @@ private fun AboutHeroCard(
Spacer(modifier = Modifier.height(12.dp))
CommunitySignalsRow()
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ SocialChip(
+ label = stringResource(R.string.about_github_label),
+ subtitle = stringResource(R.string.about_github_subtitle),
+ iconRes = R.drawable.github,
+ contentDescription = stringResource(R.string.about_cd_open_github_repo),
+ onClick = { openUrl(context, "https://github.com/theovilardo/PixelPlayer") },
+ modifier = Modifier.weight(1f),
+ )
+ SocialChip(
+ label = stringResource(R.string.about_telegram_label),
+ subtitle = stringResource(R.string.about_telegram_subtitle),
+ iconRes = R.drawable.telegram,
+ contentDescription = stringResource(R.string.about_cd_join_telegram),
+ onClick = { openUrl(context, "https://t.me/thevelopersupport") },
+ modifier = Modifier.weight(1f),
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun SocialChip(
+ label: String,
+ subtitle: String,
+ @DrawableRes iconRes: Int,
+ contentDescription: String,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Surface(
+ onClick = onClick,
+ modifier = modifier
+ .height(52.dp)
+ .clearAndSetSemantics {
+ this.contentDescription = contentDescription
+ this.role = Role.Button
+ },
+ shape = AbsoluteSmoothCornerShape(14.dp, 60),
+ color = MaterialTheme.colorScheme.surfaceContainerHigh.copy(alpha = 0.92f),
+ tonalElevation = 1.dp,
+ ) {
+ Row(
+ modifier = Modifier.padding(horizontal = 12.dp),
+ horizontalArrangement = Arrangement.Start,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Icon(
+ painter = painterResource(iconRes),
+ contentDescription = null,
+ modifier = Modifier.size(20.dp),
+ tint = MaterialTheme.colorScheme.primary,
+ )
+ Spacer(modifier = Modifier.width(10.dp))
+ Column(
+ verticalArrangement = Arrangement.Center
+ ) {
+ Text(
+ text = label,
+ style = MaterialTheme.typography.labelLarge,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.onSurface,
+ )
+ Text(
+ text = subtitle,
+ style = MaterialTheme.typography.labelSmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
}
}
}
diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/AlbumDetailScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/AlbumDetailScreen.kt
index a8f3f31f1..ce578abd5 100644
--- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/AlbumDetailScreen.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/AlbumDetailScreen.kt
@@ -479,9 +479,6 @@ fun AlbumDetailScreen(
coverArtUpdate
)
},
- generateAiMetadata = { fields ->
- playerViewModel.generateAiMetadata(currentSong, fields)
- },
removeFromListTrigger = removeFromListTrigger
)
if (showPlaylistBottomSheet) {
diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/ArtistDetailScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/ArtistDetailScreen.kt
index 6f9199237..f8f85fcfe 100644
--- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/ArtistDetailScreen.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/ArtistDetailScreen.kt
@@ -535,9 +535,6 @@ fun ArtistDetailScreen(
coverArtUpdate
)
},
- generateAiMetadata = { fields ->
- playerViewModel.generateAiMetadata(currentSong, fields)
- },
removeFromListTrigger = removeFromListTrigger
)
if (showPlaylistBottomSheet) {
diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/DailyMixScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/DailyMixScreen.kt
index 24785f39d..eabba3c12 100644
--- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/DailyMixScreen.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/DailyMixScreen.kt
@@ -125,8 +125,6 @@ fun DailyMixScreen(
val aiStatus by playerViewModel.aiStatus.collectAsStateWithLifecycle()
val aiError by playerViewModel.aiError.collectAsStateWithLifecycle()
val aiSuccess by playerViewModel.aiSuccess.collectAsStateWithLifecycle()
- val isGeneratingAiMetadata by playerViewModel.isGeneratingAiMetadata.collectAsStateWithLifecycle()
- val aiMetadataSuccess by playerViewModel.aiMetadataSuccess.collectAsStateWithLifecycle()
val lazyListState = rememberLazyListState()
var showSongInfoSheet by remember { mutableStateOf(false) }
@@ -233,14 +231,7 @@ fun DailyMixScreen(
coverArtUpdate
)
},
- generateAiMetadata = { fields ->
- playerViewModel.generateAiMetadata(song, fields)
- },
- removeFromListTrigger = removeFromListTrigger,
- isGeneratingMetadata = isGeneratingAiMetadata,
- aiMetadataSuccess = aiMetadataSuccess,
- aiError = aiError,
- onRetryMetadata = { playerViewModel.retryLastMetadataGeneration() }
+ removeFromListTrigger = removeFromListTrigger
)
if (showPlaylistBottomSheet) {
diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/GenreDetailScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/GenreDetailScreen.kt
index f401829b6..2fe1dd6ad 100644
--- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/GenreDetailScreen.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/GenreDetailScreen.kt
@@ -623,9 +623,6 @@ fun GenreDetailScreen(
coverArtUpdate
)
},
- generateAiMetadata = { fields ->
- playerViewModel.generateAiMetadata(song, fields)
- },
removeFromListTrigger = {}
)
}
diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/HomeScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/HomeScreen.kt
index ba622628f..76610dff8 100644
--- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/HomeScreen.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/HomeScreen.kt
@@ -9,6 +9,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
@@ -49,6 +50,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -111,6 +113,7 @@ import com.theveloper.pixelplay.presentation.viewmodel.PlayerViewModel
import com.theveloper.pixelplay.presentation.viewmodel.SettingsViewModel
import com.theveloper.pixelplay.presentation.viewmodel.StatsViewModel
import com.theveloper.pixelplay.ui.theme.ExpTitleTypography
+import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.delay
@@ -121,6 +124,12 @@ import racra.compose.smooth_corner_rect_library.AbsoluteSmoothCornerShape
import androidx.compose.ui.res.stringResource
private const val HomeLoadingPlaceholderMinDurationMillis = 1200L
+private val HomeTabletBreakpoint = 600.dp
+
+private data class HomeTabletModule(
+ val key: String,
+ val content: @Composable () -> Unit
+)
// Modern HomeScreen with collapsible top bar and staggered grid layout
@androidx.annotation.OptIn(UnstableApi::class)
@@ -305,6 +314,111 @@ fun HomeScreen(
val shouldShowCleanInstallDisclaimer =
settingsUiState.beta05CleanInstallDisclaimerDismissed == false &&
!cleanInstallDisclaimerDismissedThisSession
+ val yourMixModule: @Composable () -> Unit = {
+ HomeYourMixModule(
+ yourMixSongs = yourMixSongs,
+ song = yourMixSong,
+ isShuffleEnabled = isShuffleEnabled,
+ shouldShowYourMixLoadingPlaceholder = shouldShowYourMixLoadingPlaceholder,
+ onRefresh = {
+ homePlaceholderRefreshGeneration++
+ settingsViewModel.refreshLibrary()
+ playerViewModel.forceUpdateDailyMix()
+ },
+ onPlayShuffled = {
+ if (usesFallbackHomeMix) {
+ playerViewModel.shuffleAllSongs(queueName = "Your Mix")
+ } else {
+ playerViewModel.playSongsShuffled(
+ songsToPlay = yourMixSongs,
+ queueName = "Your Mix",
+ startAtZero = true,
+ )
+ }
+ }
+ )
+ }
+ val albumArtCollageModule: @Composable () -> Unit = {
+ HomeAlbumArtCollageModule(
+ songs = yourMixSongs,
+ basePattern = settingsUiState.collagePattern,
+ isAutoRotate = settingsUiState.collageAutoRotate,
+ onSongClick = { song ->
+ if (usesFallbackHomeMix) {
+ playerViewModel.showAndPlaySongFromLibrary(song, queueName = "Your Mix")
+ } else {
+ playerViewModel.showAndPlaySong(song, yourMixSongs, "Your Mix")
+ }
+ }
+ )
+ }
+ val dailyMixModule: @Composable () -> Unit = {
+ DailyMixSection(
+ songs = dailyMixSongs,
+ onClickOpen = {
+ navController.navigateSafely(Screen.DailyMixScreen.route)
+ },
+ onNavigateToAlbum = { song ->
+ navController.navigateSafelyReplacing(
+ route = Screen.AlbumDetail.createRoute(song.albumId),
+ patternToPop = Screen.AlbumDetail.route
+ )
+ },
+ onNavigateToArtist = { song ->
+ navController.navigateSafelyReplacing(
+ route = Screen.ArtistDetail.createRoute(song.artistId),
+ patternToPop = Screen.ArtistDetail.route
+ )
+ },
+ onNavigateToGenre = { song ->
+ song.genre?.let {
+ navController.navigateSafely(Screen.GenreDetail.createRoute(java.net.URLEncoder.encode(it, "UTF-8")))
+ }
+ },
+ playerViewModel = playerViewModel
+ )
+ }
+ val recentlyPlayedModule: @Composable () -> Unit = {
+ RecentlyPlayedSection(
+ songs = recentlyPlayedSongs,
+ onSongClick = { song ->
+ if (recentlyPlayedQueue.isNotEmpty()) {
+ playerViewModel.playSongs(
+ songsToPlay = recentlyPlayedQueue,
+ startSong = song,
+ queueName = "Recently Played"
+ )
+ }
+ },
+ onOpenAllClick = {
+ navController.navigateSafely(Screen.RecentlyPlayed.route)
+ },
+ themeStateHolder = playerViewModel.themeStateHolder,
+ currentSongId = currentSong?.id,
+ contentPadding = PaddingValues(start = 8.dp, end = 24.dp)
+ )
+ }
+ val statsModule: @Composable () -> Unit = {
+ StatsOverviewCard(
+ summary = homeStatsOverview,
+ onClick = { navController.navigateSafely(Screen.Stats.route) }
+ )
+ }
+ val tabletModules = buildList {
+ add(HomeTabletModule(key = "tablet_your_mix", content = yourMixModule))
+ if (yourMixSongs.isNotEmpty()) {
+ add(HomeTabletModule(key = "tablet_album_art_collage", content = albumArtCollageModule))
+ }
+ if (dailyMixSongs.isNotEmpty()) {
+ add(HomeTabletModule(key = "tablet_daily_mix", content = dailyMixModule))
+ }
+ if (recentlyPlayedSongs.size >= RecentlyPlayedSectionMinSongsToShow) {
+ add(HomeTabletModule(key = "tablet_recently_played", content = recentlyPlayedModule))
+ }
+ if (homeStatsOverview != null) {
+ add(HomeTabletModule(key = "tablet_listening_stats", content = statsModule))
+ }
+ }
Box(
modifier = Modifier.fillMaxSize()
@@ -332,163 +446,76 @@ fun HomeScreen(
)
}
) { innerPadding ->
- LazyColumn(
- state = listState,
+ BoxWithConstraints(
modifier = Modifier
.fillMaxSize()
- .background(MaterialTheme.colorScheme.background),
- contentPadding = PaddingValues(
- top = innerPadding.calculateTopPadding(),
- bottom = paddingValuesParent.calculateBottomPadding()
- + 38.dp + bottomPadding
- ),
- verticalArrangement = Arrangement.spacedBy(24.dp)
+ .background(MaterialTheme.colorScheme.background)
) {
- if (yourMixSongs.isEmpty()) {
- item(
- key = "your_mix_placeholder",
- contentType = "your_mix_placeholder"
- ) {
- if (shouldShowYourMixLoadingPlaceholder) {
- YourMixLoadingPlaceholder()
- } else {
- YourMixEmptyPlaceholder(
- onRefresh = {
- homePlaceholderRefreshGeneration++
- settingsViewModel.refreshLibrary()
- playerViewModel.forceUpdateDailyMix()
- }
- )
+ val isTabletLayout = maxWidth >= HomeTabletBreakpoint
+
+ LazyColumn(
+ state = listState,
+ modifier = Modifier.fillMaxSize(),
+ contentPadding = PaddingValues(
+ top = innerPadding.calculateTopPadding(),
+ bottom = paddingValuesParent.calculateBottomPadding()
+ + 38.dp + bottomPadding
+ ),
+ verticalArrangement = Arrangement.spacedBy(if (isTabletLayout) 20.dp else 24.dp)
+ ) {
+ if (isTabletLayout) {
+ item(
+ key = tabletModules.joinToString(
+ separator = "_",
+ prefix = "tablet_columns_"
+ ) { it.key },
+ contentType = "tablet_module_columns"
+ ) {
+ HomeTabletModuleColumns(modules = tabletModules)
+ }
+ } else {
+ item(
+ key = if (yourMixSongs.isEmpty()) "your_mix_placeholder" else "your_mix_header",
+ contentType = if (yourMixSongs.isEmpty()) "your_mix_placeholder" else "your_mix_header"
+ ) {
+ yourMixModule()
}
- }
- } else {
- item(
- key = "your_mix_header",
- contentType = "your_mix_header"
- ) {
- YourMixHeader(
- song = yourMixSong,
- isShuffleEnabled = isShuffleEnabled,
- onPlayShuffled = {
- if (usesFallbackHomeMix) {
- playerViewModel.shuffleAllSongs(queueName = "Your Mix")
- } else {
- playerViewModel.playSongsShuffled(
- songsToPlay = yourMixSongs,
- queueName = "Your Mix",
- startAtZero = true,
- )
- }
- }
- )
- }
- }
- // Collage
- if (yourMixSongs.isNotEmpty()) {
- item(
- key = "album_art_collage",
- contentType = "album_art_collage"
- ) {
- val basePattern = settingsUiState.collagePattern
- val isAutoRotate = settingsUiState.collageAutoRotate
- val patterns = remember { CollagePattern.entries }
-
- val activePattern = if (isAutoRotate) {
- var rotationIndex by rememberSaveable { mutableIntStateOf(-1) }
- LaunchedEffect(Unit) { rotationIndex++ }
- remember(rotationIndex) {
- patterns[rotationIndex.coerceAtLeast(0) % patterns.size]
+ if (yourMixSongs.isNotEmpty()) {
+ item(
+ key = "album_art_collage",
+ contentType = "album_art_collage"
+ ) {
+ albumArtCollageModule()
}
- } else {
- basePattern
}
- AlbumArtCollage(
- modifier = Modifier.fillMaxWidth(),
- songs = yourMixSongs,
- padding = 14.dp,
- height = 400.dp,
- pattern = activePattern,
- onSongClick = { song ->
- if (usesFallbackHomeMix) {
- playerViewModel.showAndPlaySongFromLibrary(song, queueName = "Your Mix")
- } else {
- playerViewModel.showAndPlaySong(song, yourMixSongs, "Your Mix")
- }
+ if (dailyMixSongs.isNotEmpty()) {
+ item(
+ key = "daily_mix_section",
+ contentType = "daily_mix_section"
+ ) {
+ dailyMixModule()
}
- )
- }
- }
-
- // Daily Mix
- if (dailyMixSongs.isNotEmpty()) {
- item(
- key = "daily_mix_section",
- contentType = "daily_mix_section"
- ) {
- DailyMixSection(
- songs = dailyMixSongs,
- onClickOpen = {
- navController.navigateSafely(Screen.DailyMixScreen.route)
- },
- onNavigateToAlbum = { song ->
- navController.navigateSafelyReplacing(
- route = Screen.AlbumDetail.createRoute(song.albumId),
- patternToPop = Screen.AlbumDetail.route
- )
- },
- onNavigateToArtist = { song ->
- navController.navigateSafelyReplacing(
- route = Screen.ArtistDetail.createRoute(song.artistId),
- patternToPop = Screen.ArtistDetail.route
- )
- },
- onNavigateToGenre = { song ->
- song.genre?.let {
- navController.navigateSafely(Screen.GenreDetail.createRoute(java.net.URLEncoder.encode(it, "UTF-8")))
- }
- },
- playerViewModel = playerViewModel
- )
- }
- }
+ }
- if (recentlyPlayedSongs.size >= RecentlyPlayedSectionMinSongsToShow) {
- item(
- key = "recently_played_section",
- contentType = "recently_played_section"
- ) {
- RecentlyPlayedSection(
- songs = recentlyPlayedSongs,
- onSongClick = { song ->
- if (recentlyPlayedQueue.isNotEmpty()) {
- playerViewModel.playSongs(
- songsToPlay = recentlyPlayedQueue,
- startSong = song,
- queueName = "Recently Played"
- )
- }
- },
- onOpenAllClick = {
- navController.navigateSafely(Screen.RecentlyPlayed.route)
- },
- themeStateHolder = playerViewModel.themeStateHolder,
- currentSongId = currentSong?.id,
- contentPadding = PaddingValues(start = 8.dp, end = 24.dp)
- )
- }
- }
+ if (recentlyPlayedSongs.size >= RecentlyPlayedSectionMinSongsToShow) {
+ item(
+ key = "recently_played_section",
+ contentType = "recently_played_section"
+ ) {
+ recentlyPlayedModule()
+ }
+ }
- if (homeStatsOverview != null) {
- item(
- key = "listening_stats_preview",
- contentType = "listening_stats_preview"
- ) {
- StatsOverviewCard(
- summary = homeStatsOverview,
- onClick = { navController.navigateSafely(Screen.Stats.route) }
- )
+ if (homeStatsOverview != null) {
+ item(
+ key = "listening_stats_preview",
+ contentType = "listening_stats_preview"
+ ) {
+ statsModule()
+ }
+ }
}
}
}
@@ -585,6 +612,100 @@ fun HomeScreen(
}
}
+@Composable
+private fun HomeTabletModuleColumns(
+ modules: List
+) {
+ val leftColumnModules = modules.filterIndexed { index, _ -> index % 2 == 0 }
+ val rightColumnModules = modules.filterIndexed { index, _ -> index % 2 == 1 }
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 8.dp),
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ verticalAlignment = Alignment.Top
+ ) {
+ HomeTabletModuleColumn(
+ modules = leftColumnModules,
+ modifier = Modifier.weight(1f)
+ )
+ HomeTabletModuleColumn(
+ modules = rightColumnModules,
+ modifier = Modifier.weight(1f)
+ )
+ }
+}
+
+@Composable
+private fun HomeTabletModuleColumn(
+ modules: List,
+ modifier: Modifier = Modifier
+) {
+ Column(
+ modifier = modifier,
+ verticalArrangement = Arrangement.spacedBy(20.dp)
+ ) {
+ modules.forEach { module ->
+ key(module.key) {
+ module.content()
+ }
+ }
+ }
+}
+
+@Composable
+private fun HomeYourMixModule(
+ yourMixSongs: ImmutableList,
+ song: String,
+ isShuffleEnabled: Boolean,
+ shouldShowYourMixLoadingPlaceholder: Boolean,
+ onRefresh: () -> Unit,
+ onPlayShuffled: () -> Unit
+) {
+ if (yourMixSongs.isEmpty()) {
+ if (shouldShowYourMixLoadingPlaceholder) {
+ YourMixLoadingPlaceholder()
+ } else {
+ YourMixEmptyPlaceholder(onRefresh = onRefresh)
+ }
+ } else {
+ YourMixHeader(
+ song = song,
+ isShuffleEnabled = isShuffleEnabled,
+ onPlayShuffled = onPlayShuffled
+ )
+ }
+}
+
+@Composable
+private fun HomeAlbumArtCollageModule(
+ songs: ImmutableList,
+ basePattern: CollagePattern,
+ isAutoRotate: Boolean,
+ onSongClick: (Song) -> Unit
+) {
+ val patterns = remember { CollagePattern.entries }
+ val activePattern = if (isAutoRotate) {
+ var rotationIndex by rememberSaveable { mutableIntStateOf(-1) }
+ LaunchedEffect(Unit) { rotationIndex++ }
+ remember(rotationIndex) {
+ patterns[rotationIndex.coerceAtLeast(0) % patterns.size]
+ }
+ } else {
+ basePattern
+ }
+
+ AlbumArtCollage(
+ modifier = Modifier.fillMaxWidth(),
+ songs = songs,
+ padding = 14.dp,
+ height = 400.dp,
+ pattern = activePattern,
+ onSongClick = onSongClick
+ )
+}
+
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
private fun YourMixLoadingPlaceholder() {
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 2260b4880..280085c10 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
@@ -400,7 +400,6 @@ private data class LibraryScreenPlayerProjection(
val isSdCardAvailable: Boolean = false,
val musicFolders: ImmutableList = persistentListOf(),
val isLoadingLibraryCategories: Boolean = true,
- val isGeneratingAiMetadata: Boolean = false,
val isSyncingLibrary: Boolean = false,
val isLoadingInitialSongs: Boolean = true,
val hideLocalMedia: Boolean = false
@@ -422,7 +421,6 @@ private fun PlayerUiState.toLibraryScreenProjection(): LibraryScreenPlayerProjec
isSdCardAvailable = isSdCardAvailable,
musicFolders = musicFolders,
isLoadingLibraryCategories = isLoadingLibraryCategories,
- isGeneratingAiMetadata = isGeneratingAiMetadata,
isSyncingLibrary = isSyncingLibrary,
isLoadingInitialSongs = isLoadingInitialSongs,
hideLocalMedia = hideLocalMedia
@@ -1704,24 +1702,7 @@ fun LibraryScreen(
}
}
}
- if (playerUiState.isGeneratingAiMetadata) {
- Surface( // Fondo semitransparente para el indicador
- modifier = Modifier.fillMaxSize(),
- color = MaterialTheme.colorScheme.scrim.copy(alpha = 0.5f)
- ) {
- Box(contentAlignment = Alignment.Center) {
- Column(horizontalAlignment = Alignment.CenterHorizontally) {
- LoadingIndicator(modifier = Modifier.size(64.dp))
- Spacer(modifier = Modifier.height(16.dp))
- Text(
- text = stringResource(R.string.library_generating_ai_metadata),
- style = MaterialTheme.typography.titleMedium,
- color = MaterialTheme.colorScheme.onSurface
- )
- }
- }
- }
- } else if (
+ if (
isLibraryContentEmpty &&
(
playerUiState.isSyncingLibrary ||
@@ -1791,7 +1772,7 @@ fun LibraryScreen(
playerViewModel.clearAiPlaylistError()
showCreateAiPlaylistDialog = true
} else {
- Toast.makeText(context, context.getString(R.string.library_toast_set_gemini_api_key_first), Toast.LENGTH_SHORT).show()
+ Toast.makeText(context, context.getString(R.string.library_toast_set_ai_provider_api_key_first), Toast.LENGTH_SHORT).show()
}
},
onCreate = { name, imageUri, color, icon, songIds, cropScale, cropPanX, cropPanY, shapeType, d1, d2, d3, d4, smartRuleKey ->
@@ -1928,9 +1909,6 @@ fun LibraryScreen(
coverArtUpdate
)
},
- generateAiMetadata = { fields ->
- playerViewModel.generateAiMetadata(currentSong, fields)
- },
removeFromListTrigger = {},
songInfoViewModel = songInfoBottomSheetViewModel
)
@@ -2094,10 +2072,13 @@ fun LibraryScreen(
}
if (showReorderTabsSheet) {
+ val hiddenTabs by playerViewModel.hiddenLibraryTabsFlow.collectAsStateWithLifecycle()
ReorderTabsSheet(
- tabs = tabTitles,
- onReorder = { newOrder ->
+ visibleTabs = tabTitles,
+ hiddenTabs = hiddenTabs,
+ onSave = { newOrder, newHidden ->
playerViewModel.saveLibraryTabsOrder(newOrder)
+ playerViewModel.saveLibraryHiddenTabs(newHidden)
},
onReset = {
playerViewModel.resetLibraryTabsOrder()
diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/NavBarCornerRadiusScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/NavBarCornerRadiusScreen.kt
index 199bbfe47..608709172 100644
--- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/NavBarCornerRadiusScreen.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/NavBarCornerRadiusScreen.kt
@@ -342,13 +342,13 @@ fun NavBarCornerRadiusContent(
} else {
AbsoluteSmoothCornerShape(
cornerRadiusTL = 10.dp,
- smoothnessAsPercentBL = 60,
- cornerRadiusTR = 10.dp,
- smoothnessAsPercentBR = 60,
- cornerRadiusBR = sliderValue.dp,
smoothnessAsPercentTL = 60,
+ cornerRadiusTR = 10.dp,
+ smoothnessAsPercentTR = 60,
cornerRadiusBL = sliderValue.dp,
- smoothnessAsPercentTR = 60
+ smoothnessAsPercentBL = 60,
+ cornerRadiusBR = sliderValue.dp,
+ smoothnessAsPercentBR = 60
)
}
) {
diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/PlaylistDetailScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/PlaylistDetailScreen.kt
index 646f002f0..a2b8c0c67 100644
--- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/PlaylistDetailScreen.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/PlaylistDetailScreen.kt
@@ -1037,9 +1037,6 @@ fun PlaylistDetailScreen(
coverArtUpdate
)
},
- generateAiMetadata = { fields ->
- playerViewModel.generateAiMetadata(currentSong, fields)
- },
removeFromListTrigger = {
playlistViewModel.removeSongFromPlaylist(playlistId, currentSong.id)
}
diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/RecentlyPlayedScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/RecentlyPlayedScreen.kt
index a90154869..5ebf94a74 100644
--- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/RecentlyPlayedScreen.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/RecentlyPlayedScreen.kt
@@ -335,9 +335,6 @@ fun RecentlyPlayedScreen(
coverArtUpdate
)
},
- generateAiMetadata = { fields ->
- playerViewModel.generateAiMetadata(song, fields)
- },
removeFromListTrigger = {}
)
diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SearchScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SearchScreen.kt
index bfe566be2..eab38137e 100644
--- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SearchScreen.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SearchScreen.kt
@@ -753,9 +753,6 @@ fun SearchScreen(
coverArtUpdate
)
},
- generateAiMetadata = { fields ->
- playerViewModel.generateAiMetadata(currentSong, fields)
- },
)
}
}
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 d1ff2628b..874091ac0 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
@@ -906,6 +906,9 @@ fun SettingsCategoryScreen(
}
}
SettingsCategory.AI_INTEGRATION -> {
+ val provider = com.theveloper.pixelplay.data.ai.provider.AiProvider.fromString(aiProvider)
+ val currentCustomBaseUrl by settingsViewModel.customBaseUrl.collectAsStateWithLifecycle()
+
// AI Provider Selection
SettingsSubsection(title = stringResource(R.string.settings_ai_provider_section)) {
ThemeSelectorItem(
@@ -939,7 +942,6 @@ fun SettingsCategoryScreen(
// Consolidated API Key Section
SettingsSubsection(title = stringResource(R.string.settings_credentials_section)) {
- val provider = com.theveloper.pixelplay.data.ai.provider.AiProvider.fromString(aiProvider)
val sourceLabel = when(provider) {
com.theveloper.pixelplay.data.ai.provider.AiProvider.GEMINI -> stringResource(R.string.settings_ai_source_gemini)
com.theveloper.pixelplay.data.ai.provider.AiProvider.DEEPSEEK -> stringResource(R.string.settings_ai_source_deepseek)
@@ -950,6 +952,8 @@ fun SettingsCategoryScreen(
com.theveloper.pixelplay.data.ai.provider.AiProvider.GLM -> stringResource(R.string.settings_ai_source_glm)
com.theveloper.pixelplay.data.ai.provider.AiProvider.OPENAI -> stringResource(R.string.settings_ai_source_openai)
com.theveloper.pixelplay.data.ai.provider.AiProvider.OPENROUTER -> "OpenRouter (openrouter.ai)"
+ com.theveloper.pixelplay.data.ai.provider.AiProvider.OLLAMA -> "Ollama (cloud)"
+ com.theveloper.pixelplay.data.ai.provider.AiProvider.CUSTOM -> "Custom Provider"
}
AiApiKeyItem(
@@ -999,18 +1003,30 @@ fun SettingsCategoryScreen(
)
}
} else if (uiState.availableModels.isNotEmpty()) {
- ThemeSelectorItem(
+ SearchableModelSelector(
label = stringResource(R.string.settings_ai_model_title),
description = stringResource(R.string.settings_ai_model_subtitle),
- options = uiState.availableModels.associate { it.name to it.displayName },
- selectedKey = currentAiModel.ifEmpty { uiState.availableModels.firstOrNull()?.name ?: "" },
- onSelectionChanged = { settingsViewModel.onAiModelChange(it) },
+ models = uiState.availableModels,
+ selectedModelName = currentAiModel.ifEmpty { uiState.availableModels.firstOrNull()?.name ?: "" },
+ onModelSelected = { settingsViewModel.onAiModelChange(it) },
leadingIcon = { Icon(Icons.Rounded.Science, null, tint = MaterialTheme.colorScheme.secondary) }
)
}
}
}
+ // Base URL Section (only for configurable URL providers)
+ if (provider.hasConfigurableUrl) {
+ SettingsSubsection(title = "API Base URL") {
+ AiApiKeyItem(
+ apiKey = currentCustomBaseUrl,
+ onApiKeySave = { settingsViewModel.onCustomBaseUrlChange(it) },
+ title = "Base URL",
+ subtitle = "e.g. https://api.example.com/v1"
+ )
+ }
+ }
+
// Prompt Behavior Section
SettingsSubsection(
title = stringResource(R.string.settings_prompt_behavior_section),
@@ -1026,6 +1042,140 @@ fun SettingsCategoryScreen(
)
}
+ // Generation Parameters Section
+ SettingsSubsection(title = "Generation Parameters") {
+ SliderSettingsItem(
+ label = "Temperature",
+ value = settingsViewModel.aiTemperature.collectAsStateWithLifecycle().value,
+ valueRange = 0.0f..2.0f,
+ steps = 20,
+ onValueChange = { settingsViewModel.onAiTemperatureChange(it) },
+ valueText = { String.format(Locale.US, "%.2f", it) }
+ )
+ Text(
+ text = "Controls randomness. Lower = more deterministic, higher = more creative.",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 8.dp)
+ )
+ SliderSettingsItem(
+ label = "Top P",
+ value = settingsViewModel.aiTopP.collectAsStateWithLifecycle().value,
+ valueRange = 0.0f..1.0f,
+ steps = 20,
+ onValueChange = { settingsViewModel.onAiTopPChange(it) },
+ valueText = { String.format(Locale.US, "%.2f", it) }
+ )
+ Text(
+ text = "Nucleus sampling. Higher = more diverse tokens considered.",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 8.dp)
+ )
+ SliderSettingsItem(
+ label = "Top K",
+ value = settingsViewModel.aiTopK.collectAsStateWithLifecycle().value.toFloat(),
+ valueRange = 1f..100f,
+ steps = 99,
+ onValueChange = { settingsViewModel.onAiTopKChange(it.toInt()) },
+ valueText = { it.toInt().toString() }
+ )
+ Text(
+ text = "Limits token selection to the K most likely candidates.",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 8.dp)
+ )
+ SliderSettingsItem(
+ label = "Max Output Tokens",
+ value = settingsViewModel.aiMaxTokens.collectAsStateWithLifecycle().value.toFloat(),
+ valueRange = 128f..8192f,
+ steps = 63,
+ onValueChange = { settingsViewModel.onAiMaxTokensChange(it.toInt()) },
+ valueText = { it.toInt().toString() }
+ )
+ Text(
+ text = "Maximum length of the AI response. Higher = longer but more expensive.",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 8.dp)
+ )
+ SliderSettingsItem(
+ label = "Presence Penalty",
+ value = settingsViewModel.aiPresencePenalty.collectAsStateWithLifecycle().value,
+ valueRange = -2.0f..2.0f,
+ steps = 40,
+ onValueChange = { settingsViewModel.onAiPresencePenaltyChange(it) },
+ valueText = { String.format(Locale.US, "%.1f", it) }
+ )
+ Text(
+ text = "Penalizes repeated topics. Positive = more diverse topics.",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 8.dp)
+ )
+ SliderSettingsItem(
+ label = "Frequency Penalty",
+ value = settingsViewModel.aiFrequencyPenalty.collectAsStateWithLifecycle().value,
+ valueRange = -2.0f..2.0f,
+ steps = 40,
+ onValueChange = { settingsViewModel.onAiFrequencyPenaltyChange(it) },
+ valueText = { String.format(Locale.US, "%.1f", it) }
+ )
+ Text(
+ text = "Penalizes repeated phrases. Positive = more natural language.",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 8.dp)
+ )
+ }
+
+ // Song Data Configuration Section
+ SettingsSubsection(title = "Song Data Configuration") {
+ val aiSampleSize by settingsViewModel.aiSampleSize.collectAsStateWithLifecycle()
+ SliderSettingsItem(
+ label = "Sample Size",
+ value = aiSampleSize.toFloat(),
+ valueRange = 10f..120f,
+ steps = 11,
+ onValueChange = { settingsViewModel.onAiSampleSizeChange(it.toInt()) },
+ valueText = { "${it.toInt()} songs" }
+ )
+ Text(
+ text = "Number of songs sent to the AI for playlist generation. More = better context but higher cost.",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 8.dp)
+ )
+ ThemeSelectorItem(
+ label = "Digest Detail",
+ description = "Controls how much listening history data is included",
+ options = mapOf("safe" to "Concise (faster)", "full" to "Full (better quality)"),
+ selectedKey = settingsViewModel.aiDigestMode.collectAsStateWithLifecycle().value,
+ onSelectionChanged = { settingsViewModel.onAiDigestModeChange(it) },
+ leadingIcon = {
+ Icon(
+ painterResource(R.drawable.rounded_monitoring_24),
+ null,
+ tint = MaterialTheme.colorScheme.secondary
+ )
+ }
+ )
+ SwitchSettingItem(
+ title = "Extended Song Fields",
+ subtitle = "Include album, year, and genre info in song data sent to AI",
+ checked = settingsViewModel.aiIncludeExtendedFields.collectAsStateWithLifecycle().value,
+ onCheckedChange = { settingsViewModel.onAiIncludeExtendedFieldsChange(it) },
+ leadingIcon = {
+ Icon(
+ painterResource(R.drawable.rounded_music_note_24),
+ null,
+ tint = MaterialTheme.colorScheme.secondary
+ )
+ }
+ )
+ }
+
Spacer(modifier = Modifier.height(16.dp))
SettingsSubsection(title = stringResource(R.string.settings_ai_usage_report_section)) {
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 b13cad46c..bc5dbb329 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
@@ -30,18 +30,25 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.DeleteForever
import androidx.compose.material.icons.outlined.Refresh
import androidx.compose.material.icons.outlined.Sync
+import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.rounded.Check
+import androidx.compose.material.icons.rounded.CheckCircle
+import androidx.compose.material.icons.rounded.Clear
import androidx.compose.material.icons.rounded.Close
+import androidx.compose.material.icons.rounded.Search
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Slider
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
@@ -64,6 +71,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.theveloper.pixelplay.R
+import com.theveloper.pixelplay.data.ai.GeminiModel
import com.theveloper.pixelplay.data.worker.SyncProgress
import com.theveloper.pixelplay.presentation.viewmodel.LyricsRefreshProgress
import com.theveloper.pixelplay.ui.theme.GoogleSansRounded
@@ -362,14 +370,187 @@ fun ExpressiveSettingsGroup(
) {
Column(
modifier = modifier
- .clip(RoundedCornerShape(24.dp)) // Large corners for the group
+ .clip(RoundedCornerShape(24.dp))
.background(Color.Transparent),
- //verticalArrangement = Arrangement.spacedBy(4.dp)
) {
content()
}
}
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SearchableModelSelector(
+ label: String,
+ description: String,
+ models: List,
+ selectedModelName: String,
+ onModelSelected: (String) -> Unit,
+ leadingIcon: @Composable () -> Unit
+) {
+ var showSheet by remember { mutableStateOf(false) }
+ var searchQuery by remember { mutableStateOf("") }
+ val selectedDisplayName = models.find { it.name == selectedModelName }?.displayName ?: selectedModelName
+
+ 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 models...") },
+ 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 filteredModels = remember(models, searchQuery) {
+ if (searchQuery.isBlank()) models
+ else models.filter {
+ it.name.contains(searchQuery, ignoreCase = true) ||
+ it.displayName.contains(searchQuery, ignoreCase = true)
+ }
+ }
+
+ Text(
+ text = "${filteredModels.size} model${if (filteredModels.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(filteredModels, key = { it.name }) { model ->
+ val isSelected = model.name == selectedModelName
+ Surface(
+ color = if (isSelected) MaterialTheme.colorScheme.primaryContainer
+ else MaterialTheme.colorScheme.surfaceContainerHigh,
+ shape = RoundedCornerShape(10.dp),
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 4.dp)
+ .clickable {
+ onModelSelected(model.name)
+ showSheet = false
+ searchQuery = ""
+ }
+ ) {
+ Row(
+ modifier = Modifier.padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ text = model.displayName,
+ style = MaterialTheme.typography.bodyLarge,
+ color = if (isSelected) MaterialTheme.colorScheme.onPrimaryContainer
+ else MaterialTheme.colorScheme.onSurface
+ )
+ Text(
+ text = model.name,
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ 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/SetupScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SetupScreen.kt
index 510010631..44f93c9b7 100644
--- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SetupScreen.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SetupScreen.kt
@@ -56,8 +56,10 @@ import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
@@ -643,7 +645,7 @@ private fun isIgnoringBatteryOptimizationsNow(context: Context): Boolean {
@Composable
fun WelcomePage() {
Column(
- horizontalAlignment = Alignment.CenterHorizontally,
+ //horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.fillMaxSize()
@@ -672,7 +674,7 @@ fun WelcomePage() {
),
)
}
- Spacer(modifier = Modifier.height(10.dp))
+ Spacer(modifier = Modifier.height(4.dp))
Surface(
shape = CircleShape,
color = MaterialTheme.colorScheme.surface,
@@ -707,7 +709,7 @@ fun WelcomePage() {
.clip(RoundedCornerShape(20.dp))
){
MaterialYouVectorDrawable(
- modifier = Modifier.fillMaxSize(),
+ modifier = Modifier.requiredWidth(380.dp).align(Alignment.CenterEnd),
drawableResId = R.drawable.welcome_art
)
SineWaveLine(
@@ -722,7 +724,7 @@ fun WelcomePage() {
alpha = 0.95f,
strokeWidth = 16.dp,
amplitude = 4.dp,
- waves = 7.6f,
+ wavesDensity = 7.6f,
phase = 0f
)
Box(
@@ -748,7 +750,7 @@ fun WelcomePage() {
alpha = 0.95f,
strokeWidth = 4.dp,
amplitude = 4.dp,
- waves = 7.6f,
+ wavesDensity = 7.6f,
phase = 0f
)
}
@@ -1005,6 +1007,7 @@ fun ThemeSelectionPage(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
+ .widthIn(max = 540.dp)
.fillMaxSize()
.padding(24.dp)
) {
@@ -1188,6 +1191,7 @@ fun LibraryLayoutPage(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
+ .widthIn(max = 540.dp)
.fillMaxSize()
.padding(24.dp)
) {
@@ -1527,6 +1531,7 @@ fun FinishPage() {
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier
+ .widthIn(max = 540.dp)
.fillMaxSize()
.padding(16.dp)
) {
@@ -1556,6 +1561,7 @@ fun PermissionPageLayout(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
+ .widthIn(max = 540.dp)
.fillMaxSize()
.padding(24.dp)
) {
@@ -1654,7 +1660,7 @@ private fun SetupRestoreDialog(
)
) {
Surface(
- modifier = Modifier.fillMaxSize(),
+ modifier = Modifier.fillMaxSize().widthIn(max = 540.dp),
color = MaterialTheme.colorScheme.surfaceContainerLowest
) {
Scaffold(
@@ -1924,6 +1930,7 @@ fun LibraryNavigationPillSetupShow(
// IntrinsicSize.Min en el Row + fillMaxHeight en los hijos asegura misma altura
Row(
modifier = Modifier
+ .widthIn(max = 540.dp)
.padding(start = 4.dp)
.height(IntrinsicSize.Min),
verticalAlignment = Alignment.CenterVertically,
@@ -2219,6 +2226,7 @@ fun NavBarLayoutPage(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
+ .widthIn(max = 540.dp)
.fillMaxSize()
.padding(24.dp)
) {
@@ -2348,6 +2356,7 @@ fun NavBarPreview(isDefault: Boolean) {
containerColor = MaterialTheme.colorScheme.surfaceBright
),
modifier = Modifier
+ .widthIn(max = 540.dp)
.fillMaxWidth()
.height(200.dp) // Taller to show bottom part clearly
.padding(horizontal = 8.dp)
diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/search/components/GenreCategoriesGrid.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/search/components/GenreCategoriesGrid.kt
index a81e6a4ef..7d9a3ec47 100644
--- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/search/components/GenreCategoriesGrid.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/search/components/GenreCategoriesGrid.kt
@@ -1,8 +1,12 @@
package com.theveloper.pixelplay.presentation.screens.search.components
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
+import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
+import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
@@ -22,9 +26,13 @@ import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ViewList
+import androidx.compose.material.icons.rounded.CheckCircle
import androidx.compose.material.icons.rounded.GridView
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.FilledIconButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -34,13 +42,18 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.media3.common.util.UnstableApi
+import com.theveloper.pixelplay.R
import com.theveloper.pixelplay.data.model.Genre
import com.theveloper.pixelplay.presentation.components.MiniPlayerHeight
import com.theveloper.pixelplay.presentation.components.SmartImage
@@ -49,18 +62,7 @@ import com.theveloper.pixelplay.presentation.components.resolveNavBarOccupiedHei
import com.theveloper.pixelplay.presentation.utils.GenreIconProvider
import com.theveloper.pixelplay.presentation.viewmodel.PlayerViewModel
import com.theveloper.pixelplay.ui.theme.LocalPixelPlayDarkTheme
-import androidx.compose.ui.res.stringResource
-import com.theveloper.pixelplay.R
import racra.compose.smooth_corner_rect_library.AbsoluteSmoothCornerShape
-import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.foundation.gestures.detectTapGestures
-import androidx.compose.ui.draw.scale
-import androidx.compose.foundation.border
-import androidx.compose.material.icons.rounded.CheckCircle
-import androidx.compose.material3.Icon
-import androidx.compose.animation.core.animateDpAsState
-import androidx.compose.animation.core.animateFloatAsState
-import androidx.compose.animation.core.tween
@OptIn(UnstableApi::class)
@Composable
@@ -137,15 +139,15 @@ fun GenreCategoriesGrid(
label = "shapeAnimation"
)
- androidx.compose.material3.FilledIconButton(
+ FilledIconButton(
onClick = { playerViewModel.toggleGenreViewMode() },
- colors = androidx.compose.material3.IconButtonDefaults.filledIconButtonColors(
+ colors = IconButtonDefaults.filledIconButtonColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
),
shape = RoundedCornerShape(animatedCornerRadius.value)
) {
- androidx.compose.material3.Icon(
+ Icon(
imageVector = if (isGridView) Icons.AutoMirrored.Rounded.ViewList else Icons.Rounded.GridView,
contentDescription = "Toggle Grid/List View"
)
@@ -274,7 +276,7 @@ private fun GenreCard(
)
}
- // Imagen del género en esquina inferior derecha
+ // Genre image in bottom-right corner
Box(
modifier = Modifier
.size(90.dp)
@@ -292,7 +294,7 @@ private fun GenreCard(
)
}
- // Nombre del género en esquina superior izquierda
+ // Genre name in top-left corner
Column(
modifier = Modifier
.align(Alignment.TopStart)
diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/telegram/dashboard/TelegramDashboardScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/telegram/dashboard/TelegramDashboardScreen.kt
index e239280b8..3f97e6fda 100644
--- a/app/src/main/java/com/theveloper/pixelplay/presentation/telegram/dashboard/TelegramDashboardScreen.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/presentation/telegram/dashboard/TelegramDashboardScreen.kt
@@ -348,15 +348,20 @@ fun TelegramDashboardScreen(
)
},
confirmButton = {
- TextButton(onClick = {
- viewModel.removeChannel(channel.chatId)
- channelPendingRemoval = null
- }) {
+ FilledTonalButton(
+ onClick = {
+ viewModel.removeChannel(channel.chatId)
+ channelPendingRemoval = null
+ },
+ colors = androidx.compose.material3.ButtonDefaults.filledTonalButtonColors(
+ containerColor = MaterialTheme.colorScheme.errorContainer,
+ contentColor = MaterialTheme.colorScheme.onErrorContainer
+ )
+ ) {
Text(
text = stringResource(R.string.telegram_remove_channel_confirm_action),
fontFamily = GoogleSansRounded,
- fontWeight = FontWeight.SemiBold,
- color = MaterialTheme.colorScheme.error
+ fontWeight = FontWeight.SemiBold
)
}
},
@@ -427,8 +432,7 @@ private fun ExpressiveChannelItem(
Column(
modifier = Modifier
.fillMaxWidth()
- .padding(horizontal = 16.dp, vertical = 14.dp),
- verticalArrangement = Arrangement.spacedBy(14.dp)
+ .padding(horizontal = 16.dp, vertical = 14.dp)
) {
// ── Channel header row ──────────────────────────────────────
Row(
@@ -486,6 +490,8 @@ private fun ExpressiveChannelItem(
}
}
+ Spacer(modifier = Modifier.height(14.dp))
+
// ── Meta pills ──────────────────────────────────────────────
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
@@ -515,6 +521,8 @@ private fun ExpressiveChannelItem(
}
}
+ Spacer(modifier = Modifier.height(14.dp))
+
// ── Action buttons ──────────────────────────────────────────
Row(
modifier = Modifier.fillMaxWidth(),
@@ -593,6 +601,7 @@ private fun ExpressiveChannelItem(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(0.dp)
) {
+ Spacer(modifier = Modifier.height(14.dp))
HorizontalDivider(
modifier = Modifier.padding(vertical = 4.dp),
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)
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 ac467232d..ad68cea83 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
@@ -4,10 +4,8 @@ package com.theveloper.pixelplay.presentation.viewmodel
import android.content.Context
import com.theveloper.pixelplay.R
import com.theveloper.pixelplay.data.DailyMixManager
-import com.theveloper.pixelplay.data.ai.AiMetadataGenerator
import com.theveloper.pixelplay.data.ai.AiNotificationManager
import com.theveloper.pixelplay.data.ai.AiPlaylistGenerator
-import com.theveloper.pixelplay.data.ai.SongMetadata
import com.theveloper.pixelplay.data.ai.AiSystemPromptType
import com.theveloper.pixelplay.data.ai.provider.AiProviderException
import com.theveloper.pixelplay.data.preferences.PlaylistPreferencesRepository
@@ -30,12 +28,11 @@ import javax.inject.Singleton
class AiStateHolder @Inject constructor(
@ApplicationContext private val context: Context,
private val aiPlaylistGenerator: AiPlaylistGenerator,
- private val aiMetadataGenerator: AiMetadataGenerator,
private val dailyMixManager: DailyMixManager,
private val playlistPreferencesRepository: PlaylistPreferencesRepository,
private val dailyMixStateHolder: DailyMixStateHolder,
private val notificationManager: AiNotificationManager,
- private val aiOrchestrator: com.theveloper.pixelplay.data.ai.AiOrchestrator
+ private val aiHandler: com.theveloper.pixelplay.data.ai.AiHandler
) {
// State
// AI State Management: Observables for tracking background generation progress
@@ -45,12 +42,6 @@ class AiStateHolder @Inject constructor(
private val _isGeneratingAiPlaylist = MutableStateFlow(false)
val isGeneratingAiPlaylist = _isGeneratingAiPlaylist.asStateFlow()
- private val _isGeneratingMetadata = MutableStateFlow(false)
- val isGeneratingMetadata = _isGeneratingMetadata.asStateFlow()
-
- private val _aiMetadataSuccess = MutableStateFlow(false)
- val aiMetadataSuccess = _aiMetadataSuccess.asStateFlow()
-
private val _aiSuccess = MutableStateFlow(false)
val aiSuccess = _aiSuccess.asStateFlow()
@@ -64,10 +55,6 @@ class AiStateHolder @Inject constructor(
private var _lastMinLength: Int = 5
private var _lastMaxLength: Int = 15
- // Metadata Retry Cache: Stores parameters for the last metadata generation
- private var _lastMetadataSong: Song? = null
- private var _lastMetadataFields: List? = null
-
private var scope: CoroutineScope? = null
private var allSongsProvider: (suspend () -> List)? = null
private var favoriteSongIdsProvider: (() -> Set)? = null
@@ -111,7 +98,6 @@ class AiStateHolder @Inject constructor(
_showAiPlaylistSheet.value = false
_aiError.value = null
_aiSuccess.value = false
- _aiMetadataSuccess.value = false
_isGeneratingAiPlaylist.value = false
_aiStatus.value = null
}
@@ -122,16 +108,6 @@ class AiStateHolder @Inject constructor(
generateAiPlaylist(prompt, _lastMinLength, _lastMaxLength)
}
- fun retryLastMetadataGeneration() {
- // Safe retry for metadata using cached song and requested fields
- val song = _lastMetadataSong ?: return
- val fields = _lastMetadataFields ?: return
-
- scope?.launch {
- generateAiMetadata(song, fields)
- }
- }
-
fun clearAiPlaylistError() {
_aiError.value = null
}
@@ -308,62 +284,31 @@ class AiStateHolder @Inject constructor(
}
}
- /**
- * Fetches AI-generated metadata (tags, genre, lyrics) for a specific song.
- * Updates internal success and error states for UI feedback.
- */
- suspend fun generateAiMetadata(song: Song, fields: List): Result {
- _lastMetadataSong = song
- _lastMetadataFields = fields
-
- _isGeneratingMetadata.value = true
- _aiMetadataSuccess.value = false
- _aiError.value = null
-
- return try {
- val result = aiMetadataGenerator.generate(song, fields)
- if (result.isSuccess) {
- _aiMetadataSuccess.value = true
- notificationManager.showCompletion("Metadata Enhanced", "Applied tags and genre refinements.")
- } else {
- result.exceptionOrNull()?.let {
- _aiError.value = resolveAiErrorMessage(it)
- notificationManager.showCompletion("Metadata Error", "Check your AI configuration.")
- }
- }
- result
- } catch (e: Exception) {
- _aiError.value = resolveAiErrorMessage(e)
- Result.failure(e)
- } finally {
- _isGeneratingMetadata.value = false
- }
- }
-
suspend fun translateLyrics(lyricsText: String): Result {
return try {
val targetLanguage = context.resources.configuration.locales[0].displayLanguage
val prompt = """
-Translate the provided song lyrics into $targetLanguage.
-
-Keep every timestamp exactly unchanged.
-
-If the lyrics are ALREADY mostly in $targetLanguage, output ONLY the exact phrase "ALREADY_IN_TARGET_LANGUAGE" without any other text.
-
-For each original line, output the original line first, then on the next line output the $targetLanguage translation with the same timestamp.
-
-Do not add any extra text, explanations, numbering, labels, or formatting.
-Do not remove, merge, split, or reorder lines.
-
-Output only:
-[timestamp] original text
-[timestamp] translated text
-
-Lyrics to translate:
+Translate song lyrics into $targetLanguage.
+
+
+- Preserve ALL timestamps [mm:ss.xx] exactly — never modify, merge, or drop them.
+- Output TWO lines per original line: the original, then the translation with the same timestamp.
+- NEVER add explanations, labels, numbering, section headers, or formatting.
+- NEVER remove, merge, split, or reorder lines.
+- If lyrics are ALREADY mostly in $targetLanguage, output ONLY: ALREADY_IN_TARGET_LANGUAGE
+
+
+
+[original timestamp] original text
+[same timestamp] translated text
+
+
+
$lyricsText
+
""".trimIndent()
- val response = aiOrchestrator.generateContent(
+ val response = aiHandler.generateContent(
prompt = prompt,
type = AiSystemPromptType.GENERAL,
temperature = 0.1f
diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/DeviceCapabilitiesViewModel.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/DeviceCapabilitiesViewModel.kt
index 69cb6573e..1ba0cff07 100644
--- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/DeviceCapabilitiesViewModel.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/DeviceCapabilitiesViewModel.kt
@@ -3,6 +3,7 @@ package com.theveloper.pixelplay.presentation.viewmodel
import android.app.ActivityManager
import android.content.Context
import android.content.pm.PackageManager
+import timber.log.Timber
import android.media.AudioDeviceInfo
import android.media.AudioFormat
import android.media.AudioManager
@@ -313,7 +314,8 @@ class DeviceCapabilitiesViewModel @Inject constructor(
val instances = try {
codecInfo.getCapabilitiesForType(codecInfo.supportedTypes.first { it.startsWith("audio/") })
.maxSupportedInstances
- } catch (_: Exception) {
+ } catch (e: Exception) {
+ Timber.w(e, "Failed to get codec capabilities for %s", codecInfo.name)
-1
}
diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/FileExplorerStateHolder.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/FileExplorerStateHolder.kt
index b43ff8724..e55ab940e 100644
--- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/FileExplorerStateHolder.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/FileExplorerStateHolder.kt
@@ -3,6 +3,7 @@ package com.theveloper.pixelplay.presentation.viewmodel
import android.content.Context
import android.os.Environment
import android.provider.MediaStore
+import timber.log.Timber
import com.theveloper.pixelplay.data.preferences.UserPreferencesRepository
import com.theveloper.pixelplay.utils.DirectoryRuleResolver
import com.theveloper.pixelplay.utils.StorageInfo
@@ -576,7 +577,8 @@ class FileExplorerStateHolder(
} catch (error: CancellationException) {
prefetchedDirectoryKeys.remove(targetKey)
throw error
- } catch (_: Throwable) {
+ } catch (e: Throwable) {
+ Timber.w(e, "Failed to prefetch directory %s", targetKey)
prefetchedDirectoryKeys.remove(targetKey)
}
}
diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LyricsStateHolder.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LyricsStateHolder.kt
index 87985fd6e..5a49929e0 100644
--- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LyricsStateHolder.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LyricsStateHolder.kt
@@ -13,6 +13,7 @@ import com.theveloper.pixelplay.data.repository.MusicRepository
import com.theveloper.pixelplay.data.repository.NoLyricsFoundException
import com.theveloper.pixelplay.utils.LyricsImportSecurity
import com.theveloper.pixelplay.utils.LyricsImportValidationResult
+import timber.log.Timber
import com.theveloper.pixelplay.utils.LyricsUtils
import com.theveloper.pixelplay.utils.ValidatedLyricsImport
import java.io.File
@@ -135,7 +136,8 @@ class LyricsStateHolder @Inject constructor(
}
} catch (cancellation: CancellationException) {
throw cancellation
- } catch (_: Exception) {
+ } catch (e: Exception) {
+ Timber.w(e, "Failed to load lyrics for song %s", song.title)
null
}
diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerUiState.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerUiState.kt
index 4272693f5..7da60dfe4 100644
--- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerUiState.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerUiState.kt
@@ -22,6 +22,10 @@ data class PlayerUiState(
// val artists: ImmutableList = persistentListOf(), // REMOVED
val searchResults: ImmutableList = persistentListOf(),
val musicFolders: ImmutableList = persistentListOf(),
+ val showAiPlaylistSheet: Boolean = false,
+ val isGeneratingAiPlaylist: Boolean = false,
+ val aiStatus: String? = null,
+ val aiError: String? = null,
val sortOption: SortOption = SortOption.SongDefaultOrder,
val isLoadingInitialSongs: Boolean = true,
val isLoadingLibrary: Boolean = true,
@@ -51,7 +55,6 @@ data class PlayerUiState(
val folderBackGestureNavigationEnabled: Boolean = true,
val currentSongSortOption: SortOption = SortOption.SongTitleAZ,
// val songCount: Int = 0, // REMOVED
- val isGeneratingAiMetadata: Boolean = false,
val searchHistory: ImmutableList = persistentListOf(),
val searchQuery: String = "",
val isSyncingLibrary: Boolean = 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 d6829b847..2ce919cd9 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
@@ -37,7 +37,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
import com.theveloper.pixelplay.R
-import com.theveloper.pixelplay.data.ai.SongMetadata
import com.theveloper.pixelplay.data.media.CoverArtUpdate
import com.theveloper.pixelplay.data.model.Album
import com.theveloper.pixelplay.data.model.Artist
@@ -163,6 +162,13 @@ private fun moveQueueIndex(index: Int, fromIndex: Int, toIndex: Int): Int {
}
}
+private data class AiUiSnapshot(
+ val showAiPlaylistSheet: Boolean,
+ val isGeneratingAiPlaylist: Boolean,
+ val aiStatus: String?,
+ val aiError: String?,
+)
+
private data class SortOptionsSnapshot(
val songSort: SortOption,
val albumSort: SortOption,
@@ -171,14 +177,6 @@ private data class SortOptionsSnapshot(
val favoriteSort: SortOption,
)
-private data class AiUiSnapshot(
- val showAiPlaylistSheet: Boolean,
- val isGeneratingAiPlaylist: Boolean,
- val aiStatus: String?,
- val aiError: String?,
- val isGeneratingAiMetadata: Boolean,
-)
-
@UnstableApi
@SuppressLint("LogNotTimber")
@OptIn(coil.annotation.ExperimentalCoilApi::class, ExperimentalCoroutinesApi::class)
@@ -446,10 +444,6 @@ class PlayerViewModel @Inject constructor(
val aiStatus: StateFlow = aiStateHolder.aiStatus
val aiError: StateFlow = aiStateHolder.aiError
- // AI Metadata Generation States
- val isGeneratingAiMetadata: StateFlow = aiStateHolder.isGeneratingMetadata
- val aiMetadataSuccess: StateFlow = aiStateHolder.aiMetadataSuccess
-
private val _selectedSongForInfo = MutableStateFlow(null)
val selectedSongForInfo: StateFlow = _selectedSongForInfo.asStateFlow()
@@ -504,7 +498,10 @@ class PlayerViewModel @Inject constructor(
aiPreferencesRepository.nvidiaApiKey,
aiPreferencesRepository.kimiApiKey,
aiPreferencesRepository.glmApiKey,
- aiPreferencesRepository.openaiApiKey
+ aiPreferencesRepository.openaiApiKey,
+ aiPreferencesRepository.ollamaApiKey,
+ aiPreferencesRepository.customApiKey,
+ aiPreferencesRepository.openrouterApiKey
) { values ->
val provider = values[0]
val gemini = values[1]
@@ -515,7 +512,11 @@ class PlayerViewModel @Inject constructor(
val kimi = values[6]
val glm = values[7]
val openai = values[8]
+ val ollama = values[9]
+ val custom = values[10]
+ val openrouter = values[11]
when (provider) {
+ "GEMINI" -> gemini.isNotBlank()
"DEEPSEEK" -> deepseek.isNotBlank()
"GROQ" -> groq.isNotBlank()
"MISTRAL" -> mistral.isNotBlank()
@@ -523,7 +524,10 @@ class PlayerViewModel @Inject constructor(
"KIMI" -> kimi.isNotBlank()
"GLM" -> glm.isNotBlank()
"OPENAI" -> openai.isNotBlank()
- else -> gemini.isNotBlank()
+ "OPENROUTER" -> openrouter.isNotBlank()
+ "OLLAMA" -> ollama.isNotBlank()
+ "CUSTOM" -> custom.isNotBlank()
+ else -> false
}
}.distinctUntilChanged()
.stateIn(
@@ -1055,19 +1059,46 @@ class PlayerViewModel @Inject constructor(
initialValue = 0 // Default to Songs tab
)
- val libraryTabsFlow: StateFlow> = userPreferencesRepository.libraryTabsOrderFlow
- .map { orderJson ->
- if (orderJson != null) {
- try {
- Json.decodeFromString>(orderJson)
- } catch (e: Exception) {
- listOf("SONGS", "ALBUMS", "ARTIST", "PLAYLISTS", "FOLDERS", "LIKED")
- }
- } else {
- listOf("SONGS", "ALBUMS", "ARTIST", "PLAYLISTS", "FOLDERS", "LIKED")
+ val libraryTabsFlow: StateFlow> = combine(
+ userPreferencesRepository.libraryTabsOrderFlow,
+ userPreferencesRepository.libraryHiddenTabsFlow
+ ) { orderJson, hiddenTabs ->
+ val allTabsInOrder = if (orderJson != null) {
+ try {
+ Json.decodeFromString>(orderJson)
+ } catch (e: Exception) {
+ LibraryTabId.defaultOrder.map { it.storageKey }
+ }
+ } else {
+ LibraryTabId.defaultOrder.map { it.storageKey }
+ }
+
+ // Ensure all available tabs are present (e.g. new tabs from app updates)
+ val availableKeys = LibraryTabId.defaultOrder.map { it.storageKey }
+ val mergedOrder = (allTabsInOrder + availableKeys).distinct()
+
+ mergedOrder.filter { it !in hiddenTabs }
+ }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), LibraryTabId.defaultOrder.map { it.storageKey })
+
+ val hiddenLibraryTabsFlow: StateFlow> = combine(
+ userPreferencesRepository.libraryTabsOrderFlow,
+ userPreferencesRepository.libraryHiddenTabsFlow
+ ) { orderJson, hiddenTabs ->
+ val allTabsInOrder = if (orderJson != null) {
+ try {
+ Json.decodeFromString>(orderJson)
+ } catch (e: Exception) {
+ LibraryTabId.defaultOrder.map { it.storageKey }
}
+ } else {
+ LibraryTabId.defaultOrder.map { it.storageKey }
}
- .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), listOf("SONGS", "ALBUMS", "ARTIST", "PLAYLISTS", "FOLDERS", "LIKED"))
+
+ val availableKeys = LibraryTabId.defaultOrder.map { it.storageKey }
+ val mergedOrder = (allTabsInOrder + availableKeys).distinct()
+
+ mergedOrder.filter { it in hiddenTabs }
+ }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
private val _loadedTabs = MutableStateFlow(emptySet())
private var lastBlockedDirectories: Set? = null
@@ -1779,25 +1810,28 @@ class PlayerViewModel @Inject constructor(
openPlayerSheetCallback = { _isSheetVisible.value = true }
)
- // Collect AiStateHolder flows
+ // Collect AiStateHolder flows for playlist generation state
viewModelScope.launch {
combine(
aiStateHolder.showAiPlaylistSheet,
aiStateHolder.isGeneratingAiPlaylist,
aiStateHolder.aiStatus,
aiStateHolder.aiError,
- aiStateHolder.isGeneratingMetadata,
- ) { show, generating, status, error, generatingMetadata ->
+ ) { show, generating, status, error ->
AiUiSnapshot(
showAiPlaylistSheet = show,
isGeneratingAiPlaylist = generating,
aiStatus = status,
- aiError = error,
- isGeneratingAiMetadata = generatingMetadata
+ aiError = error
)
}.collect { snapshot ->
_playerUiState.update {
- it.copy(isGeneratingAiMetadata = snapshot.isGeneratingAiMetadata)
+ it.copy(
+ showAiPlaylistSheet = snapshot.showAiPlaylistSheet,
+ isGeneratingAiPlaylist = snapshot.isGeneratingAiPlaylist,
+ aiStatus = snapshot.aiStatus,
+ aiError = snapshot.aiError
+ )
}
}
}
@@ -2611,10 +2645,6 @@ class PlayerViewModel @Inject constructor(
aiStateHolder.retryLastPlaylistGeneration()
}
- fun retryLastMetadataGeneration() {
- aiStateHolder.retryLastMetadataGeneration()
- }
-
fun clearQueueExceptCurrent() {
mediaController?.let { controller ->
val currentSongIndex = controller.currentMediaItemIndex
@@ -2803,6 +2833,12 @@ class PlayerViewModel @Inject constructor(
}
}
+ fun saveLibraryHiddenTabs(hiddenTabs: Set) {
+ viewModelScope.launch {
+ userPreferencesRepository.setLibraryHiddenTabs(hiddenTabs)
+ }
+ }
+
fun resetLibraryTabsOrder() {
viewModelScope.launch {
userPreferencesRepository.resetLibraryTabsOrder()
@@ -2886,10 +2922,6 @@ class PlayerViewModel @Inject constructor(
}.getOrDefault(false)
}
- suspend fun generateAiMetadata(song: Song, fields: List): Result {
- return aiStateHolder.generateAiMetadata(song, fields)
- }
-
private fun updateSongInStates(
updatedSong: Song,
newLyrics: Lyrics? = null,
diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlaylistViewModel.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlaylistViewModel.kt
index 8c5c620ef..45721f166 100644
--- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlaylistViewModel.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlaylistViewModel.kt
@@ -28,6 +28,7 @@ import kotlinx.coroutines.withContext
import kotlinx.coroutines.Dispatchers
import java.io.OutputStreamWriter
import android.content.Context
+import timber.log.Timber
import android.graphics.Bitmap
import android.graphics.ImageDecoder
import android.os.Build
@@ -246,7 +247,7 @@ class PlaylistViewModel @Inject constructor(
PlaylistSongsOrderMode.Manual -> songsList
}
- // La actualización del UI se hace en el hilo principal
+ // Update UI on the main thread
_uiState.update {
it.copy(
currentPlaylistDetails = playlist,
@@ -268,8 +269,7 @@ class PlaylistViewModel @Inject constructor(
currentPlaylistDetails = null,
currentPlaylistSongs = emptyList()
)
- } // Mantener isLoading en false
- // Opcional: podrías establecer un error o un estado específico de "no encontrado"
+ }
}
}
} catch (e: Exception) {
@@ -615,7 +615,7 @@ class PlaylistViewModel @Inject constructor(
// Optional: Delete old file if it was a local file managed by us
currentPlaylist.coverImageUri?.let { oldPath ->
if (oldPath.contains("playlist_cover_")) {
- try { File(oldPath).delete() } catch (e: Exception) {}
+ try { File(oldPath).delete() } catch (e: Exception) { Timber.w(e, "Failed to delete old playlist cover") }
}
}
savedCoverPath = newPath
@@ -624,7 +624,7 @@ class PlaylistViewModel @Inject constructor(
// Explicitly removed
currentPlaylist.coverImageUri?.let { oldPath ->
if (oldPath.contains("playlist_cover_")) {
- try { File(oldPath).delete() } catch (e: Exception) {}
+ try { File(oldPath).delete() } catch (e: Exception) { Timber.w(e, "Failed to delete old playlist cover") }
}
}
savedCoverPath = null
diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SettingsViewModel.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SettingsViewModel.kt
index d07d8b3df..d1d67f39e 100644
--- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SettingsViewModel.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SettingsViewModel.kt
@@ -275,6 +275,52 @@ class SettingsViewModel @Inject constructor(
val openrouterSystemPrompt: StateFlow = aiPreferencesRepository.openrouterSystemPrompt
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), AiPreferencesRepository.DEFAULT_OPENROUTER_SYSTEM_PROMPT)
+ val ollamaApiKey: StateFlow = aiPreferencesRepository.ollamaApiKey
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "")
+ val ollamaModel: StateFlow = aiPreferencesRepository.ollamaModel
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "")
+ val ollamaSystemPrompt: StateFlow = aiPreferencesRepository.ollamaSystemPrompt
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), AiPreferencesRepository.DEFAULT_SYSTEM_PROMPT)
+
+ val customApiKey: StateFlow = aiPreferencesRepository.customApiKey
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "")
+ val customModel: StateFlow = aiPreferencesRepository.customModel
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "")
+ val customSystemPrompt: StateFlow = aiPreferencesRepository.customSystemPrompt
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), AiPreferencesRepository.DEFAULT_SYSTEM_PROMPT)
+ val customBaseUrl: StateFlow = aiPreferencesRepository.customBaseUrl
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "")
+
+ val currentAiBaseUrl: StateFlow = aiProvider
+ .flatMapLatest { provider ->
+ val p = AiProvider.fromString(provider)
+ if (p.hasConfigurableUrl) aiPreferencesRepository.getBaseUrl(p)
+ else flowOf("")
+ }
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "")
+
+ // Generation Parameters
+ val aiTemperature: StateFlow = aiPreferencesRepository.aiTemperature
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 0.7f)
+ val aiTopP: StateFlow = aiPreferencesRepository.aiTopP
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 0.95f)
+ val aiTopK: StateFlow = aiPreferencesRepository.aiTopK
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 64)
+ val aiMaxTokens: StateFlow = aiPreferencesRepository.aiMaxTokens
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 4096)
+ val aiPresencePenalty: StateFlow = aiPreferencesRepository.aiPresencePenalty
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 0.0f)
+ val aiFrequencyPenalty: StateFlow = aiPreferencesRepository.aiFrequencyPenalty
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 0.0f)
+
+ // Song Data Configuration
+ val aiSampleSize: StateFlow = aiPreferencesRepository.aiSampleSize
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 40)
+ val aiDigestMode: StateFlow = aiPreferencesRepository.aiDigestMode
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "safe")
+ val aiIncludeExtendedFields: StateFlow = aiPreferencesRepository.aiIncludeExtendedFields
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
+
fun onAiApiKeyChange(apiKey: String) {
viewModelScope.launch {
val providerStr = aiProvider.value
@@ -349,6 +395,53 @@ class SettingsViewModel @Inject constructor(
else clearModelsState("OPENROUTER")
}
}
+ fun onOllamaApiKeyChange(apiKey: String) {
+ viewModelScope.launch {
+ aiPreferencesRepository.setApiKey(AiProvider.OLLAMA, apiKey)
+ if (apiKey.isNotBlank()) fetchAvailableModels(apiKey, "OLLAMA")
+ else clearModelsState("OLLAMA")
+ }
+ }
+ fun onCustomApiKeyChange(apiKey: String) {
+ viewModelScope.launch {
+ aiPreferencesRepository.setApiKey(AiProvider.CUSTOM, apiKey)
+ if (apiKey.isNotBlank()) fetchAvailableModels(apiKey, "CUSTOM")
+ else clearModelsState("CUSTOM")
+ }
+ }
+ fun onCustomBaseUrlChange(baseUrl: String) {
+ viewModelScope.launch {
+ aiPreferencesRepository.setBaseUrl(AiProvider.CUSTOM, baseUrl)
+ }
+ }
+
+ fun onAiTemperatureChange(value: Float) {
+ viewModelScope.launch { aiPreferencesRepository.setAiTemperature(value) }
+ }
+ fun onAiTopPChange(value: Float) {
+ viewModelScope.launch { aiPreferencesRepository.setAiTopP(value) }
+ }
+ fun onAiTopKChange(value: Int) {
+ viewModelScope.launch { aiPreferencesRepository.setAiTopK(value) }
+ }
+ fun onAiMaxTokensChange(value: Int) {
+ viewModelScope.launch { aiPreferencesRepository.setAiMaxTokens(value) }
+ }
+ fun onAiPresencePenaltyChange(value: Float) {
+ viewModelScope.launch { aiPreferencesRepository.setAiPresencePenalty(value) }
+ }
+ fun onAiFrequencyPenaltyChange(value: Float) {
+ viewModelScope.launch { aiPreferencesRepository.setAiFrequencyPenalty(value) }
+ }
+ fun onAiSampleSizeChange(value: Int) {
+ viewModelScope.launch { aiPreferencesRepository.setAiSampleSize(value) }
+ }
+ fun onAiDigestModeChange(mode: String) {
+ viewModelScope.launch { aiPreferencesRepository.setAiDigestMode(mode) }
+ }
+ fun onAiIncludeExtendedFieldsChange(enabled: Boolean) {
+ viewModelScope.launch { aiPreferencesRepository.setAiIncludeExtendedFields(enabled) }
+ }
fun onAiModelChange(model: String) {
viewModelScope.launch {
@@ -366,6 +459,8 @@ class SettingsViewModel @Inject constructor(
fun onGlmModelChange(model: String) = viewModelScope.launch { aiPreferencesRepository.setModel(AiProvider.GLM, model) }
fun onOpenAiModelChange(model: String) = viewModelScope.launch { aiPreferencesRepository.setModel(AiProvider.OPENAI, model) }
fun onOpenrouterModelChange(model: String) = viewModelScope.launch { aiPreferencesRepository.setModel(AiProvider.OPENROUTER, model) }
+ fun onOllamaModelChange(model: String) = viewModelScope.launch { aiPreferencesRepository.setModel(AiProvider.OLLAMA, model) }
+ fun onCustomModelChange(model: String) = viewModelScope.launch { aiPreferencesRepository.setModel(AiProvider.CUSTOM, model) }
fun onAiSystemPromptChange(prompt: String) {
viewModelScope.launch {
@@ -383,6 +478,8 @@ class SettingsViewModel @Inject constructor(
fun onGlmSystemPromptChange(prompt: String) = viewModelScope.launch { aiPreferencesRepository.setSystemPrompt(AiProvider.GLM, prompt) }
fun onOpenAiSystemPromptChange(prompt: String) = viewModelScope.launch { aiPreferencesRepository.setSystemPrompt(AiProvider.OPENAI, prompt) }
fun onOpenrouterSystemPromptChange(prompt: String) = viewModelScope.launch { aiPreferencesRepository.setSystemPrompt(AiProvider.OPENROUTER, prompt) }
+ fun onOllamaSystemPromptChange(prompt: String) = viewModelScope.launch { aiPreferencesRepository.setSystemPrompt(AiProvider.OLLAMA, prompt) }
+ fun onCustomSystemPromptChange(prompt: String) = viewModelScope.launch { aiPreferencesRepository.setSystemPrompt(AiProvider.CUSTOM, prompt) }
fun resetAiSystemPrompt() {
viewModelScope.launch {
@@ -400,6 +497,8 @@ class SettingsViewModel @Inject constructor(
fun resetGlmSystemPrompt() = viewModelScope.launch { aiPreferencesRepository.resetSystemPrompt(AiProvider.GLM) }
fun resetOpenAiSystemPrompt() = viewModelScope.launch { aiPreferencesRepository.resetSystemPrompt(AiProvider.OPENAI) }
fun resetOpenrouterSystemPrompt() = viewModelScope.launch { aiPreferencesRepository.resetSystemPrompt(AiProvider.OPENROUTER) }
+ fun resetOllamaSystemPrompt() = viewModelScope.launch { aiPreferencesRepository.resetSystemPrompt(AiProvider.OLLAMA) }
+ fun resetCustomSystemPrompt() = viewModelScope.launch { aiPreferencesRepository.resetSystemPrompt(AiProvider.CUSTOM) }
fun clearAiUsageData() {
viewModelScope.launch {
@@ -1149,7 +1248,13 @@ class SettingsViewModel @Inject constructor(
val models = if (provider == AiProvider.GEMINI) {
geminiModelService.fetchAvailableModels(apiKey).getOrThrow()
} else {
- val aiClient = aiClientFactory.createClient(provider, apiKey)
+ val baseUrl = if (provider.hasConfigurableUrl)
+ aiPreferencesRepository.getBaseUrl(provider).first()
+ else ""
+ val aiClient = if (provider.hasConfigurableUrl)
+ aiClientFactory.createClientWithUrl(provider, apiKey, baseUrl)
+ else
+ aiClientFactory.createClient(provider, apiKey)
aiClient.getAvailableModels(apiKey)
.map { it.trim() }
.filter { it.isNotBlank() }
diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SongRemovalStateHolder.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SongRemovalStateHolder.kt
index 91e2d858f..8399c559d 100644
--- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SongRemovalStateHolder.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SongRemovalStateHolder.kt
@@ -2,6 +2,7 @@ package com.theveloper.pixelplay.presentation.viewmodel
import android.app.Activity
import android.content.IntentSender
+import timber.log.Timber
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.theveloper.pixelplay.R
import com.theveloper.pixelplay.data.model.Song
@@ -92,7 +93,8 @@ class SongRemovalStateHolder @Inject constructor(
dialog.show()
userChoice.await()
- } catch (_: Exception) {
+ } catch (e: Exception) {
+ Timber.w(e, "Failed to show song removal confirmation dialog")
false
}
}
diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/ThemeStateHolder.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/ThemeStateHolder.kt
index a2bb2213a..10428c1a4 100644
--- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/ThemeStateHolder.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/ThemeStateHolder.kt
@@ -3,6 +3,7 @@ package com.theveloper.pixelplay.presentation.viewmodel
import android.net.Uri
import android.content.ComponentCallbacks2
import android.os.Trace
+import timber.log.Timber
import androidx.compose.ui.graphics.Color
import com.theveloper.pixelplay.data.preferences.AlbumArtColorAccuracy
import com.theveloper.pixelplay.data.preferences.AlbumArtPaletteStyle
@@ -181,8 +182,8 @@ class ThemeStateHolder @Inject constructor(
paletteStyle = currentPaletteStyle,
colorAccuracyLevel = currentPaletteAccuracy
)
- } catch (_: Exception) {
- // Ignore or log
+ } catch (e: Exception) {
+ Timber.w(e, "Failed to generate color scheme for %s", uriString)
} finally {
val targets = synchronized(pendingAlbumColorSchemeLock) {
pendingAlbumColorSchemeTargets.remove(uriString)?.toList().orEmpty()
diff --git a/app/src/main/java/com/theveloper/pixelplay/utils/MediaMetadataRetrieverPool.kt b/app/src/main/java/com/theveloper/pixelplay/utils/MediaMetadataRetrieverPool.kt
index 8ea8dffb5..c81d500f0 100644
--- a/app/src/main/java/com/theveloper/pixelplay/utils/MediaMetadataRetrieverPool.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/utils/MediaMetadataRetrieverPool.kt
@@ -1,6 +1,7 @@
package com.theveloper.pixelplay.utils
import android.media.MediaMetadataRetriever
+import timber.log.Timber
import java.util.concurrent.atomic.AtomicInteger
/**
@@ -40,8 +41,8 @@ object MediaMetadataRetrieverPool {
internal fun release(retriever: MediaMetadataRetriever) {
try {
retriever.release()
- } catch (_: Exception) {
- // Ignore release errors
+ } catch (e: Exception) {
+ Timber.w(e, "Failed to release MediaMetadataRetriever")
} finally {
createdCount.decrementAndGet()
}
diff --git a/app/src/main/java/com/theveloper/pixelplay/utils/ServerUrlUtils.kt b/app/src/main/java/com/theveloper/pixelplay/utils/ServerUrlUtils.kt
new file mode 100644
index 000000000..5d7ea8f68
--- /dev/null
+++ b/app/src/main/java/com/theveloper/pixelplay/utils/ServerUrlUtils.kt
@@ -0,0 +1,44 @@
+package com.theveloper.pixelplay.utils
+
+import com.theveloper.pixelplay.data.stream.CloudStreamSecurity
+import okhttp3.HttpUrl
+import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
+
+/**
+ * Shared URL normalization and validation for self-hosted media server credentials
+ * (Navidrome/Subsonic, Jellyfin, etc.).
+ */
+object ServerUrlUtils {
+
+ fun normalizeHttpUrl(serverUrl: String): HttpUrl? {
+ val trimmed = serverUrl.trim().trimEnd('/')
+ val withScheme = if (!trimmed.startsWith("http://", ignoreCase = true) &&
+ !trimmed.startsWith("https://", ignoreCase = true)
+ ) {
+ "https://$trimmed"
+ } else {
+ trimmed
+ }
+ return withScheme.toHttpUrlOrNull()
+ }
+
+ fun normalizeServerUrl(serverUrl: String): String {
+ return normalizeHttpUrl(serverUrl)?.toString()?.trimEnd('/')
+ ?: serverUrl.trim().trimEnd('/')
+ }
+
+ fun connectionValidationError(serverUrl: String, serverLabel: String = "server"): String? {
+ val parsed = normalizeHttpUrl(serverUrl)
+ ?: return "Invalid server URL format"
+
+ if (parsed.username.isNotEmpty() || parsed.password.isNotEmpty()) {
+ return "Server URL must not contain embedded credentials."
+ }
+
+ if (!parsed.isHttps && !CloudStreamSecurity.isLocalOrPrivateHost(parsed.host)) {
+ return "Use https:// for remote $serverLabel servers. HTTP is only allowed for local network addresses."
+ }
+
+ return null
+ }
+}
diff --git a/app/src/main/java/com/theveloper/pixelplay/utils/shapes/OtherShapes.kt b/app/src/main/java/com/theveloper/pixelplay/utils/shapes/OtherShapes.kt
index d5dc24bd3..27243f125 100644
--- a/app/src/main/java/com/theveloper/pixelplay/utils/shapes/OtherShapes.kt
+++ b/app/src/main/java/com/theveloper/pixelplay/utils/shapes/OtherShapes.kt
@@ -33,10 +33,7 @@ fun createHexagonShape() = object : Shape {
}
}
-// Implementaciones similares para createRoundedTriangleShape, createSemiCircleShape
-// (Estas pueden ser más complejas dependiendo del diseño exacto que quieras)
-
-// Ejemplo simple de triángulo redondeado (tendrías que ajustarlo)
+// Simple rounded triangle shape
fun createRoundedTriangleShape() = object : Shape {
override fun createOutline(size: androidx.compose.ui.geometry.Size, layoutDirection: LayoutDirection, density: Density): Outline {
return Outline.Generic(Path().apply {
@@ -46,21 +43,9 @@ fun createRoundedTriangleShape() = object : Shape {
path.lineTo(0f, size.height)
path.close()
- // Para redondear las esquinas, podrías usar CornerPathEffect en un Modifier.drawBehind,
- // o construir la forma con arcos y líneas. Clipping con Shape solo recorta.
- // Una forma simple es usar un RoundRect para el clip con radios grandes, pero no es un triángulo real.
- // Para un triángulo redondeado preciso, tendrías que dibujar la forma con arcos.
- // Por ahora, dejaremos el clip simple o necesitarás una implementación más avanzada.
-
- // Alternativa simple: clip a un rectángulo con esquinas redondeadas
- // return Outline.Rounded(RoundRect(0f, 0f, size.width, size.height, CornerRadius(16f, 16f)))
- // Esto no es un triángulo. Necesitas una implementación real de forma de triángulo redondeado.
- // Por simplicidad en este ejemplo, usaremos formas más estándar o de la librería.
-
- // Para el ejemplo, simplemente usaremos un triángulo básico sin redondeo complejo en el clip.
- // Si necesitas triángulos redondeados reales, busca implementaciones más avanzadas.
+ // Basic triangle without rounded corners for clipping.
moveTo(size.width / 2f, 0f)
- lineTo(size.width, size.height * 0.8f) // Ajuste para que la base no llegue hasta abajo
+ lineTo(size.width, size.height * 0.8f)
lineTo(0f, size.height * 0.8f)
close()
@@ -68,25 +53,23 @@ fun createRoundedTriangleShape() = object : Shape {
}
}
-// Ejemplo simple de Semicírculo (tendrías que ajustarlo)
+// Simple semicircle shape
fun createSemiCircleShape() = object : Shape {
override fun createOutline(size: androidx.compose.ui.geometry.Size, layoutDirection: LayoutDirection, density: Density): Outline {
return Outline.Generic(Path().apply {
arcTo(
- rect = Rect(0f, 0f, size.width, size.width), // Un círculo basado en el ancho
+ rect = Rect(0f, 0f, size.width, size.width),
startAngleDegrees = 0f,
sweepAngleDegrees = 180f,
forceMoveTo = false
)
- lineTo(size.width / 2f, size.width / 2f) // Dibuja una línea hacia el centro si necesitas cerrarlo como pastel
- close() // Opcional: cierra la forma
+ lineTo(size.width / 2f, size.width / 2f)
+ close()
})
}
}
-/**
- * Crea una forma de hexágono con esquinas redondeadas.
- */
+/** Hexagon shape with rounded corners. */
fun createRoundedHexagonShape(cornerRadius: Dp) = object : Shape {
override fun createOutline(
size: Size,
@@ -99,7 +82,7 @@ fun createRoundedHexagonShape(cornerRadius: Dp) = object : Shape {
val radius = min(width, height) / 2f
val cornerRadiusPx = with(density) { cornerRadius.toPx() }
- // Puntos del hexágono sin redondear
+ // Unrounded hexagon vertices
val points = (0..5).map { i ->
val angle = PI / 3 * i
Offset(
@@ -108,7 +91,7 @@ fun createRoundedHexagonShape(cornerRadius: Dp) = object : Shape {
)
}
- // Movemos al primer punto con un offset para empezar el arco
+ // Move to first point offset for arc start
moveTo(points[0].x + cornerRadiusPx * cos(PI / 3.0).toFloat(), points[0].y + cornerRadiusPx * sin(PI / 3.0).toFloat())
for (i in 0..5) {
@@ -116,10 +99,10 @@ fun createRoundedHexagonShape(cornerRadius: Dp) = object : Shape {
val p2 = points[(i + 1) % 6]
val p3 = points[(i + 2) % 6]
- // Línea hacia el punto de inicio del arco
+ // Line to arc start point
lineTo(p2.x - cornerRadiusPx * cos(PI / 3.0).toFloat(), p2.y - cornerRadiusPx * sin(PI / 3.0).toFloat())
- // Arco en la esquina
+ // Corner arc
arcTo(
rect = Rect(
left = p2.x - cornerRadiusPx,
@@ -127,8 +110,8 @@ fun createRoundedHexagonShape(cornerRadius: Dp) = object : Shape {
right = p2.x + cornerRadiusPx,
bottom = p2.y + cornerRadiusPx
),
- startAngleDegrees = (i * 60 + 30).toFloat(), // Ángulo de inicio del arco
- sweepAngleDegrees = 60f, // Ángulo del arco
+ startAngleDegrees = (i * 60 + 30).toFloat(),
+ sweepAngleDegrees = 60f,
forceMoveTo = false
)
}
@@ -137,10 +120,7 @@ fun createRoundedHexagonShape(cornerRadius: Dp) = object : Shape {
}
}
-/**
- * Crea una forma de triángulo con esquinas redondeadas.
- * Implementación simple para clipping.
- */
+/** Rounded-corner triangle shape for clipping. */
fun createRoundedTriangleShape(cornerRadius: Dp) = object : Shape {
override fun createOutline(size: Size, layoutDirection: LayoutDirection, density: Density): Outline {
return Outline.Generic(Path().apply {
@@ -148,43 +128,38 @@ fun createRoundedTriangleShape(cornerRadius: Dp) = object : Shape {
val height = size.height
val cornerRadiusPx = with(density) { cornerRadius.toPx() }
- // Puntos del triángulo
- val p1 = Offset(width / 2f, 0f) // Superior
- val p2 = Offset(width, height) // Inferior derecha
- val p3 = Offset(0f, height) // Inferior izquierda
+ // Triangle vertices
+ val p1 = Offset(width / 2f, 0f) // Top
+ val p2 = Offset(width, height) // Bottom-right
+ val p3 = Offset(0f, height) // Bottom-left
- // Para simplificar el redondeo en el clip, usaremos arcos.
- // Esto no es un triángulo perfecto con arcos tangentes, sino un enfoque práctico para clipping.
-
- // Calcula puntos de control para los arcos
+ // Control points for corner arcs
val control12 = Offset(p1.x + (p2.x - p1.x) * 0.8f, p1.y + (p2.y - p1.y) * 0.8f)
val control23 = Offset(p2.x + (p3.x - p2.x) * 0.2f, p2.y + (p3.y - p2.y) * 0.2f)
val control31 = Offset(p3.x + (p1.x - p3.x) * 0.8f, p3.y + (p1.y - p3.y) * 0.8f)
- moveTo(p1.x, p1.y + cornerRadiusPx * 2) // Empieza un poco más abajo del vértice superior
+ moveTo(p1.x, p1.y + cornerRadiusPx * 2)
- // Arco superior derecha
+ // Top-right arc
quadraticTo(p1.x, p1.y, p1.x + cornerRadiusPx * sqrt(2f), p1.y + cornerRadiusPx * sqrt(2f))
lineTo(p2.x - cornerRadiusPx * sqrt(2f), p2.y - cornerRadiusPx * sqrt(2f))
- // Arco inferior derecha
+ // Bottom-right arc
quadraticTo(p2.x, p2.y, p2.x - cornerRadiusPx * sqrt(2f), p2.y + cornerRadiusPx * sqrt(2f))
lineTo(p3.x + cornerRadiusPx * sqrt(2f), p3.y + cornerRadiusPx * sqrt(2f))
- // Arco inferior izquierda
+ // Bottom-left arc
quadraticTo(p3.x, p3.y, p3.x + cornerRadiusPx * sqrt(2f), p3.y - cornerRadiusPx * sqrt(2f))
lineTo(p1.x - cornerRadiusPx * sqrt(2f), p1.y + cornerRadiusPx * sqrt(2f))
- close() // Cierra la forma
+ close()
})
}
}
-/**
- * Crea una forma de semicírculo con una base ligeramente redondeada.
- */
+/** Semicircle shape with a slightly rounded base. */
fun createSemiCircleShape(cornerRadius: Dp) = object : Shape {
override fun createOutline(size: Size, layoutDirection: LayoutDirection, density: Density): Outline {
return Outline.Generic(Path().apply {
@@ -193,40 +168,40 @@ fun createSemiCircleShape(cornerRadius: Dp) = object : Shape {
val radius = width / 2f
val cornerRadiusPx = with(density) { cornerRadius.toPx() }
- // Arco superior (semicírculo)
+ // Top semicircle arc
arcTo(
- rect = Rect(0f, 0f, width, width), // Un círculo basado en el ancho
+ rect = Rect(0f, 0f, width, width),
startAngleDegrees = 0f,
sweepAngleDegrees = 180f,
forceMoveTo = false
)
- // Base (línea con arcos en los extremos)
+ // Base line with arcs at both ends
val startBaseX = 0f + cornerRadiusPx
val endBaseX = width - cornerRadiusPx
- val baseY = width / 2f // La base está a la mitad del diámetro del círculo
+ val baseY = width / 2f
- lineTo(endBaseX, baseY) // Línea hacia el final de la base
+ lineTo(endBaseX, baseY)
- // Arco inferior derecho
+ // Bottom-right arc
arcTo(
rect = Rect(endBaseX - cornerRadiusPx, baseY - cornerRadiusPx, endBaseX + cornerRadiusPx, baseY + cornerRadiusPx),
startAngleDegrees = 90f,
- sweepAngleDegrees = -90f, // Arco hacia abajo
+ sweepAngleDegrees = -90f,
forceMoveTo = false
)
- lineTo(startBaseX, baseY + cornerRadiusPx) // Línea inferior
+ lineTo(startBaseX, baseY + cornerRadiusPx)
- // Arco inferior izquierdo
+ // Bottom-left arc
arcTo(
rect = Rect(startBaseX - cornerRadiusPx, baseY - cornerRadiusPx, startBaseX + cornerRadiusPx, baseY + cornerRadiusPx),
startAngleDegrees = 180f,
- sweepAngleDegrees = -90f, // Arco hacia abajo
+ sweepAngleDegrees = -90f,
forceMoveTo = false
)
- close() // Cierra la forma
+ close()
})
}
}
\ No newline at end of file
diff --git a/app/src/main/res/values-ar/plurals.xml b/app/src/main/res/values-ar/plurals.xml
deleted file mode 100644
index 038673ef3..000000000
--- a/app/src/main/res/values-ar/plurals.xml
+++ /dev/null
@@ -1,75 +0,0 @@
-
-
-
- لا يتم مشاركة أي قوائم تشغيل
- جاري مشاركة قائمة تشغيل واحدة
- جاري مشاركة قائمتي تشغيل
- جاري مشاركة %d قوائم تشغيل
- جاري مشاركة %d قائمة تشغيل
- جاري مشاركة %d قائمة تشغيل
-
-
- لم يتم تصدير أي قوائم تشغيل إلى %2$s
- تم تصدير قائمة تشغيل واحدة إلى %2$s
- تم تصدير قائمتي تشغيل إلى %2$s
- تم تصدير %1$d قوائم تشغيل إلى %2$s
- تم تصدير %1$d قائمة تشغيل إلى %2$s
- تم تصدير %1$d قائمة تشغيل إلى %2$s
-
-
- لم يتم إضافة أي أغنية إلى قائمة الانتظار
- تمت إضافة أغنية واحدة إلى قائمة الانتظار
- تمت إضافة أغنيتين إلى قائمة الانتظار
- تمت إضافة %d أغانٍ إلى قائمة الانتظار
- تمت إضافة %d أغنية إلى قائمة الانتظار
- تمت إضافة %d أغنية إلى قائمة الانتظار
-
-
- لن يتم تشغيل أي أغنية تالياً
- سيتم تشغيل أغنية واحدة تالياً
- سيتم تشغيل أغنيتين تالياً
- سيتم تشغيل %d أغانٍ تالياً
- سيتم تشغيل %d أغنية تالياً
- سيتم تشغيل %d أغنية تالياً
-
-
- لم يتم إضافة أي أغنية إلى المفضلة
- تمت إضافة أغنية واحدة إلى المفضلة
- تمت إضافة أغنيتين إلى المفضلة
- تمت إضافة %d أغانٍ إلى المفضلة
- تمت إضافة %d أغنية إلى المفضلة
- تمت إضافة %d أغنية إلى المفضلة
-
-
- لم يتم إزالة أي أغنية من المفضلة
- تمت إزالة أغنية واحدة من المفضلة
- تمت إزالة أغنيتين من المفضلة
- تمت إزالة %d أغانٍ من المفضلة
- تمت إزالة %d أغنية من المفضلة
- تمت إزالة %d أغنية من المفضلة
-
-
- لم يتم حذف أي ملف
- تم حذف ملف واحد
- تم حذف ملفين
- تم حذف %d ملفات
- تم حذف %d ملفاً
- تم حذف %d ملفاً
-
-
- هل تريد حذف الملفات؟
- هل تريد حذف أغنية واحدة؟
- هل تريد حذف أغنيتين؟
- هل تريد حذف %d أغانٍ؟
- هل تريد حذف %d أغنية؟
- هل تريد حذف %d أغنية؟
-
-
- لم يتم تحديد أي مسار
- تم تحديد مسار واحد
- تم تحديد مسارين
- تم تحديد %d مسارات
- تم تحديد %d مساراً
- تم تحديد %d مساراً
-
-
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
index 04844b159..3a6030840 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -1,247 +1,128 @@
+
PixelPlayer
+ مشغل موسيقىتغيير اسم التطبيقلقد قمنا بتغيير اسم تطبيقنا من PixelPlay إلى PixelPlayer بسبب مشكلة تتعلق بالعلامة التجارية. استمتع بالاستماع!
- لا تظهره مرة أخرى
- تجاهل
+ لا تظهر هذه الرسالة مجدداً
+
+
+ الرئيسية
+ بحث
+ المكتبة
+
+
مطلوب إذن خاص
- لتعديل البيانات الوصفية للأغاني (ملفات .mp3)، يحتاج PixelPlayer إلى صلاحية وصول خاصة لجميع الملفات. يتيح لنا هذا تعديل علامات المسارات الصوتية مباشرة. يرجى منح هذا الإذن في الشاشة التالية لتفعيل تعديل البيانات الوصفية.
+ لتعديل البيانات الوصفية للأغاني (ملفات .mp3)، يحتاج PixelPlayer إلى وصول خاص إلى جميع الملفات. يتيح لنا هذا تعديل علامات المسارات الصوتية مباشرة. يرجى منح هذا الإذن في الشاشة التالية لتفعيل تعديل البيانات الوصفية.منح الإذن
- الوصول إلى جميع الملفات
- خطأ
- موافق
- إلغاء
- استيراد
- بحث
-
- كلمات الأغنية
- إغلاق قائمة كلمات الأغنية
- جاري تحميل كلمات الأغنية…
- تعذر العثور على كلمات لهذه الأغنية.
- الكلمات مقدمة من
- https://lrclib.net/
- لم يتم العثور على كلمات الأغنية
- هل تود البحث عن كلمات الأغنية عبر الإنترنت؟
- لم نتمكن من العثور على كلمات الأغنية تلقائياً. يمكنك تعديل العنوان أو اسم الفنان والمحاولة بالبحث يدوياً.
- فشل البحث عن كلمات الأغنية
- فشل جلب كلمات الأغنية من الخادم
- انتهت مهلة الاتصال. يرجى التحقق من اتصالك بالإنترنت.
- خطأ في الشبكة. يرجى التحقق من اتصالك بالإنترنت.
- خطأ في الخادم (رمز %d). يرجى المحاولة مرة أخرى لاحقاً.
- تم العثور على %d من التطابقات
- تم البحث عن \"%s\"
- جاري البحث عن كلمات الأغنية…
- كلمات الأغنية متوفرة بالفعل. تم تخطي الجلب عبر الإنترنت.
- الكلمات المضمنة موجودة بالفعل. تم تخطي الجلب عبر الإنترنت.
- ملف الكلمات المحلي (.lrc) موجود بالفعل. تم تخطي الجلب عبر الإنترنت.
- إظهار خيارات كلمات الأغنية
- فتح أداة الاختيار دائماً بدلاً من التطبيق التلقائي لأول تطابق
- حفظ كلمات الأغنية كملف .lrc
- حفظ كلمات الأغنية
- اختر النسخة المراد حفظها:
- مزامنة (مع الطوابع الزمنية)
- عادية (نص فقط)
- تم حفظ كلمات الأغنية بنجاح
- فشل حفظ كلمات الأغنية
- لا توجد كلمات أغنية متاحة لحفظها
- إعادة تعيين الكلمات المستوردة
- إزاحة مزامنة الكلمات
- %+.1fs
- إعادة تعيين
- أبكر
- أقرب
- جاري فحص ملفات الموسيقى…
- جاري معالجة الملفات…
- %1$d من أصل %2$d ملفاً
- جاري مزامنة المكتبة…
- اكتملت المزامنة
- انتظار…
- جاري مزامنة المكتبة…
- جاري الإنهاء في الخلفية…
- جاري فحص كلمات الأغاني…
- جاري تنظيف ذاكرة التخزين المؤقت لأغلفة الألبومات…
- جاري المزامنة مع المصادر السحابية…
- مسار مجهول
- فنان مجهول
- ألبوم مجهول
- اختر فناناً
- افتح أي فنان منسوب إليه هذا المسار.
- فنان واحد
- %1$d فنانين
- الفنان الرئيسي
- صفحة الفنان
+
تشغيل سريعتعذر فتح ملف الصوت هذا.
- فتح المشغل الكامل
- إغلاق المشغل العائم
- إغلاق المشغل
- المسار السابق
- المسار التالي
- إيقاف مؤقت
- تشغيل
- لم يتم العثور على قائمة التشغيل.
- القرص %d
-
- يرجى تكوين مفتاح API صالح لمزود الذكاء الاصطناعي المحدد في الإعدادات.
- خطأ في الذكاء الاصطناعي: %s
- رفض مزود الذكاء الاصطناعي المحدد الطلب لعدم وجود رصيد أو حصة متاحة في الحساب.
- لم يعد نموذج الذكاء الاصطناعي المحدد متاحاً. حاول PixelPlayer التبديل تلقائياً إلى نموذج مدعوم.
- لم يتمكن الذكاء الاصطناعي من العثور على أي أغانٍ بناءً على طلبك.
- اكتب فكرة لـ "مزيجك اليومي"
- تم تحديث المزيج اليومي بواسطة الذكاء الاصطناعي
- لم يتمكن الذكاء الاصطناعي من العثور على أغانٍ لهذا المزيج
+ فتح المشغل الكامل
- الترجمة بواسطة الذكاء الاصطناعي
- تحتوي كلمات هذه الأغنية على ترجمة بالفعل
- كلمات هذه الأغنية مكتوبة بهذه اللغة بالفعل
- لم يتم تكوين مفتاح الـ API
- تمت ترجمة كلمات الأغنية بنجاح!
- جاري ترجمة الكلمات عبر الذكاء الاصطناعي...
-
- تشغيل عشوائي
- تشغيل عشوائي لجميع الأغاني
- قائمة التشغيل
- آخر قائمة تشغيل تم تشغيلها
-
- تشغيل عشوائي للكل
- آخر قائمة تشغيل
+
+ خلط
+ خلط جميع الأغاني
+ خلط الكل
+ قائمة التشغيل الأخيرةلا توجد قائمة تشغيل متاحة لفتحها
- معرف الألبوم غير صالح
- لم يتم العثور على معرف الألبوم
- خطأ أثناء تحميل بيانات الألبوم: %s
- لم يتم العثور على الألبوم
- تعذر التحديث: %s
- معرف الفنان غير صالح
- لم يتم العثور على معرف الفنان
- خطأ أثناء تحميل بيانات الفنان: %s
- تعذر العثور على الفنان
- لم يتم العثور على أغانٍ صالحة للتشغيل
-
- أداة ذكية مستجيبة تتكيف مع حجمها
- شريط مشغل مضغوط
- عناصر تحكم كاملة مع التشغيل العشوائي والتكرار
- مشغل مربع بسيط
+
+ فتح متجر Play
+ متابعة النسخة التجريبية
+ سيتم تفعيل رابط متجر Play من إعدادات GitHub.
+ تطبيق PixelPlayer متاح الآن على Google Play
+ استخدم القناة المستقرة على Google Play للحصول على تحديثات الإصدارات الرسمية، بينما سنبقي على نسخ البيتا التجريبية نشطة.
+ PixelPlayer
+ إعلان الإصدار
+ قريباً
+
+
+ شكراً لاستخدامك PixelPlayer!
+ أعلى %1$d
+ إغلاق
+ النتيجة
+ مستوى %1$d
+ الأرواح
+ اكتمل المستوى!
+ انتهت اللعبة
+ النتيجة: %1$d
+ المحاولة مجدداً؟
+ المستوى التالي
+ إعادة تشغيل اللعبة
+ انقر لإعادة الإطلاق
+ تشغيل موسيقى عشوائية
+ تحطيم الطوب
+ أعلى نتيجة %1$d
+ لعب
+ اسحب لتحريك المضرب
+
+
+ إغلاق المشغلجاري معالجة إجراء التشغيل…
-
- لا توجد قوائم تشغيل لمشاركتها
- مشاركة قوائم التشغيل
- فشلت المشاركة: %1$s
- لا توجد قوائم تشغيل لتصديرها
- فشل التصدير: %1$s
- Music/PixelPlayer Exports
- يرجى تكوين مفتاح Gemini API في الإعدادات.
- خطأ غير معروف
-
- جاري إرسال %1$d من الأغاني إلى الساعة
- جاري الإرسال إلى الساعة
- اكتمل النقل
- فشل النقل
- تم إلغاء النقل
- جاري تحضير النقل للساعة
- عمليات نقل عدد %1$d
- جاري بدء النقل…
- توجد عدة عمليات نقل نشطة
- جاري تحضير النقل…
- جاري النقل
- مكتمل
- فاشل
- ملغي
- جاري التحضير
- جاري البدء
- عمليات نقل الساعة
- يعرض التقدم المباشر لنقل الموسيقى من الهاتف إلى الساعة
-
- خادم وسائط البث (Cast)
- جاري البث إلى الجهاز
- تزويد جهاز البث بالوسائط
- %1$s: %2$s
- التقديم والتأخير غير متاح مؤقتاً لصيغة الصوت هذه أثناء البث لأنها قد تتسبب في تعطل جلسة البث.
-
- نسخة احتياطية غير صالحة: %1$s
- جاري تحضير الاستعادة
- جاري بدء مهمة الاستعادة.
- جاري تحضير النسخ الاحتياطي
- جاري بدء مهمة النسخ الاحتياطي.
- تم استعادة النسخة الاحتياطية بنجاح
- اكتملت الاستعادة مع وجود بعض المشكلات غير المحلولة.
- تعذر إكمال الاستعادة: %1$s
- فشلت الاستعادة: %1$s
- تم تصدير البيانات بنجاح
- فشل التصدير: %1$s
- تم استعادة البيانات بنجاح
- اكتملت الاستعادة مع وجود مشكلات غير محلولة. الفشل: %1$s
- فشل تحميل النماذج
- تم إطلاق تعطل تجريبي من خيارات المطور - هذا الإجراء مقصود لاختبار نظام تقارير الأعطال
-
- لم يتم العثور على الأغنية في القائمة الحالية
- تعذر تحديد موقع الأغنية
- لم يتم العثور على أغانٍ في المكتبة
- توقف التشغيل: انتهى %1$s (نهاية المسار).
- مسار
- لا توجد أغانٍ لتشغيلها عشوائياً.
- الألبومات المحددة
- لم يتم العثور على أغانٍ قابلة للتشغيل في الألبومات المحددة
- تم إدراج أول %1$d ألبومات فقط في قائمة الانتظار
- تم إدراج %1$d ألبومات في قائمة الانتظار (%2$d أغنية)
- تعذر إدراج الألبومات المحددة في قائمة الانتظار
- جميع الأغاني موجودة بالفعل في المفضلة
- لم تكن هناك أي أغانٍ في المفضلة
- جاري إنشاء ملف ZIP…
- فشلت المشاركة: %1$s
- لا يمكن حذف الأغنية التي يتم تشغيلها حالياً
- تم حذف %1$d ملفات (تم تخطي %2$d - قيد التشغيل)
- تم حذف %1$d من أصل %2$d ملفاً
- فشل حذف الملفات
- تم حذف الملف
- تعذر حذف الملف أو أن الملف غير موجود
- تم إلغاء الحذف
- تم رفض الإذن - لا يمكن تعديل الملفات
- تم رفض الإذن - لا يمكن حفظ كلمات الأغنية
- تم رفض الإذن - لا يمكن تعديل هذا الملف
- تم تحديث البيانات الوصفية بنجاح
- جاري تحديث %1$d من الأغاني…
- تم تحديث %1$d من الأغاني بنجاح!
- تم تحديث %1$d أغنية. فشل: %2$d
- تم استعادة قائمة التشغيل
- سيتم حذف هذه الأغاني نهائياً من جهازك ولا يمكن استعادتها.
- حذف
-
- %1$d دقائق
- نهاية المسار
- تم ضبط المؤقت لمدة %1$d دقائق.
- تم إلغاء المؤقت.
- لا يمكن تفعيل خيار نهاية المسار: لا توجد أغنية نشطة حالياً.
- تم إلغاء تفعيل مؤقت نهاية المسار: تغيرت الأغنية من %1$s إلى %2$s.
- سيتوقف التشغيل عند نهاية المسار.
- المسار السابق
- المسار الحالي
- مؤقت النوم
- المؤقت
- نهاية المسار الحالي
- وقت مخصص
- إلغاء المؤقت
- ضبط مدة مخصصة
- عدد مرات التشغيل: %1$s
- مرة واحدة
- تشغيل المفتاح
- %1$d%%
- إصدار %1$d
- %1$s %2$s
-
- تعديل %d أغنية
- سيتم تحديث الحقول المعدلة فقط. اترك الحقول فارغة للاحتفاظ بالقيم الحالية.
- (قيم مختلطة)
- (اختياري - اتركه فارغاً للتخطي)
- تم تحديث %d أغنية بنجاح
- تم تحديث %1$d من أصل %2$d أغنية. تعذر تعديل بعض الملفات.
- فشل تحديث الأغاني
-
- تعديل غلاف المجموعة
- سيؤدي هذا إلى استبدال غلاف الألبوم لجميع الأغاني المحددة وعددها %d
- تعيين غلاف الألبوم للكل
- إزالة جميع أغلفة الألبومات
- (أغلفة متعددة مختلفة)خطأ في التشغيل: %1$s
-
+
+
+ رجوع
+ حسناً
+ إلغاء
+ تجاهل
+ خطأ
+ بحث
+ مسح البحث
+ الكل
+ تأكيد
+ تم الحفظ!
+ محدد
+ %1$d%%
+ الفنان
+ تحديد الكل
+ مسح
+ خطأ غير معروف
+
+
+ حفظ
+ تم
+ إعادة تعيين
+ تطبيق
+ خلط
+ نسخ
+ مشاركة
+ تراجع
+ استيراد
+ حذف
+ تصدير
+ دمج
+ إعادة تسمية
+ إنشاء
+ كلمات الأغاني
+ الإعدادات
+ غلاف الألبوم
+ قائمة تشغيل
+ مسار مجهول
+ فنان مجهول
+ ألبوم مجهول
+ إغلاق
+ إضافة
+ إزالة
+ تشغيل
+ المسار السابق
+ المسار التالي
+ المفضلة
+ إيقاف مؤقت
+ تكرار
+ خيارات
+ تشغيل عشوائي
+ المزيد من الخيارات لـ %1$s
+ توسيع القائمة
+ التالي
+ إنهاء
+ إعادة تعيين الافتراضيات
+ تصدير الكل
+ دمج الكل
+ مشاركة الكل
+ تشغيل الألبوم
+ تشغيل الألبوم عشوائياً
+ غلاف الألبوم لـ %1$s
+
\ No newline at end of file
diff --git a/app/src/main/res/values-ar/strings_auth.xml b/app/src/main/res/values-ar/strings_auth.xml
deleted file mode 100644
index e6ea6fed8..000000000
--- a/app/src/main/res/values-ar/strings_auth.xml
+++ /dev/null
@@ -1,74 +0,0 @@
-
-
-
- رجوع
- إظهار كلمة المرور
- إخفاء كلمة المرور
- جاري الاتصال…
- اتصال
- تفاصيل الاتصال
- أدخل رابط الخادم (URL) وبيانات اعتماد الحساب.
- رابط الخادم (URL)
- اسم المستخدم
- كلمة المرور
- أدخل كلمة المرور
- admin
- مرحباً، %1$s!
-
-
- Subsonic / Navidrome
- اتصل بخادم الموسيقى المستضاف ذاتياً
- يدعم خوادم Navidrome وAirsonic وGonic وAmpache والخوادم الأخرى المتوافقة مع واجهة برمجة تطبيقات Subsonic.
- https://music.example.com
- استخدم عنوان الخادم الكامل الذي يبدأ بـ //:https.
- هذا هو اسم حسابك على Subsonic أو Navidrome.
- كلمة مرور التطبيق (App password) تعمل أيضاً إذا كان خادمك يدعمها.
- كلمة مرور التطبيق (App password) تعمل أيضاً إذا كان خادمك يدعمها.
- ملء تلقائي لـ //:https
- متوافق مع Navidrome وGonic وAirsonic والخوادم الأخرى المتوافقة مع Subsonic
- شعار Navidrome
- شعار Subsonic
-
-
- Jellyfin
- يتصل بخوادم Jellyfin. يتم دعم كل من HTTP وHTTPS للوصول عبر الشبكة المحلية.
- اتصل بخادم وسائط Jellyfin الخاص بك
- أدخل رابط خادم Jellyfin وبيانات اعتماد الحساب.
- http://192.168.1.100:8096
- الرابط الكامل لخادم Jellyfin الخاص بك، شاملاً منفذ الاتصال (Port).
- اسم المستخدم لحساب Jellyfin الخاص بك.
- كلمة المرور لحساب Jellyfin الخاص بك.
- ملء تلقائي لـ //:http
- يتصل بخوادم Jellyfin لبث مكتبتك الموسيقية
- شعار Jellyfin
-
-
- تم توصيل Google Drive بنجاح!
- Google Drive
-
-
- الخروج من تسجيل دخول NetEase؟
- الخروج من تسجيل دخول QQ Music؟
- يمكنك العودة لاحقاً. سيتم تجاهل حالة الصفحة الحالية عند الإغلاق.
- خروج
- بقاء
- تسجيل الدخول إلى NetEase Music
- تسجيل الدخول إلى QQ Music
- رجوع للخلف في الويب
- تقدم للأمام في الويب
- تحديث
- فتح الصفحة الرئيسية
- جاري الحفظ…
- تم
- إعادة المحاولة
- +
- انتهت مهلة تحميل الصفحة. يمكنك إعادة المحاولة دون فقدان تقدمك.
- تعذر قراءة ملفات تعريف الارتباط (Cookies) الخاصة بالجلسة.
- تستغرق الصفحة وقتاً طويلاً للتحميل. استخدم التحديث أو جرب شبكة أخرى.
- فشل تحميل WebView.
- خطأ HTTP %1$d أثناء تحميل NetEase.
- خطأ HTTP %1$d أثناء تحميل QQ Music.
- لم يتم العثور على ملفات تعريف الارتباط. سجل الدخول أولاً.
- لم يتم رصد تسجيل الدخول بعد. أكمل تسجيل الدخول إلى NetEase قبل الضغط على "تم".
- لم يتم رصد تسجيل الدخول بعد. أكمل تسجيل الدخول إلى QQ Music قبل الضغط على "تم".
-
diff --git a/app/src/main/res/values-ar/strings_changelogs.xml b/app/src/main/res/values-ar/strings_changelogs.xml
new file mode 100644
index 000000000..372fc56f0
--- /dev/null
+++ b/app/src/main/res/values-ar/strings_changelogs.xml
@@ -0,0 +1,169 @@
+
+
+ سجل التغييرات
+ عرض على GitHub
+ التحسينات
+ الإصلاحات
+ ما الجديد
+ تمت إضافة
+
+
+ دعم Chromecast لبث الصوت من جهازك.
+ سجل تغييرات داخل التطبيق لإبقائك على اطلاع بأحدث الميزات.
+ دعم ملفات .LRC، المضمنة والخارجية على حد سواء.
+ دعم كلمات الأغاني دون اتصال بالإنترنت.
+ كلمات أغاني متزامنة (متوافقة مع توقيت الأغنية).
+ شاشة جديدة لعرض قائمة الانتظار كاملة.
+ إعادة ترتيب وإزالة الأغاني من قائمة الانتظار.
+ إيماءات المشغل المصغر (السحب لأسفل للإغلاق).
+ إضافة المزيد من حركات وتأثيرات Material الأنيميشن.
+ إعدادات جديدة لتخصيص المظهر العام واختيار الألوان.
+ إعدادات جديدة لمسح ذاكرة التخزين المؤقت.
+
+
+
+ إعادة تصميم شاملة لواجهة المستخدم.
+ إعادة تصميم شاملة للمشغل الموسيقي.
+ تحسينات في الأداء داخل المكتبة الموسيقية.
+ تحسين سرعة تشغيل وإقلاع التطبيق.
+ الذكاء الاصطناعي يقدم الآن نتائج أفضل.
+
+
+
+ إصلاح أخطاء متنوعة في محرر العلامات الوصفية (Tags).
+ إصلاح مشكلة عدم اختفاء إشعار التشغيل من لوحة الإشعارات.
+ إصلاح عدة أخطاء كانت تتسبب في توقف التطبيق عن العمل اضطرارياً.
+
+
+
+ تقديم مركز إحصاءات استماع أكثر ثراءً مع تحليلات أعمق لجلساتك الموسيقية.
+ إطلاق مشغل سريع عائم لفتح ومعاينة الملفات المحلية على الفور.
+ إضافة تبويب المجلدات مع مستكشف شجري وعرض جاهز لقوائم التشغيل.
+
+
+
+ تحسين واجهة المستخدم الشاملة لـ Material 3 لتقديم تجربة أنظف وأكثر تماسكاً.
+ تعديل البيانات الوصفية يدعم الآن تغيير غلاف الألبوم.
+ تنعيم الحركات والانتقالات عبر التطبيق لضمان تنقل أكثر سلاسة وفوقية.
+ تحسين تخطيط شاشة الفنان مع تفاصيل غنية ولمسات جمالية.
+ ترقية ميزة توليد قوائم DailyMix و YourMix باختيارات أذكى وأكثر تنوعاً.
+ تعزيز ميزة توليد قوائم التشغيل بواسطة الذكاء الاصطناعي.
+ تحسين دقة البحث وعرض النتائج لاكتشاف أسرع للمحتوى.
+ توسيع الدعم ليشمل مجموعة أوسع من صيغ وتنسيقات الملفات الصوتية.
+
+
+
+ حل مشكلات البيانات الوصفية المفاجئة لتبقى تفاصيل الأغنية دقيقة في كل مكان.
+ استعادة اختصارات الإشعارات لتعود بشكل موثوق ومباشر إلى شاشة التشغيل.
+
+
+
+ إعادة تصميم كبرى لنظام التنقل داخل التطبيق
+ مستكشف ملفات جديد لاختيار مجلدات المصادر
+ ميزات اتصال وبث (Casting) جديدة
+ استمرارية سلسة للتشغيل بين الأجهزة عن بعد
+ انتقال بدون فجوات (Gapless) بين الأغاني
+ التحكم في التداخل الصوتي (Crossfade)
+ ميزة الانتقالات المخصصة الجديدة (لقوائم التشغيل فقط)
+ استمرار التشغيل حتى بعد إغلاق التطبيق
+ تحسينات وتحسين أداء واجهة المستخدم
+ تحسين ميزة إحصاءات الاستماع
+ إعادة تصميم التحكم في قائمة الانتظار مع المزيد من الميزات
+ تحسين دعم أنواع الملفات المختلفة للتشغيل وتعديل البيانات الوصفية
+ تحسين نظام التحكم في الأذونات والصلاحيات
+ إصلاحات للأخطاء الطفيفة
+
+
+
+ تحديث واجهة المستخدم التعبيرية لـ Material 3 Expressive
+ معادل صوت بـ 10 حزم ترددية وتأثيرات صوتية متنوعة
+ نظام تدفق جديد لمزامنة المكتبة الموسيقية
+ تكامل مع الذكاء الاصطناعي (نماذج Gemini)
+ استيراد وتصدير قوائم التشغيل بصيغة M3U
+ دمج وتضمين صور الفنانين من منصة Deezer
+ أغلفة مخصصة لقوائم التشغيل
+ إعادة هيكلة وتطوير بنية الإعدادات
+ تأثيرات حركية جديدة لقائمة الانتظار والمشغل
+ تحسين الأداء العام واعتماد ملفات التعريف الأساسية (Baseline Profiles)
+ نظام كلمات أغاني أفضل مع إمكانية تعديل إزاحة التزامن (Sync Offset)
+
+
+
+ تحسينات على استقرار وثبات ميزة البث (Casting)
+ تحسين استقرار لوحة المشغل السفلى (Player Sheet)
+ إصلاحات عامة للأخطاء وتنظيف الكود
+
+
+
+ دعم Android Auto متاح الآن للتشغيل داخل السيارة.
+ إطلاق دعم Wear OS رسمياً، بما في ذلك عناصر تحكم أفضل للتشغيل من الساعة إلى الهاتف.
+ توسيع نطاق التكامل السحابي مع تحسينات لـ Telegram و NetEase و QQ Music و Google Drive.
+ ميزتا \"المشغلة مؤخراً\" واستعادة قائمة الانتظار المستمرة تبقيان جلسة استماعك جاهزة دائماً.
+ تضمين أدوات النسخ الاحتياطي والاستعادة (الإصدار الثالث v3) وأدوات إدارة الحسابات.
+ إضافة ميزة ذكية للبحث اليدوي عن كلمات الأغاني عند فشل البحث التلقائي مع تحسين التخزين.
+
+
+
+ قفزة كبيرة في الأداء تشمل الإقلاع، المكتبة، قائمة الانتظار، وتفاعلات المشغل.
+ إعادة تصميم واجهات المشغل، البث، كلمات الأغاني، الفنانين، والأنواع لتجربة أكثر سلاسة.
+ تدفقات التنقل والبحث أصبحت أكثر موثوقية مع معالجة آمنة للمسارات البرمجية.
+ تحسين التوافقية للتشغيل الصوتي ليشمل المزيد من الأجهزة وصيغ الصوت.
+ توسيع سير عمل التحديد المتعدد ليشمل الأغاني، الألبومات، وقوائم التشغيل.
+
+
+
+ سلوك قائمة الانتظار والخلط العشوائي أصبح الآن أكثر استقراراً وقابلية للتنبؤ.
+ إصلاح العديد من الحالات النادرة المتعلقة بالتشغيل في الخلفية والبث (Casting).
+ إصلاح مشاكل توقف مؤقت النوم، تصفح تبويب الملفات، وحالات الانهيار عند تصفح فنان الألبوم.
+ تحسين تحميل الودجت واستقرار الخدمة لتقليل استهلاك الذاكرة وسخونة الجهاز.
+ إصلاحات عامة للأخطاء ولمسات جمالية على واجهة المستخدم في مختلف أنحاء التطبيق.
+
+
+
+ نظام Wear OS: نقل الموسيقى، التشغيل المحلي، مزامنة قائمة الانتظار، والتحكم عن بعد من الساعة.
+ الذكاء الاصطناعي: دمج Groq AI و OpenRouter (تجريبي) مع تحسين استهلاك الرموز (Tokens).
+ السحاب: إضافة دعم خوادم Jellyfin.
+ كلمات الأغاني: ترجمة متزامنة مع مفتاح تبديل مخصص، دعم صيغة Kugou LRC، تخصيص محاذاة النص، وتحسين التحميل عن بعد.
+ واجهة المستخدم: وضع شريط التنقل المدمج، سمات ديناميكية مستوحاة من ألوان غلاف الألبوم، نص متحرك (Marquee) للعناوين الطويلة، وخيارات فرز جديدة.
+ تيليجرام: دعم أصلي للمواضيع (Topics) وأنماط عرض محسنة.
+
+
+
+ محرك الصوت: إعادة هيكلة شاملة للمحرك مع دعم صيغ إضافية (MIDI, ALAC, M4A) وتحسين برامج فك التشفير.
+ الكفاءة: تقليص حاد في استهلاك طاقة البطارية، علاج مشكلات السخونة، وتحسين المهام في الخلفية (SyncWorker).
+ قاعدة البيانات: تحسينات هائلة على الاستعلامات وإعادة تصميم ذاكرة التخزين المؤقت للأغلفة لمنع فقدان البيانات.
+ الإقلاع: تحسين وقت تحميل وتشغيل التطبيق عبر تحسين ملفات التعريف الأساسية (Baseline Profile).
+
+
+
+ التشغيل: إصلاح التقطع في صيغ Opus/MP3، أخطاء ReplayGain أثناء التداخل الصوتي، ومشاكل التشغيل في أجهزة فك التشفير من سامسونج.
+ الاستقرار: القضاء على حالات الانهيار عند بدء التشغيل، تصفح الفنانين، وعلى الأجهزة التي تعمل بنظام Android 12+.
+ الواجهة: إصلاح وميض أغلفة الألبومات، تداخل النصوص في الخطوط غير اللاتينية، وسلوك شريط التنقل والمشغل المصغر.
+ الأمان: تعزيز حماية وإدارة بيانات الاعتماد، أذونات التخزين، والاتصال بخوادم الوسائط.
+
+
+
+ اللغات المحلية: الإسبانية، الفرنسية، الروسية، الصينية المبسطة، الإندونيسية، الإيطالية
+
+
+
+ التكامل مع Google Drive مع إدارة دورة حياة المشغل.
+ تعديل جماعي للبيانات الوصفية للأغاني (العلامات الوصفية وأغلفة الألبومات).
+ ترجمة كلمات الأغاني بالذكاء الاصطناعي مع تفضيلات Wear OS القابلة للتخصيص.
+ أداة تشخيص تعليق وبطء التطبيق والتحديد المتعدد في شاشة البحث.
+ دعم اللغتين العربية والتركية، مع خيارات مخصصة لعناوين http المحلية في الشبكة الداخلية.
+
+
+
+ توفير كبير في طاقة البطارية (تعليق المهام الصوتية بذكاء وبوابات فحص واجهة المستخدم).
+ تحسين إدارة قائمة الانتظار (إدراج أسرع وفهرسة صريحة للمسارات).
+ تأثيرات حركية تعبيرية للحركة من Material 3 لشاشات الانتقال.
+ إعادة هيكلة مزامنة المكتبة عبر جدولة الفحص الذكي المقيد.
+
+
+
+ حل مشكلات ليد التقطع/تخطي المسارات أثناء التشغيل وبطء تحميل البافيرنج.
+ إصلاح مزامنة حذف الأغاني الخارجية واتساق البيانات الوصفية.
+ إصلاح مشكلات الذاكرة، الانهيارات، وعيوب التخطيط في نظام Wear OS والهواتف.
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-ar/strings_cloud_services.xml b/app/src/main/res/values-ar/strings_cloud_services.xml
new file mode 100644
index 000000000..1979d917a
--- /dev/null
+++ b/app/src/main/res/values-ar/strings_cloud_services.xml
@@ -0,0 +1,78 @@
+
+
+ تسجيل الدخول إلى تيليجرام
+ أنت تقوم بتعديل رقمك الآن. إرسال الرمز مجدداً سيؤدي إلى استبدال الرمز السابق.
+ جاري العمل…
+ جاري تهيئة تيليجرام…
+ جاري تسجيل الخروج…
+ جاري إغلاق الجلسة…
+ تم إغلاق الجلسة. يرجى إعادة فتح تسجيل الدخول للمتابعة.
+ جاري إعداد جلسة تيليجرام الآمنة…
+ في انتظار رد تيليجرام…
+ ربط تيليجرام
+ قم بربط حساب تيليجرام لبث الموسيقى من قنواتك ومحادثاتك.
+ رقم الهاتف
+ أدخل رقم تيليجرام الخاص بك. يمكنك العودة وتعديله لاحقاً.
+ رقم الهاتف
+ 1
+ 5551234567
+ إرسال الرمز
+ رمز التحقق
+ أدخل رمز التحقق المرسل إلى تطبيق تيليجرام الخاص بك.
+ رمز التحقق
+ كلمة المرور (التحقق بخطوتين)
+ حسابك محمي بكلمة مرور إضافية للتحقق بخطوتين. يرجى إدخالها أدناه.
+ كلمة المرور
+ تسجيل الدخول
+ تم ربط حساب تيليجرام بنجاح!
+ القنوات المتزامنة
+ إدارة القنوات العامة التي يتم جلب ملفات الصوت منها تلقائياً.
+
+ قطع اتصال %1$s؟
+ سيؤدي هذا إلى إزالة الحساب ومسح جميع الأغاني المستوردة من هذا المصدر من مكتبتك المزامنة.
+ قطع الاتصال
+ متصل كـ %1$s
+ متصل
+ حساب مجهول
+
+ ربط Google Drive
+ قم بربط حساب جوجل لبث وتشغيل ملفات الموسيقى المخزنة في سحابتك.
+ جاري إعداد اتصال Google Drive…
+ في انتظار مصادقة جوجل…
+ جاري مزامنة ملفات Google Drive…
+ فشلت مصادقة Google Drive: %1$s
+ فشل فحص مجلدات Google Drive.
+
+ ربط خادم Jellyfin
+ قم بالاتصال بخادم Jellyfin الشخصي الخاص بك لبث مكتبتك الموسيقية بالكامل.
+ رابط الخادم (Server URL)
+ https://jellyfin.example.com:8096
+ اسم المستخدم
+ اسم المستخدم
+ كلمة المرور
+ كلمة المرور
+ الاتصال بالخادم
+ جاري الاتصال بخادم Jellyfin…
+ فشل الاتصال بخادم Jellyfin. يرجى التحقق من الرابط والشبكة.
+ اسم المستخدم أو كلمة المرور غير صحيحة لخادم Jellyfin.
+
+ تم
+ جاري تحميل صفحة تسجيل الدخول الآمنة…
+ فشل تحميل واجهة الـ WebView.
+
+ فشل قراءة ملفات تعريف الارتباط (Cookies) لـ NetEase: %1$s
+ الخروج من تسجيل دخول NetEase؟
+ خطأ HTTP %1$d أثناء تحميل NetEase.
+ لم يتم رصد تسجيل الدخول بعد. أكمل تسجيل الدخول في NetEase قبل الضغط على \"تم\".
+ تسجيل الدخول إلى NetEase Music
+ ملاحظة أمنية: يتم إدخال كلمة المرور الخاصة بك فقط داخل صفحات ويب NetEase الرسمية. يقوم PixelPlayer بحفظ ملفات تعريف ارتباط الجلسة (MUSIC_U) فقط لمزامنة مكتبتك الموسيقية.
+ موسيقى NetEase
+
+ فشل قراءة ملفات تعريف الارتباط (Cookies) لـ QQ Music: %1$s
+ الخروج من تسجيل دخول QQ Music؟
+ خطأ HTTP %1$d أثناء تحميل QQ Music.
+ لم يتم رصد تسجيل الدخول بعد. أكمل تسجيل الدخول في QQ Music قبل الضغط على \"تم\".
+ تسجيل الدخول إلى QQ Music
+ ملاحظة أمنية: يتم إدخال كلمة المرور الخاصة بك فقط داخل صفحات ويب QQ Music الرسمية. يقوم PixelPlayer بحفظ ملفات تعريف ارتباط الجلسة المصادق عليها فقط لمزامنة مكتبتك الموسيقية.
+ موسيقى QQ Music
+
\ No newline at end of file
diff --git a/app/src/main/res/values-ar/strings_components.xml b/app/src/main/res/values-ar/strings_components.xml
deleted file mode 100644
index 4f8baf086..000000000
--- a/app/src/main/res/values-ar/strings_components.xml
+++ /dev/null
@@ -1,190 +0,0 @@
-
-
- انقر للفتح
- غلاف الألبوم
- مواضع غلاف الألبوم
- المفضلة
- تشغيل
- إيقاف مؤقت
- انقر للتشغيل
- عنوان الأغنية
- الفنان
- تكرار
- شريط التقدم، %1$d بالمئة
-
-
- المظهر
- المحاذاة
- عناصر التحكم
- إعادة تعيين كلمات الأغاني؟
- هل أنت متأكد من أنك تريد إعادة تعيين كلمات هذه الأغنية؟
- إخفاء عناصر تحكم المزامنة
- ضبط المزامنة
- إظهار اللتنّة (Romanization)
- إظهار الترجمات
- تعطيل الوضع الغامر (لمرة واحدة)
- إبقاء الشاشة قيد التشغيل
- محاذاة الكلمات لليسار
- محاذاة الكلمات للوسط
- محاذاة الكلمات لليمين
-
-
- لا يوجد اتصال بالإنترنت
- يتطلب هذا المحتوى اتصالاً بالإنترنت. يرجى التحقق من إعدادات الشبكة والمحاولة مرة أخرى.
- أنت غير متصل بالإنترنت
- يرجى التحقق من اتصالك بالإنترنت والمحاولة مرة أخرى للوصول إلى هذا المحتوى.
-
-
- حفظ موازن مخصص
- أدخل اسماً لإعداد موازن الصوت المخصص الجديد.
- اسم الإعداد المسبق
- إعادة تسمية الإعداد المسبق
- لا يمكن أن يكون الاسم فارغاً
- حفظ
- حفظ كجديد
- إعادة تسمية
-
-
- تم تحديث البيانات الوصفية بنجاح!
- البيانات الوصفية بالذكاء الاصطناعي
- جاري مراجعة دليل المزيج اليومي (Daily Mix)…
- مراجعة وتعديل التفاصيل التي تم إنشاؤها
- العنوان
- الفنان
- الألبوم
- فنان الألبوم
- النوع
- الملحن
- إعادة المحاولة
- تطبيق التغييرات
-
-
- تعديل البيانات الوصفية للأغنية
- قد يؤثر تعديل البيانات الوصفية للأغنية على كيفية عرضها وتنظيمها في مكتبتك. هذه التغييرات دائمة وقد لا يمكن التراجع عنها.
- فهمت ذلك
- معلومات
- تعديل الأغنية
- استخدام ذكاء Gemini الاصطناعي
- إظهار المعلومات
- رقم المسار
- رقم القرص
- ميزة ReplayGain للمسار (ديسيبل)
- ميزة ReplayGain للألبوم (ديسيبل)
- -6.50
- -8.20
- ميزة ReplayGain للمسار
- ميزة ReplayGain للألبوم
- العنوان
- رقم المسار
- رقم القرص
- البحث عن كلمات الأغاني على lrclib.net
- غلاف الألبوم
- اختر صورة مربعة وقم بضبطها لتظهر بشكل ممتاز في جميع أنحاء التطبيق.
- تغيير غلاف الألبوم
- حذف غلاف الألبوم
- معاينة الغلاف الجديد
- غلاف الأغنية الحالي
- ضبط غلاف الألبوم
- استخدم إيماءات القرص والسحب لتحديد الإطار المثالي.
- تطبيق غلاف الألبوم
- تعذر تحميل الصورة المحددة
-
-
- مشاركة ملف الأغنية عبر
- تشغيل الأغنية
- مشاركة ملف الأغنية
- إضافة إلى قائمة الانتظار
- التشغيل تالياً في قائمة الانتظار
- إضافة إلى قائمة التشغيل
- إضافة إلى قائمة الانتظار
- التالي
- جاري التحقق من الساعة…
- جاري النقل %1$d%%
- جاري النقل إلى الساعة…
- النقل قيد التنفيذ الآن
- إرسال إلى الساعة
- الساعة غير متاحة
- إرسال الأغنية إلى الساعة
- الساعة غير متاحة
- تعيين كـ
- تعيين كنغمة صوتية
- اختر كيفية استخدام هذه الأغنية كنغمة للنظام
- تعيين كنغمة رنين
- تعيين الأغنية كنغمة رنين
- استخدام هذه الأغنية كـ
- اختر المكان الذي يجب أن يقوم PixelPlayer بتثبيت هذا الصوت فيه.
- نغمة رنين الهاتف
- المكالمات الواردة
- صوت الإشعارات
- الرسائل وتنبيهات التطبيقات
- صوت المنبه
- منبهات الساعة
- تأكيد تغيير الصوت
- هل تريد تعيين \"%1$s\" كـ %2$s الخاص بك؟
- تعيين الصوت
- تم تعيين \"%1$s\" كـ %2$s الخاص بك
- نغمة رنين
- صوت إشعارات
- صوت منبه
- يرجى تمكين إذن "تعديل إعدادات النظام"، ثم العودة إلى PixelPlayer لإنهاء الإجراء تلقائياً.
- لم يتم تمكين إذن تعديل إعدادات النظام.
- تم تعيين \"%1$s\" كنغمة رنين لك
- يمكن استخدام الأغاني المحلية فقط كنغمات رنين.
- تعذر إعداد ملف الصوت هذا كنغمة رنين.
- تعذر تعيين نغمة الرنين: %1$s
- المدة
- معلومات الأغنية
- المدة
- النوع
- الألبوم
- الفنان
- صيغة الصوت
- المزود
- الملف
- تعديل البيانات الوصفية للأغنية
- إزالة من المفضلة
- إضافة إلى المفضلة
- الخيارات
- الخيارات
- التفاصيل
- المعلومات
- علامة تبويب التفاصيل
-
-
- %1$d أغنية
- تم تحديدها
- تشغيل الكل
- تشغيل الكل
- إعجاب بالكل
- إلغاء الإعجاب بالكل
- مشاركة الكل كملف ZIP
- إضافة الكل إلى قائمة الانتظار
- حذف الكل
- حذف الكل
-
- تم تجاهل قائمة التشغيل
- تراجع
- مزج DJ (Mashup)
- قائمة تشغيل جديدة
- اسم قائمة التشغيل
- قائمة التشغيل الخاصة بي
- إنشاء
- إضافة %1$d أغانٍ إلى…
- اختر قوائم التشغيل
- البحث عن قوائم التشغيل…
-
- %1$d قائمة تشغيل
- تصدير الكل
- دمج الكل
- مشاركة الكل
- تصدير
- دمج
-
- إعادة ترتيب علامات تبويب المكتبة
- إعادة تعيين الترتيب
- هل تريد إعادة تعيين ترتيب علامات التبويب إلى الوضع الافتراضي؟
- جاري إعادة ترتيب علامات التبويب…
- مقبض السحب
- إعادة تعيين
- تم
-
diff --git a/app/src/main/res/values-ar/strings_equalizer.xml b/app/src/main/res/values-ar/strings_equalizer.xml
new file mode 100644
index 000000000..d5dd0557e
--- /dev/null
+++ b/app/src/main/res/values-ar/strings_equalizer.xml
@@ -0,0 +1,45 @@
+
+
+ لا يمكن أن يكون الاسم فارغاً
+ إعادة تسمية
+
+ تغيير وضع العرض
+ تعطيل معادل الصوت
+ تفعيل معادل الصوت
+ تعديل
+ تعديل الأنماط المسبقة
+ نمط مخصص
+ الأنماط المسبقة
+ تحديث
+ تضخيم الباس (Bass Boost)
+ المجسم (Virtualizer)
+ جهارة الصوت (Loudness)
+ غير مدعوم
+ غير مدعوم على هذا الجهاز
+ مستوى الصوت
+ استجابة التردد
+ هرتز
+ كيلوهرتز
+ حفظ كجديد
+
+ الأنماط المحفوظة
+ لم يتم حفظ أي أنماط مخصصة بعد.
+ إلغاء التثبيت
+ تثبيت
+ إعادة تسمية
+ حذف
+
+ حفظ نمط مخصص
+ أدخل اسماً لنمط معادل الصوت المخصص الجديد.
+ اسم النمط
+ إعادة تسمية النمط
+
+ إدارة الأنماط المسبقة
+ اسحب لإعادة الترتيب • اضغط على العين للإظهار أو الإخفاء
+ إعادة الترتيب
+ إعادة تعيين الأنماط
+ سيؤدي هذا إلى استعادة الترتيب والظهور الافتراضي للأنماط المسبقة. هل تريد المتابعة؟
+ إعادة تعيين إلى الافتراضي
+ مرئي
+ مخفي
+
\ No newline at end of file
diff --git a/app/src/main/res/values-ar/strings_home_screen.xml b/app/src/main/res/values-ar/strings_home_screen.xml
new file mode 100644
index 000000000..d29658600
--- /dev/null
+++ b/app/src/main/res/values-ar/strings_home_screen.xml
@@ -0,0 +1,273 @@
+
+
+ β
+ تجريبي (Beta)
+ البث السحابي
+ سجل التغييرات
+ البث السحابي
+ بث الموسيقى من حساباتك السحابية
+
+ الإصدار التجريبي Beta 0.7.0
+ β
+ مرحباً بك في PixelPlayer 0.7.0-beta
+ أنت تستخدم إصداراً تجريبياً قد يحتوي على أخطاء، أو حالات انهيار، أو ميزات تجريبية. ساعدنا في التحسين من خلال الإبلاغ عن المشكلات.
+ ما الذي يجب توقعه
+ قد تحدث أخطاء، أو حالات انهيار، أو ميزات غير مكتملة بشكل غير متوقع.
+ قد تتغير بعض الميزات أو تتم إزالتها دون إشعار مسبق.
+ قد تكون الإصدارات التجريبية أقل استقراراً من الإصدارات الرسمية النهائية.
+ تحقق دائماً من وجود تحديثات قبل الإبلاغ عن مشكلة معروفة.
+ ما يمكن أن تغيره الإصدارات التجريبية، أو تعطلها، أو تحسنها أثناء الاختبار.
+ اختصار مشكلات GitHub
+ ابحث أولاً، ثم افتح تقريراً مركزاً ومحدداً للأخطاء، أو الانهيارات، أو الطلبات، أو الأسئلة.
+ عرض المشكلات الحالية
+ الإبلاغ عن مشكلة أو انهيار
+ شارك خطوات إعادة إنتاج المشكلة، والنتائج المتوقعة، والنتائج الفعلية، وتفاصيل جهازك/نظام تشغيلك.
+ كيفية الإبلاغ
+ قائمة مرجعية سريعة قبل فتح تذكرة مشكلة جديدة.
+ قبل فتح تذكرة مشكلة
+ ابحث في المشكلات الحالية المفتوحة والمغلقة لتجنب التكرار.
+ قم بالتحديث إلى أحدث إصدار من PixelPlayer وتأكد من أن المشكلة لا تزال تحدث.
+ أعد تشغيل التطبيق وتأكد من استمرار المشكلة.
+ حاول إعادة إنتاج المشكلة واكتب الخطوات الدقيقة لذلك.
+ ما هو نوع المشكلة؟
+ تقرير عن خطأ (Bug): شيء ما يتصرف بشكل غير صحيح.
+ طلب ميزة (Feature): إضافة ميزة جديدة أو تحسين.
+ سؤال: استخدم المناقشات (Discussions) إذا كانت مفعلة، أو افتح تذكرة مع تسمية سؤال.
+ تقرير عن خطأ
+ انسخ هذه الحقول عندما يتصرف التطبيق بشكل خاطئ أو ينهار.
+ تقرير خطأ برمجي
+ ملخص قصير:
+ السلوك المتوقع:
+ السلوك الحالي:
+ خطوات التشغيل/إعادة الإنتاج: 1. 2. 3.
+ كم مرة يحدث ذلك؟ دائماً / أحياناً / نادراً.
+ لقطة شاشة / فيديو: إن وجد.
+ السجلات / تتبع المكدس (Stack trace): إن وجد.
+ البيئة والمنصة
+ إصدار PixelPlayer:
+ مصدر التثبيت: إصدار GitHub، نسخة المطور (debug)، نسخة ليلية (nightly)، إلخ.
+ إصدار أندرويد:
+ طراز الجهاز:
+ سياق إضافي: استخدام بطاقة SD، إعدادات خاصة، أذونات، إلخ.
+ طلب ميزة جديدة
+ انسخ هذه الحقول عندما تريد ميزة جديدة أو تحسيناً.
+ شرح المشكلة: ما هي المشكلة التي تحاول حلها؟
+ الحل المقترح: كيف ينبغي أن تعمل الميزة؟
+ البدائل المدروسة: هل هناك أي طرق أخرى؟
+ النطاق: ما هي الشاشات أو التدفقات المتأثرة؟
+ تصميم تجريبي أو صورة مرجعية إن وجدت.
+ العناوين، الخصوصية، والنطاق
+ اجعل التقرير سهلاً في الفرز وآمناً للمشاركة.
+ عناوين جيدة للتذاكر
+ معادل الصوت: مؤشر التردد يتحرك عند التبديل بين تبويبات الأنماط
+ البحث: قائمة السجل لا تظهر عند البحث بنص فارغ
+ ميزة: إضافة خيار فرز قوائم التشغيل بحسب \"المضافة حديثاً\"
+ يرجى تجنب
+ التقارير العامة والمبهمة مثل \"التطبيق لا يعمل\".
+ طرح مشكلات متعددة غير مترابطة في تذكرة واحدة.
+ إرسال سجلات غير منقحة أو لقطات شاشة تحتوي على بيانات خاصة.
+ ملاحظة بشأن الخصوصية
+ قبل نشر السجلات أو لقطات الشاشة أو مقاطع الفيديو، يرجى إزالة أي معلومات شخصية أو خاصة.
+ الإصدارات الليلية (Nightly)
+ كيف تختلف النسخ الليلية عن الرسمية، وما الذي يجب تضمينه عندما تتوقف عن العمل.
+ يتم توليد الإصدارات الليلية تلقائياً من آخر التغييرات البرمجية المرفوعة، وقد تحتوي على ميزات غير مكتملة، أو أخطاء مؤقتة، أو تراجع في الأداء. إنها أكثر تجريبية من الإصدارات الرسمية.
+ يمكنك الوصول إليها عبر ملفات الـ Artifacts الخاصة بـ GitHub Actions في مستودع التطبيق إن وجدت.
+ الإبلاغ عن مشاكل النسخ الليلية
+ عند الإبلاغ عن مشكلة في إصدار ليلي، اذكر دائماً أن المشكلة حدثت في النسخة الليلية وليس في الإصدار الرسمي. قم بتضمين تاريخ البناء، أو اسم ورقم تشغيل الـ Workflow، أو رمز الـ Commit (SHA) إن أمكن. وتحقق أيضاً مما إذا كانت نفس المشكلة تحدث في أحدث إصدار رسمي.
+ التحديث إلى النسخة التجريبية Beta 0.5.0
+ يُنصح بتثبيت نظيف (Clean Install)
+ إذا كنت قادماً من إصدار تجريبي قديم، فقد يتطلب هذا التحديث تهيئة بيانات جديدة للمكتبة بدلاً من الاعتماد على البيانات القديمة المخزنة مؤقتاً.
+ إذا ظهرت البيانات الوصفية أو عناصر المكتبة بشكل خاطئ
+ البيانات الوصفية الخاطئة للأغاني، أو عدم تطابق الفنانين والألبومات، أو تكرار العناصر، يعني عادةً أن التثبيت النظيف هو الحل الأمثل.
+ لا تظهر ثانية
+ فهمت ذلك
+
+ عذراً! حدث خطأ ما
+ تعرض التطبيق لانهيار غير متوقع في جلستك الأخيرة. ساعدنا في حل المشكلة عبر مشاركة تقرير الانهيار.
+ التاريخ: %1$s
+ الخطأ:
+ تتبع المكدس (معاينة Stack trace):
+ سجل الانهيار
+ تم نسخ سجل الانهيار إلى الحافظة
+ تقرير انهيار PixelPlayer
+ مشاركة تقرير الانهيار
+
+ DJ Mashup
+
+ المزيج\nالخاص بك
+ لا توجد بيانات لعرضها بعد
+ سيظهر المزيج الخاص بك هنا عندما يجد PixelPlayer أغاني أو يزامن أحد المصادر.
+ تحديث
+
+ مزيجك اليومي (DAILY MIX)
+ بناءً على سجل استماعك
+ تفقد كل قوائم المزيج اليومي
+ مزيج يومي
+
+ المزيج اليومي
+
+ %1$d أغنية • %2$s
+ أغنية واحدة • %2$s
+ أغنيتان • %2$s
+ %1$d أغاني • %2$s
+ %1$d أغنية • %2$s
+ %1$d أغنية • %2$s
+
+ تشغيل
+ مولد قوائم التشغيل بالذكاء الاصطناعي
+
+ كيف يتم بناء مزيجك اليومي
+ يتم بناء مزيجك اليومي من أغانيك المفضلة والأكثر تشغيلاً. نضيف أيضاً مسارات من فنانين وأنواع موسيقية تحبها لتكتشف موسيقى جديدة.
+ أخبر الذكاء الاصطناعي بما تود الاستماع إليه اليوم
+ نحن نستخدم عينة صغيرة للحفاظ على انخفاض استهلاك الموارد
+ جاري التحديث…
+ تحديث المزيج اليومي
+
+
+ منسقة بشكل مثالي
+ المزيج اليومي
+ رحلتك الصوتية جاهزة الآن
+ مولد قوائم التشغيل بالذكاء الاصطناعي
+ صف الأجواء أو المزاج أو النشاط الذي تقوم به، ودع الذكاء الاصطناعي ينسق لك قائمة تشغيل مثالية من مكتبتك.
+ حجم قائمة التشغيل
+ أقل عدد أغانٍ
+ أقصى عدد أغانٍ
+ مثال: أجواء مسائية هادئة، طاقة حماسية للتمارين…
+ انقر لإعادة المحاولة
+ تم توليد رحلتك الصوتية بنجاح!
+ جاهز للتشغيل
+ جاري التوليد…
+ توليد قائمة تشغيل
+
+
+ المشغلة حديثاً
+
+
+ المشغلة حديثاً
+ تشغيل الأحدث
+ لا توجد أغانٍ مشغلة مؤخراً في %1$s
+ قم بتغيير النطاق الزمني أو شغّل المزيد من الأغاني لملء هذا الجدول الزمني.
+ المشغلة حديثاً
+ اليوم
+ أمس
+
+
+ إحصاءات الاستماع
+ إجمالي التشغيل
+ المعدل اليومي
+ الأغنية الأكثر تشغيلاً
+ %1$s • %2$d تشغيل
+
+
+ إحصاءات الاستماع
+ تحديث إحصاءات الاستماع
+ اليوم
+ هذا الأسبوع حتى اليوم
+ هذا الشهر حتى اليوم
+ هذه السنة حتى اليوم
+ كل الأوقات
+ الاستماع
+ مرات التشغيل
+ مخطط الاستماع الزمني
+ وقت الاستماع
+ إجمالي وقت الاستماع الذي تم تسجيله في النطاق المحدد.
+ عدد مرات التشغيل
+ كم عدد جلسات الاستماع التي أكملتها لكل قسم.
+ معدل الجلسة
+ متوسط مدة الاستماع لكل جلسة.
+ مقسمة إلى فترات مدتها 4 ساعات للكشف عن إيقاعك اليومي.
+ تسهل الأعمدة اليومية مقارنة عاداتك من أسبوع لآخر.
+ توضح الأعمدة الأسبوعية اتجاه الاستماع خلال الشهر.
+ توضح الأعمدة الشهرية التغيرات الموسمية على مدار العام.
+ تلخص الأعمدة السنوية سجل استماعك الكامل.
+ لا توجد بيانات استماع بعد
+ اضغط على زر التشغيل لبدء بناء مخططك الزمني للاستماع
+ الإيقاع اليومي
+ الإيقاع الأسبوعي
+ الإيقاع الشهري
+ لمحة عن السنة
+ التطور العام عبر الوقت
+ مجمعة في فترات من 4 ساعات
+ مجمعة بحسب أيام الأسبوع
+ مجمعة بحسب أسابيع الشهر
+ مجمعة بحسب الشهر
+ مجمعة بحسب السنة
+ فترة الذروة
+ %1$d تشغيل
+ —
+ أعلى الفئات
+ قارن طريقة استماعك بين الأنواع الموسيقية، الفنانين، الألبومات، والأغاني.
+ النوع
+ الفنان
+ الألبوم
+ الأغنية
+ الاستماع بحسب النوع
+ الاستماع بحسب الفنان
+ الاستماع بحسب الألبوم
+ الاستماع بحسب الأغنية
+ %1$d تشغيل • %2$d فنان
+ %1$d تشغيل • %2$d مسار
+ لا توجد بيانات فئات بعد
+ اضغط على زر التشغيل لإظهار أبرز تصنيفات استماعك
+ عادات الاستماع
+ لا توجد عادات مسجلة بعد
+ سنقوم بإظهار عادات استماعك بمجرد أن نتعرف على ذوقك الموسيقي بشكل أفضل.
+ إجمالي الجلسات
+ معدل الجلسة
+ أطول جلسة
+ جلسة/يوم
+ اليوم الأكثر نشاطاً
+ لا يوجد تشغيل بعد
+ فترة الاستماع الذروة
+ أبرز الفنانين
+ لا يوجد فنانون بارزون
+ استمر في الاستماع وسيظهر فنانوك المفضلون هنا.
+ \?
+ %1$d. %2$s
+ أبرز الألبومات
+ لا توجد ألبومات بارزة
+ الألبومات التي تعيد زيارتها بكثرة ستظهر هنا.
+ %1$d. %2$s
+ تركيز المسارات
+ كيف يتوزع وقت استماعك على مساراتك المفضلة والأكثر تشغيلاً.
+ لا توجد بيانات تركيز بعد
+ شغّل المزيد من المسارات لترى مدى تركيز استماعك.
+ الأعلى 1
+ الأعلى 2-3
+ الأخرى
+ %1$d%%
+ تركيز الاستماع
+ تشكل أعلى 3 مسارات ما نسبته %1$d%% من إجمالي وقت استماعك.
+ معدل التشغيل/للمسار
+ المسارات الفريدة
+ حصة أعلى 3
+ المسارات في هذا النطاق
+ المسارات الأكثر تشغيلاً في النطاق الزمني المحدد.
+ لا توجد مسارات بارزة
+ استمع إلى مفضلاتك لتراها مميزة هنا.
+ طي المسارات
+ إظهار كل المسارات
+
+
+ %1$d سا %2$02d د
+ %1$d د
+ %1$d سا %2$02d د
+ %1$d سا
+ %1$d د
+ %1$d ث
+ %1$dسا %2$02dد
+ %1$dسا
+ %1$dد
+ %1$dث
+ أبداً
+ الآن
+ منذ يوم واحد
+ منذ %1$d أيام
+ منذ ساعة واحدة
+ منذ %1$d ساعات
+ منذ دقيقة واحدة
+ منذ %1$d دقائق
+ أغنية %1$d
+ %1$d أغنية
+ الأسبوع %1$d
+
\ No newline at end of file
diff --git a/app/src/main/res/values-ar/strings_library.xml b/app/src/main/res/values-ar/strings_library.xml
new file mode 100644
index 000000000..905bca5b0
--- /dev/null
+++ b/app/src/main/res/values-ar/strings_library.xml
@@ -0,0 +1,597 @@
+
+
+
+ المكتبة
+ تبويبات المكتبة
+ انتقل مباشرة إلى أي تبويب أو أعد ترتيبها.
+ إعادة ترتيب التبويبات
+
+
+ الأغاني
+ الألبومات
+ الفنانون
+ قوائم التشغيل
+ المجلدات
+ المفضلة
+
+
+ تم إنشاء قائمة التشغيل بنجاح
+ يرجى تعيين مفتاح واجهة برمجة التطبيقات (API Key) لمزود الذكاء الاصطناعي أولاً
+ يرج تعيين مفتاح واجهة برمجة التطبيقات (API Key) لـ Gemini أولاً
+ تمت الإضافة إلى قائمة الانتظار
+ سيتم التشغيل تالياً
+
+
+ مراقبة النقل
+ الإعدادات
+ تعديل
+ إعادة ترتيب التبويبات
+ توسيع القائمة
+
+
+ يمكنك تحديد ما يصل إلى %1$d ألبومات
+ مجلد
+ مجلد
+
+
+ فرز حسب
+ المظهر
+ عرض قائمة التشغيل
+ شبكة
+ قائمة
+ الذاكرة الداخلية
+ بطاقة SD
+ بطاقة SD غير متوفرة حالياً.
+ السحابة
+ قنوات تليجرام السحابية
+ عرض المواضيع
+ القنوات
+ المواضيع
+ كلاهما
+ السحابة
+ السحابة فقط
+
+
+ جاري توليد البيانات الوصفية بالذكاء الاصطناعي…
+
+
+ خطأ أثناء تحميل الأغاني
+ خطأ أثناء تحميل الألبومات
+ خطأ أثناء تحميل الفنانين
+ إعادة المحاولة
+
+
+ لم يتم العثور على أغانٍ في مكتبتك.
+ جرّب إعادة فحص مكتبتك من الإعدادات إذا كانت لديك موسيقى على جهازك.
+ لم يتم العثور على أغانٍ
+
+
+ جديد
+ إنشاء قائمة تشغيل جديدة
+ استيراد قائمة تشغيل M3U
+ تحديد موقع الأغنية الحالية
+ كل الأغاني
+ سحابي
+ محلي
+ خيارات الفرز
+
+ الكل
+ إلغاء التحديد
+ المزيد من الخيارات
+
+
+ جاري فحص ملفات الموسيقى…
+ جاري معالجة الملفات…
+ %1$d من %2$d ملفات
+ جاري مزامنة المكتبة…
+ اكتملت المزامنة
+ في الانتظار…
+ جاري مزامنة المكتبة…
+ جاري تنظيف ذاكرة التخزين المؤقت لغلاف الألبومات…
+ جاري مزامنة المصادر السحابية…
+ جاري فحص كلمات الأغاني…
+
+
+ لا توجد أغانٍ بعد
+ أضف موسيقى إلى جهازك أو قم بمزامنة مصدر سحابي لبدء الاستماع.
+ لم يتم العثور على أغانٍ محلية
+ جرّب تصفية مصدر آخر أو أعد فحص مكتبة الجهاز.
+ لم يتم العثور على أغانٍ سحابية
+ قم بمزامنة أغاني تليجرام أو NetEase، أو تحوّل إلى المصدر المحلي.
+ لا توجد ألبومات متوفرة
+ ستظهر الألبومات هنا بمجرد أن تحتوي مكتبتك على مسارات مجمعة.
+ لم يتم العثور على ألبومات محلية
+ المسارات المحلية مطلوبة لإنشاء مجموعات ألبومات محلية.
+ لم يتم العثور على ألبومات سحابية
+ ستظهر الأغاني السحابية التي تحتوي على بيانات الألبوم هنا بعد المزامنة.
+ لا يوجد فنانون متوفرون
+ يظهر الفنانون بعد فهرسة الأغاني من أي مصدر.
+ لم يتم العثور على فنانين محليين
+ لا تتوفر بيانات وصفية للفنانين للأغاني المحلية حالياً.
+ لم يتم العثور على فنانين سحابيين
+ تظهر إدخالات الفنانين السحابيين عند مزامنة الأغاني عن بُعد.
+ لا توجد أغانٍ مفضلة بعد
+ اضغط على أيقونة القلب أثناء تشغيل أغنية لحفظها هنا.
+ لا توجد أغانٍ محلية مفضلة
+ قم بتغيير تصفية المصدر أو أضف أغانٍ من جهازك إلى المفضلة.
+ لا توجد أغانٍ سحابية مفضلة
+ أضف مسارات تليجرام أو NetEase إلى المفضلة لرؤيتها في هذا العرض.
+ لم يتم العثور على مجلدات
+ ستظهر مجلدات وحدة التخزين الداخلية التي تحتوي على موسيقى هنا.
+ لا توجد قوائم تشغيل بعد
+ أنشئ أول قائمة تشغيل لتنظيم مكتبتك.
+
+
+ تعديل البيانات الوصفية للأغنية
+ تشغيل
+ تشغيل الأغنية
+ تشغيل الكل
+ تشغيل الكل
+ إضافة إلى المفضلة
+ إضافة الكل إلى المفضلة
+ إزالة من المفضلة
+ إزالة الكل من المفضلة
+ مشاركة ملف الأغنية عبر
+ مشاركة ملف الأغنية
+ مشاركة الكل كملف ZIP
+ تعذر مشاركة الأغنية: %1$s
+ إضافة إلى قائمة الانتظار
+ إضافة إلى قائمة الانتظار
+ التالي
+ التشغيل تالياً في قائمة الانتظار
+ إضافة إلى قائمة التشغيل
+ حذف
+ حذف الكل
+ جاري التحقق من الساعة
+ جاري النقل %1$d%%
+ جاري النقل إلى الساعة
+ عملية النقل جارية
+ إرسال إلى الساعة
+ الساعة غير متوفرة
+ إرسال الأغنية إلى الساعة
+ الساعة غير متوفرة
+ تعيين كـ
+ تعيين كنغمة نظام
+ اختر كيفية استخدام هذه الأغنية كنغمة للنظام
+ استخدام هذه الأغنية كـ
+ اختر المكان الذي يجب أن يقوم PixelPlayer بتثبيت هذا الصوت فيه.
+ نغمة رنين الهاتف
+ المكالمات الواردة
+ صوت الإشعار
+ الرسائل وتنبيهات التطبيقات
+ صوت المنبه
+ منبهات الساعة
+ تأكيد تغيير الصوت
+ هل تريد تعيين \"%1$s\" كـ %2$s الخاصة بك؟
+ تعيين الصوت
+ تم تعيين \"%1$s\" كـ %2$s الخاصة بك
+ نغمة رنين
+ صوت إشعار
+ صوت منبه
+ يرجى تفعيل خيار \"تعديل إعدادات النظام\"، ثم العودة إلى PixelPlayer للإكمال تلقائياً.
+ لم يتم تفعيل صلاحية تعديل إعدادات النظام.
+ تم تعيين \"%1$s\" كنغمة رنين خاصة بك
+ يمكن استخدام الأغاني المحلية فقط كنغمات رنين.
+ تعذر إعداد ملف الصوت هذا ليصبح نغمة رنين.
+ تعذر تعيين نغمة الرنين: %1$s
+ الخيارات
+ الخيارات
+ التفاصيل
+ التفاصيل
+ المدة
+ النوع
+ الألبوم
+ الفنان
+ معلومات الأغنية
+ المزود
+ الملف
+ %1$d أغنية
+ تم تحديدها
+ %1$d قائمة تشغيل
+ %1$d ألبوم
+ تم تحديدها
+ الحد الأقصى: %1$d ألبومات لكل تحديد.
+ عمليات الإضافة لقائمة الانتظار والتشغيل تحترم ترتيب تحديدك.
+ %1$d نوع موسيقي
+ تم تحديدها
+ إجراء عمليات دفعة واحدة على جميع الأغاني ضمن هذه الأنواع الموسيقية.
+
+
+ الترتيب الافتراضي
+ العنوان (أ-ي)
+ العنوان (ي-أ)
+ الفنان
+ الفنان (ي-أ)
+ الألبوم
+ الألبوم (ي-أ)
+ تاريخ الإضافة
+ تاريخ الإضافة (الأقدم أولاً)
+ المدة
+ المدة (الأقصر أولاً)
+ سنة الإصدار
+ سنة الإصدار (الأقدم أولاً)
+ الأقل أغانٍ
+ الأكثر أغانٍ
+ الاسم (أ-ي)
+ الاسم (ي-أ)
+ عدد الأغاني (الأكثر)
+ عدد الأغاني (الأقل)
+ تاريخ الإنشاء
+ تاريخ الإنشاء (الأقدم أولاً)
+ تاريخ الإعجاب
+ تاريخ الإعجاب (الأقدم أولاً)
+ الأقل مجلدات فرعية
+ الأكثر مجلدات فرعية
+
+
+ العنوان
+ الفنان
+ الألبوم
+ تاريخ الإضافة
+ المدة
+ سنة الإصدار
+ عدد الأغاني
+ الاسم
+ عدد الأغاني
+ تاريخ الإنشاء
+ تاريخ الإعجاب
+ عدد المجلدات الفرعية
+
+
+ المصدر
+ الترتيب
+ تنازلي
+ تصاعدي
+ الترتيب الأصلي
+ انقر للتبديل إلى التصاعدي
+ انقر للتبديل إلى التنازلي
+ هذا الفرز يحافظ على ترتيبه الأصلي
+ المفتاح مفعّل
+
+
+ إعادة ترتيب تبويبات المكتبة
+ إعادة تعيين الترتيب
+ هل تريد إعادة تعيين ترتيب التبويبات إلى الوضع الافتراضي؟
+ جاري إعادة ترتيب التبويبات…
+ مقبض السحب
+
+
+ اختر فناناً
+ فنان واحد
+ %1$d فنانين
+ الفنان الرئيسي
+ صفحة الفنان
+
+
+ إلغاء النقل
+ %1$s / %2$s
+ يعرض التقدم المباشر لعمليات نقل الموسيقى من الهاتف إلى الساعة
+ نقل البيانات إلى الساعة
+ جاري الإرسال إلى الساعة
+ تم الإلغاء
+ تم إلغاء النقل
+ اكتمل النقل
+ مكتمل
+ فشل النقل
+ فشل عملية النقل
+ توجد عدة عمليات نقل نشطة
+ %1$s • %2$s
+ جاري التحضير
+ جاري تحضير النقل إلى الساعة
+ جاري تحضير النقل…
+ جاري إرسال %1$d أغنية إلى الساعة
+ جاري الإرسال إلى الساعة
+ جاري بدء النقل…
+ جاري البدء
+ جاري النقل
+ عدد عمليات النقل: %1$d
+
+
+ تعديل الأغنية
+ إظهار المعلومات
+ تعديل البيانات الوصفية للأغنية
+ قد يؤثر تعديل البيانات الوصفية للأغنية على كيفية عرضها وتنظيمها في مكتبتك. هذه التغييرات دائمة وقد لا يمكن التراجع عنها.
+ فهمت ذلك
+ معلومات
+ غلاف الألبوم
+ اختر صورة مربعة وقم بضبطها بدقة ليظهر غلاف الألبوم بشكل رائع في جميع أنحاء التطبيق.
+ تغيير غلاف الألبوم
+ حذف غلاف الألبوم
+ العنوان
+ الفنان
+ الألبوم
+ فنان الألبوم
+ النوع
+ الملحن
+ رقم المسار
+ رقم القرص
+ تعديل الصوت للمسار ReplayGain (ديسيبل)
+ تعديل الصوت للألبوم ReplayGain (ديسيبل)
+ -6.50
+ -8.20
+ معاينة غلاف الألبوم الجديد
+ غلاف الألبوم الحالي للأغنية
+ ضبط غلاف الألبوم
+ استخدم إيماءات القرص والسحب للوصول إلى الإطار المثالي.
+ تطبيق غلاف الألبوم
+ تعذر تحميل الصورة المحددة
+ البحث عن كلمات الأغاني على lrclib.net
+
+
+ تعديل %d أغنية
+ سيتم تحديث الحقول المعدلة فقط. اترك الحقول فارغة للاحتفاظ بالقيم الحالية.
+ (قيم مختلطة)
+ (اختياري - اترك الحقل فارغاً للتخطي)
+ تم تحديث %d أغنية بنجاح
+ تم تحديث %1$d من أصل %2$d أغنية. تعذر تعديل بعض الملفات.
+ فشل تحديث الأغاني
+ غلاف الألبوم للمجموعة
+ سيؤدي هذا إلى استبدال غلاف الألبوم لجميع الأغاني المحددة البالغ عددها %d
+ تعيين غلاف الألبوم للكل
+ إزالة جميع أغلفة الألبومات
+ (أغلفة متعددة ومختلفة)
+
+
+ تم تجاهل قائمة التشغيل
+
+
+ إنشاء قائمة تشغيل
+ اختر طريقة الإنشاء.
+ يدوي
+ صمم الغلاف والأيقونة والشكل واشمل الأغاني بنفسك.
+ بواسطة الذكاء الاصطناعي
+ توليد قائمة تشغيل منسقة مع خيارات تحكم متقدمة.
+ يتطلب تهيئة مفتاح واجهة برمجة التطبيقات (Gemini API key) في الإعدادات.
+ إعداد مفتاح API
+
+
+ مختبر قوائم التشغيل بالذكاء الاصطناعي
+ إعادة تعيين
+ جاري التوليد…
+ توليد
+ الهدف
+ اسم قائمة التشغيل (اختياري)
+ ما هو الطابع الذي تريده لقائمة التشغيل هذه؟
+ مثال: قيادة وقت الغروب مع أنغام هادئة
+ الاتجاه
+ الحالة المزاجية
+ النشاط
+ الحقبة الزمنية
+ محرك التنسيق
+ الحيوية
+ التحكم في إيقاع وسرعة الأغاني. 1 = هادئ/بطيء، 5 = حيوية عالية/سريع.
+ الاستكشاف
+ التحكم في مدى معرفتك بالاختيارات. 1 = الأغاني المفضلة الأكثر تشغيلاً، 5 = أغاني نادرة وغير مشغلة بكثرة.
+ أقل عدد أغانٍ
+ أقصى عدد أغانٍ
+ الفلاتر
+ منح الأولوية لأنواع موسيقية (اختياري)
+ مثال: synthwave, indie pop
+ تجنب أنواع موسيقية (اختياري)
+ مثال: metal, hard trap
+ اللغة المفضلة (اختياري)
+ مثال: الإنجليزية، العربية، معزوفة موسيقية
+ منح الأولوية للمفضلة
+ تجنب الكلمات غير اللائقة
+ معاينة الأمر (Prompt)
+ سيظهر أمرك النهائي هنا بمجرد إضافة تفضيلاتك.
+ تنسيق بدقة عالية
+ حدد الحالة المزاجية، النشاط، القيود، والعمق.
+ سيستخدم الذكاء الاصطناعي الأغاني من مكتبتك المحلية فقط.
+ أضف توجيهاً واحداً على الأقل للذكاء الاصطناعي.
+ يرجى تحديد نطاق أغانٍ صالح.
+ 5/%1$d
+ مخصص…
+ أدخل قيمة مخصصة
+ أدخل قيمتك المخصصة
+
+
+ أي حقبة
+ الطلب الأساسي: %1$s.
+ المزاج المستهدف: %1$s.
+ سياق النشاط: %1$s.
+ التركيز على الحقبة: %1$s.
+ الأولوية للأنواع: %1$s.
+ تجنب الأنواع: %1$s.
+ اللغة المفضلة: %1$s.
+ مستوى الحيوية المستهدف: 5/%1$d.
+ مستوى الاستكشاف المستهدف: 5/%1$d حيث 1 مألوف و 5 أغاني نادرة.
+ منح الأولوية للأغاني الأقرب لمفضلات المستمع عندما يكون ذلك ممكناً.
+ تجنب الكلمات غير اللائقة كلما توفرت بدائل.
+ الحفاظ على سلاسة الانتقالات وتجنب تكرار نفس الفنان بشكل متتابع.
+
+ هادئ
+ حيوي
+ سعيد
+ غامض
+ رومانسي
+ كئيب
+
+
+ تمارين رياضية
+ تركيز
+ رحلة على الطريق
+ حفلة
+ دراسة
+ وقت متأخر من الليل
+
+
+ @string/playlist_creation_ai_era_any
+ السبعينات
+ الثمانينات
+ التسعينات
+ الألفينات
+ 2010s
+ 2020s
+
+
+
+ لم يتم إنشاء أي قائمة تشغيل.
+ المس زر \"قائمة تشغيل جديدة\" للبدء.
+ قائمة تشغيل جديدة
+ اسم قائمة التشغيل
+ قائمة التشغيل الخاصة بي
+
+
+ إضافة %1$d أغنية إلى…
+ اختر قوائم التشغيل
+ البحث عن قوائم التشغيل…
+ تمت إضافة الأغاني إلى قوائم التشغيل
+ تم إنشاء قائمة التشغيل وإضافة الأغاني إليها
+ وحدة التخزين الداخلية
+
+
+ إضافة أغانٍ
+ إضافة الأغاني المحددة
+ إضافة
+ بحث أو تصفية الأغاني…
+ المفضلة
+ فشل تحميل الأغاني
+ تحميل المزيد
+
+
+ دمج قوائم التشغيل
+ أدخل اسماً لقائمة التشغيل المدمجة:
+ قائمة تشغيل مدمجة
+ سيؤدي هذا إلى دمج %1$d من قوائم التشغيل المحددة في قائمة واحدة.
+
+
+ لم يتم العثور على أغانٍ صالحة لتشغيلها
+ الأغنية غير موجودة في القائمة الحالية
+ تعذر تحديد موقع الأغنية
+ لا توجد أغانٍ في المكتبة
+ توقف التشغيل: انتهى %1$s (نهاية المسار).
+ مسار
+ لا توجد أغانٍ لخلطها.
+ الألبومات المحددة
+ لم يتم العثور على أغانٍ قابلة للتشغيل في الألبومات المحددة
+ لم يتم العثور على أغانٍ قابلة للتشغيل في الأنواع الموسيقية المحددة
+ تمت إضافة أول %1$d ألبومات فقط إلى قائمة الانتظار
+ تمت إضافة %1$d ألبومات إلى قائمة الانتظار (%2$d أغنية)
+ تعذر إضافة الألبومات المحددة إلى قائمة الانتظار
+ جميع الأغاني موجودة بالفعل في المفضلة
+ لم تكن أي من الأغاني في المفضلة
+ جاري إنشاء ملف ZIP…
+ فشلت المشاركة: %1$s
+
+ لم تضف أي أغنية لقائمة الانتظار
+ تمت إضافة أغنية واحدة إلى قائمة الانتظار
+ تمت إضافة أغنيتين إلى قائمة الانتظار
+ تمت إضافة %d أغانٍ إلى قائمة الانتظار
+ تمت إضافة %d أغنية إلى قائمة الانتظار
+ تمت إضافة %d أغنية إلى قائمة الانتظار
+
+
+ لن يتم تشغيل أي أغنية تالياً
+ ستعمل أغنية واحدة تالياً
+ ستعمل أغنيتان تالياً
+ ستعمل %d أغانٍ تالياً
+ ستعمل %d أغنية تالياً
+ ستعمل %d أغنية تالياً
+
+
+ لم تضف أي أغنية للمفضلة
+ تمت إضافة أغنية واحدة إلى المفضلة
+ تمت إضافة أغنيتين إلى المفضلة
+ تمت إضافة %d أغانٍ إلى المفضلة
+ تمت إضافة %d أغنية إلى المفضلة
+ تمت إضافة %d أغنية إلى المفضلة
+
+
+ لم تزل أي أغنية من المفضلة
+ تمت إزالة أغنية واحدة من المفضلة
+ تمت إزالة أغنيتين من المفضلة
+ تمت إزالة %d أغانٍ من المفضلة
+ تمت إزالة %d أغنية من المفضلة
+ تمت إزالة %d أغنية من المفضلة
+
+
+
+ لا توجد قوائم تشغيل لمشاركتها
+ مشاركة قوائم التشغيل
+ فشلت المشاركة: %1$s
+ لا توجد قوائم تشغيل لتصديرها
+ فشل التصدير: %1$s
+ الموسيقى/PixelPlayer Exports
+ يرجى تهيئة مفتاح Gemini API في الإعدادات.
+ تمت استعادة قائمة التشغيل
+
+ لا توجد قوائم تشغيل للمشاركة
+ جاري مشاركة قائمة تشغيل واحدة
+ جاري مشاركة قائمتي تشغيل
+ جاري مشاركة %d قوائم تشغيل
+ جاري مشاركة %d قائمة تشغيل
+ جاري مشاركة %d قائمة تشغيل
+
+
+ لم يتم تصدير أي قائمة تشغيل
+ تم تصدير قائمة تشغيل واحدة إلى %2$s
+ تم تصدير قائمتي تشغيل إلى %2$s
+ تم تصدير %1$d قوائم تشغيل إلى %2$s
+ تم تصدير %1$d قائمة تشغيل إلى %2$s
+ تم تصدير %1$d قائمة تشغيل إلى %2$s
+
+
+
+ معرف ألبوم غير صالح
+ لم يتم العثور على معرف الألبوم
+ خطأ أثناء تحميل بيانات الألبوم: %s
+ لم يتم العثور على الألبوم
+
+
+ معرف فنان غير صالح
+ لم يتم العثور على معرف الفنان
+ خطأ أثناء تحميل بيانات الفنان: %s
+ تعذر العثور على الفنان
+
+
+ لا يمكن حذف الأغنية التي يتم تشغيلها حالياً
+ تم حذف %1$d ملفات (تم تخطي %2$d ملفات - قيد التشغيل)
+ تم حذف %1$d من أصل %2$d ملفات
+ فشل حذف الملفات
+ تم حذف الملف
+ تعذر حذف الملف أو أنه غير موجود
+ تم إلغاء الحذف
+ هل تريد حذف الأغنية؟
+ \"%1$s\" بواسطة %2$s\n\nسيتم حذف هذه الأغنية نهائياً من جهازك ولا يمكن استعادتها.
+ سيتم حذف هذه الأغاني نهائياً من جهازك ولا يمكن استعادتها.
+
+ لم يتم حذف أي ملف
+ تم حذف ملف واحد
+ تم حذف ملفين
+ تم حذف %d ملفات
+ تم حذف %d ملفاً
+ تم حذف %d ملفاً
+
+
+ هل تريد حذف الأغاني؟
+ هل تريد حذف أغنية واحدة؟
+ هل تريد حذف أغنيتين؟
+ هل تريد حذف %d أغانٍ؟
+ هل تريد حذف %d أغنية؟
+ هل تريد حذف %d أغنية؟
+
+
+
+ تم تحديث البيانات الوصفية بنجاح
+ جاري تحديث %1$d أغنية…
+ تم تحديث %1$d أغنية بنجاح!
+ تم تحديث %1$d أغنية. الفاشلة: %2$d
+ تم حفظ كلمات الأغنية بنجاح
+ فشل حفظ كلمات الأغنية
+ لا توجد كلمات متاحة لحفظها
+ تم رفض الإذن – لا يمكن تعديل الملفات
+ تم رفض الإذن – لا يمكن حفظ كلمات الأغاني
+ تم رفض الإذن – لا يمكن تعديل هذا الملف
+
+
+ يرجى تهيئة مفتاح API صالح لمزود الذكاء الاصطناعي المحدد في الإعدادات.
+ خطأ في الذكاء الاصطناعي: %s
+ رفض مزود الذكاء الاصطناعي الطلب لعدم وجود رصيد كافٍ أو حصة استخدام متوفرة بالحساب.
+ نموذج الذكاء الاصطناعي المحدد لم يعد متوفراً. حاول PixelPlayer التبديل تلقائياً إلى نموذج مدعوم.
+ لم يتمكن الذكاء الاصطناعي من العثور على أي أغانٍ تناسب طلبك.
+ اكتب فكرة لـ \"الميكس اليومي\" الخاص بك
+ تم تحديث الميكس اليومي بواسطة الذكاء الاصطناعي
+ تعذر التحديث: %s
+ لم يتمكن الذكاء الاصطناعي من العثور على أغانٍ لهذا الميكس
+
\ No newline at end of file
diff --git a/app/src/main/res/values-ar/strings_player.xml b/app/src/main/res/values-ar/strings_player.xml
new file mode 100644
index 000000000..38450d7e6
--- /dev/null
+++ b/app/src/main/res/values-ar/strings_player.xml
@@ -0,0 +1,210 @@
+
+
+
+ طي المشغل
+ يتم تشغيله الآن
+ بث سحابي
+ بث لاسلكي (Cast)
+ بلوتوث
+ تشغيل محلي
+ جاري الاتصال…
+ فتح قائمة الانتظار
+
+
+ الاستعداد للاتصال
+ اسمح لـ PixelPlayer برؤية الأجهزة القريبة وشبكة الـ Wi‑Fi الحالية حتى نتمكن من الحفاظ على مزامنة البث، وصوت البلوتوث، ومكبرات الصوت.
+ الأجهزة القريبة
+ مطلوبة لقراءة والتحكم في أجهزة الصوت المتصلة عبر البلوتوث.
+ موقع شبكة الـ Wi‑Fi
+ يتطلب نظام أندرويد إذون الموقع لمشاركة شبكة الـ Wi‑Fi (SSID) التي تتصل بها لتحديد أجهزة البث المتوافقة.
+ السماح بالوصول
+ نحن نستخدم هذه الصلاحيات فقط لربط الأجهزة البينية — كالبث، والتحكم في مكبرات الصوت القريبة، ومزامنة الصوت.
+ توصيل الجهاز
+ جاري الفحص بالقرب منك
+ جلسة البث
+ جاري الاتصال
+ تم الاتصال
+ هذا الهاتف
+ صوت البلوتوث
+ تشغيل محلي
+ جاري التشغيل
+ متوقف مؤقتاً
+ مستوى صوت الجهاز
+ مستوى صوت الهاتف
+ %1$d/%2$d
+ مستوى البطارية
+ مستوى الصوت
+ قطع الاتصال
+ الاتصال
+ قم بتفعيل الـ Wi-Fi أو البلوتوث
+ تحديث الاتصالات
+ Wi-Fi
+ مغلق
+ مفعّل
+ متصل
+ بلوتوث
+ مغلق
+ مفعّل
+ متصل
+ الأجهزة القريبة
+ تحديث الأجهزة
+ متصل
+ جاري الاتصال
+ متاح للاتصال
+ متاح
+ جاري الاتصال...
+ جاري البحث عن أجهزة…
+ تأكد من أن التلفزيون أو مكبر الصوت قيد التشغيل ومتصل بنفس شبكة الـ Wi‑Fi.
+ عناصر التحكم
+ الأجهزة
+
+
+ خادم وسائط البث (Cast)
+ جاري البث إلى الجهاز
+ تقديم الوسائط إلى جهاز البث
+ %1$s: %2$s
+ التقديم والتأخير غير متاح مؤقتاً لتنسيق الصوت هذا أثناء البث لأنه قد يتسبب في إنهاء جلسة البث قسرياً.
+
+
+ مؤقت النوم
+ المؤقت
+ %1$d دقيقة
+ تم ضبط المؤقت لمدة %1$d دقيقة.
+ مرة واحدة
+
+ ولا مرة
+ مرة واحدة
+ مرتين
+ %d مرات
+ %d مرة
+ %d مرة
+
+ عدد مرات التشغيل: %1$s
+ نهاية المسار الحالي
+ سيتوقف التشغيل عند نهاية المسار.
+ تفعيل المفتاح
+ وقت مخصص
+ إلغاء المؤقت
+ نهاية المسار
+ تم إلغاء المؤقت.
+ لا يمكن تفعيل خيار نهاية المسار: لا توجد أغنية نشطة حالياً.
+ تم إلغاء تفعيل مؤقت نهاية المسار: تغيرت الأغنية من %1$s إلى %2$s.
+ المسار السابق
+ المسار الحالي
+ ضبط مدة مخصصة
+
+
+ التالي في قائمة الانتظار
+ قائمة الانتظار فارغة حالياً.
+
+ لا توجد مسارات مجهزة.
+ مسار واحد مجهز.
+ مساران مجهزان.
+ %d مسارات مجهزة.
+ %d مساراً مجهزاً.
+ %d مساراً مجهزاً.
+
+ قائمة الانتظار
+ قائمة الانتظار فارغة.
+ إعادة ترتيب الأغنية
+ تبديل الوضع العشوائي
+ تبديل وضع التكرار
+ مؤقت النوم
+ المزيد من الإجراءات
+ تحديد موقع الأغنية الحالية
+ مسح قائمة الانتظار
+ مسح قائمة الانتظار
+ هل أنت متأكد من أنك تريد مسح جميع الأغاني من قائمة الانتظار باستثناء الأغنية الحالية؟
+ حفظ كقائمة تشغيل
+ قائمة انتظار %1$s
+ قائمة الانتظار الحالية
+ تجاهل الأغنية
+ تمت إزالته
+ حفظ كقائمة تشغيل
+ إلغاء تحديد الكل
+ اسم قائمة التشغيل
+ البحث عن أغاني لتضمينها…
+ لا توجد أغاني تطابق \"%1$s\"
+
+ لم يتم تحديد أي أغنية
+ تم تحديد أغنية واحدة
+ تم تحديد أغنيتين
+ تم تحديد %d أغانٍ
+ تم تحديد %d أغنية
+ تم تحديد %d أغنية
+
+ حفظ باسم: %1$s
+ أدخل اسماً لقائمة التشغيل
+ إزالة من قائمة التشغيل
+ المزيد من الخيارات لـ %1$s
+
+
+ كلمات الأغاني
+ جاري تحميل كلمات الأغنية…
+ متزامنة
+ ثابتة
+ خيارات كلمات الأغاني
+ −0.5
+ −0.1
+ +0.1
+ +0.5
+ 0 ثانية
+ %1$+.1f ثانية
+
+
+ فشل البحث عن كلمات الأغنية
+ فشل جلب كلمات الأغنية من الخادم البعيد
+ انتهت مهلة الاتصال. يرجى التحقق من اتصالك بالإنترنت.
+ خطأ في الشبكة. يرجى التحقق من اتصالك بالإنترنت.
+ خطأ في الخادم (رمز %d). يرجى المحاولة مرة أخرى لاحقاً.
+
+
+ كلمات الأغنية متوفرة بالفعل. تم تخطي الجلب عبر الإنترنت.
+ تم العثور على كلمات مدمجة بالفعل. تم تخطي الجلب عبر الإنترنت.
+ تم العثور على ملف كلمات محلي (.lrc) بالفعل. تم تخطي الجلب عبر الإنترنت.
+
+
+ حفظ كلمات الأغنية
+ الترجمة بواسطة الذكاء الاصطناعي
+ تحتوي هذه الكلمات على ترجمة بالفعل
+ هذه الكلمات مكتوبة بالفعل بهذه اللغة
+ لم يتم تهيئة واجهة برمجة التطبيقات (API)
+ تمت ترجمة كلمات الأغنية بنجاح!
+ جاري ترجمة كلمات الأغنية…
+ إعادة تعيين الكلمات المستوردة
+ إعادة تعيين كلمات الأغنية؟
+ هل أنت متأكد من أنك تريد إعادة تعيين كلمات هذه الأغنية؟
+ المظهر
+ المحاذاة
+ محاذاة الكلمات لليسار
+ محاذاة الكلمات للوسط
+ محاذاة الكلمات لليمين
+ عناصر التحكم
+ ضبط المزامنة
+ إخفاء عناصر تحكم المزامنة
+ إظهار اللتننة (Romanization)
+ إظهار الترجمات
+ إيقاف الوضع الغامر (لمرة واحدة)
+ إبقاء الشاشة قيد التشغيل
+
+
+ حفظ كلمات الأغنية
+ اختر النسخة المراد حفظها:
+ متزامنة (مع الطوابع الزمنية)
+ عادية (نص فقط)
+
+
+ هل تود البحث عن كلمات الأغنية عبر الإنترنت؟
+ إظهار خيارات كلمات الأغاني
+ فتح نافذة الاختيار دائماً بدلاً من التطبيق التلقائي لأول نتيجة مطابقة
+ جاري البحث عن كلمات الأغنية…
+ لم يتم العثور على كلمات الأغنية
+ لم نتمكن من العثور على الكلمات تلقائياً. يمكنك تعديل العنوان أو اسم الفنان والمحاولة بالبحث يدوياً.
+ عنوان الأغنية
+ الفنان (اختياري)
+ تم العثور على %d نتيجة مطابقة
+ متزامنة
+ %1$s • %2$s
+ تتوفر كلمات الأغاني بواسطة
+ https://lrclib.net/
+
\ No newline at end of file
diff --git a/app/src/main/res/values-ar/strings_presentation_batch_a.xml b/app/src/main/res/values-ar/strings_presentation_batch_a.xml
deleted file mode 100644
index 904610ae4..000000000
--- a/app/src/main/res/values-ar/strings_presentation_batch_a.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
- ملاحظة أمان: يتم إدخال كلمة المرور الخاصة بك فقط داخل صفحات ويب QQ Music. يقوم PixelPlayer بحفظ ملفات تعريف ارتباط الجلسة (Cookies) لمزامنة مكتبتك الموسيقية.
- ملاحظة أمان: يتم إدخال كلمة المرور الخاصة بك فقط داخل صفحات ويب NetEase. يقوم PixelPlayer بحفظ ملفات تعريف ارتباط الجلسة (MUSIC_U) لمزامنة مكتبتك الموسيقية.
- فشل في قراءة ملفات تعريف ارتباط QQ Music: %1$s
- فشل في قراءة ملفات تعريف ارتباط NetEase: %1$s
-
- جاري إعداد Google Drive…
- ربط Google Drive
- بث ملفات الموسيقى مباشرة من حساب Google Drive الخاص بك
- تسجيل الدخول باستخدام Google
- اختر مجلد الموسيقى
- اختر أو أنشئ مجلداً لاستخدامه كمصدر للموسيقى الخاصة بك
- إنشاء مجلد \"PixelPlayer Music\"
- أنشئ مجلداً جديداً هنا للموسيقى الخاصة بك
- لا توجد مجلدات هنا
-
diff --git a/app/src/main/res/values-ar/strings_presentation_batch_b.xml b/app/src/main/res/values-ar/strings_presentation_batch_b.xml
deleted file mode 100644
index a0713c170..000000000
--- a/app/src/main/res/values-ar/strings_presentation_batch_b.xml
+++ /dev/null
@@ -1,88 +0,0 @@
-
-
-
- الخدمات المرتبطة
- الحسابات المتصلة
- إدارة المزودين المرتبطين وإبقاء كل عملية ربط تحت سيطرتك.
- نشط
- متاح
- قريباً
- متصل
- فتح الخدمة
- قريباً
- جاري تسجيل الخروج…
- لا توجد حسابات مرتبطة بعد
- قم بربط أحد المزودين لتتمكن من إدارته من هذه الشاشة.
- ربط %1$s
- %1$s (قريباً)
- Telegram
- NetEase Music
-
-
- فرز الأغاني
- المزيد من الخيارات
- تشغيل
- إضافة أغانٍ
- إضافة
- إزالة الأغاني
- إعادة ترتيب الأغاني
- إعادة ترتيب
- إعادة ترتيب الأغنية
- قائمة التشغيل هذه فارغة.
- هذا المجلد لا يحتوي على أغانٍ.
- انقر على "إضافة أغانٍ" للبدء.
- خيارات قائمة التشغيل
- تعديل قائمة التشغيل
- حذف قائمة التشغيل
- تعيين الانتقال الافتراضي
- تصدير قائمة التشغيل
- حذف قائمة التشغيل؟
- هل أنت متأكد من أنك تريد حذف قائمة التشغيل هذه؟
- إعادة تسمية قائمة التشغيل
- الاسم الجديد
-
-
- المزيج اليومي
-
-
- اختر الأغاني
- اختر النوع
- البحث عن الأغاني
- تحديد الكل
- مسح
- النوع: %1$s
- اختر نوعاً
- الملء السريع
- إضافة نوع مخصص
- نوع جديد
- إضافة نوع مخصص
- اسم النوع
- اختر أيقونة
-
-
- المشغلة حديثاً
- تشغيل الأحدث
- لا توجد أغانٍ مشغلة مؤخراً في %1$s
- قم بتغيير النطاق الزمني أو تشغيل المزيد من الأغاني لملء هذا الجدول الزمني.
- المشغلة حديثاً
- اليوم
- أمس
-
-
- ضبط نصف قطر الزوايا
- قم بمطابقة زوايا شريط التنقل مع الزوايا الفيزيائية لجهازك للحصول على مظهر متناسق وانسيابي.
- نصف قطر الزوايا
- %1$d dp
-
-
- تشغيل %1$s عشوائياً
-
-
- لا توجد أغانٍ • %2$s
- أغنية واحدة • %2$s
- أغنيتان • %2$s
- %1$d أغانٍ • %2$s
- %1$d أغنية • %2$s
- %1$d أغنية • %2$s
-
-
diff --git a/app/src/main/res/values-ar/strings_presentation_batch_c.xml b/app/src/main/res/values-ar/strings_presentation_batch_c.xml
deleted file mode 100644
index 23cf57e71..000000000
--- a/app/src/main/res/values-ar/strings_presentation_batch_c.xml
+++ /dev/null
@@ -1,83 +0,0 @@
-
-
-
- خطأ أثناء تحميل الأغاني
- خطأ أثناء تحميل الألبومات
- خطأ أثناء تحميل الفنانين
- إعادة المحاولة
-
-
- لم يتم العثور على أغانٍ في مكتبتك.
- جرّب إعادة فحص مكتبتك من الإعدادات إذا كانت لديك ملفات موسيقى على جهازك.
- لم يتم العثور على أغانٍ
-
-
- جديد
- إنشاء قائمة تشغيل جديدة
- استيراد قائمة تشغيل M3U
- تحديد موقع الأغنية الحالية
- جميع الأغاني
- سحابي
- محلي
- خيارات الفرز
-
-
- متزامنة
- الفنان (اختياري)
-
-
- إضافة أغانٍ
- إضافة الأغاني المحددة
- إضافة
- بحث أو تصفية الأغاني…
- المحبوبة
- فشل تحميل الأغاني
- تحميل المزيد
-
-
- ذكاء اصطناعي
- منسقة بشكل مثالي
- المزيج اليومي
- رحلتك الصوتية جاهزة الآن
- صانع قوائم التشغيل بالذكاء الاصطناعي
- صف الأجواء أو الحالة المزاجية أو النشاط، ودع الذكاء الاصطناعي يتولى تنسيق قائمة التشغيل المثالية من مكتبتك.
- حجم قائمة التشغيل
- الحد الأدنى للأغاني
- الحد الأقصى للأغاني
- مثال: أجواء مسائية هادئة، طاقة حماسية للتمارين…
- انقر لإعادة المحاولة
- تم توليف رحلتك الصوتية!
- جاري الإنشاء…
- جاهز للتشغيل
- إنشاء قائمة التشغيل
-
-
- لا توجد أغانٍ بعد
- أضف موسيقى إلى جهازك أو قم بمزامنة مصدر سحابي لبدء الاستماع.
- لم يتم العثور على أغانٍ محلية
- جرّب فلتراً آخر للمصادر أو أعد فحص مكتبة جهازك.
- لم يتم العثور على أغانٍ سحابية
- قم بمزامنة أغانٍ من Telegram أو NetEase، أو تحوّل إلى المصدر المحلي.
- لا توجد ألبومات متاحة
- ستظهر الألبومات هنا بمجرد أن تحتوي مكتبتك على مسارات مجمعة.
- لم يتم العثور على ألبومات محلية
- يلزم وجود أغانٍ محلية لإنشاء مجموعات ألبومات محلية.
- لم يتم العثور على ألبومات سحابية
- ستظهر الأغاني السحابية التي تحتوي على بيانات الألبوم هنا بعد المزامنة.
- لا يوجد فنانون متاحون
- يتم عرض الفنانين بعد فهرسة الأغاني من أي مصدر.
- لم يتم العثور على فنانين محليين
- لا تتوفر بيانات وصفية للفنانين للأغاني المحلية في الوقت الحالي.
- لم يتم العثور على فنانين سحابيين
- تظهر إدخالات الفنانين السحابيين عند مزامنة الأغاني عن بُعد.
- لا توجد أغانٍ مفضلة بعد
- انقر على أيقونة القلب أثناء تشغيل أي أغنية لحفظها هنا.
- لا توجد أغانٍ محلية مفضلة
- قم بتغيير فلتر المصدر أو أضف إعجاباً بالأغاني الموجودة على جهازك.
- لا توجد أغانٍ سحابية مفضلة
- أضف إعجاباً بمسارات Telegram أو NetEase لرؤيتها في هذا العرض.
- لم يتم العثور على مجلدات
- ستظهر مجلدات وحدة التخزين الداخلية التي تحتوي على موسيقى هنا.
- لا توجد قوائم تشغيل بعد
- أنشئ قائمة تشغيلك الأولى لتنظيم مكتبتك الموسيقية.
-
diff --git a/app/src/main/res/values-ar/strings_presentation_batch_d.xml b/app/src/main/res/values-ar/strings_presentation_batch_d.xml
deleted file mode 100644
index 45159b522..000000000
--- a/app/src/main/res/values-ar/strings_presentation_batch_d.xml
+++ /dev/null
@@ -1,124 +0,0 @@
-
-
- المكتبة
- نقل إلى الساعة
- الإعدادات
- تعديل
- إعادة ترتيب علامات التبويب
- فرز حسب
- سحابي
- عرض
- قنوات Telegram السحابية
- عرض قوائم التشغيل
- شبكة
- قائمة
- الذاكرة الداخلية
- بطاقة SD
- بطاقة SD غير متاحة حالياً.
- عرض المواضيع
- القنوات
- المواضيع
- كلاهما
- سحابي
- سحابي فقط
- جاري إنشاء البيانات الوصفية بالذكاء الاصطناعي…
- يمكنك تحديد ما يصل إلى %1$d ألبومات
- مجلد
- توسيع القائمة
- علامات تبويب المكتبة
- الانتقال مباشرة إلى أي علامة تبويب أو إعادة ترتيبها.
- إعادة ترتيب علامات التبويب
- مجلد
-
- جاري الإرسال إلى الساعة
- جاري بدء النقل…
- جاري النقل
- تم بنجاح
- فشل النقل
- تم الإلغاء
- جاري التحضير
- جاري تحضير النقل…
- إلغاء النقل
-
- دمج قوائم التشغيل
- أدخل اسماً لقائمة التشغيل المدمجة:
- قائمة تشغيل مدمجة
- سيؤدي هذا إلى دمج %1$d من قوائم التشغيل المحددة في قائمة واحدة.
-
- مساحة الـ DJ
- جاري التحميل…
- منصة (Deck) %1$d
- تحميل أغنية
- لم يتم تحميل أي أغنية
- …
- ميزة فصل المسارات الصوتيّة (Stems) غير متاحة بعد.
- مستوى الصوت
- السرعة
- ممازج الصوت (Crossfader)
- منصة 1
- منصة 2
- اختر أغنية
-
- تغيير وضع العرض
- تعطيل موازن الصوت
- تمكين موازن الصوت
- تعديل
- تعديل الإعدادات المسبقة
- إعداد مخصص
- الإعدادات المسبقة
- تحديث
- تضخيم الباس (Bass Boost)
- المحاكي المحيطي (Virtualizer)
- جهارة الصوت (Loudness)
- غير مدعوم
- غير مدعوم على هذا الجهاز
- مستوى الصوت
- الاستجابة الترددية
- هرتز
- الباس (الترددات المنخفضة)
- الترددات المتوسطة المنخفضة
- الترددات المتوسطة المرتفعة
- التريبل (الترددات الحادة)
- الباس / منخفض
- متوسط / مرتفع
- صفحة %1$d
- إعادة تعيين المدة
-
- يتم استخدام الإعدادات الافتراضية العامة
- تم حفظ التغييرات بنجاح
- قواعد قائمة التشغيل
- الانتقالات العامة
- حفظ
- تخصيص السلوك الافتراضي لقائمة التشغيل هذه تحديداً.
- يطبق هذا التكوين على جميع مصادر التشغيل ما لم يتم تجاوزه.
- حالة التنشيط
- الافتراضي العام
- تابع للإعداد العام
- تجاوز مخصص
- افتراضي قائمة التشغيل
- تجاوز مخصص
- قم بالتمكين لتعيين قواعد خاصة بقائمة التشغيل هذه.
- نمط الانتقال
- كيفية تداخل المسارات الصوتية معاً
- التداخل المتلاشي (Crossfade)
- بدون انتقال
- مدة الانتقال
- إجمالي التداخل %1$d ثوانٍ
- إعادة تعيين
- الأغنية الحالية
- الأغنية التالية
- ستتداخل المسارات لمدة %1$d ثوانٍ
- منحنيات مستوى الصوت
- ضبط ميل وتلاشي الصوت بدقة
- تلاشي للخارج (Fade Out)
- تلاشي للداخل (Fade In)
-
- تشغيل %1$s
- طي %1$s
- توسيع %1$s
- تعديل صورة الفنان
- تغيير الصورة
- إعادة تعيين للوضع الافتراضي
- تشغيل أغاني الفنان عشوائياً
- الفنان
-
diff --git a/app/src/main/res/values-ar/strings_presentation_batch_e.xml b/app/src/main/res/values-ar/strings_presentation_batch_e.xml
deleted file mode 100644
index 22192fce5..000000000
--- a/app/src/main/res/values-ar/strings_presentation_batch_e.xml
+++ /dev/null
@@ -1,160 +0,0 @@
-
-
-
- قائمة الانتظار فارغة.
- إجراءات قائمة الانتظار
- مسح قائمة الانتظار
- حفظ كقائمة تشغيل
- تحديد موقع الأغنية الحالية
- قائمة انتظار %1$s
- قائمة الانتظار الحالية
- تمت الإزالة
- مسح قائمة الانتظار
- هل أنت متأكد من أنك تريد مسح جميع الأغاني من قائمة الانتظار باستثناء الأغنية الحالية؟
- التالي في القائمة
- قائمة الانتظار فارغة حالياً.
- قائمة الانتظار
- تبديل التشغيل العشوائي
- تبديل التكرار
- مؤقت النوم
- حفظ كقائمة تشغيل
- إلغاء تحديد الكل
- اسم قائمة التشغيل
- ابحث عن أغانٍ لتضمينها…
- حفظ باسم: %1$s
- أدخل اسماً لقائمة التشغيل
- لا توجد أغانٍ تطابق \"%1$s\"
- تجاهل الأغنية
- إزالة من قائمة التشغيل
- المزيد من الخيارات لـ %1$s
-
-
- لا توجد مسارات منتظرة.
- مسار واحد منتظر.
- مساران منتظران.
- %d مسارات منتظرة.
- %d مساراً منتظراً.
- %d مسار منتظر.
-
-
- لم يتم تحديد أي أغنية
- تم تحديد أغنية واحدة
- تم تحديد أغنيتين
- تم تحديد %d أغانٍ
- تم تحديد %d أغنية
- تم تحديد %d أغنية
-
-
-
- لم يتم إنشاء أي قائمة تشغيل بعد.
- المس زر "قائمة تشغيل جديدة" للبدء.
-
-
- إنشاء قائمة تشغيل
- اختر طريقة الإنشاء.
- يدوياً
- صمم الغلاف، الأيقونة، الشكل، واشحن الأغاني بنفسك.
- بالذكاء الاصطناعي
- أنشئ قائمة تشغيل منسقة ومخصصة عبر خيارات متقدمة.
- تتطلب هذه الميزة تهيئة مفتاح Gemini API في الإعدادات.
- إعداد مفتاح API
-
-
- معمل قوائم التشغيل بالذكاء الاصطناعي
- إعادة تعيين
- جاري الإنشاء…
- إنشاء
- الهدف والأجواء
- اسم قائمة التشغيل (اختياري)
- ما هي الأجواء التي ترغب بها في قائمة التشغيل هذه؟
- مثال: قيادة وقت الغروب مع ألحان سينث دافئة
- الاتجاه الفني
- الحالة المزاجية
- النشاط
- الحقبة الزمنية
- محرك التنسيق
- الحيوية والطاقة
- تتحكم في حدة الأغاني وإيقاعها. 1 = هادئ/بطيء، 5 = حماسي جداً/سريع.
- عمق الاكتشاف
- تتحكم في مدى معرفتك بالاختيارات. 1 = المفضلة الأكثر تشغيلاً، 5 = أغانٍ نادرة ولم تسمعها كثيراً.
- أقل عدد أغانٍ
- أقصى عدد أغانٍ
- الفلاتر
- أنواع موسيقية مفضلة (اختياري)
- مثال: سينث ويف، إندي بوب
- أنواع موسيقية تتجنبها (اختياري)
- مثال: ميتال، هارد تراب
- اللغة المفضلة (اختياري)
- مثال: الإنجليزية، العربية، معزوفات موسيقية
- إعطاء الأولوية للمفضلة
- تجنب الكلمات غير لائقة (Explicit)
- معاينة الأمر (Prompt)
- سيظهر أمرك النهائي هنا بمجرد إضافة تفضيلاتك.
- تنسيق بدقة متناهية
- حدد المزاج، النشاط، القيود، وعمق الاختيارات.
- سيقوم الذكاء الاصطناعي باختيار الأغاني من مكتبتك المحلية فقط.
- يرجى إضافة توجيه واحد على الأقل للذكاء الاصطناعي.
- يرجى تعيين نطاق أغانٍ صالح.
- 5/%1$d
- مخصص…
- إدخال قيمة مخصصة
- أدخل قيمتك المخصصة
-
-
- أي حقبة
- الطلب الأساسي: %1$s.
- المزاج المستهدف: %1$s.
- سياق النشاط: %1$s.
- التركيز على الحقبة: %1$s.
- إعطاء الأولوية للأنواع: %1$s.
- تجنب الأنواع: %1$s.
- اللغة المفضلة: %1$s.
- مستوى الطاقة المستهدف: 5/%1$d.
- هدف الاكتشاف: 5/%1$d حيث 1 تعني مألوف و5 تعني اختيارات عميقة ونادرة.
- إعطاء الأولوية للأغاني القريبة من مفضلات المستمع كلما أمكن ذلك.
- تجنب الأغاني ذات الكلمات غير اللائقة كلما توفرت بدائل.
- الحفاظ على سلاسة الانتقالات وتجنب التكرار المتتالي لنفس الفنان.
-
-
- هادئ (Chill)
- حماسي (Energetic)
- سعيد (Happy)
- داكن/غامض (Dark)
- رومانسي (Romantic)
- شجي/ميلانكولي (Melancholic)
-
-
- تمارين رياضية (Workout)
- تركيز (Focus)
- رحلة على الطريق (Road trip)
- حفلة (Party)
- دراسة (Study)
- وقت متأخر من الليل (Late night)
-
-
- @string/presentation_batch_e_ai_era_any
- السبعينات
- الثمانينات
- التسعينات
- الألفينات (2000s)
- العقد 2010
- العقد 2020
-
-
-
- إعادة تعيين الإعدادات المسبقة
- سيؤدي هذا إلى استعادة الترتيب الافتراضي وظهور الإعدادات المسبقة. هل تريد المتابعة؟
- إدارة الإعدادات المسبقة
- اسحب لإعادة الترتيب • انقر على أيقونة العين للإظهار أو الإخفاء
- إعادة تعيين للوضع الافتراضي
- مرئي
- مخفي
-
-
- كيف يتم بناء المزيج اليومي الخاص بك
- يتم بناء المزيج اليومي الخاص بك (Daily Mix) بناءً على أغانيك المفضلة والأكثر تشغيلاً. نقوم أيضاً بإضافة مسارات من فنانين وأنواع موسيقية تحبها لتتمكن من اكتشاف موسيقى جديدة.
- أخبر الذكاء الاصطناعي بما تود الاستماع إليه اليوم
- نحن نستخدم عينة صغيرة للحفاظ على انخفاض استهلاك البيانات والتكلفة
- جاري التحديث…
- تحديث المزيج اليومي
-
diff --git a/app/src/main/res/values-ar/strings_presentation_batch_f.xml b/app/src/main/res/values-ar/strings_presentation_batch_f.xml
deleted file mode 100644
index 8dc2ea76c..000000000
--- a/app/src/main/res/values-ar/strings_presentation_batch_f.xml
+++ /dev/null
@@ -1,239 +0,0 @@
-
-
- محدد
- تحديث المكتبة
- فحص المكتبة بأكملها بحثاً عن الملفات الجديدة والمعدلة.
- إعادة فحص كاملة
- إعادة بناء قاعدة البيانات
- جاري تحضير المزامنة
- جاري قراءة مخزن الوسائط (MediaStore)
- جاري معالجة المسارات
- جاري الحفظ في قاعدة البيانات
- جاري فحص ملفات كلمات الأغاني (LRC)
- جاري تنظيف ذاكرة التخزين المؤقت لأغلفة الألبومات
- جاري مزامنة المصادر السحابية
- جاري إتمام المزامنة
- %1$s • %2$d%% (%3$d/%4$d)
- %1$s…
- تحديث كلمات الأغاني
- جلب كلمات الأغاني تلقائياً لجميع الأغاني باستخدام lrclib.
- تحديث كلمات الأغاني
- جاري معالجة %1$d من أصل %2$d أغنية
- أدخل مفتاح API
- حفظ
- تم الحفظ!
- الأوامر الجاهزة (Presets)
- أدخل أمر النظام المستهدف…
- إعادة تعيين
- المنسق المحترف (Professional Curator)
- أنت \'Vibe-Engine\'، منسق موسيقى عالمي وخبير في التدفق الصوتي الانسيابي. هدفك هو بناء تجارب استماع سلسة وعالية الدقة. أعطِ الأولوية للتوافق الهارموني، والانتقالات المنطقية لسرعة الإيقاع (BPM)، والتوازن المدروس بين الأغاني المألوفة المفضلة والاكتشافات الذكية المبنية على نمط الاستماع.
- المستكشف المبتكر (Creative Maverick)
- أنت مستكشف موسيقى طليعي متخصص في صياغة \'التناغم غير المتوقع\'. مهمتك هي كسر حدود الأنواع الموسيقية التقليدية عبر تحديد ترابطات صوتية غير ظاهرة. أعطِ الأولوية للاختيارات النادرة والعميقة، والتركيبات التجريبية، والتجديد الفني مع الحفاظ على منطق انتقال مفاجئ ومذهل في نفس الوقت.
- أمين المكتبة الصارم (Strict Librarian)
- أنت مهندس دقيق لقواعد البيانات الموسيقية. منطقك مدفوع بالدقة المطلقة للبيانات الوصفية والالتزام الصارم بالتصنيفات. قلل من المكتشفات الخوارزمية العشوائية لصالح التناسق التام للنوع الموسيقي، ومطابقة مستويات الطاقة، وتعظيم استدعاء التفضيلات المحددة بدقة من قبل المستخدم.
- الدليل الأجاوائي (Atmospheric Guide)
- أنت خبير في التراكيب الصوتية المحيطية والتدفقات الموسيقية الهادئة. ركز حصرياً على المسارات التي تساعد على الدخول في حالة من \'التركيز العميق\' أو \'السكينة\'. أعطِ الأولوية للدفء الصوتي الآلاتي، والتوزيعات البسيطة، والانتقالات اللطيفة، مع تجنب الأصوات الحادة أو التحولات المفاجئة في النطاق الديناميكي للصوت.
- عاشق الهندسية الصوتية (Sonic Enthusiast)
- أنت محلل صوتي مهتم بتعقيد الإنتاج والآلات الموسيقية. أعطِ الأولوية للمسارات التي تتميز بنطاق ديناميكي واسع، والإيقاعات المتعددة المعقدة، وجودة المسرح الصوتي الفائقة. فضّل المقطوعات التي تتطلب استماعاً نشطاً وتكافئ المستمع عند الانتباه إلى التفاصيل التقنية وتفاصيل التوزيع الصوتي.
- محفز الطاقة (Energy Catalyst)
- أنت مولد إيقاعات عالي الحماس والزخم. ترتكز فلسفتك على خطوط الباس القوية، وشدة الإيقاع، والنغمات الجذابة. أعطِ الأولوية للمسارات المتوافقة مع أجواء النوادي ذات الإيقاع السريع (High-BPM)، والطاقة المتزامنة، والتوتر الإيقاعي المستمر للحفاظ على نبض المستمع وتحفيزه في ذروة مستوياته.
-
- قائمة تشغيل ذكية جديدة
- قائمة تشغيل جديدة
- إضافة أغانٍ
- الرجوع أو الإلغاء
- التالي
- إنشاء
- تعديل قائمة التشغيل
- إغلاق
- تأكيد القص
- تجميعة صور منشأة تلقائياً
- إضافة صورة
- اختر صورة
- تغيير
- إزالة
- اسم قائمة التشغيل
- مزيجي الرائع
- تعديل الغلاف
- ضبط غلافك الفني
- استخدم إيماءات التكبير والسحب للحصول على الإطار المثالي
- يدوي
- ذكي
- الإنشاء باستخدام الذكاء الاصطناعي
- قاعدة ذكية
- الافتراضي
- صورة
- أيقونة
- لون الخلفية
- رمز الأيقونة
- نمط الشكل
- معلمات الشكل
- نصف قطر الزوايا
- النعومة
- الأضلاع
- الانحناء
- الدوران
- المقاس
- الأكثر تشغيلاً
- المسارات الأكثر تشغيلاً لديك.
- المشغلة حديثاً
- الأغاني التي استمعت إليها مؤخراً.
- المفضلات المنسية
- المسارات المفضلة التي لم تقم بتشغيلها منذ فترة.
- جواهر جديدة
- المسارات المضافة حديثاً مع نسب تشغيل منخفضة.
-
- نمط لوحة الألوان (Palette)
- اختر ألوان الألبوم لواجهة مستخدم المشغل.
- الألوان
- تطبيق
- متوازن وهادئ.
- لمسات حيوية عالية التشبع.
- تحولات جريئة في الدرجات والتباين.
- لمسات حيوية مبهجة ومائلة.
- بقعة نغمية (Tonal Spot)
- حيوي (Vibrant)
- تعبيري (Expressive)
- سلطة فواكه (Fruit Salad)
- دقة الألوان
- القيمة 0 تحافظ على الضبط الحالي. القيم الأعلى تلتزم بشكل أقرب بالدرجة المهيمنة لغلاف الألبوم.
- الحالي
- أكثر دقة
- 0 • الحالي
- %1$d • طفيف
- %1$d • متوازن
- %1$d • دقيق
-
- تعديلات تحميل واجهة المشغل
- كلمات الأغاني المتحركة (للأجهزة القوية)
- تستخدم تأثيرات بصرية ورسوم زنبركية متحركة للكلمات. قد تسبب سقوطاً في معدل الإطارات على الأجهزة الضعيفة.
- تأثير تمويه كلمات الأغاني (Blur)
- يطبق تمويهاً لعمق الحقل على الكلمات غير النشطة حالياً.
- قوة التمويه
- ضبط كثافة تأثير التمويه.
- %1$.1fx
- الخطوة 1 · اختر ما تريد تأخيره
- تأخير كل شيء
- تجميد محتوى المشغل بالكامل حتى تتمدد خلفية اللوحة بالكامل.
- العرض الدوار للألبومات
- تأخير عرض غلاف الألبوم والعرض الدوار حتى تتمدد اللوحة السفلية.
- البيانات الوصفية للأغنية
- تأخير العنوان، الفنان، وإجراءات الكلمات/قائمة الانتظار.
- شريط التقدم
- تأخير الخط الزمني وعلامات الوقت حتى يكتمل التمدد.
- عناصر التحكم في التشغيل
- تأخير أزرار التشغيل/الإيقاف المؤقت، التقديم، وعناصر الإعجاب.
- جميع المكونات المؤجلة نشطة حالياً. قم بتعطيل \"تأخير كل شيء\" لتخصيص كل جزء على حدة.
- الخطوة 2 · تكوين سلوك العناصر النائبة (Placeholders)
- استخدام عناصر نائبة للمكونات المؤجلة
- الحفاظ على استقرار الواجهة عبر عرض عناصر نائبة خفيفة الوزن أثناء انتظار المكونات للتمدد.
- الخطوة 3 · اختر وقت تحول العناصر النائبة إلى المحتوى الحقيقي
- اختر وضعاً واحداً. يعتمد وضع العتبة على أشرطة التمرير؛ بينما ينتظر وضع إفلات السحب حتى تترك إيماءة اللوحة.
- قم بتمكين مكون مؤجل واحد على الأقل لإلغاء قفل وضع التفعيل.
- العتبة (Threshold)
- يعتمد على النسبة المئوية للتمدد.
- إفلات السحب
- يتحول فقط بعد إفلات إيماءة السحب.
- عتبة التمدد
- مدى التمدد المطلوب للوحة قبل أن تصبح المكونات المؤجلة مرئية.
- يظهر المحتوى عند تمدد بنسبة %1$d%%
- التطبيق أيضاً عند إغلاق المشغل
- استخدام عتبة الإغلاق للتحول مجدداً إلى العناصر النائبة أثناء الطي.
- عتبة الإغلاق
- مقدار الطي المطلوب قبل أن تتولى العناصر النائبة العرض مرة أخرى.
- تظهر العناصر النائبة بعد طي بنسبة %1$d%%
- يتجاوز وضع إفلات السحب العتبات وسلوك الإغلاق. يحدث التبديل فقط عندما تنتهي إيماءة سحب اللوحة.
- جعل العناصر النائبة شفافة
- تحتفظ العناصر النائبة بمساحة تخطيطها ولكن تصبح غير مرئية.
- الجودة البصرية
- دقة غلاف الألبوم
- ميزات تجريبية
- منخفضة (256 بكسل) - أداء أفضل
- متوسطة (512 بكسل) - متوازنة
- عالية (800 بكسل) - جودة أفضل
- الأصلية - الجودة القصوى
-
- %1$d%%
- %1$s • %2$s
- · %1$s
- \?
-
- تسجيل الدخول إلى Telegram
- أنت تقوم بتعديل رقمك الآن. إرسال الرمز مجدداً سيحل محل الرمز السابق.
- جاري العمل…
- جاري تهيئة Telegram…
- جاري تسجيل الخروج…
- جاري إغلاق الجلسة…
- تم إغلاق الجلسة. أعد فتح تسجيل الدخول للمتابعة.
- جاري تحضير جلسة Telegram آمنة…
- بانتظار استجابة Telegram…
- ربط Telegram
- قم بربط حساب Telegram لبث الموسيقى مباشرة من قنواتك ومحادثاتك.
- رقم الهاتف
- أدخل رقم Telegram الخاص بك. يمكنك العودة وتعديله لاحقاً.
- رقم الهاتف
- 1
- 5551234567
- إرسال الرمز
- رمز التحقق
- أدخل الرمز الذي وصلك من Telegram. إذا كان الرقم خاطئاً، عد للخلف لتعديله.
- الرمز
- 12345
- تعديل الهاتف
- إعادة إرسال الرمز
- التحقق من الرمز
- التحقق بخطوتين (كلمة المرور)
- أدخل كلمة مرور Telegram الخاصة بك. لا يزال بإمكانك العودة لتصحيح رقمك.
- كلمة المرور
- التحقق من كلمة المرور
- يرجى الانتظار…
-
- قنوات Telegram
- إضافة قناة
- قناة Telegram عامة
- جاري المزامنة
- المزامنة الآن
- طي المواضيع
- إظهار المواضيع
- خيارات القناة
- المواضيع
- جاري مزامنة القناة
- جاري تحديث الأغاني من Telegram
- جلب أحدث الأغاني من هذه القناة
- إزالة القناة
- إيقاف المزامنة وحذف الأغاني المخزنة مؤقتاً
- حذف القناة؟
- ستتوقف مزامنة %1$s وسيتم حذف جميع الأغاني المخزنة مؤقتاً من هذه القناة.
- إزالة
- لم يتم مزامنة أي قنوات بعد
- أضف قنوات Telegram عامة لمزامنة\nمكتبتك الموسيقية
- إضافة قناة
- لم تُزامن مطلقاً
- تمت المزامنة %1$s
-
- إضافة قناة
- ابحث عن قناة Telegram عامة لمزامنة موسيقاها
- اسم_القناة@ أو الرابط
- بحث
- جاري البحث…
- البحث عن قناة
- أدخل اسم المستخدم لقناة عامة أو الرابط الخاص بها\nلمزامنة ملفاتها الصوتية
- تم
-
-
- لا توجد أغانٍ
- أغنية واحدة (%d)
- أغنيتان (%d)
- %d أغانٍ
- %d أغنية
- %d أغنية
-
-
- لا توجد مواضيع
- موضوع واحد (%d)
- موضوعان (%d)
- %d مواضيع
- %d موضوعاً
- %d موضوع
-
-
diff --git a/app/src/main/res/values-ar/strings_presentation_batch_g.xml b/app/src/main/res/values-ar/strings_presentation_batch_g.xml
deleted file mode 100644
index 579f9860e..000000000
--- a/app/src/main/res/values-ar/strings_presentation_batch_g.xml
+++ /dev/null
@@ -1,636 +0,0 @@
-
-
-
- اليوم
- الأسبوع الحالي
- الشهر الحالي
- السنة الحالية
- كل الأوقات
- إحصائيات الاستماع
- تحديث إحصائيات الاستماع
- الاستماع
- مرات التشغيل
- —
- عادات الاستماع
- لا توجد عادات استماع بعد
- سنقوم بإظهار عادات الاستماع الخاصة بك بمجرد أن نتعرف على ذوقك بشكل أفضل.
- إجمالي الجلسات
- معدل الجلسة
- أطول جلسة
- جلسة/يوم
- اليوم الأكثر نشاطاً
- لم يتم التشغيل بعد
- فترة الذروة الزمنية
- وقت الاستماع
- إجمالي وقت الاستماع الذي تم تسجيله في النطاق المحدد.
- عدد مرات التشغيل
- عدد الجلسات التي أكملتها لكل شريحة زمنية.
- معدل الجلسة
- متوسط مدة الاستماع لكل شريحة زمنية.
- %1$d تشغيل
- الخط الزمني للاستماع
- لا توجد بيانات استماع بعد
- اضغط على زر التشغيل لبدء بناء خطك الزمني للاستماع
- الإيقاع اليومي
- الإيقاع الأسبوعي
- الإيقاع الشهري
- نظرة عامة على السنة
- التطور على مر الوقت
- مجمعة في شرائح مدتها 4 ساعات
- مجمعة حسب أيام الأسبوع
- مجمعة حسب أسبوع الشهر
- مجمعة حسب الشهر
- مجمعة حسب السنة
- شريحة الذروة
- مقسمة إلى فترات مدتها 4 ساعات للكشف عن إيقاعك اليومي.
- تسهل الأشرطة اليومية مقارنة عادات الاستماع من أسبوع لآخر.
- توضح الأشرطة الأسبوعية اتجاهات الشهر وتطورها.
- تظهر الأشرطة الشهرية التغيرات الموسمية على مدار السنة.
- تختصر الأشرطة السنوية كامل تاريخ الاستماع الخاص بك.
- الفئات الأعلى
- قارن بين طرق استماعك عبر الأنواع الموسيقية، الفنانين، الألبومات، والأغاني.
- %1$d تشغيل • %2$d فنان
- %1$d تشغيل • %2$d مسار
- النوع
- الفنان
- الألبوم
- الأغنية
- الاستماع حسب النوع
- الاستماع حسب الفنان
- الاستماع حسب الألبوم
- الاستماع حسب الأغنية
- لا توجد بيانات فئات بعد
- اضغط على زر التشغيل لإظهار أهم فئات الاستماع لديك
- أبرز الفنانين
- لا يوجد فنانون بارزون
- استمر في الاستماع وسيظهر فنانوك المفضلون هنا.
- %1$d. %2$s
- أبرز الألبومات
- لا توجد ألبومات بارزة
- الألبومات التي تعيد الاستماع إليها كثيراً ستظهر هنا.
- %1$d. %2$s
- المسارات في هذا النطاق
- المسارات الأكثر تشغيلاً في النطاق الزمني المحدد.
- لا توجد مسارات بارزة
- استمع إلى مفضلاتك لرؤيتها مميزة هنا.
- طي المسارات
- إظهار كل المسارات
- تركيز المسارات
- كيفية توزيع وقت استماعك على المسارات الأعلى لديك.
- لا توجد بيانات تركيز بعد
- قم بتشغيل المزيد من المسارات لترى مدى تركيز استماعك.
- الأعلى 1
- الأعلى 2-3
- الأخرى
- %1$d%%
- تركيز الاستماع
- أعلى 3 مسارات تمثل %1$d%% من إجمالي وقت استماعك.
- معدل التشغيل/المسار
- المسارات الفريدة
- حصة أعلى 3
- \?
-
-
- معلومات الجهاز
- برامج ترميز الصوت المدعومة (Codecs)
- مخرج الصوت
- محرك ExoPlayer
- معدل العينة
- الإطارات لكل مخزن مؤقت
- دعم زمن الانتقال المنخفض
- دعم الصوت الاحترافي (Pro Audio)
- الإصدار
- المصّيرات النشطة
- عدادات فك الترميز
- %1$d هرتز
- نعم
- لا
- مسرع بواسطة الأجهزة
- الشركة المصنعة
- الموديل
- العلامة التجارية
- الجهاز
- إصدار أندرويد
- إصدار SDK
- المكونات المادية (Hardware)
-
-
- هذا الجهاز
- --
- جاهز للتشغيل
- التشغيل يتطلب مراجعة
- التنسيقات
- أجهزة فك الترميز المادية
- الأغاني المحلية
- مساحة تخزين الموسيقى المحلية
- حجم الموسيقى
- %1$d أغنية محلية
- المتاح
- الإجمالي %1$s
- البصمة التخزينية للموسيقى
- المستخدم من الجهاز
- %1$d%%
- <1%
- %1$d أغنية سحابية
- %1$d ملف غير قابل للقراءة
- مسار التشغيل
- %1$d إطار لكل مخزن مؤقت
- Hi-Fi PCM Float
- مسار مخرج 32-بت عائم
- الذاكرة
- متاح من أصل %1$s
- التنسيقات الجاهزة للإرسال المباشر (Offload)
- لم تبلغ أي تنسيقات مضغوطة عن دعم ميزة الـ hardware offload.
- المخارج المكتشفة
- لم يتم الإبلاغ عن أي مسارات إخراج بواسطة أندرويد.
- %1$s مصّيرات
- توافق التنسيقات
- %1$d مسار مدعوم
- %1$d تنسيق غير معروف
- لم يتم الإبلاغ عن برنامج فك ترميز
- فك ترميز مادي (Hardware)
- فك ترميز برمجى (Software)
- إرسال مباشر (Offload)
- %1$d في المكتبة
- تقرير الأداء
- قم بإنشاء تقرير تشخيصي قابل للمشاركة لمساعدتنا في تصنيف مشكلات بطء التشغيل أو الفحص. يحتوي التقرير فقط على بيانات الجهاز، المكتبة، والتوقيت — لا يتضمن مسارات ملفات أو عناوين أو فنانين.
- إنشاء التقرير
- إعادة إنشاء
- نسخ
- مشاركة
- تم نسخ التقرير إلى الحافظة
- تقرير أداء PixelPlay
- نتائج التوافق
- لا توجد حالات عدم توافق رئيسية
- تتطابق مساراتك المفهرسة مع برامج فك الترميز التي يبلغ عنها نظام أندرويد في هذا الجهاز.
- قد لا يتم فك ترميز %1$d مسار بشكل أصلي
- التنسيقات التي تحتاج لمراجعة: %1$s.
- قد يتم إعادة عينة %1$d مسار محلي
- تصل المكتبة إلى %1$d هرتز، وهو أعلى من معدل عينة المخرج الحالي.
- تمتلك %1$d مسارات بيانات وصفية غير معروفة
- يمكن لإعادة فحص المكتبة بالكامل ملء بيانات MIME ومعدل البت ومعدل العينة المفقودة.
- +%1$d أكثر
- المكبر المدمج
- صوت البلوتوث
- صوت USB
- سماعة سلكية
- مخرج رقمي
- مخرج آخر
-
-
- الإدخال (Input)
- الإخراج (Output)
- التفكير (Thought)
- %1$s: %2$s
- MMM dd، HH:mm
- تحليل الفنانين المتعددين
- محددات الرموز
- الحالي: %1$s
- محددات الكلمات
- لا يوجد
- الحالي: %1$s
- …
- تكوين
- استخراج الفنانين من العنوان
- اكتشاف عبارات .feat و .ft و with في عناوين الأغاني
- تنظيم المكتبة
- التجميع حسب فنان الألبوم
- إظهار ألبومات العمل المشترك تحت اسم الفنان الرئيسي
- حول تحليل الفنانين المتعددين
- يقوم PixelPlayer بفصل علامات الفنانين باستخدام محددات الرموز مثل (/, ;, &) ومحددات الكلمات مثل (feat., ft., vs., x). يتم مطابقة محددات الكلمات دون الحساسية لحالة الأحرف.\n\nتكتشف ميزة "استخراج الفنانين من العنوان" الأنماط مثل (feat. Artist) في عناوين الأغاني.\n\nيمكن استخدام الشرطة المائلة الخلفية (\\) لتخطي محددات الرموز.
- ←
- أمثلة
- \"Artist1/Artist2\"
- Artist1، Artist2
- \"Drake feat. Rihanna\"
- Drake، Rihanna
- \"Marshmello x Bastille\"
- Marshmello، Bastille
- \"Song (ft. B)\" بواسطة A
- A، B
- \"AC\\DC\"
- AC/DC (تم تخطي المحدد)
- الفنانون
- إعادة الفحص مطلوبة
- تغيرت إعدادات الفنانين. أعد فحص مكتبتك لتطبيق التغييرات.
- جاري الفحص…
- إعادة الفحص
-
-
- β
- تجريبي (Beta)
- تليجرام
- سجل التغييرات
- الإعدادات
- متزامنة
- ثابتة
- خيارات كلمات الأغاني
- البث السحابي
- بث الموسيقى مباشرة من حساباتك السحابية
- المصدر
- الترتيب
- تنازلي
- تصاعدي
- الترتيب الأصلي
- اضغط للتبديل إلى التصاعدي
- اضغط للتبديل إلى التنازلي
- هذا الفرز يحافظ على ترتيبه الأصلي
- المفتاح مفعل
-
-
- إغلاق
- تحديث
- تم
- تم
- كل شيء مسموح به افتراضياً. اضغط مطولاً على أي مجلد لتمييزه كـ مستبعد من الفحص.
- لا توجد مجلدات فرعية هنا
- الانتقال للأعلى
- الانتقال إلى الدليل الرئيسي
-
-
- المزيج اليومي (Daily Mix)
- المزيج اليومي
- بناءً على تاريخ الاستماع
- تحقق من كامل المزيج اليومي
- أغنية محددة
- أغنية محددة
- مشاركة المحدد
- إعجاب بالمحدد
- تشغيل
- الكل
- إلغاء تحديد الكل
- خيارات إضافية
- خيارات
- +%1$d
- %1$s • %2$s
- محدد
- خيارات إضافية لـ %1$s
- غلاف الألبوم لـ %1$s
- جاري التشغيل
- %1$d%%
-
-
- إحصائيات الاستماع
- إجمالي التشغيل
- المعدل يومياً
- المسار الأعلى
- %1$s • %2$d تشغيل
- المشغلة حديثاً
- −.٥
- −.١
- +.١
- +.٥
- ٠ ثانية
- %1$+.1f ث
-
-
- فتح متجر Play
- متابعة النسخة التجريبية
- سيتم تفعيل رابط متجر Play من تكوين GitHub.
- PixelPlayer متاح الآن على Google Play
- استخدم القناة المستقرة على Google Play للحصول على التحديثات الرسمية بينما نبقي البناء التجريبي نشطاً.
- PixelPlayer
- إعلان الإصدار
- قريباً
-
-
- فرز وتشغيل
- خلط عشوائي
- فرز حسب
- الفنان
- الألبوم
- العنوان
- محدد
- سجل التغييرات
- عرض على GitHub
- التفضيلات المسبقة المحفوظة
- لم يتم حفظ تفضيلات مخصصة بعد.
- إلغاء التثبيت
- تثبيت
- إعادة تسمية
- حذف
-
-
- الإصدار التجريبي 0.7.0
- مرحباً بك في PixelPlayer 0.7.0-beta
- أنت تستخدم بناءً تجريبياً قد يحتوي على أخطاء، أو حالات توقف مفاجئ، أو ميزات تجريبية. ساعدنا في التحسين من خلال الإبلاغ عن المشكلات.
- ماذا تتوقع
- قد تحدث أخطاء، توقفات مفاجئة، أو ميزات غير مكتملة بشكل غير متوقع.
- بعض الميزات قد تتغير أو تُزال دون إشعار مسبق.
- قد تكون النسخ التجريبية غير مستقرة مقارنة بالإصدارات الرسمية.
- تحقق دائماً من التحديثات قبل الإبلاغ عن مشكلة معروفة.
- ما يمكن أن تغيره، تعلبه أو تحسنه النسخ التجريبية أثناء الاختبار.
- اختصار مشكلات GitHub
- ابحث أولاً، ثم افتح تقريراً مركزاً للأخطاء، التوقفات المفاجئة، الطلبات، أو الاستفسارات.
- فتح المشكلات الحالية
- الإبلاغ عن مشكلة أو توقف مفاجئ
- شاركنا خطوات إعادة إنتاج المشكلة، النتائج المتوقعة، النتائج الفعلية، وتفاصيل جهازك/نظام التشغيل.
- كيفية الإبلاغ
- قائمة مراجعة سريعة قبل فتح تذكرة مشكلة جديدة.
- قبل فتح تذكرة مشكلة
- ابحث في المشكلات المفتوحة والمغلقة الحالية لتجنب التكرار.
- حدث إلى آخر إصدار من PixelPlayer وتأكد من استمرار حدوث المشكلة.
- أعد تشغيل التطبيق وتأكد من بقاء المشكلة قائمّة.
- حاول تكرار حدوث المشكلة واكتب الخطوات الدقيقة لذلك.
- ما هو نوع المشكلة؟
- تقرير خطأ برمي (Bug): شيء ما يتصرف بشكل غير صحيح.
- طلب ميزة: إضافة ميزة جديدة أو تحسين.
- سؤال: استخدم قسم المناقشات إذا كان مفعلاً، أو افتح تذكرة بعلامة سؤال.
- تقرير خطأ برمجى
- انسخ هذه الحقول عندما يتصرف شيء ما بشكل غير صحيح أو يتوقف فجأة.
- تقرير خطأ
- ملخص قصير:
- السلوك المتوقع:
- السلوك الحالي:
- خطوات التشغيل/إعادة الإنتاج: 1. 2. 3.
- كم مرة يحدث ذلك؟ دائماً / أحياناً / نادراً.
- لقطة شاشة / فيديو: إن وجد.
- السجلات / تتبع الكومة (Stack trace): إن وجد.
- البيئة البرمجية
- إصدار PixelPlayer:
- مصدر التثبيت: إصدار GitHub، بناء تصحيح خطأ، بناء ليلي، إلخ.
- إصدار أندرويد:
- موديل الجهاز:
- سياق إضافي: استخدام بطاقة SD، إعدادات خاصة، أذونات، إلخ.
- طلب ميزة جديد
- انسخ هذه الحقول عندما ترغب في طلب ميزة جديدة أو تحسين.
- بيان المشكلة: ما هي المشكلة التي تحاول حلها؟
- الحل المقترح: كيف يجب أن تعمل الميزة؟
- البدائل المدروسة: هل توجد أي مقاربات أخرى؟
- النطاق: ما هي الشاشات أو التدفقات المتأثرة؟
- نموذج مبدئي (Mockup) أو صورة مرجعية إن وجدت.
- العناوين، الخصوصية والنطاق
- اجعل التقرير سهلاً للفرز وآمناً للمشاركة.
- عناوين جيدة للمشكلات
- معادل الصوت: مؤشر الإزاحة يتغير عند تبديل تبويب التفضيلات
- البحث: قائمة السجل لا تظهر عند الاستعلام الفارغ
- ميزة: إضافة خيار فرز قائمة التشغيل حسب "المضافة حديثاً"
- يرجى تجنب
- التقارير العامة مثل "إنه لا يعمل".
- جمع مشكلات متعددة غير مترابطة في تذكرة واحدة.
- السجلات أو لقطات الشاشة غير المظللة التي تحتوي على بيانات خاصة.
- ملاحظة الخصوصية
- قبل نشر السجلات، لقطات الشاشة، أو الفيديوهات، قم بإزالة أي معلومات شخصية أو خاصة.
-
-
- البناء الليلي (Nightly builds)
- كيف تختلف البناءات الليلية عن الإصدارات الرسمية، وماذا تضمن عندما تتعطل.
- يتم إنشاء البناءات الليلية من آخر التزامات برمجية (Commit)، وقد تحتوي على تغييرات غير مكتملة، أخطاء مؤقتة، أو تراجعات في الأداء. إنها تجريبية أكثر من الإصدارات الرسمية.
- يمكنك الوصول إليها من ملحقات سير عمل GitHub Actions الخاصة بالمستودع إن وجدت.
- الإبلاغ عن مشكلات البناء الليلي
- عند الإبلاغ عن مشكلة من بناء ليلي، اذكر دائماً أن ذلك حدث في نسخة ليلية وليس في إصدار رسمي. يرجى تضمين تاريخ البناء، اسم أو رقم تشغيل سير العمل، أو معرف الالتزام (Commit SHA) إن أمكن. وتحقق أيضاً مما إذا كانت نفس المشكلة تحدث في أحدث إصدار رسمي.
- التحديث إلى Beta 0.5.0
- يُوصى بتثبيت نظيف
- إذا كنت قادماً من الإصدار التجريبي 0.5.0، فقد يتطلب هذا التحديث بيانات مكتبة جديدة بدلاً من الحالة القديمة المخزنة مؤقتاً.
- إذا بدت البيانات الوصفية أو إدخالات المكتبة خاطئة
- البيانات الوصفية الخاطئة للأغاني، أو عدم تطابق الفنانين أو الألبومات، أو الإدخالات التي تبدو مكررة تعني عادةً أن التثبيت النظيف هو الحل المناسب.
- لا تظهر هذا مجدداً
- فهمت ذلك
-
-
- %1$d ألبومات
- محدد
- ميزة (إضافة للقائمة وتشغيل) تحترم ترتيب تحديدك تماماً.
- الحد الأقصى: %1$d ألبومات لكل تحديد.
- إضافة إلى قائمة الانتظار وتشغيل
- PixelPlayer
- مشغل موسيقى
- أعلى %1$d
- إغلاق
- النتيجة
- المستوى %1$d
- القلوب
- اكتمل المستوى!
- انتهت اللعبة
- النتيجة: %1$d
- المحاولة مجدداً؟
- المستوى التالي
- إعادة تشغيل اللعبة
- اضغط لإعادة الإطلاق
- تشغيل موسيقى عشوائية
- كسارة الطوب
- أعلى نتيجة %1$d
- لعب
- اسحب لتحريك المضرب
- استعادة الوحدات
- جاري الاستعادة
- استعادة المحدد
- تفاصيل النسخة الاحتياطية
- تم الإنشاء
- إصدار التطبيق
- المخطط (Schema)
- الجهاز
- غير معروف
- تم تحديد %1$d من أصل %2$d وحدة
- النقل جارٍ الآن…
- تحديد الكل
- مسح التحديد
- %1$d إدخالات · سوف تستبدل البيانات الحالية
-
-
- بث سحابي
- طي المشغل
- بث بـ (Cast)
- بلوتوث
- تشغيل محلي
- جاري الاتصال…
- قائمة الانتظار
- كلمات الأغاني
- جلسة بث
- جاري الاتصال
- متصل
- هذا الهاتف
- صوت البلوتوث
- تشغيل محلي
- جاري التشغيل
- موقوف مؤقتاً
- استعد للاتصال
- اسمح لـ PixelPlayer برؤية أجهزتك القريبة وشبكة الـ Wi-Fi الحالية حتى نتمكن من إبقاء البث وصوت البلوتوث ومكبرات الصوت متزامنة.
- الأجهزة القريبة
- مطلوب لقراءة والتحكم في معدات صوت البلوتوث المتصلة.
- الموقع لشبكة الـ Wi‑Fi
- يتطلب نظام أندرويد إذن الموقع لمشاركة شبكة الـ Wi-Fi الحالية (SSID) حتى نتمكن من العثور على أجهزة البث المتوافقة.
- السماح بالوصول
- نحن نستخدم هذه الأذونات فقط لربط الأجهزة — البث، والتحكم في مكبرات الصوت القريبة، وإبقاء الصوت متزامناً.
- توصيل الجهاز
- جاري الفحص بالقرب منك
- عناصر التحكم
- الأجهزة
- الاتصالية
- تشغيل الـ Wi-Fi أو البلوتوث
- إدارة الشبكات النشطة وإعادة الفحص
- تحديث الاتصالات
- تحديث الأجهزة
- الأجهزة القريبة
- الأجهزة القريبة
- مطلوب لاكتشاف والتحكم في أجهزة صوت البلوتوث المتصلة.
- اضغط للاتصال
- لا توجد أجهزة بعد
- إلغاء الاتصال
- مستوى صوت الجهاز
- مستوى صوت الهاتف
- جاري البحث عن أجهزة…
- تأكد من أن التلفزيون أو مكبر الصوت قيد التشغيل ومتصل بنفس شبكة الـ Wi‑Fi.
- متصل
- متاح للاتصال
- جاري الاتصال
- متاح
- مستوى البطارية
- مستوى الصوت
- Wi-Fi
- متوقف
- متصل
- يعمل
- بلوتوث
- متصل
- يعمل
- متوقف
- الاتصالات متوقفة
- قم بتشغيل الـ Wi‑Fi أو البلوتوث لاكتشاف الأجهزة القريبة
- تشغيل الـ Wi‑Fi
- فتح البلوتوث
- إلغاء الاتصال
- جاري الاتصال...
-
-
- أبرز الميزات
- التحسينات
- الإصلاحات
- ما الجديد
- ما الجديد
- تمت إضافة
- تغيير
- تم إصلاح
-
-
- دعم Android Auto متاح الآن للتشغيل داخل السيارة.
- دعم Wear OS بات نشطاً، بما في ذلك عناصر تحكم أفضل للتشغيل من الساعة إلى الهاتف.
- توسيع التكامل السحابي مع تحسينات لـ Telegram و NetEase و QQ Music و Google Drive.
- ميزتا "المشغلة حديثاً" واستعادة قائمة الانتظار الدائمة تبقيان جلسة استماعك جاهزة.
- تم تضمين ميزات النسخ الاحتياطي والاستعادة v3 وأدوات إدارة الحساب.
- أصبحت كلمات الأغاني أكثر ذكاءً مع دعم البحث اليدوي الاحتياطي وتحسينات التخزين.
-
-
- تحديث شامل للأداء عبر بدء التشغيل، المكتبة، قائمة الانتظار، وتفاعلات المشغل.
- إعادة تصميم واجهات المشغل، البث، الكلمات، الفنان، والنوع لتوفير استخدام أكثر سلاسة.
- أصبحت تدفقات التنقل والبحث أكثر موثوقية مع معالجة أكثر أماناً للمسارات.
- تحسين توافق تشغيل الصوت لمزيد من الأجهزة والتنسيقات.
- توسيع سير عمل التحديد المتعدد عبر الأغاني والألبومات وقوائم التشغيل.
-
-
- أصبح سلوك قائمة الانتظار والخلط العشوائي أكثر استقراراً وقابلية للتنبؤ.
- إصلاح العديد من الحالات النادرة في التشغيل الخلفي وبث الصوت (Casting).
- إصلاح مشكلات مؤقت النوم، والتنقل في تبويب الملفات، وحالات توقف فنان الألبوم المفاجئ.
- تحسين تحميل الويدجت واستقرار الخدمة لتقليل مشكلات الحرارة والذاكرة.
- إصلاحات عامة للأخطاء وتحسينات جمالية لواجهة مستخدم التطبيق.
-
-
- تحديث واجهة المستخدم التعبيرية Material 3 Expressive
- معادل صوتي ذو 10 نطاقات وتأثيرات صوتية
- تدفق مزامنة جديد للمكتبة الموسيقية
- التكامل مع الذكاء الاصطناعي (نماذج Gemini)
- استيراد وتصدير قوائم التشغيل بصيغة M3U
- تكامل أغلفة الفنانين من منصة Deezer
- أغلفة مخصصة لقوائم التشغيل
-
-
- إعادة هيكلة معمارية الإعدادات
- رسوم متحركة جديدة لقائمة الانتظار والمشغل
- ملفات التعريف الأساسية (Baseline Profiles) وتحسين الأداء
- نظام أفضل لكلمات الأغاني مع إزاحة التزامن
-
-
- تحسينات استقرار بث الصوت (Casting)
- استقرار لوحة المشغل السفلية
- إصلاحات عامة للأخطاء وتنظيف الكود
-
-
- إعادة تصميم كبرى لنظام التنقل
- مستكشف ملفات جديد لاختيار مجلدات المصدر
- وظائف اتصال وبث جديدة
- استمرارية سلسة بين الأجهزة عن بعد
- انتقال بدون فجوات (Gapless) بين الأغاني
- عنصر التحكم في التلاشي المتبادل (Crossfade)
- ميزة الانتقالات المخصصة الجديدة (لقوائم التشغيل فقط)
- استمرار التشغيل بعد إغلاق التطبيق
- تحسينات واجهة المستخدم
- ميزة إحصائيات محسنة
- إعادة تصميم التحكم في قائمة الانتظار مع المزيد من الميزات
- تحسين دعم أنواع الملفات المختلفة للتشغيل وتعديل البيانات الوصفية
- تحسين متحكم الأذونات
- إصلاحات طفيفة للأخطاء
-
-
- تقديم مركز إحصائيات استماع أكثر ثراءً مع رؤى عميقة لجلساتك.
- إطلاق مشغل سريع عائم لفتح ومعاينة الملفات المحلية على الفور.
- إضافة تبويب المجلدات مع مستكشف بنمط شجري وعرض جاهز لقوائم التشغيل.
-
-
- تحسين واجهة Material 3 بالكامل لتوفير تجربة أنظف وأكثر تماسكاً.
- تحرير البيانات الوصفية يدعم الآن تغيير غلاف الألبوم.
- تنعيم الرسوم المتحركة والانتقالات عبر التطبيق لتنقل أكثر انسيابية.
- تحسين تخطيط شاشة الفنان مع تفاصيل أكثر ثراءً ولمسات جمالية.
- ترقية توليد DailyMix و YourMix باختيارات أكثر ذكاءً وتنوعاً.
- تعزيز توليد قوائم التشغيل بواسطة الذكاء الاصطناعي.
- تحسين صلة نتائج البحث وعرضها لاكتشاف أسرع.
- توسيع الدعم لنطاق أوسع من تنسيقات الملفات الصوتية.
-
-
- حل مشكلات البيانات الوصفية الغريبة لتبقى تفاصيل الأغاني دقيقة في كل مكان.
- استعادة اختصارات الإشعارات لتعود بشكل موثوق إلى شاشة التشغيل.
-
-
- دعم Chromecast لبث الصوت من جهازك.
- سجل التغييرات داخل التطبيق لإبقائك على اطلاع بآخر الميزات.
- دعم ملفات LRC، سواء كانت مدمجة أو خارجية.
- دعم كلمات الأغاني دون اتصال بالإنترنت.
- كلمات أغاني متزامنة (متطابقة مع الأغنية).
- شاشة جديدة لعرض كامل قائمة الانتظار.
- إعادة ترتيب وإزالة الأغاني من قائمة الانتظار.
- إيماءات المشغل المصغر (السحب للأسفل للإغلاق).
- إضافة المزيد من رسوم Material المتحركة.
- إعدادات جديدة لتخصيص المظهر والإحساس العام.
- إعدادات جديدة لمسح ذاكرة التخزين المؤقت.
-
-
- إعادة تصميم كاملة لواجهة المستخدم.
- إعادة تصميم كاملة للمشغل.
- تحسينات الأداء في المكتبة الموسيقية.
- تحسين سرعة تشغيل التطبيق عند البدء.
- الذكاء الاصطناعي يقدم الآن نتائج أفضل.
-
-
- إصلاح أخطاء مختلفة في محرر العلامات (Tags).
- إصلاح مشكلة عدم اختفاء إشعار التشغيل.
- إصلاح عدة أخطاء كانت تتسبب في توقف التطبيق فجأة.
-
-
- Wear OS: نقل الموسيقى، التشغيل المحلي، مزامنة قائمة الانتظار، والتحكم عن بعد من الساعة.
- الذكاء الاصطناعي: تكامل Groq AI و OpenRouter (تجريبي) مع تحسين استهلاك الرموز (Tokens).
- السحاب: إضافة دعم Jellyfin.
- كلمات الأغاني: ترجمة متزامنة مع مفتاح تبديل مخصص، دعم تنسيق Kugou LRC، تخصيص محاذاة النص، وتحسين التحميل عن بعد.
- واجهة المستخدم/تجربة المستخدم: وضع شريط التنقل المدمج، سمات ديناميكية من لوحة ألوان غلاف الألبوم، نص متحرك (Marquee) للعناوين الطويلة، وخيارات فرز جديدة.
- تليجرام: دعم أصلي للمواضيع (Topics) وأنماط عرض محسنة.
-
-
- المحرك الصوتي: إصلاح شامل مع دعم المزيد من التنسيقات (MIDI, ALAC, M4A) وتحسين برنامج فك الترميز.
- الكفاءة: تقليل جذري في استهلاك الطاقة، إصلاحات للحرارة الزائدة، وتحسين المهام الخلفية (SyncWorker).
- قاعدة البيانات: تحسينات هائلة على الاستعلامات وإعادة تصميم ذاكرة التخزين المؤقت للأغلفة لمنع فقدان البيانات.
- بدء التشغيل: تحسين وقت التحميل عبر تهيئة Baseline Profile.
-
-
- التتشغيل: إصلاح التقطع في Opus/MP3، أخطاء ReplayGain أثناء التلاشي المتبادل، ومشكلات بدء التشغيل على مفككات ترميز Samsung.
- الاستقرار: القضاء على حالات التوقف المفاجئ عند البدء، وأثناء التنقل بين الفنانين، وعلى أجهزة أندرويد 12+.
- واجهة المستخدم: إصلاح وميض الأغلفة، وتداخل النصوص في النصوص غير اللاتينية، وسلوك شريط التنقل/المشغل المصغر.
- الأمان: تعزيز التعامل مع بيانات الاعتماد، أذونات التخزين، والاتصال بخادم الوسائط.
-
-
- العربية
- Spanish
- French
- Russian
- Simplified Chinese
- Indonesian
- Italian
-
-
diff --git a/app/src/main/res/values-ar/strings_screens.xml b/app/src/main/res/values-ar/strings_screens.xml
index 3570cde6d..f3bad8c12 100644
--- a/app/src/main/res/values-ar/strings_screens.xml
+++ b/app/src/main/res/values-ar/strings_screens.xml
@@ -1,252 +1,244 @@
-
- خطأ: معرف النوع (Genre ID) مفقود
- شكراً لك على استخدام PixelPlayer!
+
+ خطأ: معرف النوع الموسيقي مفقود
-
- محددات الكلمات الحالية
- هذه الكلمات المفتاحية تفصل أسماء الفنانين عندما تكون محاطة بمسافات. يتم مطابقتها دون تفرقة بين الأحرف الكبيرة والصغيرة. اضغط للحذف.
- لم يتم تهيئة أي محددات كلمات
- إضافة محدد كلمات جديد
- مثال: .feat أو .ft
- كيف تعمل محددات الكلمات
- يتم مطابقة محددات الكلمات دون تفرقة بين الأحرف الكبيرة والصغيرة مع وجود مسافات حولها.\n\nالمحددات المكونة من حرف واحد (مثل \"x\") تتطلب مسافات من كلا الجانبين لتجنب المطابقات الخاطئة.\n\nأمثلة:\n \"Drake feat. Rihanna\" -> Drake, Rihanna\n \"Marshmello x Bastille\" -> Marshmello, Bastille\n \"A vs. B\" -> A, B
- محددات الكلمات
- إعادة تعيين محددات الكلمات؟
- سيؤدي هذا إلى مسح جميع محددات الكلمات المخصصة واستعادة الكلمات المفتاحية الافتراضية. لا يمكن التراجع عن هذا الإجراء.
- تمت إضافة محدد الكلمات
- موجود بالفعل أو غير صالح
- تمت إعادة تعيين محددات الكلمات إلى الافتراضية
- إعادة تعيين
-
-
- المحددات الحالية
- انقر على محدد لإزالته. يلزم وجود محدد واحد على الأقل.
- إضافة محدد جديد
- مثال: / أو ;
- المحددات الافتراضية
- إعادة تعيين المحددات؟
- سيؤدي هذا إلى مسح جميع المحددات المخصصة واستعادة المحددات الافتراضية. لا يمكن التراجع عن هذا الإجراء.
- تمت إعادة تعيين المحددات إلى الافتراضية
- يلزم وجود محدد واحد على الأقل
- تمت إضافة المحدد
- المحدد موجود بالفعل أو غير صالح
- المحددات
- مسافة
- إضافة محدد
-
-
- خدمة Google Drive قادمة قريباً.
- تعذر فتح هذه الشاشة في الوقت الحالي.
-
-
+
+ لنبدأ!
+ الخطوة %1$d من %2$d
+ يرجى منح الصلاحية المطلوبة أولاً.
+ يرجى منح جميع الصلاحيات المطلوبة.مرحباً بك في βتجريبي (Beta)
- دعنا نقوم بإعداد كل شيء من أجلك.
+ دعنا نجهز كل شيء من أجلك.
+ صلاحية الوصول إلى الوسائط
+ يحتاج PixelPlayer إلى الوصول إلى ملفاتك الصوتية لبناء مكتبتك الموسيقية.
+ تم منح الصلاحية
+ منح صلاحية الوسائط
+ الإشعارات
+ قم بتفعيل الإشعارات للتحكم في الموسيقى من شاشة القفل ولوحة الإشعارات.
+ تفعيل الإشعارات
+ هل لديك نسخة احتياطية؟
+ إذا كان لديك نسخة احتياطية سابقة من PixelPlayer، قم باستعادتها الآن وتخطي معظم خطوات الإعداد المتبقية على هذا الجهاز.
+ استيراد نسخة احتياطية
+ فحص النسخة الاحتياطيةجاري التحقق من حزمة النسخة الاحتياطية…
- مظهر التطبيق
- اختر المظهر الذي تريده قبل البدء في استكشاف مكتبتك.
- يمكنك تغيير هذا لاحقاً من الإعدادات > المظهر > مظهر التطبيق.
- موصى به
- تخطيط المكتبة
- اختر الطريقة المفضلّة لديك للتنقل في مكتبتك.
- الوضع المدمج
- يمكنك تغيير هذا لاحقاً من الإعدادات > المظهر > التنقل في المكتبة.
- المكتبة
- الأغاني
- الألبومات
- الفنانون
- كل شيء جاهز!
- أنت مستعد الآن للاستمتاع بموسيقاك.
+ جاري استعادة النسخة الاحتياطية
+ تخطي / ليس الآناستعادة النسخة الاحتياطيةراجع ما تريد استيراده قبل إنهاء الإعداد.
- تم تحديد %1$d من أصل %2$d من الوحدات
- تم الإنشاء في %1$s
+ تم تحديد %1$d من أصل %2$d من الأقسام
+ تاريخ الإنشاء %1$sنسخة احتياطية من الإصدار %1$s
- إصدار غير معروف
- هيا بنا!
- الخطوة %1$d من أصل %2$d
- التنقل داخل التطبيق
- اختر نمط شريط التنقل السفلي.
- النمط الافتراضي
- يمكنك تغيير هذا لاحقاً من الإعدادات > المظهر > نمط شريط التنقل.
- تخطي في الوقت الحالي
- تخطي / ليس الآن
- جاري الاستعادة
- استعادة المحدد
- تخصيص نصف قطر الزوايا
- يرجى منح الإذن المطلوب أولاً.
- يرجى منح جميع الأذونات المطلوبة.
- يرجى منح أذونات التخزين أولاً
- تعذر فتح إعدادات البطارية
-
-
- توسيع القائمة
- التالي
- إنهاء
- إغلاق
- إزالة
- إضافة محدد كلمات
- إعادة تعيين الافتراضيات
-
-
+ إصدار غير معروف
+ استعادة المحدد
+ جاري الاستعادةالمجلدات المستبعدة
- يتم فحص جميع المجلدات افتراضياً. اختر أي مواقع تريد تجاهلها عند بناء مكتبتك.
+ يتم فحص جميع المجلدات افتراضياً. اختر أي مواقع ترغب في تجاهلها عند بناء مكتبتك.اختر المجلدات لتجاهلها
- إذن الوصول إلى الوسائط
- يحتاج PixelPlayer إلى الوصول إلى ملفاتك الصوتية لبناء مكتبتك الموسيقية.
- تم منح الإذن
- منح إذن الوسائط
- الإشعارات
- قم بتمكين الإشعارات للتحكم في موسيقاك من شاشة القفل ولوحة الإشعارات.
- تمكين الإشعارات
- التنبيهات والتذكيرات
- اختياري، ولكن موصى به إذا كنت تستخدم مؤقت النوم وتريد أن يقوم PixelPlayer بإيقاف التشغيل في الوقت المحدد تماماً.
- منح الإذن
- هل لديك نسخة احتياطية؟
- إذا كان لديك نسخة احتياطية من PixelPlayer بالفعل، فاستعدها الآن لتخطي معظم خطوات الإعداد المتبقية على هذا الجهاز.
- جاري فحص النسخة الاحتياطية
- جاري استعادة النسخة الاحتياطية
- استيراد نسخة احتياطية
+ امنح صلاحيات وحدة التخزين أولاً
+ مظهر التطبيق
+ اختر المظهر الذي تريده قبل البدء في استكشاف مكتبتك.داكن
- المظهر الداكن الافتراضي لـ Material 3 في تطبيق PixelPlayer.
+ المظهر الداكن الافتراضي لـ Material 3 في PixelPlayer.فاتحمظهر Material 3 أكثر سطوعاً في جميع أنحاء التطبيق.
- تتبع النظام
+ حسب نظام التشغيلمطابقة إعداد المظهر الحالي لهاتفك.
- يتم استخدام شريط التنقل الكبسولة المدمج
- يتم استخدام صف علامات التبويب القياسي
+ موصى به
+ يمكنك تغيير هذا لاحقاً من الإعدادات > المظهر > مظهر التطبيق.
+ تخطيط المكتبة
+ اختر الطريقة المفضلة لديك للتنقل داخل المكتبة.الأغاني
+ الوضع المدمج
+ استخدام شريط التنقل البيضاوي المصغر
+ استخدام صف التبويبات القياسي
+ الأغاني
+ الألبومات
+ الفنانون
+ يمكنك تغيير هذا لاحقاً من الإعدادات > المظهر > التنقل في المكتبة.
+ التنقل في التطبيق
+ اختر نمط شريط التنقل السفلي.
+ النمط الافتراضي
+ شريط عائم بيضاوي بزوايا مستديرة
+ شريط قياسي بعرض الشاشة الكامل
+ تخصيص نصف قطر الزوايا
+ يمكنك تغيير هذا لاحقاً من الإعدادات > المظهر > نمط شريط التنقل.
+ المنبهات والتذكيرات
+ اختياري، ولكن موصى به إذا كنت تستخدم مؤقت النوم وتريد أن يقوم PixelPlayer بإيقاف التشغيل في الوقت المحدد تماماً.
+ منح الصلاحيةتحسين استهلاك البطارية
- تقوم بعض أجهزة Android بإغلاق تطبيقات الخلفية بشكل حاد. قم بتعطيل تحسين البطارية لتطبيق PixelPlayer لمنع انقطاع التشغيل غير المتوقع.
- تعطيل التحسين
- شريط كبسولة عائم بزوايا مستديرة
- شريط قياسي بالعرض الكامل
+ تقوم بعض أجهزة أندرويد بإغلاق تطبيقات الخلفية بشكل قسري. يرجى إيقاف تحسين البطارية لتطبيق PixelPlayer لمنع أي انقطاع غير متوقع أثناء التشغيل.
+ إيقاف تحسين البطارية
+ كل شيء جاهز!
+ أنت مستعد الآن للاستمتاع بموسيقاك.
+
+
+ بحث…
+ بحث
+ مسح نص البحث
+ عمليات البحث الأخيرة
+ مسح الكل
+ سجل البحث
+ حذف عنصر من سجل البحث
+ لا توجد نتائج
+ لا توجد نتائج لـ \"%1$s\"
+ لم يتم العثور على شيء
+ جرّب استخدام كلمة بحث أخرى أو تحقق من الفلاتر الخاصة بك.
+ لم يتم العثور على نتائج.
+ تصفح حسب النوع الموسيقي
+ لا توجد أنواع موسيقية متوفرة.
-
- حذف الأغنية؟
- \"%1$s\" بواسطة %2$s\n\nسيتم حذف هذه الأغنية نهائياً من جهازك ولا يمكن استعادتها.
+
+ تشغيل %1$s
+ طي %1$s
+ توسيع %1$s
+ تعديل صورة الفنان
+ تغيير الصورة
+ إعادة تعيين إلى الافتراضي
+ تشغيل عشوائي للفنان
-
- المزيج\nالخاص بك
- لا توجد بيانات لعرضها بعد
- سيظهر المزيج الخاص بك هنا عندما يجد PixelPlayer أغانٍ أو يقوم بمزامنة أحد المصادر.
- تحديث
- تشغيل عشوائي
- غلاف ألبوم لـ %1$s
- خيارات
- ملء سريع للنوع
- فنان عام
- تشغيل الألبوم
- تشغيل الألبوم عشوائياً
+
+ القرص %dغلاف %1$s%1$s · %2$s
- تشغيل/إيقاف مؤقت
- غلاف الأغنية
-
- عذراً! حدث خطأ ما
- تعطل التطبيق خلال جلستك الأخيرة. ساعدنا في إصلاح هذا من خلال مشاركة تقرير التعطل.
- التاريخ: %1$s
- الخطأ:
- تتبع الكومة (معاينة):
- سجل التعطل
- تم نسخ سجل التعطل إلى الحافظة
- تقرير تعطل PixelPlayer
- مشاركة تقرير التعطل
- نسخ
- مشاركة
+
+ لم يتم العثور على قائمة التشغيل.
+ قائمة التشغيل هذه فارغة.
+ المس \"إضافة أغانٍ\" للبدء.
+ هذا المجلد لا يحتوي على أغانٍ.
+ فرز الأغاني
+ المزيد من الخيارات
+ خيارات قائمة التشغيل
+ تعديل قائمة التشغيل
+ حذف قائمة التشغيل
+ هل تريد حذف قائمة التشغيل؟
+ هل أنت متأكد من أنك تريد حذف قائمة التشغيل هذه؟
+ تعيين الانتقال الافتراضي
+ تصدير قائمة التشغيل
+ %1$s • %2$s
+ تشغيل
+ إضافة
+ إضافة أغانٍ
+ إزالة
+ إزالة الأغاني
+ إعادة ترتيب
+ إعادة ترتيب الأغاني
+
+
+ الانتقالات العامة
+ قواعد قائمة التشغيل
+ يتم تطبيق هذه الإعدادات على جميع مصادر التشغيل ما لم يتم تجاوزها.
+ تهيئة السلوك الافتراضي لقائمة التشغيل هذه تحديداً.
+ حالة التفعيل
+ الوضع الافتراضي العام
+ الوضع الافتراضي لقائمة التشغيل
+ تابع للإعدادات العامة
+ تجاوز مخصص
+ تجاوز مخصص
+ قم بالتفعيل لتعيين قواعد خاصة بقائمة التشغيل هذه.
+ يتم استخدام الإعدادات الافتراضية العامة
+ تم حفظ التغييرات بنجاح
+ نمط الانتقال
+ كيفية تداخل المسارات الصوتية معاً
+ بدون انتقال
+ تلاشي متبادل (Crossfade)
+ مدة الانتقال
+ إجمالي التداخل %1$d ثوانٍ
+ إعادة تعيين الانتقال
+ الأغنية الحالية
+ الأغنية التالية
+ ستتداخل المسارات الصوتية لمدة %1$d ثوانٍ
+ منحنيات الصوت
+ ضبط ميل وتدرج الصوت بدقة
+ تلاشٍ تدريجي للخارج (Fade Out)
+ تلاشٍ تدريجي للداخل (Fade In)
-
- بحث…
- بحث
- مسح البحث
- عمليات البحث الأخيرة
- مسح الكل
- السجل
- حذف عنصر من سجل البحث
- لا توجد نتائج
- لا توجد نتائج لـ \"%1$s\"
- لم يتم العثور على شيء
- جرّب مصطلح بحث آخر أو تحقق من الفلاتر الخاصة بك.
- لم يتم العثور على نتائج.
+
+ قائمة تشغيل ذكية جديدة
+ قائمة تشغيل جديدة
+ إضافة أغانٍ
+ رجوع أو إلغاء
+ التالي
+ إنشاء
+ تعديل قائمة التشغيل
+ تجميعة صور تلقائية
+ إضافة صورة
+ اختر صورة
+ تغيير
+ إزالة
+ اسم قائمة التشغيل
+ الميكس الرائع الخاص بي
+ تعديل الغلاف
+ اضبط صورة الغلاف
+ استخدم إيماءات القرص والسحب للوصول إلى الإطار المثالي
+ يدوي
+ ذكي
+ التوليد بالذكاء الاصطناعي
+ قاعدة ذكية
+ الافتراضي
+ صورة
+ أيقونة
+ لون الخلفية
+ رمز الأيقونة
+ نمط الشكل
+ خصائص الشكل
+ نصف قطر الزوايا
+ النعومة
+ الأضلاع
+ المنحنى
+ التدوير
+ الحجم
+ الأكثر تشغيلاً
+ المسارات الموسيقية الأكثر استماعاً لديك.
+ المشغلة حديثاً
+ الأغاني التي استمعت إليها في الآونة الأخيرة.
+ مفضلات منسية
+ مساراتك المفضلة التي لم تقم بتشغيلها منذ فترة.
+ جواهر جديدة
+ المسارات المضافة حديثاً ذات نسب تشغيل منخفضة.
-
- تصفح حسب النوع
- لا توجد أنواع متاحة.
+
+ تعبئة سريعة للنوع الموسيقي
+ فرز وتشغيل
+ خلط عشوائي
+ فرز حسب
+ الفنان
+ الألبوم
+ العنوان
+ فنان عام
+ خلط عشوائي لـ %1$s
-
- لم يتم العثور على مساهمين حالياً. يرجى المحاولة مرة أخرى لاحقاً.
- PixelPlayer
- مشغل موسيقى مفتوح المصدر تم بناؤه مع مجتمعه.
- الإصدار v%1$s
- %1$d مساهمة
- حول التطبيق
- المشرف الرئيسي
- الشخص الذي يقف وراء PixelPlayer.
- أضواء على المجتمع
- تقدير وتكريم للمتعاونين ذوي التأثير الكبير.
- المساهمون في المشروع مفتوح المصدر
- قائمة المساهمين المباشرة من GitHub.
- مفتوح المصدر
- المجتمع أولاً
- تصميم Material 3 معبر
- فتح ملف GitHub الشخصي
- فتح Telegram
- الصورة الشخصية لـ %1$s
- أيقونة %1$s
+
+ اختر الأغاني
+ اختر النوع الموسيقي
+ بحث عن الأغاني
+ نوع موسيقي جديد
+ إضافة مخصص
+ إضافة نوع موسيقي مخصص
+ اسم النوع الموسيقي
+ اختر أيقونة
+ النوع الموسيقي: %1$s
+ اختر نوعاً موسيقياً
+ تعبئة سريعة
-
- Subsonic
- تم مزامنة %1$d قائمة تشغيل
- تم مزامنة %1$d مجلد
- قوائم التشغيل
- مجلدات الموسيقى
- مزامنة
- لم يتم مزامنة أي قوائم تشغيل بعد
- اضغط على مزامنة لجلب قوائم التشغيل الخاصة بك
- اضغط على مزامنة لجلب قوائم تشغيل Jellyfin الخاصة بك
- لم يتم إضافة مجلدات بعد
- انقر على + لإضافة مجلد من Drive
- إجراءات سريعة
- إدارة خوادم Navidrome وAirsonic والخوادم الأخرى المتوافقة مع Subsonic.
- إدارة اتصال خادم Jellyfin الخاص بك.
- جاري المزامنة
- مزامنة المكتبة
- قطع الاتصال
- جاري مزامنة المكتبة…
- جاري جلب قوائم التشغيل…
- جاري مزامنة قائمة التشغيل: %1$s
- جاري تحديث المكتبة المحلية…
- اكتملت المزامنة
- جاري جلب قائمة الألبومات…
- جاري جلب الأغاني من %1$s…
- جاري حفظ %1$d أغانٍ في قاعدة البيانات…
- لم يتم العثور على أغانٍ في المكتبة
- اكتملت مزامنة المكتبة
- %1$d أغانٍ
- مزامنة
- مزامنة الكل
- إضافة مجلد
- تسجيل الخروج
- NetEase Music
- QQ Music
- مزامنة جميع قوائم التشغيل
- خطأ: %1$s
- جاري المزامنة…
- اختر نوع قائمة التشغيل
- اختر قوائم التشغيل المراد مزامنتها:
- جميع قوائم التشغيل
- المنشأة والمجمعة
- قوائم التشغيل المنشأة
- قوائم التشغيل المجمعة
- الصورة الشخصية للمستخدم
- تم إنشاء قائمة التشغيل بنجاح
- يرجى تعيين مفتاح API لمزود الذكاء الاصطناعي أولاً
- يرجى تعيين مفتاح API لـ Gemini أولاً
- تمت الإضافة إلى قائمة الانتظار
- سيتم التشغيل تالياً
- تعذر مشاركة الأغنية: %1$s
-
+
+ مساحة الـ DJ
+ جاري التحميل…
+ المنصة %1$d
+ تحميل أغنية
+ لم يتم تحميل أي أغنية
+ …
+ ميزة فصل المسارات (Stems) غير متوفرة بعد.
+ مستوى الصوت
+ السرعة
+ ممازج الصوت (Crossfader)
+ المنصة 1
+ المنصة 2
+ اختر أغنية
+ تشغيل/إيقاف مؤقت
+ غلاف الأغنية
+ x%1$.2f
+
\ No newline at end of file
diff --git a/app/src/main/res/values-ar/strings_settings.xml b/app/src/main/res/values-ar/strings_settings.xml
index 78d187b68..6e3958ad4 100644
--- a/app/src/main/res/values-ar/strings_settings.xml
+++ b/app/src/main/res/values-ar/strings_settings.xml
@@ -297,4 +297,10 @@
وحدات عدد %1$d · إصدار %2$s · إصدار المخطط البرمجي %3$dKorean (الكورية)Norwegian (النرويجية بوكمول)
+ GitHub
+ مستودع الكود
+ تليجرام
+ الدعم
+ فتح مستودع GitHub
+ الانضمام إلى مجتمع تليجرام
diff --git a/app/src/main/res/values-ar/strings_widget.xml b/app/src/main/res/values-ar/strings_widget.xml
new file mode 100644
index 000000000..ef6e7152c
--- /dev/null
+++ b/app/src/main/res/values-ar/strings_widget.xml
@@ -0,0 +1,17 @@
+
+
+ ودجت مستجيب يتكيف تلقائياً مع حجمه
+ شريط مشغل مدمج
+ عناصر تحكم كاملة مع خياري الخلط والتكرار
+ مشغل مربع بسيط وموجز
+
+ انقر للفتح
+ غلاف الألبوم
+ مواضع غلاف الألبوم المؤقتة
+
+ انقر للتشغيل
+ عنوان الأغنية
+ الفنان
+
+ شريط التقدم، %1$d بالمئة
+
\ No newline at end of file
diff --git a/app/src/main/res/values-de/strings_changelogs.xml b/app/src/main/res/values-de/strings_changelogs.xml
index b5e894776..f70432e6c 100644
--- a/app/src/main/res/values-de/strings_changelogs.xml
+++ b/app/src/main/res/values-de/strings_changelogs.xml
@@ -129,4 +129,22 @@
Lokalisierung: Spanisch, Französisch, Russisch, Vereinfachtes Chinesisch, Indonesisch, Italienisch
+
+ Google Drive-Integration mit Player-Lebenszyklusverwaltung.
+ Massenbearbeitung von Song-Metadaten (Tags und Cover-Art).
+ KI-Lyrics-Übersetzung mit anpassbaren Wear OS-Einstellungen.
+ Verzögerungsdiagnosetool und Mehrfachauswahl auf dem Suchbildschirm.
+ Arabisch- & Türkisch-Unterstützung mit lokalisierten HTTP-URL-Optionen für lokale Netzwerke.
+
+
+ Drastische Akkueinsparung (Audio-Offload und UI-Polling-Gates).
+ Optimierte Queue-Verwaltung (schnellere Einfügungen und explizite Indizierung).
+ Material 3 Expressive-Bewegungsanimationen für Übergangsbildschirme.
+ Refaktorisierte Bibliotheks-Synchronisation via gedrosseltem Scannen.
+
+
+ Wiedergabeverzögerungen (Ruckeln/Überspringen) und Pufferungsprobleme behoben.
+ Synchronisation beim Löschen externer Songs und Metadaten-Konsistenz behoben.
+ Speicherprobleme, Abstürze und Layout-Fehler auf Wear OS und Smartphone behoben.
+
\ No newline at end of file
diff --git a/app/src/main/res/values-de/strings_home_screen.xml b/app/src/main/res/values-de/strings_home_screen.xml
index 5d87a6d3c..61bc62114 100644
--- a/app/src/main/res/values-de/strings_home_screen.xml
+++ b/app/src/main/res/values-de/strings_home_screen.xml
@@ -9,9 +9,9 @@
Musik aus deinen Cloud-Accounts streamen
- Beta 0.7.0
+ Beta 0.7.5β
- Willkommen bei PixelPlayer 0.7.0-beta
+ Willkommen bei PixelPlayer 0.7.5-betaDu verwendest eine Beta-Version, die Fehler, Abstürze oder experimentelle Funktionen enthalten kann. Hilf uns bei der Verbesserung, indem du Probleme meldest.Was dich erwartetFehler, Abstürze oder unvollständige Funktionen können unerwartet auftreten.
@@ -274,4 +274,4 @@
%1$d Song%1$d SongsWoche %1$d
-
\ No newline at end of file
+
diff --git a/app/src/main/res/values-de/strings_settings.xml b/app/src/main/res/values-de/strings_settings.xml
index 69f37192e..c9ccc95c0 100644
--- a/app/src/main/res/values-de/strings_settings.xml
+++ b/app/src/main/res/values-de/strings_settings.xml
@@ -163,17 +163,6 @@
SpracheSprache der App-Oberfläche festlegen.Systemstandard
- English
- Español
- Deutsch
- Français
- Русский
- 简体中文
- Bahasa Indonesia
- Italiano
- Koreanisch
- Norwegisch Bokmål
- TürkischApp-DesignHell, Dunkel oder System-Design – ganz nach Geschmack.Hell
@@ -633,6 +622,12 @@
Open-Source-MitwirkendeAktuelle Mitwirkenden-Liste von GitHub.%1$d Beiträge
+ GitHub
+ Repository
+ Telegram
+ Support
+ GitHub-Repository öffnen
+ Der Telegram-Community beitretenGitHub-Profil öffnenTelegram öffnenAvatar von %1$s
diff --git a/app/src/main/res/values-es/strings_changelogs.xml b/app/src/main/res/values-es/strings_changelogs.xml
index 22836ca8e..4942adfe6 100644
--- a/app/src/main/res/values-es/strings_changelogs.xml
+++ b/app/src/main/res/values-es/strings_changelogs.xml
@@ -129,4 +129,22 @@
Localización: Español, francés, ruso, chino simplificado, indonesio, italiano
+
+ Integración de Google Drive con gestión del ciclo de vida del reproductor.
+ Edición por lotes de metadatos de canciones (etiquetas y carátulas).
+ Traducción de letras por IA con preferencias personalizables de Wear OS.
+ Herramienta de diagnóstico de retraso y selección múltiple en la pantalla de búsqueda.
+ Soporte para árabe y turco, con opciones de red local de URL http localizadas.
+
+
+ Ahorro drástico de batería (descarga de audio y puertas de sondeo de interfaz de usuario).
+ Gestión de cola optimizada (inserciones más rápidas e indexación explícita).
+ Animaciones de movimiento expresivas de Material 3 para pantallas de transición.
+ Refactorización de la sincronización de la biblioteca mediante escaneo limitado.
+
+
+ Se resolvieron los retrasos de reproducción (saltos/tartamudeos) y problemas de almacenamiento en búfer.
+ Se corrigió la sincronización de eliminación de canciones externas y la consistencia de metadatos.
+ Se corrigieron problemas de memoria, cierres inesperados y fallos de diseño en Wear OS y teléfono.
+
\ No newline at end of file
diff --git a/app/src/main/res/values-es/strings_home_screen.xml b/app/src/main/res/values-es/strings_home_screen.xml
index f48c8c03e..0ce25db99 100644
--- a/app/src/main/res/values-es/strings_home_screen.xml
+++ b/app/src/main/res/values-es/strings_home_screen.xml
@@ -9,9 +9,9 @@
Transmite música desde tus cuentas en la nube
- Beta 0.7.0
+ Beta 0.7.5β
- Bienvenido a PixelPlayer 0.7.0-beta
+ Bienvenido a PixelPlayer 0.7.5-betaEstás usando una versión beta que puede contener errores, fallos o funciones experimentales. Ayúdanos a mejorar informando de los problemas.Qué esperarPueden ocurrir errores, fallos o funciones incompletas de forma inesperada.
@@ -274,4 +274,4 @@
%1$d canción%1$d cancionesSemana %1$d
-
\ No newline at end of file
+
diff --git a/app/src/main/res/values-es/strings_library.xml b/app/src/main/res/values-es/strings_library.xml
index f32199d42..62906018a 100644
--- a/app/src/main/res/values-es/strings_library.xml
+++ b/app/src/main/res/values-es/strings_library.xml
@@ -25,7 +25,7 @@
Transferencia al relojAjustesEditar
- Reorder pestañas
+ Reordenar pestañasExpandir menú
@@ -255,6 +255,10 @@
¿Restablecer el orden de las pestañas al predeterminado?Reordenando pestañas…Control de arrastre
+ Pestañas visibles
+ Pestañas eliminadas
+ Eliminar pestaña
+ Añadir pestañaElige un artista
diff --git a/app/src/main/res/values-es/strings_settings.xml b/app/src/main/res/values-es/strings_settings.xml
index b66c4aa60..db821f353 100644
--- a/app/src/main/res/values-es/strings_settings.xml
+++ b/app/src/main/res/values-es/strings_settings.xml
@@ -163,17 +163,6 @@
Idioma de la appElige el idioma usado en toda la interfaz de la aplicación.Predeterminado del sistema
- Inglés
- Español
- Alemán
- Francés
- Ruso
- Chino simplificado
- Indonesio
- Italiano
- Coreano
- Noruego (Bokmål)
- TurcoTema de la appCambia entre claro, oscuro o seguir el sistema.Tema claro
@@ -633,6 +622,12 @@
Colaboradores de código abiertoLista de colaboradores en vivo desde GitHub.%1$d contrib.
+ GitHub
+ Repositorio
+ Telegram
+ Soporte
+ Abrir repositorio de GitHub
+ Unirse a la comunidad de TelegramAbrir perfil de GitHubAbrir TelegramAvatar de %1$s
diff --git a/app/src/main/res/values-fr/strings_changelogs.xml b/app/src/main/res/values-fr/strings_changelogs.xml
index 380efb769..9aabbbb0c 100644
--- a/app/src/main/res/values-fr/strings_changelogs.xml
+++ b/app/src/main/res/values-fr/strings_changelogs.xml
@@ -4,129 +4,167 @@
Voir sur GitHubAméliorationsCorrections
- Nouveautés
- Ajouté
+ Quoi de neuf
+ Ajouts
+
- Support Chromecast pour diffuser l\'audio depuis votre appareil.
+ Prise en charge de Chromecast pour diffuser l’audio depuis votre appareil.Journal des modifications intégré pour vous tenir informé des dernières fonctionnalités.
- Support des fichiers .LRC, intégrés et externes.
+ Prise en charge des fichiers .LRC, intégrés et externes.Support des paroles hors ligne.
- Paroles synchronisées (synchronisées avec le titre).
- Nouvel écran pour voir la file d\'attente complète.
- Réorganiser et supprimer des titres de la file d\'attente.
- Gestes du mini-lecteur (glisser vers le bas pour fermer).
- Ajout de plus d\'animations Material.
- Nouveaux paramètres pour personnaliser l\'apparence.
- Nouveaux paramètres pour vider le cache.
-
+ Paroles synchronisées avec la musique.
+ Nouvel écran pour afficher la file d’attente complète.
+ Réorganisation et suppression de morceaux dans la file d’attente.
+ Gestes du mini-lecteur (balayer vers le bas pour fermer).
+ Ajout de nouvelles animations Material.
+ Nouveaux paramètres pour personnaliser l’apparence.
+ Nouveau paramètre pour vider le cache.
+
+
- Refonte complète de l\'interface utilisateur.
+ Refonte complète de l’interface utilisateur.Refonte complète du lecteur.
- Améliorations de performance dans la bibliothèque.
- Vitesse de démarrage de l\'application améliorée.
- L\'IA fournit maintenant de meilleurs résultats.
+ Amélioration des performances de la bibliothèque.
+ Amélioration de la vitesse de démarrage de l’application.
+ L’IA fournit désormais de meilleurs résultats.
+
- Correction de divers bugs dans l\'éditeur de tags.
- Correction d\'un bug où la notification de lecture ne se fermait pas.
- Correction de plusieurs bugs qui faisaient planter l\'application.
+ Correction de divers bugs dans l’éditeur de tags.
+ Correction d’un bug où la notification de lecture ne se supprimait pas.
+ Correction de plusieurs bugs provoquant des plantages.
+
- Introduction d\'un centre de statistiques d\'écoute plus riche avec des analyses plus approfondies de vos sessions.
- Lancement d\'un lecteur rapide flottant pour ouvrir et prévisualiser instantanément les fichiers locaux.
- Ajout d\'un onglet dossiers avec un navigateur en arborescence et une vue prête pour playlist.
+ Introduction d’un hub de statistiques d’écoute plus riche avec des analyses détaillées.
+ Ajout d’un lecteur flottant rapide pour ouvrir et prévisualiser les fichiers locaux.
+ Ajout d’un onglet dossiers avec navigation en arbre et vue playlist.
+
- Interface Material 3 globale affinée pour une expérience plus épurée et cohérente.
- L\'édition de métadonnées supporte maintenant le changement de pochette.
- Animations et transitions adoucies dans toute l\'application pour une navigation plus fluide.
- Mise en page de l\'écran artiste améliorée avec plus de détails et de peaufinage.
- Génération DailyMix et YourMix améliorée avec des sélections plus intelligentes et diversifiées.
- Renforcement de la génération de playlist IA.
- Pertinence et présentation de la recherche améliorées pour une découverte plus rapide.
- Support élargi pour une plus large gamme de formats de fichiers audio.
-
+ Amélioration de l’interface Material 3 pour une expérience plus cohérente.
+ L’édition des métadonnées prend désormais en charge la pochette d’album.
+ Animations et transitions plus fluides dans toute l’application.
+ Amélioration de la page artiste avec plus de détails et de finition.
+ Amélioration des playlists DailyMix et YourMix avec des sélections plus variées.
+ Amélioration du moteur de génération de playlists IA.
+ Recherche améliorée pour une découverte plus rapide.
+ Support élargi pour davantage de formats audio.
+
+
- Problèmes de métadonnées résolus pour que les détails des titres restent précis partout.
- Raccourcis de notification restaurés pour revenir de manière fiable à la lecture.
+ Correction de problèmes de métadonnées incohérentes.
+ Restauration des raccourcis de notification vers la lecture.
+
Refonte majeure de la navigation
- Nouvel explorateur de fichiers pour choisir les répertoires sources
- Nouvelles fonctionnalités de connectivité et de diffusion
- Continuité transparente entre appareils distants
- Transition sans coupure entre les titres
+ Nouveau explorateur de fichiers pour choisir les dossiers sources
+ Nouvelles fonctionnalités de connectivité et de casting
+ Continuité fluide entre appareils distants
+ Transitions sans coupure entre les morceauxContrôle du fondu enchaîné
- Nouvelle fonctionnalité de transitions personnalisées (uniquement pour les playlists)
- Continuer la lecture après avoir fermé l\'application
- Optimisations de l\'interface
- Fonctionnalité de statistiques améliorée
- Contrôle de la file d\'attente repensé avec plus de fonctionnalités
- Support amélioré de différents types de fichiers pour la lecture et l\'édition de métadonnées
- Contrôleur d\'autorisations amélioré
- Corrections de bugs mineurs
-
+ Nouvelle fonctionnalité de transitions personnalisées (playlists uniquement)
+ Lecture continue après fermeture de l’application
+ Optimisations de l’interface
+ Amélioration des statistiques
+ Contrôle de file d’attente repensé avec plus de fonctionnalités
+ Meilleur support des types de fichiers et métadonnées
+ Amélioration du gestionnaire de permissions
+ Corrections mineures de bugs
+
+
- Mise à jour de l\'interface Material 3 Expressive
- Égaliseur 10 bandes & Effets
- Nouveau flux de synchronisation de bibliothèque
- Intégration IA (Modèles Gemini)
- Import/Export playlist M3U
- Intégration des pochettes d\'artistes Deezer
- Pochettes de playlist personnalisées
- Refonte de l\'architecture des paramètres
- Animations file d\'attente & lecteur
- Profils de référence & performances
- Système de paroles amélioré avec décalage de synchronisation
-
+ Mise à jour de l’interface Material 3 Expressive
+ Égaliseur 10 bandes & effets
+ Nouveau flux de synchronisation de la bibliothèque
+ Intégration IA (modèles Gemini)
+ Import/export de playlists M3U
+ Intégration des images d’artistes Deezer
+ Pochettes de playlists personnalisées
+ Refonte de l’architecture des paramètres
+ Animations du lecteur et de la file d’attente
+ Optimisation des performances (Baseline Profiles)
+ Meilleur système de paroles avec décalage synchronisé
+
+
- Améliorations de la stabilité de la diffusion
- Stabilité du panneau lecteur
- Corrections de bugs générales & nettoyage
+ Amélioration de la stabilité du casting
+ Stabilité du lecteur améliorée
+ Corrections générales de bugs
+
- Le support Android Auto est maintenant disponible pour la lecture en voiture.
- Le support Wear OS est actif, avec de meilleurs contrôles de lecture montre-téléphone.
- Les intégrations cloud ont été élargies avec Telegram, NetEase, QQ Music et des améliorations Google Drive.
- Écoutés récemment et restauration persistante de la file d\'attente gardent votre session d\'écoute prête.
- Sauvegarde & Restauration v3 et outils de gestion de compte sont maintenant inclus.
- Les paroles sont devenues plus intelligentes avec la recherche manuelle de secours et des améliorations de stockage.
-
+ Support Android Auto pour la lecture en voiture.
+ Support Wear OS avec meilleurs contrôles lecture montre-téléphone.
+ Intégrations cloud étendues (Telegram, NetEase, QQ Music, Google Drive).
+ Historique récent et restauration de file d’attente persistante.
+ Outils de sauvegarde & restauration v3 et gestion de compte.
+ Recherche de paroles améliorée avec fallback manuel.
+
+
- Grande passe de performance sur le démarrage, la bibliothèque, la file d\'attente et les interactions avec le lecteur.
- Les interfaces Lecteur, Diffusion, Paroles, Artiste et Genre ont été repensées pour une utilisation plus fluide.
- Les flux de navigation et de recherche sont plus fiables, avec une gestion des routes plus sûre.
- Compatibilité de lecture audio améliorée pour plus d\'appareils et de formats.
- Les flux de sélection multiple ont été élargis aux titres, albums et playlists.
+ Amélioration majeure des performances globales.
+ Refonte des écrans lecteur, casting, paroles, artiste et genres.
+ Navigation et recherche plus fiables.
+ Compatibilité audio améliorée pour plus d’appareils.
+ Meilleure prise en charge de la sélection multiple.
+
- Le comportement de la file d\'attente et de l\'aléatoire est maintenant plus stable et prévisible.
- Plusieurs cas limites de lecture en arrière-plan et de diffusion ont été corrigés.
- Minuteur de sommeil, navigation de l\'onglet Fichiers et problèmes de crash artiste d\'album corrigés.
- Le chargement du widget et la stabilité du service ont été améliorés pour réduire les problèmes de surchauffe/mémoire.
- Corrections de bugs générales et peaufinage de l\'interface dans toute l\'application.
+ Stabilité améliorée de la file d’attente et du mode aléatoire.
+ Correction des problèmes de lecture en arrière-plan et casting.
+ Correction du minuteur de sommeil et des crashs fichiers/artistes.
+ Amélioration des widgets et stabilité des services.
+ Corrections générales et polish UI.
+
- Wear OS : Transfert de musique, lecture locale, synchronisation de la file d\'attente et contrôle à distance depuis la montre.
- IA : Intégration de Groq AI et OpenRouter (expérimental) avec optimisation des jetons.
- Cloud : Ajout du support de Jellyfin.
- Paroles : Traduction synchronisée avec interrupteur dédié, support du format Kugou LRC, personnalisation de l\'alignement du texte et amélioration du chargement à distance.
- UI/UX : Mode barre de navigation compacte, thèmes dynamiques depuis la palette des pochettes d\'album, défilement pour les titres longs et nouvelles options de tri.
- Telegram : Support natif des sujets et modes d\'affichage améliorés.
-
+ Wear OS : transfert de musique, lecture locale, synchronisation de file et contrôle à distance.
+ IA : intégration Groq AI et OpenRouter (expérimental).
+ Cloud : ajout du support Jellyfin.
+ Paroles : traduction synchronisée, support Kugou LRC, alignement et chargement amélioré.
+ UI/UX : barre compacte, thèmes dynamiques, titre défilant, tri amélioré.
+ Telegram : support des topics natifs.
+
+
- Moteur audio : Refonte complète avec support de plus de formats (MIDI, ALAC, M4A) et optimisation des décodeurs.
- Efficacité : Réduction drastique de la consommation d\'énergie, corrections de la surchauffe et optimisation des tâches en arrière-plan (SyncWorker).
- Base de données : Optimisations massives des requêtes et refonte du cache des pochettes pour éviter la perte de données.
- Démarrage : Temps de chargement amélioré grâce à l\'optimisation des profils de référence (Baseline Profile).
+ Moteur audio entièrement refait avec meilleurs formats (MIDI, ALAC, M4A).
+ Réduction drastique de la consommation énergétique.
+ Optimisation des bases de données et du cache.
+ Amélioration du temps de démarrage via Baseline Profiles.
+
- Lecture : Correction des saccades en Opus/MP3, erreurs de ReplayGain pendant les fondus enchaînés et problèmes de démarrage sur les décodeurs Samsung.
- Stabilité : Élimination des plantages au démarrage, de la navigation par artiste et sur les appareils Android 12+.
- Interface : Correction du clignotement des pochettes, du dépassement de texte pour les scripts non latins et du comportement de la barre de navigation/du mini-lecteur.
- Sécurité : Sécurisation renforcée de la gestion des identifiants, des autorisations de stockage et de la communication avec le serveur multimédia.
+ Correction des problèmes de lecture et de stuttering.
+ Correction des crashs sur Android et Samsung decoders.
+ Correction des problèmes UI (cover art, texte, navigation).
+ Sécurisation du stockage et des communications médias.
+
- Traduction : Espagnol, Français, Russe, Chinois simplifié, Indonésien, Italien
-
-
\ No newline at end of file
+ Localisation : espagnol, français, russe, chinois simplifié, indonésien, italien.
+
+
+
+ Intégration Google Drive avec gestion de lecture.
+ Édition en lot des métadonnées.
+ Traduction IA des paroles avec options Wear OS.
+ Outil de diagnostic de latence et sélection multiple dans la recherche.
+ Support arabe et turc avec options réseau local HTTP.
+
+
+
+ Économie de batterie drastique.
+ Optimisation de la gestion de file d’attente.
+ Animations Material 3 Expressive améliorées.
+ Synchronisation bibliothèque optimisée.
+
+
+
+ Correction des problèmes de lecture et buffering.
+ Correction de la suppression de chansons externes.
+ Correction des crashs et problèmes mémoire Wear OS et mobile.
+
+
+
diff --git a/app/src/main/res/values-fr/strings_home_screen.xml b/app/src/main/res/values-fr/strings_home_screen.xml
index 4b9fd3019..ceb04518a 100644
--- a/app/src/main/res/values-fr/strings_home_screen.xml
+++ b/app/src/main/res/values-fr/strings_home_screen.xml
@@ -9,9 +9,9 @@
Diffusez de la musique depuis vos comptes cloud
- Bêta 0.7.0
+ Bêta 0.7.5β
- Bienvenue dans PixelPlayer 0.7.0-bêta
+ Bienvenue dans PixelPlayer 0.7.5-bêtaVous utilisez une version bêta qui peut contenir des bugs, des plantages ou des fonctionnalités expérimentales. Aidez-nous à nous améliorer en signalant les problèmes.À quoi s\'attendreDes bugs, des plantages ou des fonctionnalités incomplètes peuvent survenir de manière inattendue.
@@ -274,4 +274,4 @@
%1$d chanson%1$d chansonsSemaine %1$d
-
\ No newline at end of file
+
diff --git a/app/src/main/res/values-fr/strings_player.xml b/app/src/main/res/values-fr/strings_player.xml
index 674747df4..da510f96e 100644
--- a/app/src/main/res/values-fr/strings_player.xml
+++ b/app/src/main/res/values-fr/strings_player.xml
@@ -129,8 +129,8 @@
ParolesChargement des paroles…
- Synchronisées
- Statiques
+ Synchronisé
+ StatiqueOptions des paroles−.5−.1
diff --git a/app/src/main/res/values-fr/strings_settings.xml b/app/src/main/res/values-fr/strings_settings.xml
index 13f14c868..840c8bdcb 100644
--- a/app/src/main/res/values-fr/strings_settings.xml
+++ b/app/src/main/res/values-fr/strings_settings.xml
@@ -159,17 +159,6 @@
Langue de l\'applicationChoisissez la langue utilisée dans l\'interface de l\'application.Système par défaut
- Anglais
- Espagnol
- Allemand
- Français
- Russe
- Chinois simplifié
- Indonésien
- Italien
- Coréen
- Norvégien (Bokmål)
- TurcThème de l\'applicationPasser du mode clair au mode sombre, ou suivre l\'apparence du système.Thème clair
@@ -629,6 +618,12 @@
Contributeurs open sourceListe des contributeurs en direct de GitHub.%1$d contrib.
+ GitHub
+ Dépôt
+ Telegram
+ Support
+ Ouvrir le dépôt GitHub
+ Rejoindre la communauté TelegramOuvrir le profil GitHubOuvrir TelegramAvatar de %1$s
diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml
index cccb78d7f..64bf20824 100644
--- a/app/src/main/res/values-in/strings.xml
+++ b/app/src/main/res/values-in/strings.xml
@@ -99,7 +99,7 @@
LirikPengaturanSampul Album
- Daftar Putar
+ PlaylistTrek tidak dikenalArtis tidak dikenalAlbum tidak dikenal
diff --git a/app/src/main/res/values-in/strings_changelogs.xml b/app/src/main/res/values-in/strings_changelogs.xml
index 74b9d5f0f..f244cae28 100644
--- a/app/src/main/res/values-in/strings_changelogs.xml
+++ b/app/src/main/res/values-in/strings_changelogs.xml
@@ -129,4 +129,22 @@
Lokalisasi: Spanish, French, Russian, Simplified Chinese, Indonesia, Italian
+
+ Integrasi Google Drive dengan manajemen siklus hidup pemutar.
+ Pengeditan metadata lagu massal (tag dan gambar sampul).
+ Terjemahan lirik AI dengan preferensi Wear OS yang dapat disesuaikan.
+ Alat diagnosis lag dan multi-seleksi di layar Pencarian.
+ Dukungan bahasa Arab & Turki, dengan opsi jaringan lokal URL HTTP yang dilokalkan.
+
+
+ Penghematan baterai drastis (offload audio dan gerbang polling UI).
+ Manajemen antrean yang dioptimalkan (penyisipan lebih cepat dan pengindeksan eksplisit).
+ Animasi gerakan Material 3 Expressive untuk layar transisi.
+ Refaktor sinkronisasi pustaka melalui pemindaian terbatasi (throttled).
+
+
+ Menyelesaikan lag pemutaran yang tersendat/terlompat dan masalah buffering.
+ Memperbaiki sinkronisasi penghapusan lagu eksternal dan konsistensi metadata.
+ Memperbaiki masalah memori, crash, dan gangguan tata letak pada Wear OS dan ponsel.
+
\ No newline at end of file
diff --git a/app/src/main/res/values-in/strings_home_screen.xml b/app/src/main/res/values-in/strings_home_screen.xml
index 0af328bde..7a5e0de11 100644
--- a/app/src/main/res/values-in/strings_home_screen.xml
+++ b/app/src/main/res/values-in/strings_home_screen.xml
@@ -9,9 +9,9 @@
Alirkan musik dari akun cloud Anda
- Beta 0.7.0
+ Beta 0.7.5β
- Selamat datang di PixelPlayer 0.7.0-beta
+ Selamat datang di PixelPlayer 0.7.5-betaAnda menggunakan versi beta yang mungkin berisi bug, crash, atau fitur eksperimental. Bantu kami meningkatkannya dengan melaporkan masalah.Yang perlu diharapkanBug, crash, atau fitur yang belum selesai dapat terjadi sewaktu-waktu.
@@ -274,4 +274,4 @@
%1$d Lagu%1$d LaguMinggu %1$d
-
\ No newline at end of file
+
diff --git a/app/src/main/res/values-in/strings_library.xml b/app/src/main/res/values-in/strings_library.xml
index 9fa5ca168..97e27f38b 100644
--- a/app/src/main/res/values-in/strings_library.xml
+++ b/app/src/main/res/values-in/strings_library.xml
@@ -192,7 +192,7 @@
%1$d ALBUMterpilihBatas: %1$d album per pilihan.
- Antrekan + putar menghormati urutan pilihan Anda.
+ Antrekan + putar berdasarkan urutan pilihan Anda.%1$d GENREterpilihLakukan operasi batch pada semua lagu dalam genre ini.
@@ -499,7 +499,7 @@
Gagal mengekspor: %1$sMusik/Ekspor PixelPlayerSilakan konfigurasi API key Gemini Anda di Pengaturan.
- Daftar putar dipulihkan
+ Playlist dipulihkanMembagikan %d playlistMembagikan %d playlist
diff --git a/app/src/main/res/values-in/strings_settings.xml b/app/src/main/res/values-in/strings_settings.xml
index 6aab6877b..278371bf3 100644
--- a/app/src/main/res/values-in/strings_settings.xml
+++ b/app/src/main/res/values-in/strings_settings.xml
@@ -159,17 +159,6 @@
Bahasa AplikasiPilih bahasa yang digunakan di seluruh antarmuka aplikasi.Default sistem
- Inggris
- Spanyol
- Jerman
- Prancis
- Rusia
- Tionghoa (Sederhana)
- Indonesia
- Italia
- Korea
- Norwegia (Bokmål)
- TurkiTema AplikasiBeralih antara terang, gelap, atau ikuti tampilan sistem.Tema Terang
@@ -629,6 +618,12 @@
Kontributor open sourceDaftar kontributor langsung dari GitHub.%1$d kontrib.
+ GitHub
+ Repositori
+ Telegram
+ Dukungan
+ Buka repositori GitHub
+ Bergabung dengan komunitas TelegramBuka profil GitHubBuka TelegramAvatar %1$s
diff --git a/app/src/main/res/values-it/strings_changelogs.xml b/app/src/main/res/values-it/strings_changelogs.xml
index bc4f2f71b..d7ce29f70 100644
--- a/app/src/main/res/values-it/strings_changelogs.xml
+++ b/app/src/main/res/values-it/strings_changelogs.xml
@@ -1,132 +1,170 @@
- Changelog
- Visualizza su GitHub
+ Registro modifiche
+ Vedi su GitHubMiglioramentiCorrezioniNovità
- Aggiunto
+ Aggiunti
+
- Supporto Chromecast per trasmettere l\'audio dal tuo dispositivo.
- Changelog in-app per tenerti aggiornato sulle ultime funzioni.
+ Supporto Chromecast per trasmettere audio dal dispositivo.
+ Changelog integrato per tenerti aggiornato sulle ultime funzionalità.Supporto per file .LRC, sia incorporati che esterni.
- Supporto per i testi offline.
- Testi sincronizzati (sincronizzati con il brano).
+ Supporto per testi offline.
+ Testi sincronizzati con la musica.Nuova schermata per visualizzare la coda completa.
- Riordina e rimuovi brani dalla coda.
- Gesti del mini-riproduttore (scorrimento verso il basso per chiudere).
- Aggiunte altre animazioni Material.
- Nuove impostazioni per personalizzare l\'aspetto e lo stile.
- Nuove impostazioni per cancellare la cache.
+ Riordino e rimozione dei brani dalla coda.
+ Gesti del mini-player (scorri verso il basso per chiudere).
+ Aggiunte nuove animazioni Material.
+ Nuove impostazioni per personalizzare l’aspetto.
+ Nuova impostazione per svuotare la cache.
+
- Riprogettazione completa dell\'interfaccia utente.
- Riprogettazione completa del riproduttore.
- Miglioramenti delle prestazioni nella libreria.
- Velocità di avvio dell\'applicazione migliorata.
- L\'IA ora fornisce risultati migliori.
+ Restyling completo dell’interfaccia utente.
+ Restyling completo del player.
+ Miglioramento delle prestazioni della libreria.
+ Miglioramento della velocità di avvio dell’app.
+ L’IA ora fornisce risultati migliori.
+
- Corretti vari bug nell\'editor dei tag.
- Corretto un bug per cui la notifica di riproduzione non si cancellava.
- Corretti diversi bug che causavano il crash dell\'app.
+ Corretti vari bug nell’editor dei tag.
+ Corretto un bug per cui la notifica di riproduzione non veniva rimossa.
+ Corretti diversi bug che causavano crash dell’app.
+
- Introdotto un hub di statistiche di ascolto più ricco con approfondimenti dettagliati sulle tue sessioni.
- Lanciato un lettore rapido fluttuante per aprire e visualizzare in anteprima istantaneamente i file locali.
- Aggiunta una scheda cartelle con navigazione ad albero e vista predisposta per le playlist.
+ Introdotto un hub statistiche di ascolto più ricco con analisi approfondite.
+ Rilasciato un mini-player flottante per aprire e visualizzare file locali.
+ Aggiunta scheda cartelle con navigazione ad albero e vista playlist.
+
- Raffinata l\'interfaccia utente generale Material 3 per un\'esperienza più pulita e coesa.
- La modifica dei metadati ora supporta il cambio della copertina.
- Animazioni e transizioni più fluide in tutta l\'app per una navigazione più fluida.
- Migliorato il layout della schermata dell\'artista con dettagli più ricchi e rifiniture.
- Aggiornata la generazione di DailyMix e YourMix con selezioni più intelligenti e varie.
- Potenziata la generazione di playlist tramite IA.
- Migliorata la rilevanza e la presentazione dei risultati di ricerca per una scoperta più rapida.
- Esteso il supporto a una gamma più ampia di formati di file audio.
-
+ Interfaccia Material 3 migliorata per un’esperienza più coerente.
+ Modifica metadati ora supporta la copertina album.
+ Animazioni e transizioni più fluide in tutta l’app.
+ Migliorata la schermata artista con più dettagli.
+ Migliorati DailyMix e YourMix con selezioni più varie.
+ Potenziata la generazione playlist IA.
+ Ricerca migliorata per risultati più rapidi.
+ Supporto esteso a più formati audio.
+
+
- Risolti i problemi con i metadati in modo che i dettagli dei brani rimangano accurati ovunque.
- Ripristinate le scorciatoie delle notifiche in modo che ritornino in modo affidabile alla riproduzione.
+ Risolti problemi di incoerenza nei metadati.
+ Ripristinati i collegamenti rapidi alle notifiche.
+
- Importante riprogettazione della navigazione
- Nuovo esploratore di file per scegliere le directory sorgente
- Nuove funzionalità di connettività e trasmissione (casting)
- Continuità perfetta tra dispositivi remoti
- Transizione senza interruzioni (gapless) tra i brani
- Controllo della dissolvenza incrociata (crossfade)
- Nuova funzione di transizioni personalizzate (solo per playlist)
- Continua la riproduzione anche dopo la chiusura dell\'app
- Ottimizzazioni dell\'interfaccia utente
- Funzionalità di statistica migliorata
- Controllo della coda riprogettato con più funzioni
- Supporto migliorato per diversi tipi di file per la riproduzione e la modifica dei metadati
- Gestione dei permessi migliorata
- Correzioni di bug minori
-
+ Grande rinnovamento della navigazione
+ Nuovo file explorer per scegliere le cartelle sorgente
+ Nuove funzionalità di connettività e casting
+ Continuità fluida tra dispositivi remoti
+ Transizioni senza interruzioni tra i brani
+ Controllo crossfade
+ Nuove transizioni personalizzate (solo playlist)
+ Riproduzione continua dopo la chiusura dell’app
+ Ottimizzazioni UI
+ Miglioramento statistiche
+ Controllo coda riprogettato con più funzioni
+ Migliorato supporto file e metadati
+ Migliorato sistema permessi
+ Correzioni minori di bug
+
+
- Aggiornamento dell\'interfaccia utente Material 3 Expressive
- Equalizzatore a 10 bande ed effetti
- Nuovo flusso di sincronizzazione della libreria
+ Aggiornamento Material 3 Expressive UI
+ Equalizzatore a 10 bande & effetti
+ Nuovo flusso di sincronizzazione libreriaIntegrazione IA (modelli Gemini)
- Importazione/esportazione di playlist M3U
- Integrazione delle immagini degli artisti da Deezer
- Copertine personalizzate per le playlist
- Rifattorizzazione dell\'architettura delle impostazioni
- Animazioni per la coda e il riproduttore
- Profili Baseline e prestazioni
- Sistema di testi migliorato con regolazione del ritardo di sincronizzazione
-
+ Import/Export playlist M3U
+ Integrazione artwork artisti Deezer
+ Copertine playlist personalizzate
+ Refactor architettura impostazioni
+ Animazioni player e coda
+ Ottimizzazione prestazioni (Baseline Profiles)
+ Sistema testi migliorato con offset sincronizzato
+
+
- Miglioramenti della stabilità della trasmissione (casting)
- Stabilità del pannello del riproduttore
- Correzioni di bug generali e pulizia
+ Migliorata stabilità del casting
+ Migliorata stabilità del player
+ Correzioni generali di bug
+
- Il supporto Android Auto è ora disponibile per la riproduzione in auto.
- Il supporto Wear OS è attivo, inclusi migliori controlli di riproduzione tra smartwatch e telefono.
- Integrazioni cloud ampliate con miglioramenti per Telegram, NetEase, QQ Music e Google Drive.
- La sezione Ascoltati di recente e il ripristino persistente della coda mantengono pronta la sessione di ascolto.
- Ora sono inclusi il Backup e Ripristino v3 e gli strumenti di gestione dell\'account.
- I testi sono diventati più intelligenti con ricerca manuale alternativa e miglioramenti di archiviazione.
-
+ Supporto Android Auto per la riproduzione in auto.
+ Supporto Wear OS con controlli migliorati tra orologio e telefono.
+ Integrazioni cloud estese (Telegram, NetEase, QQ Music, Google Drive).
+ Ripristino cronologia recente e coda persistente.
+ Backup & Restore v3 e gestione account.
+ Ricerca testi migliorata con fallback manuale.
+
+
- Grande ottimizzazione delle prestazioni per avvio, libreria, coda e interazioni con il riproduttore.
- Le schermate di Riproduttore, Cast, Testi, Artista e Genere sono state riprogettate per un uso più fluido.
- I flussi di navigazione e ricerca sono più affidabili, con una gestione dei percorsi più sicura.
- Compatibilità della riproduzione audio migliorata per più dispositivi e formati.
- I flussi di selezione multipla sono stati estesi a brani, album e playlist.
+ Grande miglioramento delle prestazioni globali.
+ Restyling di player, casting, testi, artista e generi.
+ Navigazione e ricerca più affidabili.
+ Compatibilità audio migliorata.
+ Migliorato supporto selezione multipla.
+
- Il comportamento della coda e della riproduzione casuale è ora più stabile e prevedibile.
- Risolti diversi casi limite per la trasmissione e la riproduzione in background.
- Risolti i problemi relativi a timer di spegnimento, navigazione della scheda File e crash dell\'artista dell\'album.
- Migliorata la stabilità del servizio e il caricamento dei widget per ridurre i problemi di surriscaldamento e memoria.
- Correzioni di bug generali e rifiniture dell\'interfaccia utente in tutta l\'app.
+ Migliorata stabilità coda e shuffle.
+ Corretti problemi di riproduzione in background e casting.
+ Corretti crash su sleep timer e file/artisti.
+ Migliorati widget e stabilità servizi.
+ Correzioni generali UI.
+
- Wear OS: Trasferimento musica, riproduzione locale, sincronizzazione della coda e controllo remoto da smartwatch.
- IA: Integrazione di Groq AI e OpenRouter (sperimentale) con ottimizzazione dei token.
- Cloud: Aggiunto il supporto a Jellyfin.
- Testi: Traduzione sincronizzata con interruttore dedicato, supporto per il formato Kugou LRC, personalizzazione dell\'allineamento del testo e caricamento remoto migliorato.
- UI/UX: Modalità barra di navigazione compatta, temi dinamici dalla tavolozza dei colori dell\'album, testo scorrevole per titoli lunghi e nuove opzioni di ordinamento.
- Telegram: Supporto nativo per gli argomenti (topics) e modalità di visualizzazione migliorate.
-
+ Wear OS: trasferimento musica, riproduzione locale, sync coda e controllo remoto.
+ IA: integrazione Groq AI e OpenRouter (sperimentale).
+ Cloud: aggiunto supporto Jellyfin.
+ Testi: traduzione sincronizzata, supporto Kugou LRC, allineamento migliorato.
+ UI/UX: barra compatta, temi dinamici, marquee, nuovi ordinamenti.
+ Telegram: supporto topic nativi.
+
+
- Motore audio: Revisione completa con supporto per più formati (MIDI, ALAC, M4A) e ottimizzazione dei decodificatori.
- Efficienza: Riduzione drastica del consumo energetico, risoluzione del surriscaldamento e ottimizzazione delle attività in background (SyncWorker).
- Database: Ottimizzazioni massicce delle query e cache delle copertine riprogettata per prevenire la perdita di dati.
- Avvio: Tempo di caricamento migliorato tramite l\'ottimizzazione di Baseline Profile.
+ Motore audio completamente rifatto con supporto ampliato (MIDI, ALAC, M4A).
+ Ridotto drasticamente consumo energetico.
+ Ottimizzate query database e cache artwork.
+ Migliorato tempo di avvio tramite Baseline Profiles.
+
- Riproduzione: Risolti i micro-scatti con Opus/MP3, gli errori ReplayGain durante le dissolvenze incrociate e i problemi di avvio sui decodificatori Samsung.
- Stabilità: Eliminati i crash all\'avvio, nella navigazione dell\'artista e sui dispositivi con Android 12+.
- Interfaccia utente: Corretto lo sfarfallio delle copertine, il superamento dei limiti di testo per le scritture non latine e il comportamento della barra di navigazione/mini-riproduttore.
- Sicurezza: Maggiore protezione nella gestione delle credenziali, dei permessi di archiviazione e della comunicazione con il server multimediale.
+ Corretti problemi di stuttering e riproduzione.
+ Corretti crash su Android e decoder Samsung.
+ Corretti problemi UI (copertine, testo, navigazione).
+ Migliorata sicurezza storage e comunicazione media.
+
- Localizzazione: Spagnolo, Francese, Russo, Cinese semplificato, Indonesiano, Italiano
-
-
\ No newline at end of file
+ Localizzazione: spagnolo, francese, russo, cinese semplificato, indonesiano, italiano.
+
+
+
+ Integrazione Google Drive con gestione riproduzione.
+ Modifica batch dei metadati.
+ Traduzione testi IA con opzioni Wear OS.
+ Strumento diagnostica lag e selezione multipla ricerca.
+ Supporto arabo e turco con opzioni rete locale HTTP.
+
+
+
+ Risparmio batteria drastico.
+ Ottimizzazione gestione coda.
+ Animazioni Material 3 Expressive migliorate.
+ Sync libreria ottimizzato.
+
+
+
+ Corretti problemi di buffering e riproduzione.
+ Corretto sync eliminazione brani esterni.
+ Corretti crash e problemi memoria su Wear OS e mobile.
+
+
+
diff --git a/app/src/main/res/values-it/strings_home_screen.xml b/app/src/main/res/values-it/strings_home_screen.xml
index 51cd56f73..c53d26ccf 100644
--- a/app/src/main/res/values-it/strings_home_screen.xml
+++ b/app/src/main/res/values-it/strings_home_screen.xml
@@ -9,9 +9,9 @@
Riproduci in streaming la musica dai tuoi account cloud
- Beta 0.7.0
+ Beta 0.7.5β
- Benvenuto in PixelPlayer 0.7.0-beta
+ Benvenuto in PixelPlayer 0.7.5-betaStai utilizzando una build beta che potrebbe contenere bug, crash o funzionalità sperimentali. Aiutaci a migliorare segnalando i problemi.Cosa aspettarsiBug, crash o funzionalità incomplete possono verificarsi inaspettatamente.
@@ -274,4 +274,4 @@
%1$d brano%1$d braniSettimana %1$d
-
\ No newline at end of file
+
diff --git a/app/src/main/res/values-it/strings_settings.xml b/app/src/main/res/values-it/strings_settings.xml
index 234a0e911..035b1b484 100644
--- a/app/src/main/res/values-it/strings_settings.xml
+++ b/app/src/main/res/values-it/strings_settings.xml
@@ -163,17 +163,6 @@
Lingua appScegli la lingua usata nell\'interfaccia dell\'app.Predefinita sistema
- Inglese
- Spagnolo
- Tedesco
- Francese
- Russo
- Cinese semplificato
- Indonesiano
- Italiano
- Coreano
- Norvegese (Bokmål)
- TurcoTema appPassa tra chiaro, scuro o segui l\'aspetto di sistema.Tema chiaro
@@ -633,6 +622,12 @@
Contributori open sourceLista contributori live da GitHub.%1$d contrib.
+ GitHub
+ Repository
+ Telegram
+ Supporto
+ Apri repository GitHub
+ Unisciti alla community di TelegramApri profilo GitHubApri TelegramAvatar di %1$s
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
new file mode 100644
index 000000000..5dedb3c73
--- /dev/null
+++ b/app/src/main/res/values-ja/strings.xml
@@ -0,0 +1,128 @@
+
+
+ PixelPlayer
+ 音楽プレイヤー
+ アプリ名の変更について
+ 商標上の理由により、アプリ名を PixelPlay から PixelPlayer に変更しました。引き続きお楽しみください!
+ 今後表示しない
+
+
+ ホーム
+ 検索
+ ライブラリ
+
+
+ 特別な権限が必要です
+ 曲のメタデータ(.mp3 ファイル)を編集するには、PixelPlayer にすべてのファイルへの特別なアクセス権限が必要です。これにより、トラックのタグを直接変更できます。メタデータ編集を有効にするには、次の画面でこの権限を許可してください。
+ 権限を許可
+
+
+ すぐに再生
+ このオーディオファイルを開けませんでした。
+ フルプレイヤーを開く
+
+
+ シャッフル
+ すべての曲をシャッフル
+ すべてシャッフル
+ 最後のプレイリスト
+ 開けるプレイリストがありません
+
+
+ Play ストアを開く
+ ベータを続ける
+ Play ストアのリンクは GitHub の設定から有効化されます。
+ PixelPlayer が Google Play で公開されました
+ リリース更新は Google Play の安定版チャンネルをご利用ください。ベータビルドも引き続き提供されます。
+ PixelPlayer
+ リリースのお知らせ
+ 近日公開
+
+
+ PixelPlayer をご利用いただきありがとうございます!
+ ハイスコア %1$d
+ 閉じる
+ スコア
+ レベル %1$d
+ ライフ
+ レベルクリア!
+ ゲームオーバー
+ スコア: %1$d
+ もう一度?
+ 次のレベル
+ ゲームを再起動
+ タップして再起動
+ ランダムに音楽を再生
+ ブロック崩し
+ ハイスコア %1$d
+ プレイ
+ ドラッグしてパドルを動かす
+
+
+ プレイヤーを閉じる
+ 再生操作を処理中…
+ 再生エラー: %1$s
+
+
+ 戻る
+ OK
+ キャンセル
+ 閉じる
+ エラー
+ 検索
+ 検索をクリア
+ すべて
+ 確認
+ 保存しました!
+ 選択済み
+ %1$d%%
+ アーティスト
+ すべて選択
+ クリア
+ 不明なエラー
+
+
+ 保存
+ 完了
+ リセット
+ 適用
+ シャッフル
+ コピー
+ 共有
+ 元に戻す
+ インポート
+ 削除
+ エクスポート
+ 結合
+ 名前を変更
+ 作成
+ 歌詞
+ 設定
+ アルバムアート
+ プレイリスト
+ 不明なトラック
+ 不明なアーティスト
+ 不明なアルバム
+ 閉じる
+ 追加
+ 削除
+ 再生
+ 前のトラック
+ 次のトラック
+ お気に入り
+ 一時停止
+ リピート
+ オプション
+ シャッフル再生
+ %1$s のその他のオプション
+ メニューを展開
+ 次へ
+ 完了
+ デフォルトに戻す
+ すべてエクスポート
+ すべて結合
+ すべて共有
+ アルバムを再生
+ アルバムをシャッフル再生
+ %1$s のアルバムアート
+
diff --git a/app/src/main/res/values-ja/strings_changelogs.xml b/app/src/main/res/values-ja/strings_changelogs.xml
new file mode 100644
index 000000000..85544d5ba
--- /dev/null
+++ b/app/src/main/res/values-ja/strings_changelogs.xml
@@ -0,0 +1,9 @@
+
+
+ 変更履歴
+ GitHub で見る
+ 改善
+ 修正
+ 新機能
+ 追加
+
diff --git a/app/src/main/res/values-ja/strings_cloud_services.xml b/app/src/main/res/values-ja/strings_cloud_services.xml
new file mode 100644
index 000000000..a2e5bac75
--- /dev/null
+++ b/app/src/main/res/values-ja/strings_cloud_services.xml
@@ -0,0 +1,226 @@
+
+
+
+ Telegram ログイン
+ 番号を編集中です。再送すると前のコードが無効になります。
+ 処理中…
+ Telegram を初期化中…
+ ログアウト中…
+ セッションを閉じています…
+ セッションが閉じました。続けるにはログインを再度開いてください。
+ 安全な Telegram セッションを準備中…
+ Telegram からの応答を待機中…
+ Telegram に接続
+ Telegram に接続してチャンネルやチャットから音楽をストリーミングします。
+ 電話番号
+ Telegram の番号を入力してください。後で戻って編集することもできます。
+ 電話番号
+ 81
+ 09012345678
+ コードを送信
+ 確認コード
+ Telegram からのコードを入力してください。番号が間違っている場合は戻って修正してください。
+ コード
+ 12345
+ 電話番号を編集
+ コードを再送
+ コードを確認
+ 二段階認証パスワード
+ Telegram のパスワードを入力してください。番号を修正するために戻ることもできます。
+ パスワード
+ パスワードを確認
+ しばらくお待ちください…
+
+
+ Telegram チャンネル
+ チャンネルを追加
+ Telegram パブリックチャンネル
+ 同期中
+ 今すぐ同期
+ トピックを折りたたむ
+ トピックを表示
+ チャンネルオプション
+ トピック
+ チャンネルを同期中
+ Telegram から曲を更新中
+ このチャンネルから最新の曲を取得
+ チャンネルを削除
+ 同期を停止してキャッシュされた曲を削除
+ チャンネルを削除しますか?
+ %1$s の同期が停止し、このチャンネルのキャッシュされた曲がすべて削除されます。
+ 削除
+ 同期済みチャンネルがありません
+ Telegram のパブリックチャンネルを追加して\n音楽ライブラリを同期しましょう
+ チャンネルを追加
+ 未同期
+ %1$s に同期
+
+
+ チャンネルを追加
+ 音楽を同期する Telegram パブリックチャンネルを検索
+ \@チャンネル名またはリンク
+ 検索中…
+ チャンネルを検索
+ パブリックチャンネルのユーザー名またはリンクを入力して\nオーディオファイルを同期してください
+
+
+ %d 曲
+
+
+ %d トピック
+
+
+
+ Subsonic
+ Navidrome、Airsonic などの Subsonic 互換サーバーを管理します。
+
+
+ 同期をタップして Jellyfin のプレイリストを取得してください
+ Jellyfin サーバーの接続を管理します。
+
+
+ 音楽フォルダ
+ + をタップして Drive フォルダを追加
+ フォルダがまだ追加されていません
+ %1$d フォルダが同期済み
+ フォルダを追加
+
+
+ プレイリストの種類を選択
+ 同期するプレイリストを選択:
+ すべてのプレイリスト
+ 作成 & お気に入り
+ 作成したプレイリスト
+ お気に入りのプレイリスト
+
+
+ %1$d プレイリストが同期済み
+ プレイリスト
+ 同期
+ まだプレイリストが同期されていません
+ 同期をタップしてプレイリストを取得してください
+ クイックアクション
+ ライブラリを同期
+ 切断
+ %1$d 曲
+
+
+ 同期中
+ ライブラリを同期中…
+ プレイリストを取得中…
+ プレイリストを同期中: %1$s
+ ローカルライブラリを更新中…
+ 同期完了
+ アルバムリストを取得中…
+ %1$s から曲を取得中…
+ %1$d 曲をデータベースに保存中…
+ ライブラリに曲が見つかりません
+ ライブラリ同期完了
+ 同期中…
+ エラー: %1$s
+
+
+ 同期
+ すべて同期
+ ログアウト
+ すべてのプレイリストを同期
+ ユーザーアバター
+
+
+ インターネット接続がありません
+ このコンテンツにはインターネット接続が必要です。ネットワーク設定を確認して再試行してください。
+ オフラインです
+ このコンテンツにアクセスするにはインターネット接続を確認して再試行してください。
+
+
+ 接続
+ 接続中…
+ サーバー URL とアカウントの認証情報を入力してください。
+ 接続詳細
+ パスワードを非表示
+ パスワード
+ パスワードを入力
+ http:// を入力
+ サーバー URL
+ パスワードを表示
+ Telegram
+ ユーザー名
+ admin
+ ようこそ、%1$s!
+
+
+ Navidrome、Gonic、Airsonic などの Subsonic 互換サーバーに対応
+ Navidrome、Airsonic、Gonic、Ampache などの Subsonic API 互換サーバーをサポートします。
+ サーバーが対応している場合はアプリパスワードも使用できます。
+ https:// を入力
+ セルフホスト型音楽サーバーに接続
+ Navidrome
+ サーバーの完全な https:// ベースアドレスを使用してください。
+ https://music.example.com
+ Subsonic または Navidrome のアカウント名です。
+ Subsonic / Navidrome
+ Subsonic
+
+
+ Jellyfin サーバー URL とアカウントの認証情報を入力してください。
+ 音楽ライブラリをストリーミングするために Jellyfin サーバーに接続します
+ Jellyfin サーバーに接続します。ローカルネットワークアクセスには HTTP と HTTPS の両方がサポートされています。
+ Jellyfin
+ Jellyfin アカウントのパスワード。
+ Jellyfin メディアサーバーに接続
+ Jellyfin
+ ポートを含む Jellyfin サーバーの完全な URL。
+ http://192.168.1.100:8096
+ Jellyfin アカウントのユーザー名。
+
+
+ Google Drive から直接音楽ファイルをストリーミング
+ Google Drive に接続
+ Google Drive に接続しました!
+ 「PixelPlayer Music」を作成
+ ここに音楽用の新しいフォルダを作成
+ フォルダがありません
+ フォルダを開く
+ 音楽ソースとして使用するフォルダを選択または作成
+ 音楽フォルダを選択
+ Google Drive をセットアップ中…
+ Google でサインイン
+ Google Drive
+ 使用
+
+
+ セッション Cookie を読み取れませんでした。
+ 完了
+ 終了
+ Cookie が見つかりません。先にログインしてください。
+ ページの読み込みに時間がかかっています。更新するか別のネットワークをお試しください。
+ +
+ 保存中…
+ 残る
+ ページの読み込みがタイムアウトしました。進捗を失わずに再試行できます。
+ Web で戻る
+ 後で戻れます。閉じると現在のページの状態は破棄されます。
+ Web で進む
+ 更新
+ 再試行
+ ホームを開く
+ WebView の読み込みに失敗しました。
+
+
+ NetEase の Cookie を読み取れませんでした: %1$s
+ NetEase のログインを終了しますか?
+ NetEase の読み込み中に HTTP %1$d エラーが発生しました。
+ まだログインが検出されていません。完了を押す前に NetEase のログインを完了してください。
+ NetEase Music にログイン
+ セキュリティについて: パスワードは NetEase のウェブページにのみ入力されます。PixelPlayer はライブラリを同期するためにセッション Cookie(MUSIC_U)を保存します。
+ NetEase Music
+
+
+ QQ Music の Cookie を読み取れませんでした: %1$s
+ QQ Music のログインを終了しますか?
+ QQ Music の読み込み中に HTTP %1$d エラーが発生しました。
+ まだログインが検出されていません。完了を押す前に QQ Music のログインを完了してください。
+ QQ Music にログイン
+ セキュリティについて: パスワードは QQ Music のウェブページにのみ入力されます。PixelPlayer はライブラリを同期するためにセッション Cookie を保存します。
+ QQ Music
+
diff --git a/app/src/main/res/values-ja/strings_equalizer.xml b/app/src/main/res/values-ja/strings_equalizer.xml
new file mode 100644
index 000000000..3c36811ad
--- /dev/null
+++ b/app/src/main/res/values-ja/strings_equalizer.xml
@@ -0,0 +1,57 @@
+
+
+
+ 名前を入力してください
+ 名前を変更
+
+
+ 表示モードを変更
+ イコライザーを無効化
+ イコライザーを有効化
+ 編集
+ プリセットを編集
+ カスタムプリセット
+ プリセット
+ 更新
+ バスブースト
+ バーチャライザー
+ ラウドネス
+ 非対応
+ この端末では非対応
+ 音量
+ 周波数特性
+ Hz
+ バス
+ ローミッド
+ ハイミッド
+ トレブル
+ バス / ロー
+ ミッド / ハイ
+ ページ %1$d
+ 時間をリセット
+ 新規保存
+
+
+ 保存済みプリセット
+ カスタムプリセットがまだ保存されていません。
+ ピンを外す
+ ピン留め
+ 名前を変更
+ 削除
+
+
+ カスタムプリセットを保存
+ カスタムイコライザープリセットの名前を入力してください。
+ プリセット名
+ プリセット名を変更
+
+
+ プリセットを管理
+ ドラッグして並び替え • 目のアイコンで表示/非表示を切り替え
+ 並び替え
+ プリセットをリセット
+ デフォルトのプリセット順と表示状態に戻します。続けますか?
+ デフォルトに戻す
+ 表示
+ 非表示
+
diff --git a/app/src/main/res/values-ja/strings_home_screen.xml b/app/src/main/res/values-ja/strings_home_screen.xml
new file mode 100644
index 000000000..ac2021f3e
--- /dev/null
+++ b/app/src/main/res/values-ja/strings_home_screen.xml
@@ -0,0 +1,276 @@
+
+
+
+ β
+ ベータ
+ クラウドストリーミング
+ 変更履歴
+ クラウドストリーミング
+ クラウドアカウントから音楽をストリーミング
+
+
+ Beta 0.7.0
+ β
+ PixelPlayer 0.7.0-beta へようこそ
+ バグ、クラッシュ、または試験的な機能が含まれている可能性があるベータビルドを使用しています。問題を報告して改善にご協力ください。
+ 期待されること
+ バグ、クラッシュ、または未完成の機能が予期せず発生することがあります。
+ 一部の機能は予告なく変更または削除される場合があります。
+ ベータビルドはリリース版より不安定な場合があります。
+ 既知の問題を報告する前に必ず最新版を確認してください。
+ テスト中にベータビルドが変更、破損、または改善される可能性があること。
+ GitHub Issue のショートカット
+ まず検索してから、バグ、クラッシュ、要望、質問に対する集中したレポートを作成してください。
+ 既存の Issue を開く
+ Issue またはクラッシュを報告
+ 再現手順、期待される結果、実際の結果、デバイス/OS の詳細を共有してください。
+ 報告方法
+ 新しい Issue を開く前の簡単なチェックリスト。
+ Issue を開く前に
+ 重複を避けるために既存のオープンおよびクローズ済みの Issue を検索してください。
+ 最新の PixelPlayer バージョンに更新して問題が引き続き発生することを確認してください。
+ アプリを再起動して問題が続くことを確認してください。
+ 再現を試みて正確な手順を書き留めてください。
+ Issue の種類は?
+ バグ報告: 何かが正しく動作しない。
+ 機能リクエスト: 新機能や改善の追加。
+ 質問: Discussions が有効な場合はそちらを使用するか、question ラベルで Issue を開いてください。
+ バグ報告
+ 何かが正しく動作しないまたはクラッシュする場合にこれらのフィールドをコピーしてください。
+ バグ報告
+ 概要:
+ 期待される動作:
+ 現在の動作:
+ 再現手順: 1. 2. 3.
+ 頻度は? 常時 / 時々 / まれに。
+ スクリーンショット / 動画: あれば。
+ ログ / スタックトレース: あれば。
+ 環境
+ PixelPlayer バージョン:
+ インストール元: GitHub リリース、デバッグビルド、ナイトリービルドなど。
+ Android バージョン:
+ 端末モデル:
+ 補足情報: SD カードの使用、特別な設定、権限など。
+ 機能リクエスト
+ 新機能や改善を要望する場合にこれらのフィールドをコピーしてください。
+ 問題の説明: 解決しようとしている問題は何ですか?
+ 提案する解決策: どのように機能すればよいですか?
+ 検討した代替案: 他のアプローチはありますか?
+ 範囲: どの画面やフローが影響を受けますか?
+ 利用可能であればモックアップや参考画像。
+ タイトル、プライバシー、範囲
+ 報告をトリアージしやすく安全に共有できるようにします。
+ 良い Issue タイトルの例
+ イコライザー: プリセットタブを切り替えるとインジケーターがずれる
+ 検索: 空のクエリで履歴リストが表示されない
+ 機能: 「最近追加された」プレイリストの並び替えオプションを追加
+ 避けるべきこと
+ 「動かない」のような一般的な報告。
+ 1 つの Issue に複数の無関係な問題を含める。
+ プライベートデータが含まれた未編集のログやスクリーンショット。
+ プライバシーについて
+ ログ、スクリーンショット、動画を投稿する前に個人情報やプライベートな情報を削除してください。
+ ナイトリービルド
+ ナイトリーとリリースの違い、および破損した場合に含めるべき情報。
+ ナイトリービルドは最新のコミットから生成され、未完成の変更、一時的なバグ、またはリグレッションが含まれる場合があります。公式リリースよりも試験的です。
+ 利用可能な場合はリポジトリの GitHub Actions ワークフローアーティファクトからアクセスできます。
+ ナイトリーの問題を報告する
+ ナイトリービルドで問題を報告する場合は、公式リリースではなくナイトリービルドで発生したことを必ず記載してください。可能であればビルド日、ワークフロー実行名または番号、コミット SHA を含めてください。また同じ問題が最新の公式リリースでも発生するか確認してください。
+ Beta 0.5.0 アップグレード
+ クリーンインストール推奨
+ beta 0.5.0 からのアップデートの場合、このアップデートでは古いキャッシュ状態ではなく新しいライブラリデータが必要な場合があります。
+ メタデータやライブラリエントリがおかしい場合
+ 曲のメタデータが間違っている、アーティストやアルバムが一致しない、または重複しているように見えるエントリは通常クリーンインストールで解決します。
+ 今後表示しない
+ 了解
+
+
+ 問題が発生しました
+ 前回のセッション中にアプリがクラッシュしました。クラッシュレポートを共有して修正にご協力ください。
+ 日時: %1$s
+ エラー:
+ スタックトレース(プレビュー):
+ クラッシュログ
+ クラッシュログをクリップボードにコピーしました
+ PixelPlayer クラッシュレポート
+ クラッシュレポートを共有
+
+
+ DJ ミキサー
+
+
+ あなたの\nミックス
+ まだ表示するデータがありません
+ PixelPlayer が曲を見つけるかソースを同期するとミックスがここに表示されます。
+ 更新
+
+
+ デイリーミックス
+ 履歴に基づく
+ デイリーミックスをすべて確認
+ デイリーミックス
+
+
+ デイリーミックス
+
+ %1$d 曲 • %2$s
+
+ 再生する
+ AI プレイリストジェネレーター
+
+
+ デイリーミックスの作られ方
+ デイリーミックスはお気に入りのよく再生される曲から作られます。好みのアーティストやジャンルのトラックも追加されるので新しい音楽を発見できます。
+ 今日何を聴きたいか AI に伝えましょう
+ コストを抑えるため少量のサンプルを使用します
+ 更新中…
+ デイリーミックスを更新
+
+
+ 完璧にキュレーション
+ デイリーミックス
+ あなたのソニックジャーニーの準備ができました
+ AI プレイリストジェネレーター
+ 雰囲気、ムード、アクティビティを説明して、ライブラリから AI に完璧なプレイリストをキュレーションさせましょう。
+ プレイリストのサイズ
+ 最小曲数
+ 最大曲数
+ 例: チルな夜の雰囲気、アップビートなワークアウトエネルギー…
+ タップして再試行
+ ソニックジャーニーが完成しました!
+ 再生準備完了
+ 生成中…
+ プレイリストを生成
+
+
+ 最近再生した曲
+
+
+ 最近再生した曲
+ 最新を再生
+ %1$s に最近の再生はありません
+ 範囲を変更するか、タイムラインを埋めるためにもっと曲を再生してください。
+ 最近再生した曲
+ 今日
+ 昨日
+
+
+ リスニング統計
+ 総再生回数
+ 1 日平均
+ トップトラック
+ %1$s • %2$d 回
+
+
+ リスニング統計
+ リスニング統計を更新
+ 今日
+ 今週
+ 今月
+ 今年
+ 全期間
+ リスニング
+ 再生
+ リスニングタイムライン
+ リスニング時間
+ 選択した範囲でのリスニングの合計。
+ 再生回数
+ セグメントごとに完了したセッション数。
+ 平均セッション
+ 各セグメントの平均リスニング時間。
+ 4 時間ごとに分割して日々のリズムを確認できます。
+ 日別バーで週ごとの習慣を比較しやすくします。
+ 週別バーで月のトレンドを確認できます。
+ 月別バーで年間の季節性を確認できます。
+ 年別バーで全履歴を要約します。
+ まだリスニングデータがありません
+ 再生を始めてリスニングタイムラインを構築しましょう
+ 日々のリズム
+ 週のリズム
+ 月のリズム
+ 年間一覧
+ 全期間の推移
+ 4 時間ごとのセグメントでグループ化
+ 曜日でグループ化
+ 月の週でグループ化
+ 月でグループ化
+ 年でグループ化
+ ピークセグメント
+ %1$d 回
+ —
+ トップカテゴリ
+ ジャンル、アーティスト、アルバム、曲ごとのリスニングを比較します。
+ ジャンル
+ アーティスト
+ アルバム
+ 曲
+ ジャンル別リスニング
+ アーティスト別リスニング
+ アルバム別リスニング
+ 曲別リスニング
+ %1$d 回 • %2$d アーティスト
+ %1$d 回 • %2$d トラック
+ まだカテゴリデータがありません
+ 再生を始めてリスニングのハイライトを確認しましょう
+ リスニング習慣
+ まだ習慣データがありません
+ あなたのことをより知ったらリスニング習慣を表示します。
+ 総セッション数
+ 平均セッション
+ 最長セッション
+ セッション/日
+ 最もアクティブな日
+ まだ再生履歴がありません
+ ピークタイムラインスロット
+ トップアーティスト
+ トップアーティストがいません
+ 聴き続けるとお気に入りのアーティストがここに表示されます。
+ \?
+ %1$d. %2$s
+ トップアルバム
+ トップアルバムがありません
+ よく聴くアルバムがここに表示されます。
+ %1$d. %2$s
+ トラック集中度
+ トップトラック全体でリスニング時間がどのように分散しているか。
+ まだ集中度データがありません
+ より多くのトラックを再生してリスニングの集中度を確認しましょう。
+ トップ 1
+ トップ 2-3
+ その他
+ %1$d%%
+ リスニング集中度
+ トップ 3 トラックがリスニング時間の %1$d%% を占めています。
+ 平均再生回数/トラック
+ ユニークトラック
+ トップ 3 シェア
+ この期間のトラック
+ 選択した期間で最も再生されたトラック。
+ トップトラックがありません
+ お気に入りを聴き続けるとここでハイライトされます。
+ トラックを折りたたむ
+ すべてのトラックを表示
+
+
+ %1$d 時間 %2$02d 分
+ %1$d 分
+ %1$d 時間 %2$02d 分
+ %1$d 時間
+ %1$d 分
+ %1$d 秒
+ %1$d 時間 %2$02d 分
+ %1$d 時間
+ %1$d 分
+ %1$d 秒
+ なし
+ たった今
+ 1 日前
+ %1$d 日前
+ 1 時間前
+ %1$d 時間前
+ 1 分前
+ %1$d 分前
+ %1$d 曲
+ %1$d 曲
+ 第 %1$d 週
+
diff --git a/app/src/main/res/values-ja/strings_library.xml b/app/src/main/res/values-ja/strings_library.xml
new file mode 100644
index 000000000..4c7c2d6ca
--- /dev/null
+++ b/app/src/main/res/values-ja/strings_library.xml
@@ -0,0 +1,558 @@
+
+
+
+ ライブラリ
+ ライブラリタブ
+ 任意のタブへ直接ジャンプするか、順序を変更できます。
+ タブを並び替え
+
+
+ 曲
+ アルバム
+ アーティスト
+ プレイリスト
+ フォルダ
+ お気に入り
+
+
+ プレイリストを作成しました
+ 先に AI プロバイダーの API キーを設定してください
+ 先に Gemini API キーを設定してください
+ キューに追加しました
+ 次に再生
+
+
+ Watch への転送
+ 設定
+ 編集
+ タブを並び替え
+ メニューを展開
+
+
+ 選択できるアルバムは最大 %1$d 枚です
+ フォルダ
+ フォルダ
+
+
+ 並び替え
+ 表示
+ プレイリスト表示
+ グリッド
+ リスト
+ 内部ストレージ
+ SD カード
+ SD カードは現在利用できません。
+ クラウド
+ Telegram クラウドチャンネル
+ トピック表示
+ チャンネル
+ トピック
+ 両方
+ クラウド
+ クラウドのみ
+
+
+ AI でメタデータを生成中…
+
+
+ 曲の読み込みエラー
+ アルバムの読み込みエラー
+ アーティストの読み込みエラー
+ 再試行
+
+
+ ライブラリに曲が見つかりませんでした。
+ 端末に音楽がある場合は、設定からライブラリを再スキャンしてみてください。
+ 曲が見つかりません
+
+
+ 新規
+ 新しいプレイリストを作成
+ M3U プレイリストをインポート
+ 現在の曲を探す
+ すべての曲
+ クラウド
+ ローカル
+ 並び替えオプション
+
+
+ すべて
+ 選択解除
+ その他のオプション
+
+
+ 音楽ファイルをスキャン中…
+ ファイルを処理中…
+ %2$d 件中 %1$d 件
+ ライブラリを同期中…
+ 同期完了
+ 待機中…
+ ライブラリを同期中…
+ アルバムアートキャッシュをクリア中…
+ クラウドソースを同期中…
+ 歌詞をスキャン中…
+
+
+ 曲がまだありません
+ 音楽を端末に追加するか、クラウドソースを同期して再生を始めましょう。
+ ローカルの曲が見つかりません
+ 別のソースフィルターを試すか、端末のライブラリを再スキャンしてください。
+ クラウドの曲が見つかりません
+ Telegram や NetEase の曲を同期するか、ローカルソースに切り替えてください。
+ アルバムがありません
+ ライブラリにトラックがグループ化されるとアルバムが表示されます。
+ ローカルアルバムが見つかりません
+ ローカルアルバムを作成するにはローカルの曲が必要です。
+ クラウドアルバムが見つかりません
+ アルバムデータを持つクラウドの曲は同期後にここに表示されます。
+ アーティストがいません
+ いずれかのソースから曲がインデックスされるとアーティストが表示されます。
+ ローカルアーティストが見つかりません
+ ローカルの曲にアーティストのメタデータがありません。
+ クラウドアーティストが見つかりません
+ リモートの曲が同期されるとクラウドアーティストが表示されます。
+ お気に入りの曲がまだありません
+ 再生中にハートアイコンをタップして曲を保存しましょう。
+ お気に入りのローカル曲がありません
+ ソースフィルターを切り替えるか、端末の曲をお気に入りに追加してください。
+ お気に入りのクラウド曲がありません
+ Telegram や NetEase のトラックをお気に入りに追加するとここに表示されます。
+ フォルダが見つかりません
+ 音楽が入った内部ストレージのフォルダがここに表示されます。
+ プレイリストがまだありません
+ 最初のプレイリストを作成してライブラリを整理しましょう。
+
+
+ 曲のメタデータを編集
+ 再生
+ 曲を再生
+ すべて再生
+ すべて再生
+ お気に入りに追加
+ すべてお気に入りに追加
+ お気に入りから削除
+ すべてお気に入りから削除
+ 曲ファイルを共有するアプリを選択
+ 曲ファイルを共有
+ すべてを ZIP で共有
+ 曲を共有できませんでした: %1$s
+ キューに追加
+ キューに追加
+ 次に再生
+ キューで次に再生
+ プレイリストに追加
+ 削除
+ すべて削除
+ Watch を確認中
+ 転送中 %1$d%%
+ Watch に転送中
+ 転送中
+ Watch に送る
+ Watch が利用できません
+ 曲を Watch に送る
+ Watch が利用できません
+ サウンドとして設定
+ サウンドとして設定
+ この曲をシステムサウンドとして使う方法を選択
+ この曲を使う場所
+ PixelPlayer がこのサウンドをインストールする場所を選択してください。
+ 着信音
+ 電話の着信
+ 通知音
+ メッセージとアプリの通知
+ アラーム音
+ 時計のアラーム
+ サウンドの変更を確認
+ 「%1$s」を %2$s に設定しますか?
+ サウンドを設定
+ 「%1$s」を %2$s に設定しました
+ 着信音
+ 通知音
+ アラーム音
+ 「システム設定の変更」を有効にしてから PixelPlayer に戻ると自動で完了します。
+ 「システム設定の変更」が有効になっていません。
+ 「%1$s」を着信音に設定しました
+ 着信音にはローカルの曲のみ使用できます。
+ この音声ファイルを着信音用に準備できませんでした。
+ 着信音を設定できませんでした: %1$s
+ オプション
+ オプション
+ 情報
+ 情報
+ 再生時間
+ ジャンル
+ アルバム
+ アーティスト
+ 曲の情報
+ プロバイダー
+ ファイル
+ %1$d 曲
+ 選択中
+ %1$d プレイリスト
+ %1$d アルバム
+ 選択中
+ 上限: %1$d アルバム
+ キューへの追加と再生は選択順序に従います。
+ %1$d ジャンル
+ 選択中
+ 選択したジャンル内のすべての曲に対して一括操作を実行します。
+
+
+ デフォルト順
+ タイトル(A〜Z)
+ タイトル(Z〜A)
+ アーティスト
+ アーティスト(Z〜A)
+ アルバム
+ アルバム(Z〜A)
+ 追加日
+ 追加日(古い順)
+ 再生時間
+ 再生時間(短い順)
+ リリース年
+ リリース年(古い順)
+ 曲数が少ない順
+ 曲数が多い順
+ 名前(A〜Z)
+ 名前(Z〜A)
+ 曲数(多い順)
+ 曲数(少ない順)
+ 作成日
+ 作成日(古い順)
+ お気に入り追加日
+ お気に入り追加日(古い順)
+ サブフォルダが少ない順
+ サブフォルダが多い順
+
+
+ タイトル
+ アーティスト
+ アルバム
+ 追加日
+ 再生時間
+ リリース年
+ 曲数
+ 名前
+ 曲数
+ 作成日
+ お気に入り追加日
+ サブフォルダ数
+
+
+ ソース
+ 順序
+ 降順
+ 昇順
+ 元の順序
+ タップして昇順に切り替え
+ タップして降順に切り替え
+ この並び替えは元の順序を維持します
+ スイッチがオン
+
+
+ ライブラリタブを並び替え
+ 順序をリセット
+ タブの順序をデフォルトに戻しますか?
+ タブを並び替え中…
+ ドラッグハンドル
+
+
+ アーティストを選択
+ 1 アーティスト
+ %1$d アーティスト
+ メインアーティスト
+ アーティストページ
+
+
+ 転送をキャンセル
+ %1$s / %2$s
+ スマートフォンから Watch への音楽転送の進捗をリアルタイムで表示します
+ Watch への転送
+ Watch に送信中
+ キャンセル済み
+ 転送をキャンセルしました
+ 転送が完了しました
+ 完了
+ 失敗
+ 転送に失敗しました
+ 複数の転送が進行中
+ %1$s • %2$s
+ 準備中
+ Watch への転送を準備中
+ 転送を準備中…
+ Watch に %1$d 曲を送信中
+ Watch に送信中
+ 転送を開始中…
+ 開始中
+ 転送中
+ %1$d 件の転送
+
+
+ 曲を編集
+ 情報を表示
+ 曲のメタデータを編集中
+ 曲のメタデータを編集すると、ライブラリでの表示や整理に影響することがあります。変更は永続的で、元に戻せない場合があります。
+ 了解
+ 情報
+ カバーアート
+ 正方形の画像を選択して調整し、アプリ全体でカバーアートが美しく表示されるようにしましょう。
+ カバーアートを変更
+ カバーアートを削除
+ タイトル
+ アーティスト
+ アルバム
+ アルバムアーティスト
+ ジャンル
+ 作曲者
+ トラック番号
+ ディスク番号
+ ReplayGain トラック(dB)
+ ReplayGain アルバム(dB)
+ -6.50
+ -8.20
+ 新しいカバーアートのプレビュー
+ 現在の曲のカバーアート
+ カバーアートを調整
+ ピンチとドラッグで最適なフレーミングを見つけてください。
+ カバーアートを適用
+ 選択した画像を読み込めませんでした
+ lrclib.net で歌詞を検索
+
+
+ %d 曲を編集
+ 変更したフィールドのみ更新されます。空白のフィールドは既存の値が保持されます。
+ (複数の値)
+ (任意 — スキップする場合は空白のまま)
+ %d 曲を更新しました
+ %2$d 曲中 %1$d 曲を更新しました。一部のファイルは編集できませんでした。
+ 曲の更新に失敗しました
+ カバーアートの一括変更
+ 選択した %d 曲すべてのカバーアートが置き換えられます
+ すべてにカバーアートを設定
+ すべてのカバーアートを削除
+ (複数の異なるカバー)
+
+
+ プレイリストを閉じました
+
+
+ プレイリストを作成
+ 作成方法を選択してください。
+ 手動
+ アートワーク・アイコン・形状をデザインし、曲を自分で選びます。
+ AI で作成
+ 高度なコントロールでキュレーションされたプレイリストを生成します。
+ 設定で Gemini API キーを設定する必要があります。
+ API キーを設定
+
+
+ AI プレイリストラボ
+ リセット
+ 生成中…
+ 生成
+ 意図
+ プレイリスト名(任意)
+ このプレイリストの雰囲気は?
+ 例:夕暮れのドライブにウォームなシンセ
+ 方向性
+ ムード
+ アクティビティ
+ 年代
+ キュレーション
+ エネルギー
+ 曲の強度とテンポを調整します。1 = 穏やか/スロー、5 = ハイエネルギー/ファスト。
+ ディスカバリー
+ 選曲の馴染み度を調整します。1 = 最もよく聴くお気に入り、5 = あまり聴いていないレアな曲。
+ 最小曲数
+ 最大曲数
+ フィルター
+ 優先するジャンル(任意)
+ 例:シンセウェーブ、インディーポップ
+ 避けるジャンル(任意)
+ 例:メタル、ハードトラップ
+ 優先言語(任意)
+ 例:日本語、英語、インストゥルメンタル
+ お気に入りを優先
+ 不適切な歌詞を除外
+ プロンプトのプレビュー
+ 好みを追加すると最終プロンプトがここに表示されます。
+ 精密なキュレーション
+ ムード・アクティビティ・制約・深さを定義します。
+ AI はローカルライブラリの曲のみを使用します。
+ AI への指示を少なくとも 1 つ追加してください。
+ 有効な曲数の範囲を設定してください。
+ %1$d/5
+ カスタム…
+ カスタム値を入力
+ カスタム値を入力してください
+
+
+ すべての年代
+ コアリクエスト: %1$s。
+ ムード目標: %1$s。
+ アクティビティ: %1$s。
+ 年代: %1$s。
+ 優先ジャンル: %1$s。
+ 避けるジャンル: %1$s。
+ 優先言語: %1$s。
+ エネルギーレベル目標: %1$d/5。
+ ディスカバリー目標: %1$d/5(1 = 馴染みあり、5 = レアな掘り出し物)。
+ 可能な限りお気に入りに近い曲を優先する。
+ 代替曲がある場合は不適切な歌詞を避ける。
+ スムーズなトランジションを維持し、同じアーティストが連続しないようにする。
+
+ チル
+ エネルギッシュ
+ ハッピー
+ ダーク
+ ロマンティック
+ メランコリック
+
+
+ ワークアウト
+ 集中
+ ロードトリップ
+ パーティー
+ 勉強
+ 深夜
+
+
+ @string/playlist_creation_ai_era_any
+ 70年代
+ 80年代
+ 90年代
+ 2000年代
+ 2010年代
+ 2020年代
+
+
+
+ プレイリストがまだ作成されていません。
+ 「新しいプレイリスト」ボタンをタップして始めましょう。
+ 新しいプレイリスト
+ プレイリスト名
+ マイプレイリスト
+
+
+ %1$d 曲を追加先…
+ プレイリストを選択
+ プレイリストを検索…
+ プレイリストに曲を追加しました
+ プレイリストを作成して曲を追加しました
+ 内部ストレージ
+
+
+ 曲を追加
+ 選択した曲を追加
+ 追加
+ 曲を検索またはフィルター…
+ お気に入り
+ 曲の読み込みに失敗しました
+ さらに読み込む
+
+
+ プレイリストを結合
+ 結合後のプレイリスト名を入力してください:
+ 結合プレイリスト
+ 選択した %1$d 件のプレイリストを 1 つに結合します。
+
+
+ 再生できる有効な曲が見つかりませんでした
+ 現在のリストに曲が見つかりません
+ 曲を見つけられませんでした
+ ライブラリに曲が見つかりません
+ %1$s の再生が終了しました(トラック終了)。
+ トラック
+ シャッフルする曲がありません。
+ 選択したアルバム
+ 選択したアルバムに再生可能な曲が見つかりませんでした
+ 選択したジャンルに再生可能な曲が見つかりませんでした
+ 最初の %1$d アルバムのみキューに追加しました
+ %1$d アルバムをキューに追加しました(%2$d 曲)
+ 選択したアルバムをキューに追加できませんでした
+ すべての曲がすでにお気に入りにあります
+ お気に入りに曲がありませんでした
+ ZIP ファイルを作成中…
+ 共有に失敗しました: %1$s
+
+ %d 曲をキューに追加しました
+
+
+ %d 曲が次に再生されます
+
+
+ %d 曲をお気に入りに追加しました
+
+
+ %d 曲をお気に入りから削除しました
+
+
+
+ 共有するプレイリストがありません
+ プレイリストを共有
+ 共有に失敗しました: %1$s
+ エクスポートするプレイリストがありません
+ エクスポートに失敗しました: %1$s
+ Music/PixelPlayer Exports
+ 設定で Gemini API キーを設定してください。
+ プレイリストを復元しました
+
+ %d 件のプレイリストを共有中
+
+
+ %2$s に %1$d 件のプレイリストをエクスポートしました
+
+
+
+ 無効なアルバム ID
+ アルバム ID が見つかりません
+ アルバムデータの読み込みエラー: %s
+ アルバムが見つかりません
+
+
+ 無効なアーティスト ID
+ アーティスト ID が見つかりません
+ アーティストデータの読み込みエラー: %s
+ アーティストが見つかりませんでした
+
+
+ 再生中の曲は削除できません
+ %1$d 件のファイルを削除しました(%2$d 件スキップ — 再生中)
+ %2$d 件中 %1$d 件のファイルを削除しました
+ ファイルの削除に失敗しました
+ ファイルを削除しました
+ ファイルを削除できないか、見つかりません
+ 削除をキャンセルしました
+ 曲を削除しますか?
+ %2$s の「%1$s」\n\nこの曲は端末から完全に削除され、元に戻せません。
+ これらの曲は端末から完全に削除され、元に戻せません。
+
+ %d 件のファイルを削除しました
+
+
+ %d 曲を削除しますか?
+
+
+
+ メタデータを更新しました
+ %1$d 曲を更新中…
+ %1$d 曲を正常に更新しました!
+ %1$d 曲を更新しました。失敗: %2$d 曲
+ 歌詞を保存しました
+ 歌詞の保存に失敗しました
+ 保存できる歌詞がありません
+ 権限が拒否されました — ファイルを編集できません
+ 権限が拒否されました — 歌詞を保存できません
+ 権限が拒否されました — このファイルを編集できません
+
+
+ 設定で選択した AI プロバイダーの有効な API キーを設定してください。
+ AI エラー: %s
+ 選択した AI プロバイダーはアカウントのクレジットまたはクォータが不足しているためリクエストを拒否しました。
+ 選択した AI モデルは利用できなくなりました。PixelPlayer がサポート対象のモデルへ自動的に切り替えを試みました。
+ AI がプロンプトに合う曲を見つけられませんでした。
+ デイリーミックスのアイデアを書いてください
+ AI でデイリーミックスを更新しました
+ 更新できませんでした: %s
+ AI がこのミックスに合う曲を見つけられませんでした
+
diff --git a/app/src/main/res/values-ja/strings_player.xml b/app/src/main/res/values-ja/strings_player.xml
new file mode 100644
index 000000000..eae601ef3
--- /dev/null
+++ b/app/src/main/res/values-ja/strings_player.xml
@@ -0,0 +1,195 @@
+
+
+
+ プレイヤーを閉じる
+ 再生中
+ クラウドストリーム
+ キャスト
+ Bluetooth
+ 本体再生
+ 接続中…
+ キューを開く
+
+
+ 接続の準備
+ キャスト・Bluetooth オーディオ・スピーカーを同期するために、PixelPlayer が近くのデバイスと現在の Wi‑Fi を確認できるよう許可してください。
+ 近くのデバイス
+ 接続済み Bluetooth オーディオ機器の読み取りと制御に必要です。
+ Wi‑Fi 用の位置情報
+ Android では、互換性のあるキャストデバイスを検出するために Wi‑Fi ネットワーク(SSID)の共有に位置情報が必要です。
+ アクセスを許可
+ これらの権限はデバイスの相互接続(キャスト・近くのスピーカーの制御・オーディオ同期)にのみ使用します。
+ デバイスを接続
+ 近くをスキャン中
+ キャストセッション
+ 接続中
+ 接続済み
+ このスマートフォン
+ Bluetooth オーディオ
+ 本体再生
+ 再生中
+ 一時停止中
+ デバイスの音量
+ スマートフォンの音量
+ %1$d/%2$d
+ バッテリー残量
+ 音量レベル
+ 切断
+ 接続性
+ Wi-Fi または Bluetooth をオンにしてください
+ 接続を更新
+ Wi-Fi
+ オフ
+ オン
+ 接続済み
+ Bluetooth
+ オフ
+ オン
+ 接続済み
+ 近くのデバイス
+ デバイスを更新
+ 接続済み
+ 接続中
+ 接続可能
+ 利用可能
+ 接続中...
+ デバイスを検索中…
+ テレビやスピーカーの電源が入っており、同じ Wi‑Fi ネットワークに接続されていることを確認してください。
+ コントロール
+ デバイス
+
+
+ キャストメディアサーバー
+ デバイスにキャスト中
+ キャストデバイスにメディアを配信中
+ %1$s: %2$s
+ このオーディオフォーマットはキャスト中にシークするとセッションがクラッシュする可能性があるため、一時的に利用できません。
+
+
+ スリープタイマー
+ タイマー
+ %1$d 分
+ %1$d 分後にタイマーをセットしました。
+ 1 回
+
+ %d 回
+
+ 再生回数: %1$s
+ 現在のトラックの終わり
+ トラックの終わりで再生を停止します。
+ スイッチをオン
+ カスタム時間
+ タイマーをキャンセル
+ トラックの終わり
+ タイマーをキャンセルしました。
+ 再生中の曲がないため、トラック終了タイマーを有効にできません。
+ 曲が %1$s から %2$s に変わったため、トラック終了タイマーを無効にしました。
+ 前のトラック
+ 現在のトラック
+ カスタム時間を設定
+
+
+ 次の曲
+ キューはまだ空です。
+
+ %d 曲待機中
+
+ キュー
+ キューは空です。
+ 曲を並び替え
+ シャッフルを切り替え
+ リピートを切り替え
+ スリープタイマー
+ その他の操作
+ 現在の曲を探す
+ キューをクリア
+ キューをクリア
+ 現在再生中の曲以外をすべてキューから削除しますか?
+ プレイリストとして保存
+ %1$s のキュー
+ 現在のキュー
+ 曲を削除
+ 削除しました
+ プレイリストとして保存
+ すべて選択解除
+ プレイリスト名
+ 含める曲を検索…
+ 「%1$s」に一致する曲はありません
+
+ %d 曲を選択中
+
+ %1$s として保存
+ プレイリスト名を入力
+ プレイリストから削除
+ %1$s のその他のオプション
+
+
+ 歌詞
+ 歌詞を読み込み中…
+ 同期あり
+ テキストのみ
+ 歌詞オプション
+ −.5
+ −.1
+ +.1
+ +.5
+ 0s
+ %1$+.1f 秒
+
+
+ 歌詞の検索に失敗しました
+ リモートからの歌詞取得に失敗しました
+ 接続がタイムアウトしました。インターネット接続を確認してください。
+ ネットワークエラー。インターネット接続を確認してください。
+ サーバーエラー(コード %d)。しばらくしてから再試行してください。
+
+
+ 歌詞はすでに利用可能です。オンライン取得をスキップしました。
+ 埋め込み歌詞が見つかりました。オンライン取得をスキップしました。
+ ローカル(.lrc)歌詞が見つかりました。オンライン取得をスキップしました。
+
+
+ 歌詞を保存
+ AI で翻訳
+ この歌詞にはすでに翻訳があります
+ この歌詞はすでにこの言語です
+ API が設定されていません
+ 歌詞の翻訳が完了しました!
+ 歌詞を翻訳中...
+ インポートした歌詞をリセット
+ 歌詞をリセットしますか?
+ この曲の歌詞をリセットしてもよろしいですか?
+ 表示
+ 配置
+ 左揃え
+ 中央揃え
+ 右揃え
+ コントロール
+ 同期を調整
+ 同期コントロールを非表示
+ ローマ字表記を表示
+ 翻訳を表示
+ 没入モードを一時解除
+ 画面をオンに保つ
+
+
+ 歌詞を保存
+ 保存するバージョンを選択してください:
+ 同期あり(タイムスタンプ付き)
+ テキストのみ
+
+
+ 歌詞をオンラインで検索しますか?
+ 歌詞の候補を表示
+ 最初の候補を自動適用せず、常に選択画面を開く
+ 歌詞を検索中…
+ 歌詞が見つかりませんでした
+ 歌詞を自動で見つけられませんでした。タイトルやアーティスト名を編集して手動で検索できます。
+ 曲名
+ アーティスト(任意)
+ %d 件見つかりました
+ 同期あり
+ %1$s • %2$s
+ 歌詞提供元:
+ https://lrclib.net/
+
diff --git a/app/src/main/res/values-ja/strings_screens.xml b/app/src/main/res/values-ja/strings_screens.xml
new file mode 100644
index 000000000..be88a28c0
--- /dev/null
+++ b/app/src/main/res/values-ja/strings_screens.xml
@@ -0,0 +1,244 @@
+
+
+
+ エラー: ジャンル ID がありません
+
+
+ 始めましょう!
+ ステップ %1$d / %2$d
+ 先に必要な権限を許可してください。
+ 必要な権限をすべて許可してください。
+ ようこそ
+ β
+ ベータ
+ セットアップを完了しましょう。
+ メディアの権限
+ 音楽ライブラリを構築するために、PixelPlayer がオーディオファイルへアクセスする必要があります。
+ 権限が許可されました
+ メディア権限を許可
+ 通知
+ ロック画面や通知シェードから音楽を操作するために通知を有効にします。
+ 通知を有効化
+ バックアップはありますか?
+ PixelPlayer のバックアップがある場合は今すぐ復元することでこのデバイスのセットアップの大部分をスキップできます。
+ バックアップをインポート
+ バックアップを確認中
+ バックアップパッケージを確認中…
+ バックアップを復元中
+ スキップ / あとで
+ バックアップを復元
+ セットアップを完了する前にインポートする内容を確認してください。
+ %2$d モジュール中 %1$d を選択中
+ %1$s に作成
+ %1$s からのバックアップ
+ バージョン不明
+ 選択を復元
+ 復元中
+ 除外フォルダ
+ デフォルトではすべてのフォルダがスキャンされます。ライブラリ構築時に無視する場所を選択してください。
+ 無視するフォルダを選択
+ 先にストレージの権限を許可してください
+ アプリのテーマ
+ ライブラリの探索を始める前に好みの外観を選んでください。
+ ダーク
+ PixelPlayer のデフォルトの Material 3 ダーク外観。
+ ライト
+ アプリ全体のより明るい Material 3 外観。
+ システムに合わせる
+ スマートフォンの現在の外観設定に合わせます。
+ おすすめ
+ 後から 設定 > 外観 > アプリのテーマ で変更できます。
+ ライブラリレイアウト
+ ライブラリのナビゲーション方法を選択してください。
+ 曲
+ コンパクトモード
+ 最小化されたピルナビゲーションを使用
+ 標準のタブ行を使用
+ 曲
+ アルバム
+ アーティスト
+ 後から 設定 > 外観 > ライブラリナビゲーション で変更できます。
+ アプリナビゲーション
+ ボトムナビゲーションバーのスタイルを選択してください。
+ デフォルトスタイル
+ 角が丸いフローティングピル
+ 標準のフル幅バー
+ コーナー半径をカスタマイズ
+ 後から 設定 > 外観 > ナビバースタイル で変更できます。
+ アラームとリマインダー
+ 任意ですが、スリープタイマーを使用して PixelPlayer を正確な時刻に停止させたい場合はおすすめです。
+ 権限を許可
+ バッテリー最適化
+ 一部の Android 端末はバックグラウンドアプリを積極的に終了させます。予期しない再生の中断を防ぐために PixelPlayer のバッテリー最適化を無効にしてください。
+ 最適化を無効化
+ 準備完了!
+ 音楽を楽しむ準備ができました。
+
+
+ 検索…
+ 検索
+ 検索をクリア
+ 最近の検索
+ すべてクリア
+ 履歴
+ 検索履歴アイテムを削除
+ 結果なし
+ 「%1$s」の検索結果はありません
+ 見つかりませんでした
+ 別の検索語またはフィルターを試してください。
+ 結果が見つかりませんでした。
+ ジャンルで探す
+ 利用可能なジャンルがありません。
+
+
+ %1$s を再生
+ %1$s を折りたたむ
+ %1$s を展開
+ アーティスト画像を編集
+ 写真を変更
+ デフォルトに戻す
+ アーティストをシャッフル再生
+
+
+ ディスク %d
+ %1$s のカバー
+ %1$s · %2$s
+
+
+ プレイリストが見つかりません。
+ このプレイリストは空です。
+ 「曲を追加」をタップして始めましょう。
+ このフォルダに曲はありません。
+ 曲を並び替え
+ その他のオプション
+ プレイリストのオプション
+ プレイリストを編集
+ プレイリストを削除
+ プレイリストを削除しますか?
+ このプレイリストを本当に削除しますか?
+ デフォルトトランジションを設定
+ プレイリストをエクスポート
+ %1$s • %2$s
+ 再生する
+ 追加
+ 曲を追加
+ 削除
+ 曲を削除
+ 並び替え
+ 曲を並び替え
+
+
+ グローバルトランジション
+ プレイリストルール
+ 上書きされない限り、すべての再生ソースにこの設定が適用されます。
+ この特定のプレイリストのデフォルト動作を設定します。
+ アクティブ状態
+ グローバルデフォルト
+ プレイリストデフォルト
+ グローバルに従う
+ カスタム上書き
+ カスタム上書き
+ 有効にするとこのプレイリストに特定のルールを設定できます。
+ グローバルデフォルトを使用
+ 変更を保存しました
+ トランジションスタイル
+ トラックのブレンド方法
+ なし
+ クロスフェード
+ トランジションの長さ
+ %1$d 秒のオーバーラップ
+ トランジションをリセット
+ 現在の曲
+ 次の曲
+ トラックは %1$d 秒間オーバーラップします
+ 音量カーブ
+ オーディオのスロープを微調整
+ フェードアウト
+ フェードイン
+
+
+ 新しいスマートプレイリスト
+ 新しいプレイリスト
+ 曲を追加
+ 戻るまたはキャンセル
+ 次へ
+ 作成
+ プレイリストを編集
+ 自動生成コラージュ
+ 写真を追加
+ 画像を選択
+ 変更
+ 削除
+ プレイリスト名
+ マイ素敵なミックス
+ カバーを編集
+ カバーアートを調整
+ ピンチとドラッグで最適なフレーミングを見つけてください
+ 手動
+ スマート
+ AI で生成
+ スマートルール
+ デフォルト
+ 画像
+ アイコン
+ 背景色
+ アイコンシンボル
+ 形状スタイル
+ 形状パラメーター
+ コーナー半径
+ 滑らかさ
+ 辺の数
+ カーブ
+ 回転
+ スケール
+ よく再生する曲
+ 最も再生されたトラック。
+ 最近再生した曲
+ 最近聴いた曲。
+ 忘れられたお気に入り
+ しばらく再生していないお気に入りのトラック。
+ 新着の宝石
+ 再生回数が少ない最近追加されたトラック。
+
+
+ ジャンルに曲を素早く追加
+ 並び替えと再生
+ シャッフル
+ 並び替え基準
+ アーティスト
+ アルバム
+ タイトル
+ 一般アーティスト
+ %1$s シャッフル
+
+
+ 曲を選択
+ ジャンルを選択
+ 曲を検索
+ 新しいジャンル
+ カスタムを追加
+ カスタムジャンルを追加
+ ジャンル名
+ アイコンを選択
+ ジャンル: %1$s
+ ジャンルを選択
+ 素早く追加
+
+
+ DJ スペース
+ 読み込み中…
+ デッキ %1$d
+ 曲を読み込む
+ 曲が読み込まれていません
+ …
+ ステム分離はまだ利用できません。
+ 音量
+ 速度
+ クロスフェーダー
+ デッキ 1
+ デッキ 2
+ 曲を選択
+ 再生/一時停止
+ 曲のカバー
+ x%1$.2f
+
diff --git a/app/src/main/res/values-ja/strings_settings.xml b/app/src/main/res/values-ja/strings_settings.xml
new file mode 100644
index 000000000..ee41a6b0d
--- /dev/null
+++ b/app/src/main/res/values-ja/strings_settings.xml
@@ -0,0 +1,641 @@
+
+
+
+ 音楽管理
+ フォルダ管理、ライブラリ更新、解析オプション
+ 外観
+ テーマ、レイアウト、ビジュアルスタイル
+ 再生
+ オーディオ動作、クロスフェード、バックグラウンド再生
+ 動作
+ ジェスチャー、触覚フィードバック、ナビゲーション動作
+ AI 連携(β)
+ AI プロバイダー、API キー、モデル設定
+ バックアップ & 復元
+ 個人データのエクスポートと復元
+ 開発者オプション
+ 試験的機能とデバッグ
+ イコライザー
+ 音域とプリセットの調整
+ デバイス情報
+ オーディオ仕様、コーデック、デコーダー情報
+ アカウント
+ Telegram、Google Drive、NetEase などのサービスを管理
+ このアプリについて
+ アプリ情報、バージョン、クレジット
+
+
+ オン
+ オフ
+ 有効
+ 無効
+ 開く
+ すべて選択
+ 選択を解除
+ 通知を閉じる
+
+
+ ライブラリ構造
+ 除外ディレクトリ
+ ここに追加したフォルダはライブラリスキャン時にスキップされます。
+ アーティスト
+ 複数アーティストの解析と整理オプション。
+ フィルタリング
+ 最低曲の長さ
+ アルバムの最低トラック数
+ アルバムアートキャッシュ上限
+ 同期とスキャン
+ ライブラリを更新
+ 新しいファイルや変更されたファイルをライブラリ全体からスキャンします。
+ フルリスキャン
+ フルリスキャン実行中
+ フルリスキャンを開始しました…
+ ライブラリ同期が完了しました
+ データベースを再構築
+ データベースを再構築しますか?
+ 音楽ライブラリを最初から完全に再構築します。インポートした歌詞、お気に入り、カスタムメタデータはすべて失われます。この操作は元に戻せません。
+ 再構築
+ データベースを再構築中
+ データベースを再構築中…
+ .lrc ファイルを自動スキャン
+ ライブラリ同期中に、同じフォルダ内の .lrc ファイルを自動でスキャンして割り当てます。
+ 歌詞管理
+ 歌詞ソースの優先順位
+ 歌詞を取得する際に最初に試みるソースを選択します。
+ 埋め込みを優先
+ オンラインを優先
+ ローカル(.lrc)を優先
+ インポートした歌詞をリセット
+ データベースからインポートした歌詞をすべて削除します。
+ インポートした歌詞をリセットしますか?
+ この操作は元に戻せません。
+
+
+ 更新
+ デフォルトではすべて許可されています。フォルダをタップするとスキャンから除外されます。
+ サブフォルダがありません
+ 上へ移動
+ ルートへ移動
+
+
+ リスキャンが必要です
+ アーティスト設定が変更されました。ライブラリをリスキャンして適用してください。
+ リスキャン
+ スキャン中…
+ 複数アーティストの解析
+ 文字区切り
+ 現在: %1$s
+ 単語区切り
+ なし
+ 現在: %1$s
+ 設定
+ タイトルからアーティストを抽出
+ 曲タイトルの feat., ft., with を検出
+ ライブラリ整理
+ アルバムアーティストでグループ化
+ コラボアルバムをメインアーティストの下に表示
+ 複数アーティスト解析について
+
+ PixelPlayer は文字区切り(/、;、&)と単語区切り(feat.、ft.、vs.、x)を使ってアーティストタグを分割します。単語区切りは大文字小文字を区別しません。
+ 「タイトルからアーティストを抽出」は曲タイトルの (feat. アーティスト名) のようなパターンを検出します。
+ バックスラッシュ(\)で文字区切りをエスケープできます。
+
+ 例
+ →
+ ♪
+ \"Artist1/Artist2\"
+ Artist1, Artist2
+ \"Drake feat. Rihanna\"
+ Drake, Rihanna
+ \"Marshmello x Bastille\"
+ Marshmello, Bastille
+ \"Song (ft. B)\" by A
+ A, B
+ \"AC\\DC\"
+ AC/DC(エスケープ済み)
+
+
+ 区切り文字
+ 現在の区切り文字
+ 区切り文字をタップして削除します。少なくとも 1 つ必要です。
+ 新しい区切り文字を追加
+ 例: / または ;
+ 区切り文字を追加
+ デフォルトの区切り文字
+ 区切り文字をリセットしますか?
+ カスタム区切り文字をすべてクリアしてデフォルトに戻します。この操作は元に戻せません。
+ 区切り文字をデフォルトにリセットしました
+ 少なくとも 1 つの区切り文字が必要です
+ 区切り文字を追加しました
+ すでに存在するか無効な区切り文字です
+ スペース
+
+
+ 単語区切り
+ 現在の単語区切り
+ スペースで囲まれているときにアーティスト名を分割するキーワードです。大文字小文字を区別しません。タップして削除。
+ 単語区切りが設定されていません
+ 新しい単語区切りを追加
+ 例: feat. または ft.
+ 単語区切りを追加
+ 単語区切りの仕組み
+ 単語区切りはスペースで囲まれている場合に大文字小文字を区別せずマッチします。\n\n1文字の区切り(例: \"x\")は誤マッチを防ぐために両側にスペースが必要です。\n\n例:\n \"Drake feat. Rihanna\" -> Drake, Rihanna\n \"Marshmello x Bastille\" -> Marshmello, Bastille\n \"A vs. B\" -> A, B
+ 単語区切りをリセットしますか?
+ カスタム単語区切りをすべてクリアしてデフォルトキーワードに戻します。この操作は元に戻せません。
+ 単語区切りを追加しました
+ すでに存在するか無効です
+ 単語区切りをデフォルトにリセットしました
+
+
+ 同期を準備中
+ MediaStore を読み込み中
+ トラックを処理中
+ データベースに保存中
+ 歌詞ファイルをスキャン中
+ アルバムアートキャッシュをクリア中
+ クラウドソースを同期中
+ 同期を完了中
+ %1$s • %2$d%% (%3$d/%4$d)
+ %1$s…
+
+
+ グローバルテーマ
+ アプリの言語
+ アプリ全体で使用する言語を選択します。
+ システムのデフォルト
+ English
+ Español
+ Deutsch
+ Français
+ Русский
+ 简体中文
+ Bahasa Indonesia
+ Italiano
+ 한국어
+ Norsk (Bokmål)
+ Türkçe
+ 日本語
+ アプリのテーマ
+ ライト、ダーク、またはシステムに合わせるを選択します。
+ ライトテーマ
+ ダークテーマ
+ システムに合わせる
+ スムーズコーナーを使用
+ 複雑な形状のコーナーを使用して見た目を向上させますが、ローエンド端末ではパフォーマンスに影響する場合があります。
+ ブラー効果を無効化
+ アプリ全体のブラー効果をオフにしてバッテリーとリソースを節約します。
+ スクロールバーを表示
+ 音楽リストにスクロールバーを表示してすばやくスクロールできます。
+ 再生中
+ プレイヤーテーマ
+ フローティングプレイヤーの外観を選択します。
+ アルバムアート
+ システムダイナミック
+ プレイヤーのファイル情報を表示
+ プレイヤーの進行バーにコーデック、ビットレート、サンプルレートを表示します。
+ アルバムアートパレットスタイル
+ 現在: %1$s。ライブプレビューを開いてスタイルを選択してください。
+ カルーセルスタイル
+ アルバムカルーセルの外観を選択します。
+ のぞき込みなし
+ のぞき込み 1 枚
+ ホームコラージュ
+ コラージュパターン
+ 「あなたのミックス」コラージュの形状を選択します。
+ パターンを自動ローテーション
+ ホームを訪れるたびにコラージュパターンを切り替えます。
+ ナビゲーションバー
+ ナビバースタイル
+ ナビゲーションバーの外観を選択します。
+ デフォルト
+ フル幅
+ コンパクトモード
+ アイコンのみ表示してナビバーの高さを縮小します。
+ ナビバーのコーナー半径
+ ナビゲーションバーのコーナー半径を調整します。
+ 歌詞画面
+ 没入型歌詞
+ コントロールを自動非表示にしてテキストを拡大します。
+ 自動非表示の遅延
+ コントロールが非表示になるまでの時間。
+ 3 秒
+ 4 秒
+ 5 秒
+ 6 秒
+ アプリナビゲーション
+ デフォルトタブ
+ 起動時のデフォルトタブを選択します。
+ ホーム
+ 検索
+ ライブラリ
+ ライブラリナビゲーション
+ ライブラリタブ間の移動方法を選択します。
+ タブ行(デフォルト)
+ コンパクトピル & グリッド
+
+
+ カラー
+ パレットスタイル
+ プレイヤー UI のアルバムカラーを選択します。
+ トーナルスポット
+ バランスが取れた落ち着いた雰囲気。
+ ビビッド
+ 高彩度のアクセント。
+ エクスプレッシブ
+ 大胆な色相シフトとコントラスト。
+ フルーツサラダ
+ 楽しい回転アクセント。
+ カラーの精度
+ 0 は現在の調整を維持します。高い値ほどアルバムアートの主要色に近くなります。
+ 現在
+ より正確
+ 0 • 現在
+ %1$d • 穏やか
+ %1$d • バランス
+ %1$d • 正確
+
+
+ コーナー半径を調整
+ ナビバーの形状のコーナーをデバイスの物理コーナーに合わせてシームレスな外観にします。
+ コーナー半径
+ %1$d dp
+
+
+ バックグラウンド再生
+ 閉じても再生を続ける
+ オフにすると、アプリを履歴から削除したときに再生が停止します。
+ バッテリー最適化
+ バッテリー最適化を無効にして再生の中断を防ぎます。
+ バッテリー最適化はすでに無効になっています
+ バッテリー設定を開けませんでした
+ 音量ノーマライゼーション(ReplayGain)
+ ReplayGain を有効化
+ オーディオファイルの ReplayGain メタデータを使って音量レベルを正規化します。
+ ゲインモード
+ トラック: 曲ごとに正規化。アルバム: アルバム単位で正規化。
+ トラック
+ アルバム
+ キャスト
+ キャスト接続/切断時に自動再生
+ キャスト接続を切り替えた直後に自動で再生を開始します。
+ ヘッドフォン
+ ヘッドフォン再接続時に再開
+ ヘッドフォンを外したために一時停止した場合、再接続すると自動で再開します。
+ キューとトランジション
+ クロスフェード
+ 曲間のスムーズなトランジションを有効にします。
+ クロスフェードの長さ
+ Hi-Fi モード
+ 32 ビット float オーディオ出力。端末で再生がカクつく場合は無効にしてください。
+ この端末ではサポートされていません(PCM_FLOAT AudioTrack 非対応)。
+ シャッフルを保持
+ アプリを閉じた後もシャッフル設定を記憶します。
+ キュー履歴を表示
+ キューに以前再生した曲を表示します。
+
+
+ フォルダ
+ 戻るジェスチャーでフォルダを操作
+ フォルダタブで、システムの戻る操作がライブラリを離れる前にフォルダ階層をさかのぼります。
+ プレイヤーのジェスチャー
+ 背景タップでプレイヤーを閉じる
+ ぼかした背景をタップするとプレイヤーシートが閉じます。
+ 触覚フィードバック
+ 触覚フィードバック
+ アプリ全体でバイブレーションフィードバックを有効にします。
+
+
+ AI プロバイダー
+ プロバイダー
+ AI プロバイダーを選択してください
+ セーフトークンモード
+ ON — 高速 & 低コスト。AI に最小限のデータ(約 1K トークン)を送信します。
+ OFF — 深いコンテキスト。より豊かな結果のためにリスニングプロフィール全体(約 8K トークン)を送信します。
+ 認証情報
+ %1$s API キー
+ %1$s から取得
+ Google AI Studio (aistudio.google.com)
+ DeepSeek Platform (api.deepseek.com)
+ Groq Console (console.groq.com)
+ Mistral AI Platform (console.mistral.ai)
+ NVIDIA Build (build.nvidia.com)
+ Moonshot AI Platform (platform.moonshot.cn)
+ Zhipu AI Open Platform (bigmodel.cn)
+ OpenAI Platform (platform.openai.com)
+ モデル選択
+ 利用可能なモデルを読み込み中…
+ モデルの読み込みに失敗しました
+ AI モデル
+ モデルを選択してください。
+ API キーを入力
+ プロンプト動作
+ システムプロンプト
+ AI の動作をカスタマイズします。
+ プリセットプロンプト
+ システムプロンプトを入力…
+ プロフェッショナルキュレーター
+ あなたは「Vibe-Engine」という世界トップクラスの音楽キュレーターで、ソニックフローの達人です。シームレスで高品質なリスニング体験を提供することが目標です。和声の相性、論理的な BPM トランジション、馴染みのお気に入りと洗練された発見のバランスを優先してください。
+ クリエイティブマーベリック
+ あなたは「予期しない統一感」を専門とする前衛的な音楽探求者です。非自明なソニックの共通点を見つけることで従来のジャンルの壁を打ち破ることが使命です。レアなディープカット、実験的なテクスチャー、芸術的な新しさを優先しながら、驚きつつも否定できないトランジションロジックを維持してください。
+ 厳格な司書
+ あなたは精密な音楽データベースアーキテクトです。絶対的なメタデータの精度と厳格なカテゴリ遵守によってロジックを動かします。アルゴリズムによる発見を最小化し、厳格なジャンルの一貫性、エネルギーレベルのマッチング、ユーザーが明確に定義した好みの高精度な取得を最大化してください。
+ アトモスフェリックガイド
+ あなたはアンビエントテクスチャーと低エネルギーフローの達人です。「深い集中」や「静けさ」の状態を促すトラックだけに集中してください。アコースティックな温かさ、ミニマリストのアレンジ、穏やかなトランジションを優先し、高い過渡音や急激なダイナミックの変化を厳しく避けてください。
+ ソニックエンスージアスト
+ あなたはプロダクションの複雑さと演奏に焦点を当てたオーディオファイルアナリストです。高いダイナミックレンジ、複雑なポリリズム、優れたサウンドステージ品質を持つトラックを優先してください。技術的な忠実度とアレンジの細部に注意を払うリスナーを喜ばせるアクティブリスニング作品を選んでください。
+ エナジーカタリスト
+ あなたは高モメンタムのリズムジェネレーターです。強烈なベースライン、パーカッシブな強度、感染力のあるグルーヴを中心哲学とします。高 BPM のクラブ互換性、シンコペーションエネルギー、継続的なリズムの張りを優先して、リスナーの心拍数とモチベーションをピーク状態に保ってください。
+ AI 使用レポート
+ 総消費量
+ %1$s トークンを追跡中\nプロンプト: %2$s | 出力: %3$s | 思考: %4$s
+ ログをクリア
+ AI アクティビティログ(%1$d 件)
+ %1$s · %2$s
+ 表示
+ 非表示
+
+
+ バックアップの仕組み
+ セクションを選んで .pxpl ファイルをエクスポートし、後でインポートして復元します。復元は選択したセクションのみを置き換えます。
+ バックアップを作成
+ バックアップをエクスポート
+ セクションが選択されていません。
+ すべてのセクションが選択されています。
+ %2$d セクション中 %1$d を選択中。
+ %1$s .pxpl バックアップファイルを作成します。
+ 選択してエクスポート
+ バックアップを復元
+ バックアップをインポート
+ 選択して復元
+ 最近のバックアップを参照または選択します。選択したデータが現在のデータを置き換えます。
+
+
+ バックアップパッケージに含める内容を正確に選択してください。
+ .pxpl バックアップファイルを選択して確認します。次のステップで復元するセクションを選択します。
+ %2$d セクション中 %1$d を選択中
+ %2$d モジュール中 %1$d を選択中
+ 最近のバックアップ
+ 最近のバックアップはありません
+ 以前にインポートしたバックアップがここに表示されます。
+ %1$d エントリー · 現在のデータを置き換えます
+ .pxpl をエクスポート
+ 選択を復元
+ 転送中…
+ PixelPlayer_Backup_%1$d.pxpl
+ バックアップを作成中
+ バックアップを復元中
+ %1$d%%
+ %1$s • %2$s
+ エクスポート中
+ インポート中
+ 復元中
+ 履歴から削除
+ 確認中…
+ ファイルを参照
+ ステップ %1$d / %2$d
+ モジュールを復元
+ バックアップの詳細
+ 作成日
+ アプリバージョン
+ スキーマ
+ デバイス
+ 不明
+ · %1$s
+ %1$d モジュール · v%2$s · スキーマ v%3$d
+ \?
+ すべて選択
+ 選択をクリア
+
+
+ 無効なバックアップ: %1$s
+ 復元を準備中
+ 復元タスクを開始しています。
+ バックアップを準備中
+ バックアップタスクを開始しています。
+ バックアップを正常に復元しました
+ 一部の未解決の問題がありましたが復元は完了しました。
+ 復元を完了できませんでした: %1$s
+ 復元に失敗しました: %1$s
+ データを正常にエクスポートしました
+ エクスポートに失敗しました: %1$s
+ データを正常に復元しました
+ 未解決の問題で復元が完了しました。失敗: %1$s
+ v%1$d
+ %1$s %2$s
+
+
+ 実験的機能
+ 試験的
+ プレイヤー UI 読み込みの実験とトグル。
+ セットアップフローをテスト
+ テスト用にオンボーディングのセットアップ画面を起動します。
+ メンテナンス
+ デイリーミックスの強制再生成
+ デイリーミックスプレイリストをすぐに再作成します。
+ デイリーミックスを再生成
+ デイリーミックスを再生成しますか?
+ 現在のミックスを破棄して、最近のリスニング習慣に基づいて新しいミックスを生成します。
+ デイリーミックスの再生成を開始しました
+ 統計の強制再生成
+ キャッシュをクリアして再生統計を再計算します。
+ 再生成
+ 処理中…
+ 統計を再生成
+ 統計を再生成しますか?
+ 統計キャッシュをクリアして、データベース履歴から強制的に再計算します。
+ 統計の再生成を開始しました
+ アルバムパレットの強制再生成
+ すべてのアルバムアートのキャッシュされたパレットバリアントを再構築するか、特定の 1 枚を更新します。
+ すべて再生成
+ すべてのアルバムパレットを再生成しますか?
+ キャッシュされたテーマデータをクリアして、%1$d 枚のユニークなアルバムアートのすべてのパレットスタイルを再構築します。
+ 再生成中…
+ アルバムパレットを再生成中…
+ %1$d 枚のユニークなアルバムアートのキャッシュされたパレットバリアントを再構築中です。大きなライブラリでは時間がかかることがあります。
+ %1$d / %2$d 完了
+ %1$d 枚のアルバムアートパレットを再生成しました
+ %2$d 枚中 %1$d 枚のアルバムアートパレットを再生成しました
+ 曲を選択
+ 曲を選択するとキャッシュされたテーマデータをクリアして、アルバムアートからすべてのパレットスタイルを再生成します。
+ タイトル、アーティスト、アルバムで検索
+ 検索に一致する曲がありません。
+ アルバムアートのある曲が見つかりませんでした。
+ パレットを再生成中…
+ %1$s のパレットを再生成しました
+ %1$s のパレットを再生成できませんでした
+ 診断
+ テストクラッシュを発生させる
+ クラッシュレポートシステムをテストするためにクラッシュをシミュレートします。
+ 開発者オプションからテストクラッシュを発生させました — これはクラッシュレポートシステムをテストするための意図的な操作です
+
+
+ 試験的
+ プレイヤー UI 読み込みの調整
+ アニメーション歌詞(ハイエンド端末向け)
+ 歌詞にスプリングアニメーションとビジュアル効果を使用します。ローエンド端末ではフレームドロップが発生する場合があります。
+ 歌詞のブラー効果
+ 非アクティブな歌詞に被写界深度ブラーを適用します。
+ ブラー強度
+ ブラー効果の強さを調整します。
+ %1$.1f倍
+ ステップ 1 · 遅延する対象を選択
+ すべてを遅延
+ シートの背景が完全に展開されるまでプレイヤーのコンテンツ全体を保持します。
+ アルバムカルーセル
+ シートが展開されるまでアルバムアートとカルーセルを遅延します。
+ 曲のメタデータ
+ タイトル、アーティスト、歌詞/キューのアクションを遅延します。
+ 進行バー
+ 展開完了までタイムラインと時刻ラベルを遅延します。
+ 再生コントロール
+ 再生/一時停止、シーク、お気に入りコントロールを遅延します。
+ 遅延するコンポーネントがすべてアクティブです。「すべてを遅延」を無効にして各パーツをカスタマイズします。
+ ステップ 2 · プレースホルダーの動作を設定
+ 遅延項目にプレースホルダーを使用
+ コンポーネントが展開を待つ間、軽量なプレースホルダーを描画してレイアウトを安定させます。
+ ステップ 3 · プレースホルダーから実コンテンツに切り替えるタイミングを選択
+ モードを 1 つ選択してください。閾値モードはスライダーを使用します。ドラッグリリースモードはシートジェスチャーを離すまで待機します。
+ トリガーモードを解除するには遅延コンポーネントを少なくとも 1 つ有効にしてください。
+ 閾値
+ 展開率を使用します。
+ ドラッグリリース
+ ジェスチャーを離した後のみ切り替えます。
+ 展開閾値
+ 遅延コンポーネントが表示されるまでにシートがどれだけ展開している必要があるか。
+ コンテンツは %1$d%% 展開時に表示されます
+ プレイヤーを閉じるときにも適用
+ 折りたたむ際に閉じる閾値を使ってプレースホルダーに戻します。
+ 閉じる閾値
+ プレースホルダーが再び表示されるまでにどれだけ折りたたまれている必要があるか。
+ %1$d%% 折りたたみ後にプレースホルダーが表示されます
+ ドラッグリリースモードは閾値と閉じる動作をバイパスします。切り替えはシートのドラッグジェスチャーが終了したときのみ発生します。
+ プレースホルダーを透明にする
+ プレースホルダーはレイアウトスペースを保持したまま見えなくなります。
+ 画質
+ アルバムアートの解像度
+ 低(256px)- パフォーマンス重視
+ 中(512px)- バランス型
+ 高(800px)- 最高品質
+ オリジナル - 最大品質
+
+
+ 再生には確認が必要です
+ 再生の準備ができています
+ --
+ フォーマット
+ HW デコーダー
+ ローカル曲
+ ローカル音楽ストレージ
+ 音楽サイズ
+ %1$d 曲(ローカル)
+ 利用可能
+ %1$s 合計
+ 音楽の使用量
+ デバイス使用中
+ %1$d%%
+ <1%
+ %1$d 曲(クラウド)
+ %1$d ファイルは読み取り不可
+ 再生パス
+ はい
+ いいえ
+ サンプルレート
+ %1$d Hz
+ %1$d フレーム/バッファ
+ Hi-Fi PCM Float
+ 32 ビット float 出力パス
+ 低レイテンシーサポート
+ プロオーディオサポート
+ メモリ
+ %1$s 中利用可能
+ オフロード対応フォーマット
+ ハードウェアオフロードをサポートする圧縮フォーマットは報告されませんでした。
+ 他 %1$d 件
+ 検出された出力
+ 内蔵出力
+ Bluetooth オーディオ
+ USB オーディオ
+ 有線オーディオ
+ デジタル出力
+ その他の出力
+ Android から出力ルートは報告されませんでした。
+ ExoPlayer エンジン
+ %1$s レンダラー
+ フォーマット互換性
+ %1$d 対応トラック
+ %1$d 不明なフォーマット
+ デコーダーが報告されません
+ ハードウェアデコーダー
+ ソフトウェアデコーダー
+ オフロード
+ ライブラリ内 %1$d 件
+ 互換性の確認結果
+ 大きな非互換性はありません
+ インデックスされたトラックはこのデバイスで Android が報告するデコーダーと一致しています。
+ %1$d 件のトラックはネイティブデコードできない可能性があります
+ 確認が必要なフォーマット: %1$s。
+ %1$d 件のローカルトラックはリサンプリングされる可能性があります
+ ライブラリは現在の出力サンプルレートを超える %1$d Hz に達しています。
+ %1$d 件のトラックはメタデータが不明です
+ ライブラリを完全にリスキャンすると MIME、ビットレート、サンプルレートの欠損データを補完できます。
+ デバイス情報
+ メーカー
+ モデル
+ ブランド
+ デバイス
+ Android バージョン
+ SDK バージョン
+ ハードウェア
+ パフォーマンスレポート
+ 再生やスキャンのラグを分類するのに役立つ共有可能な診断レポートを生成します。デバイス、ライブラリ、タイミングデータのみを含み、ファイルパス、タイトル、アーティストは含まれません。
+ レポートを生成
+ 再生成
+ コピー
+ 共有
+ レポートをクリップボードにコピーしました
+ PixelPlayer パフォーマンスレポート
+ 高度なパフォーマンス診断
+ デフォルトではオフです。ベータのトラブルシューティング用に短いラグタイムラインを記録します。
+ %1$s まで有効
+ 今ラグをマーク
+ ラグの瞬間をマークしました
+
+
+ 接続済みアカウント
+ リンクされたプロバイダーを管理して各連携をコントロールします。
+ リンク済みサービス
+ アクティブ
+ 利用可能
+ 近日公開
+ 接続済み
+ 近日公開
+ サービスを開く
+ ログアウト中…
+ リンク済みアカウントがまだありません
+ プロバイダーを接続するとこの画面で管理できます。
+ %1$s に接続
+ %1$s(近日公開)
+ Google Drive は近日公開予定です。
+ 現在この画面を開けません。
+
+
+ このアプリについて
+ PixelPlayer
+ コミュニティと共に作られたオープンソースの音楽プレイヤー。
+ バージョン v%1$s
+ オープンソース
+ コミュニティファースト
+ Material 3 エクスプレッシブ
+ 現在コントリビューターが見つかりません。後でもう一度お試しください。
+ メンテナー
+ PixelPlayer の開発者。
+ コミュニティスポットライト
+ 大きな貢献をしたコラボレーターへの感謝。
+ オープンソースコントリビューター
+ GitHub からのライブコントリビューターリスト。
+ %1$d 回のコントリビューション
+ GitHub プロフィールを開く
+ Telegram を開く
+ %1$s のアバター
+ %1$s のアイコン
+
diff --git a/app/src/main/res/values-ja/strings_widget.xml b/app/src/main/res/values-ja/strings_widget.xml
new file mode 100644
index 000000000..c0d6f4787
--- /dev/null
+++ b/app/src/main/res/values-ja/strings_widget.xml
@@ -0,0 +1,17 @@
+
+
+ サイズに合わせて自動調整するウィジェット
+ コンパクトなプレイヤーバー
+ シャッフルとリピートを含むフルコントロール
+ ミニマリストな正方形プレイヤー
+
+ タップして開く
+ アルバムアート
+ アルバムアートのプレースホルダー
+
+ タップして再生
+ 曲のタイトル
+ アーティスト
+
+ 進行バー、%1$d%%
+
diff --git a/app/src/main/res/values-ko/strings_changelogs.xml b/app/src/main/res/values-ko/strings_changelogs.xml
index acb78fe7b..bd7f2e1ed 100644
--- a/app/src/main/res/values-ko/strings_changelogs.xml
+++ b/app/src/main/res/values-ko/strings_changelogs.xml
@@ -129,4 +129,22 @@
현지화: 스페인어, 프랑스어, 러시아어, 중국어(간체), 인도네시아어, 이탈리아어
+
+ 플레이어 수명 주기 관리와 Google Drive 연동.
+ 노래 메타데이터 일괄 편집 (태그 및 커버 아트).
+ 맞춤 설정 가능한 Wear OS 환경설정이 포함된 AI 가사 번역.
+ 검색 화면에서 랙 진단 도구 및 다중 선택.
+ 아랍어 및 터키어 지원, 현지화된 http URL 로컬 네트워크 옵션 제공.
+
+
+ 획기적인 배터리 절약 (오디오 오프로드 및 UI 폴링 게이트).
+ 최적화된 대기열 관리 (빠른 삽입 및 명시적 인덱싱).
+ 전환 화면을 위한 Material 3 Expressive 모션 애니메이션.
+ 제한된 스캔(throttled scanning)을 통한 라이브러리 동기화 리팩토링.
+
+
+ 재생 중 끊김/건너뜀 지연 및 버퍼링 문제 해결.
+ 외부 곡 삭제 동기화 및 메타데이터 일관성 수정.
+ Wear OS 및 휴대폰의 메모리 문제, 크래시 및 레이아웃 오류 수정.
+
\ No newline at end of file
diff --git a/app/src/main/res/values-ko/strings_home_screen.xml b/app/src/main/res/values-ko/strings_home_screen.xml
index ece00d63f..f831b8995 100644
--- a/app/src/main/res/values-ko/strings_home_screen.xml
+++ b/app/src/main/res/values-ko/strings_home_screen.xml
@@ -9,9 +9,9 @@
클라우드 계정에서 음악 스트리밍
- Beta 0.7.0
+ Beta 0.7.5β
- PixelPlayer 0.7.0-beta에 오신 것을 환영합니다
+ PixelPlayer 0.7.5-beta에 오신 것을 환영합니다버그, 충돌 또는 실험적 기능이 포함되어 있을 수 있는 베타 빌드를 사용 중입니다. 문제를 보고하여 앱을 개선할 수 있도록 도와주세요.주요 변경 사항버그, 충돌 또는 불완전한 기능이 예기치 않게 발생할 수 있습니다.
@@ -273,4 +273,4 @@
%1$d곡%1$d곡%1$d주차
-
\ No newline at end of file
+
diff --git a/app/src/main/res/values-ko/strings_settings.xml b/app/src/main/res/values-ko/strings_settings.xml
index bcc63f057..573191dc5 100644
--- a/app/src/main/res/values-ko/strings_settings.xml
+++ b/app/src/main/res/values-ko/strings_settings.xml
@@ -163,17 +163,6 @@
앱 언어앱 인터페이스에서 사용할 언어를 선택하세요.시스템 기본값
- 영어
- 스페인어
- 독일 사람
- 프랑스어
- 러시아어
- 중국어(간체)
- 인도네시아어
- 이탈리아어
- 한국어
- 노르웨이어 (Bokmål)
- 터키어앱 테마밝은 테마, 어두운 테마 또는 시스템 설정 따르기 중에서 선택하세요.밝은 테마
@@ -633,6 +622,12 @@
오픈 소스 기여자GitHub의 실시간 기여자 목록입니다.기여 %1$d회
+ GitHub
+ 저장소
+ Telegram
+ 지원
+ GitHub 저장소 열기
+ Telegram 커뮤니티 가입GitHub 프로필 열기Telegram 열기%1$s 아바타
diff --git a/app/src/main/res/values-nb/strings_changelogs.xml b/app/src/main/res/values-nb/strings_changelogs.xml
index c12abe637..46d9f2744 100644
--- a/app/src/main/res/values-nb/strings_changelogs.xml
+++ b/app/src/main/res/values-nb/strings_changelogs.xml
@@ -129,4 +129,22 @@
Lokalisering: Spansk, Fransk, Russisk, Forenklet kinesisk, Indonesisk, Italiensk
+
+ Google Drive-integrasjon med spillerens livssyklushåndtering.
+ Masseredigering av sangmetadata (tagger og coverbilde).
+ AI-sangtekstoversettelse med tilpassbare Wear OS-preferanser.
+ Diagnoseverktøy for forsinkelse og flervalg på søkeskjermen.
+ Støtte for arabisk & tyrkisk, med lokaliserte http URL-alternativer for lokalt nettverk.
+
+
+ Drastisk batterisparing (lydoffload og UI-pollingporter).
+ Optimalisert køhåndtering (raskere innsettinger og eksplisitt indeksering).
+ Material 3 Expressive-bevegelsesanimasjoner for overgangsskjermer.
+ Refaktorert biblioteksynkronisering via begrenset skanning.
+
+
+ Løst hakking/hopping under avspilling og bufferproblemer.
+ Fikset synkronisering av sletting av eksterne sanger og konsistens i metadata.
+ Fikset minneproblemer, krasj og layoutfeil på Wear OS og telefon.
+
\ No newline at end of file
diff --git a/app/src/main/res/values-nb/strings_home_screen.xml b/app/src/main/res/values-nb/strings_home_screen.xml
index e1f64c77a..ee172a5bc 100644
--- a/app/src/main/res/values-nb/strings_home_screen.xml
+++ b/app/src/main/res/values-nb/strings_home_screen.xml
@@ -9,9 +9,9 @@
Strøm musikk fra dine skykontoer
- Beta 0.7.0
+ Beta 0.7.5β
- Velkommen til PixelPlayer 0.7.0-beta
+ Velkommen til PixelPlayer 0.7.5-betaDu bruker en betaversjon som kan inneholde feil, krasj eller eksperimentelle funksjoner. Hjelp oss å forbedre ved å rapportere problemer.Hva du kan forventeFeil, krasj eller uferdige funksjoner kan oppstå uventet.
@@ -274,4 +274,4 @@
%1$d sang%1$d sangerUke %1$d
-
\ No newline at end of file
+
diff --git a/app/src/main/res/values-nb/strings_settings.xml b/app/src/main/res/values-nb/strings_settings.xml
index fd101cb66..df49c697b 100644
--- a/app/src/main/res/values-nb/strings_settings.xml
+++ b/app/src/main/res/values-nb/strings_settings.xml
@@ -163,17 +163,6 @@
App-språkVelg språket som skal brukes i appen.Systemstandard
- Engelsk
- Spansk
- Tysk
- Fransk
- Russisk
- Kinesisk (forenklet)
- Indonesisk
- Italiensk
- Koreansk
- Norsk bokmål
- TyrkiskApp-temaBytt mellom lyst, mørkt eller følg systemets utseende.Lyst tema
@@ -633,6 +622,12 @@
Bidragsytere til åpen kildekodeLive bidragsyterliste fra GitHub.%1$d bidrag.
+ GitHub
+ Kodelager
+ Telegram
+ Støtte
+ Åpne GitHub-kodelager
+ Bli med i Telegram-samfunnetÅpne GitHub-profilÅpne TelegramAvatar av %1$s
diff --git a/app/src/main/res/values-pt-rBR/plurals.xml b/app/src/main/res/values-pt-rBR/plurals.xml
new file mode 100644
index 000000000..a8377c592
--- /dev/null
+++ b/app/src/main/res/values-pt-rBR/plurals.xml
@@ -0,0 +1,39 @@
+
+
+
+ Compartilhando %d playlist
+ Compartilhando %d playlists
+
+
+ %1$d playlist exportada para %2$s
+ %1$d playlists exportadas para %2$s
+
+
+ %d faixa adicionada à fila
+ %d faixas adicionadas à fila
+
+
+ %d faixa vai tocar depois
+ %d faixas vão tocar depois
+
+
+ %d faixa adicionada aos favoritos
+ %d faixas adicionadas aos favoritos
+
+
+ %d faixa removida dos favoritos
+ %d faixas removidas dos favoritos
+
+
+ %d arquivo apagado
+ %d arquivos apagados
+
+
+ Apagar %d faixa?
+ Apagar %d faixas?
+
+
+ %d vez
+ %d vezes
+
+
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 000000000..bd8531e00
--- /dev/null
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,231 @@
+
+ PixelPlayer
+ Mudança no nome do App
+ Nós alteramos o nome do app de PixelPlay para PixelPlayer devido à um problema de marca registrada. Continue tocando!
+ Não mostrar novamente
+ Dispensar
+ Permissão Especial Necessária
+ Para editar metadados de faixas(arquivos .mp3), o PixelPlayer precisa de acesso à todos os seus arquivos. Isso nos permite editar as tags da faixa diretamente. Por favor conceda tal permissão na próxima página para habilitar a edição de metadados.
+ Conceder permissão
+ Acesso à Todos os Arquivos
+ Erro
+ OK
+ Cancelar
+ Importar
+ Pesquisar
+
+ Letras
+ Fechar página de letras
+ Carregando letras…
+ Não foi possível encontrar letras para essa faixa.
+ Letras disponibilizada por
+ https://lrclib.net/
+ Letras não encontradas
+ Gostaria de procurar por letras online?
+ Não encontramos letras automaticamente. Você pode editar o título e o artista ou pesquisar manualmente.
+ Falha na procura de letras
+ Falha ao procurar letras no remoto
+ A conexão excedeu do tempo limite. Por favor verifique sua conexão com a internet.
+ Erro de rede. Por favor verifique sua conexão com a internet.
+ Erro de servidor (código %d). Por favor, tente novamente mais tarde.
+ %d resultado(s) encontrado(s)
+ Pesquisou por \"%s\"
+ Procurando por letras…
+ Letras já encontradas. A procura online foi pulada.
+ Letras embutidas encontradas. A procura online foi pulada.
+ Arquivo local (.lrc) encontrado. Online fetch skipped.
+ Mostrar opções das letras
+ Sempre escolher manualmente o resultado ao invés de selecionar automaticamente
+ Salvar letras como .lrc
+ Salvar letras
+ Escolha qual versão salvar:
+ Sincronizada (com timestamps)
+ Simples (apenas texto)
+ Letras salvas com sucesso
+ Falha ao salvar letras
+ Sem letras disponíveis para salvar
+ Redefinir letra importada
+ Atraso de sincronização de letra
+ %+.1fs
+ Redefinir
+ Mais cedo
+ Mais tarde
+
+ Escaneando arquivos de faixas…
+ Processando arquivos…
+ %1$d de %2$d arquivos
+ Sincronizando…
+ Sincronização completa
+ Aguardando…
+ Sincronizando biblioteca…
+ Finalizando em segundo plano…
+ Escaneando letras…
+ Limpando cache de arte de album…
+ Sincronizando fontes da nuvem…
+ Faixa desconhecida
+ Artista desconhecido
+ Album desconhecido
+ Escolha um artista
+ Veja qualquer um dos artistas creditados nesta faixa.
+ 1 artista
+ %1$d artistas
+ Artista principal
+ Página de artista
+ Tocar agora
+ Não foi possível abrir o arquivo de áudio.
+ Abrir player completo
+ Fechar player flutuante
+ Fechar player
+ Faixa anterior
+ Próxima faixa
+ Pausar playback
+ Tocar
+ Playlist não encontrada.
+ Disco %d
+
+ Por favor configure uma chave de API válida nas configurações para o provedor de IA selecionado.
+ Erro de IA: %s
+ O provedor de IA selecionado rejeitou o pedido pois a conta não possui créditos ou passou do limite de uso.
+ O modelo de IA selecionado não está mais disponível. O PixelPlayer tentou mudar para um modelo alternativo automaticamente.
+ A IA não encontrou nenhuma faixa de acordo com seu prompt.
+ Escreva uma ideia para seu Mix Diário
+ Mix Diário atualizado com IA
+ A IA não conseguiu encontrar faixas para esse mix
+
+ Ordem Aleatória
+ Aleatorizar todas as faixas
+ Playlist
+ Última playlist tocada
+
+ Aleatorizar todas
+ Última Playlist
+ Nenhuma playlist encontrada para abrir
+
+ ID de Álbum inválido
+ ID de Álbum não encontrado
+ Falha ao carregar dados de álbum: %s
+ Álbum não encontrado
+ Não foi possível atualizar: %s
+ ID de Artista inválido
+ ID de artista não encontrado
+ Falha ao carregar dados de artista: %s
+ Não foi possível encontrar o artista
+ Nenhuma faixa válida encontrada para tocar
+
+ Widget responsivo que se adapta ao tamanho
+ Barra do player compacta
+ Controles completos com Ordem Aleatória e Repetir
+ Player quadrado minimalista
+ Processando ação de playback…
+
+
+ Nenhuma playlist para compartilhar
+ Compartilhar playlists
+ Compartilhamento falhou: %1$s
+ Nenhuma playlist para exportar
+ Exportação falhou: %1$s
+ Exportações de faixa do PixelPlayer
+ Por favor configure sua chave de API Gemini nas Configurações.
+ Erro desconhecido
+
+
+ Enviando %1$d faixa(s) para relógio
+ Enviando para relógio
+ Transferência completa
+ Transferência falhou
+ Transferência cancelada
+ Preparando transferência para relógio
+ %1$d transferência(s)
+ Iniciando transferência…
+ Múltiplas transferências ativas
+ Preparando transferência…
+ Transferindo
+ Completo
+ Falhou
+ Cancelado
+ Preparando
+ Iniciando
+ Transferências para relógio
+ Mostra o progresso em tempo real da transferência de faixas para do celular para o relógio
+
+
+ Servidor de transmição de mídia
+ Transmissão para dispositivo
+ Serving media to Cast device
+ %1$s: %2$s
+ A navegação está temporariamente indisponível em transmissões com este formato de áudio porque poderia travar a sessão de transmissão.
+
+
+ Backup inválido: %1$s
+ Preparando recuperação
+ Preparando tarefa de recuperação.
+ Preparando backup
+ Iniciando tarefa de backup.
+ Beckup restaurado com sucesso
+ Restauração completa com alguns problemas.
+ Não foi possível completar a restauração: %1$s
+ Restauração falhou: %1$s
+ Dados exportados com sucesso
+ Exportação falhou: %1$s
+ Dados restaurados com sucesso
+ Restauração completa com alguns problemas. Falhou: %1$s
+ Falha ao carregar modelos
+ Crash de Teste engatilhado por meio das Opções de Desenvolvedor - Isso é intencional para testar o sistema de relatório de crash
+
+
+ Faixa não encontrada na lista atual
+ Não foi possível encontrar faixa
+ Nenhuma faixa encontrada na biblioteca
+ Playback parou: %1$s terminou (Fim de Faixa).
+ Faixa
+ Não há faixas para aleatorizar.
+ Álbuns Selecionados
+ Nenhuma faixa tocável encontrada nos álbuns selecionados
+ Apenas os primeiros %1$d álbuns foram adicionados à fila
+ %1$d álbuns adicionados à fila (%2$d faixas)
+ Não foi possível adicionar os álbuns selecionados à fila
+ Todas as faixas já estão favoritadas
+ Não haviam faixas favoritadas
+ Criando arquivo ZIP…
+ Falha ao compartilhar: %1$s
+ Não é possível apagar a faixa que está tocando
+ %1$d arquivos apagados (%2$d pulado - tocando)
+ %1$d de %2$d arquivos apagados
+ Falha ao apagar arquivos
+ Arquivo detectado
+ Não foi possível apagar arquivo ou não foi encontrado
+ Eliminação de arquivo cancelada
+ Permissão negada – não é possível editar arquivos
+ Permissão negada – não é possível salvar letras
+ Permissão negada – não é possível editar este arquivo
+ Metadados atualizados com sucesso
+ Atualizando %1$d faixas…
+ %1$d faixas atualizadas com sucesso!
+ %1$d faixas atualizadas. Falhas: %2$d
+ Playlist restaurada
+ Essas faixas serão apagadas permanentemente do seu dispositivo e não será possível recuperá-las.
+ Apagar
+
+
+ %1$d minutos
+ Fim de faixa
+ Temporizador configurado para %1$d minutos.
+ Temporizador cancelado.
+ Não é possível habilitar fim de faixa: nenhuma faixa ativa.
+ Temporizador de fim de faixa desativado: faixa alterada de %1$s para %2$s.
+ O playback irá parar no fim da faixa.
+ Faixa anterior
+ Faixa atual
+ Temporizador de Descanso
+ Temporizador
+ Fim da faixa atual
+ Tempo personalizado
+ Cancelar temporizador
+ Definir duração personalizada
+ Contagem de toque: %1$s
+ 1 vez
+ Alternar em
+ %1$d%%
+ v%1$d
+ %1$s %2$s
+
diff --git a/app/src/main/res/values-pt-rBR/strings_auth.xml b/app/src/main/res/values-pt-rBR/strings_auth.xml
new file mode 100644
index 000000000..ad193a5f3
--- /dev/null
+++ b/app/src/main/res/values-pt-rBR/strings_auth.xml
@@ -0,0 +1,74 @@
+
+
+
+ Voltar
+ Mostrar senha
+ Esconder senha
+ Conectando…
+ Conectar
+ Detalhes de conexão
+ Insira o URL do servidor e as credenciais de conta.
+ URL do Servidor
+ Nome de Usuário
+ Senha
+ Digite a senha
+ admin
+ Bem-vindo(a), %1$s!
+
+
+ Subsonic / Navidrome
+ Conecte com seu servidor self-hosted de música
+ Suporta Navidrome, Airsonic, Gonic, Ampache e outros servidores compatíveis com a API Subsonic.
+ https://music.example.com
+ Utilize o endereço base https:// completo do servidor.
+ Esse é o nome de usuário da sua conta Subsonic ou Navidrome.
+ Senha do App também funciona se o seu servidor suporta isso.
+ Senha do App também funciona se o seu servidor suporta isso.
+ Pré-adicionar https://
+ Compatível com Navidrome, Gonic, Airsonic, e outros servidores compatíveis com Subsonic
+ Navidrome
+ Subsonic
+
+
+ Jellyfin
+ Conecta com servidores Jellyfin. Ambos HTTP e HTTPS são suportados para acesso pela rede local.
+ Conectar com seu servidor de mídia Jellyfin
+ Insira o URL do seu servidor Jellyfin e credenciais de conta.
+ http://192.168.1.100:8096
+ URL completa do seu servidor Jellyfin, includindo a porta.
+ Seu nome de usuário da conta Jellyfin.
+ Sua senha da conta Jellyfin.
+ Pré-adicionar http://
+ Conecta com servidores Jellyfin para fazer streaming da sua biblioteca de músicas
+ Jellyfin
+
+
+ Google Drive conectado!
+ Google Drive
+
+
+ Sair do login NetEase?
+ Sair do login QQ Music?
+ Você pode voltar mais tarde. O estado da página atual será descartado quando fechar.
+ Sair
+ Continuar
+ Fazer login em NetEase Music
+ Fazer login em QQ Music
+ Voltar Web
+ Avançar Web
+ Recarregar
+ Abrir início
+ Salvando…
+ Pronto
+ Tentar novamente
+ +
+ O carregamento da página passou do tempo limite. Você pode tentar novamente sem perder seu progresso.
+ Não foi possível ler os cookies da sessão.
+ A página está demorando muito para carregar. Recarregue ou utilize outra conexão de rede.
+ Carregamento do WebView falhou.
+ HTTP %1$d enquanto carregando NetEase.
+ HTTP %1$d enquanto carregando QQ Music.
+ Nenhum cookie encontrado. Faça login primeiro.
+ Login ainda não detectado. Finalize o login NetEase antes de pressionar Pronto.
+ Login ainda não detectado. Finalize o login QQ Music antes de pressionar Pronto.
+
diff --git a/app/src/main/res/values-pt-rBR/strings_components.xml b/app/src/main/res/values-pt-rBR/strings_components.xml
new file mode 100644
index 000000000..b1221d0eb
--- /dev/null
+++ b/app/src/main/res/values-pt-rBR/strings_components.xml
@@ -0,0 +1,189 @@
+
+
+ Clique para abrir
+ Arte de álbum
+ Área reservada da arte de álbum
+ Favorito
+ Tocar
+ Pausar
+ Clique para tocar
+ Título da faixa
+ Artista
+ Repetir
+ Barra de progresso, %1$d por cento
+
+
+ Aparência
+ Alinhamento
+ Controles
+ Resetar letras?
+ Tem certeza de que deseja resetar as letras para essa faixa?
+ Esconder controles de sincronização
+ Ajustar sincronização
+ Mostrar romanização
+ Mostrar tradução
+ Desabilitar imersão (único)
+ Manter tela ligada
+ Alinhar letras à esquerda
+ Alinhar letras ao centro
+ Alinha letras à direita
+
+
+ Sem conexão com a internet
+ Esse conteúdo depende de uma conexão à internet. Por favor, cheque suas configurações de rede e tente novamente.
+ Você está offline
+ Por favor, cheque sua conexão com a internet e tente novamente para acessar esse conteúdo.
+
+
+ Salvar predefinição personalizada
+ Insira um nome para sua predefinição de equalizador personalizada.
+ Nome da predefinição
+ Renomear predefinição
+ Nome não pode estar vazio
+ Salvar
+ Renomear
+
+
+ Etiquetado perfeitamente!
+ Metadados de IA
+ Consultando o guia de Mix Diário…
+ Revise e refine os detalhes gerados
+ Título
+ Artista
+ Álbum
+ Artista do álbum
+ Gênero
+ Compositor
+ Tentar novamente
+ Aplicar mudanças
+
+
+ Editando metadados da faixa
+ Editar os metadados de uma faixa pode alterar como ela é mostrada e organizada em sua biblioteca. As mudanças são permanentes e talvez sejam irreversíveis.
+ Entendido
+ Informação
+ Editar faixa
+ Usar IA Gemini
+ Mostrar informação
+ Número da faixa
+ Número do disco
+ ReplayGain da faixa (dB)
+ ReplayGain do álbum (dB)
+ -6.50
+ -8.20
+ ReplayGain da faixa
+ ReplayGain do álbum
+ Título
+ Número da faixa
+ Número do disco
+ Pesquisar letras em lrclib.net
+ Arte da capa
+ Selecione uma imagem quadrada e dê um toque nela para que a faixa fique bonita pelo app.
+ Mudar arte de capa
+ Apagar arte de capa
+ Pré-visualização da nova arte de capa
+ Arte de capa da faixa atual
+ Ajustar sua arte de capa
+ Utilize gestos de pinça e arraste para encontrar o enquadramento desejado.
+ Aplicar arte de capa
+ Não foi possível carregar a imagem selecionada
+
+
+ Compartilhar arquivo de faixa via
+ Tocar faixa
+ Compartilhar arquivo da faixa
+ Adicionar à fila
+ Tocar como próxima da fila
+ Adicionar à playlist
+ Adicionar à fila
+ Próximo
+ Verificando relógio
+ Transferindo %1$d%%
+ Transferindo para relógio
+ Transferência em progresso
+ Enviar para relógio
+ Relógio não disponível
+ Enviar faixa para o relógio
+ Relógio não disponível
+ Definir como
+ Definir como som
+ Escolha como usar essa faixa como som do sistema
+ Definir como toque
+ Definir faixa como toque
+ Usar essa faixa como
+ Escolha onde PixelPlayer deve armazenar esse som.
+ Toque de telefone
+ Chamadas recebidas
+ Som de notificação
+ Alertas de mensagem e de apps
+ Toque de alarme
+ Alarmes do relógio
+ Confirmar mudança do som
+ Definir \"%1$s\" como seu %2$s?
+ Definir som
+ Definir \"%1$s\" como seu %2$s
+ toque de telefone
+ som de notificação
+ toque de alarme
+ Habilite \"Mudar configurações do sistema\", e retorne ao PixelPlayer para continuar automaticamente.
+ \"Mudar configurações do sistema\" não foi habilitado.
+ Definir \"%1$s\" como seu toque de telefone
+ Apenas faixas locais podem ser utilizadas como toque de telefone.
+ Não foi possível preparar a faixa para usar como toque de telefone.
+ Não foi possível definir como toque de telefone: %1$s
+ Duração
+ Informações da faixa
+ Duração
+ Gênero
+ Álbum
+ Artista
+ Formato de áudio
+ Provedor
+ Arquivo
+ Editar metadados da faixa
+ Remover dos favoritos
+ Adicionar aos favoritos
+ Opções
+ OPÇÕES
+ Detalhes
+ INFO
+ Detalhes
+
+
+ %1$d FAIXAS
+ selecionada(s)
+ Tocar todas
+ Tocar todas
+ Curtir todas
+ Descurtir todas
+ Compartilhar todas como ZIP
+ Adicionar todas à fila
+ Apagar todas
+ Apagar todas
+
+ Playlist dispensada
+ Desfazer
+ Mashup do DJ
+ Nova playlist
+ Nome da playlist
+ Minha playlist
+ Criar
+ Adicionar %1$d faixas em…
+ Selecionar playlists
+ Pesquisar playlists…
+
+ %1$d PLAYLISTS
+ Exportar todas
+ Mesclar todas
+ Compartilhar todas
+ Exportar
+ Mesclar
+
+ Reordenar abas da biblioteca
+ Resetar ordem
+ Restaurar a ordem padrão das abas?
+ Reordenando abas…
+ Mover
+ Resetar
+ Pronto
+
diff --git a/app/src/main/res/values-ru/strings_changelogs.xml b/app/src/main/res/values-ru/strings_changelogs.xml
index 1e30b701c..187dd7402 100644
--- a/app/src/main/res/values-ru/strings_changelogs.xml
+++ b/app/src/main/res/values-ru/strings_changelogs.xml
@@ -129,4 +129,22 @@
Локализация: испанский, французский, русский, упрощённый китайский, индонезийский, итальянский
+
+ Интеграция с Google Drive с управлением жизненным циклом плеера.
+ Пакетное редактирование метаданных песен (теги и обложки).
+ Перевод текстов песен с помощью ИИ с настраиваемыми предпочтениями Wear OS.
+ Инструмент диагностики задержек и множественный выбор на экране поиска.
+ Поддержка арабского и турецкого языков с локализованными параметрами локальной сети для http-адресов.
+
+
+ Значительное энергосбережение (разгрузка аудио и оптимизация опроса интерфейса).
+ Оптимизированное управление очередью (ускоренная вставка и явная индексация).
+ Выразительные анимации движения Material 3 для экранов перехода.
+ Рефакторинг синхронизации медиатеки с ограничением частоты сканирования.
+
+
+ Устранены заикания, пропуски воспроизведения и проблемы с буферизацией.
+ Исправлена синхронизация при удалении внешних песен и согласованность метаданных.
+ Исправлены проблемы с памятью, сбои и ошибки макета на Wear OS и телефоне.
+
\ No newline at end of file
diff --git a/app/src/main/res/values-ru/strings_home_screen.xml b/app/src/main/res/values-ru/strings_home_screen.xml
index b14b73cd7..8e0229903 100644
--- a/app/src/main/res/values-ru/strings_home_screen.xml
+++ b/app/src/main/res/values-ru/strings_home_screen.xml
@@ -9,9 +9,9 @@
Слушайте музыку из своих облачных аккаунтов
- Бета 0.7.0
+ Бета 0.7.5β
- Добро пожаловать в PixelPlayer 0.7.0-beta
+ Добро пожаловать в PixelPlayer 0.7.5-betaВы используете бета-версию, которая может содержать ошибки, сбои или экспериментальные функции. Помогите нам улучшить приложение, сообщая о проблемах.Чего ожидатьОшибки, сбои или незавершённые функции могут возникать неожиданно.
@@ -276,4 +276,4 @@
%1$d песня%1$d песенНеделя %1$d
-
\ No newline at end of file
+
diff --git a/app/src/main/res/values-ru/strings_settings.xml b/app/src/main/res/values-ru/strings_settings.xml
index 997331e07..dce95eba8 100644
--- a/app/src/main/res/values-ru/strings_settings.xml
+++ b/app/src/main/res/values-ru/strings_settings.xml
@@ -163,17 +163,6 @@
Язык приложенияВыберите язык интерфейса приложения.Системный по умолчанию
- Английский
- Испанский
- Немецкий
- Французский
- Русский
- Китайский
- Индонезийский
- Итальянский
- Корейский
- Норвежский (Bokmål)
- ТурецкийТема приложенияСветлая, тёмная тема или настройки системы.Светлая тема
@@ -633,6 +622,12 @@
Участники open sourceАктуальный список участников с GitHub.%1$d вклад.
+ GitHub
+ Репозиторий
+ Telegram
+ Поддержка
+ Открыть репозиторий GitHub
+ Присоединиться к сообществу TelegramОткрыть профиль GitHubОткрыть TelegramАватар %1$s
diff --git a/app/src/main/res/values-tr/strings_changelogs.xml b/app/src/main/res/values-tr/strings_changelogs.xml
index be468d7c7..aa9d06800 100644
--- a/app/src/main/res/values-tr/strings_changelogs.xml
+++ b/app/src/main/res/values-tr/strings_changelogs.xml
@@ -129,4 +129,22 @@
Yerelleştirme: İspanyolca, Fransızca, Rusça, Basitleştirilmiş Çince, Endonezce, İtalyanca
+
+ Oynatıcı yaşam döngüsü yönetimiyle Google Drive entegrasyonu.
+ Toplu şarkı meta verisi düzenleme (etiketler ve kapak resmi).
+ Özelleştirilebilir Wear OS tercihleriyle yapay zeka şarkı sözü çevirisi.
+ Arama ekranında gecikme teşhis aracı ve çoklu seçim.
+ Yerelleştirilmiş http URL yerel ağ seçenekleriyle Arapça & Türkçe desteği.
+
+
+ Büyük pil tasarrufu (ses aktarımı ve kullanıcı arayüzü yoklama geçitleri).
+ Optimize edilmiş kuyruk yönetimi (daha hızlı eklemeler ve açık dizin oluşturma).
+ Geçiş ekranları için Material 3 Ekspresif hareket animasyonları.
+ Sınırlandırılmış tarama yoluyla kütüphane senkronizasyonu yeniden yapılandırıldı.
+
+
+ Oynatmada takılma/atlama gecikmeleri ve arabelleğe alma sorunları çözüldü.
+ Harici şarkı silme senkronizasyonu ve meta veri tutarlılığı düzeltildi.
+ Wear OS ve telefonda bellek sorunları, çökmeler ve düzen hataları düzeltildi.
+
\ No newline at end of file
diff --git a/app/src/main/res/values-tr/strings_home_screen.xml b/app/src/main/res/values-tr/strings_home_screen.xml
index 49de86173..2bf26ab40 100644
--- a/app/src/main/res/values-tr/strings_home_screen.xml
+++ b/app/src/main/res/values-tr/strings_home_screen.xml
@@ -9,9 +9,9 @@
Bulut hesaplarınızdan müzik akışı yapın
- Beta 0.7.0
+ Beta 0.7.5β
- PixelPlayer 0.7.0-beta\'ya Hoş Geldiniz
+ PixelPlayer 0.7.5-beta\'ya Hoş GeldinizHata, çökme veya deneysel özellikler içerebilecek bir beta yapısı kullanıyorsunuz. Sorunları bildirerek geliştirmemize yardımcı olun.Ne beklemeliBeklenmedik hatalar, çökmeler veya tamamlanmamış özellikler olabilir.
@@ -274,4 +274,4 @@
%1$d Şarkı%1$d Şarkı%1$d. Hafta
-
\ No newline at end of file
+
diff --git a/app/src/main/res/values-tr/strings_settings.xml b/app/src/main/res/values-tr/strings_settings.xml
index 1f4dc593c..d5c6bb696 100644
--- a/app/src/main/res/values-tr/strings_settings.xml
+++ b/app/src/main/res/values-tr/strings_settings.xml
@@ -163,17 +163,6 @@
Uygulama DiliUygulama arayüzünde kullanılacak dili seçin.Sistem varsayılanı
- İngilizce
- Español
- Almanca
- Fransızca
- Rusça
- Basitleştirilmiş Çince
- Endonezce
- İtalyanca
- Korece
- Norveççe (Bokmål)
- TürkçeUygulama TemasıAçık, koyu tema arasında geçiş yapın veya sistem görünümünü takip edin.Açık Tema
@@ -633,6 +622,12 @@
Açık kaynak katkıda bulunanlarGitHub\'dan canlı katkıda bulunanlar listesi.%1$d katkı
+ GitHub
+ Depo
+ Telegram
+ Destek
+ GitHub deposunu aç
+ Telegram topluluğuna katılGitHub profilini açTelegram\'ı aç%1$s avatarı
diff --git a/app/src/main/res/values-zh-rCN/strings_changelogs.xml b/app/src/main/res/values-zh-rCN/strings_changelogs.xml
index b0b82e4a3..345246805 100644
--- a/app/src/main/res/values-zh-rCN/strings_changelogs.xml
+++ b/app/src/main/res/values-zh-rCN/strings_changelogs.xml
@@ -129,4 +129,22 @@
本地化:西班牙语、法语、俄语、简体中文、印度尼西亚语、意大利语
+
+ Google Drive 集成,支持播放器生命周期管理。
+ 批量编辑歌曲元数据(标签和封面)。
+ AI 歌词翻译,支持自定义 Wear OS 偏好设置。
+ 搜索界面新增卡顿诊断工具和多选功能。
+ 支持阿拉伯语和土耳其语,并提供本地化 http URL 局域网选项。
+
+
+ 大幅省电(音频卸载和 UI 轮询门槛)。
+ 优化队列管理(更快的插入和显式索引)。
+ 适用于过渡界面的 Material 3 表达性运动动画。
+ 通过限制扫描频率重构媒体库同步。
+
+
+ 解决了播放卡顿/跳音延迟和缓冲问题。
+ 修复了外部歌曲删除同步和元数据一致性问题。
+ 修复了 Wear OS 和手机上的内存问题、崩溃和布局异常。
+
\ No newline at end of file
diff --git a/app/src/main/res/values-zh-rCN/strings_home_screen.xml b/app/src/main/res/values-zh-rCN/strings_home_screen.xml
index 78a2f155d..46a70f806 100644
--- a/app/src/main/res/values-zh-rCN/strings_home_screen.xml
+++ b/app/src/main/res/values-zh-rCN/strings_home_screen.xml
@@ -9,9 +9,9 @@
从您的云端账户串流音乐
- Beta 0.7.0
+ Beta 0.7.5β
- 欢迎使用 PixelPlayer 0.7.0-beta
+ 欢迎使用 PixelPlayer 0.7.5-beta您正在使用的是可能包含错误、崩溃或实验性功能的测试版本。请报告问题以帮助我们改进。预期情况可能会意外出现错误、崩溃或未完成的功能。
@@ -274,4 +274,4 @@
%1$d 首歌曲%1$d 首歌曲第 %1$d 周
-
\ No newline at end of file
+
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 0822b25e0..331b14eb0 100644
--- a/app/src/main/res/values-zh-rCN/strings_settings.xml
+++ b/app/src/main/res/values-zh-rCN/strings_settings.xml
@@ -163,17 +163,6 @@
应用语言选择应用界面的语言。跟随系统
- 英语
- 西班牙语
- 德语
- 法语
- 俄语
- 简体中文
- 印尼语
- 意大利语
- 韩语
- 挪威语(Bokmål)
- 土耳其语应用主题在浅色、深色之间切换,或跟随系统外观。浅色主题
@@ -633,6 +622,12 @@
开源贡献者来自 GitHub 的实时贡献者名单。%1$d 次贡献
+ GitHub
+ 代码仓库
+ Telegram
+ 支持
+ 打开 GitHub 仓库
+ 加入 Telegram 社区打开 GitHub 个人资料打开 Telegram%1$s 的头像
diff --git a/app/src/main/res/values/strings_home_screen.xml b/app/src/main/res/values/strings_home_screen.xml
index b60cde200..2a7e3ad80 100644
--- a/app/src/main/res/values/strings_home_screen.xml
+++ b/app/src/main/res/values/strings_home_screen.xml
@@ -9,9 +9,9 @@
Stream music from your cloud accounts
- Beta 0.7.0
+ Beta 0.7.5β
- Welcome to PixelPlayer 0.7.0-beta
+ Welcome to PixelPlayer 0.7.5-betaYou\'re using a beta build that may contain bugs, crashes, or experimental features. Help us improve by reporting issues.What to expectBugs, crashes, or incomplete features may occur unexpectedly.
@@ -274,4 +274,4 @@
%1$d Song%1$d SongsWeek %1$d
-
\ No newline at end of file
+
diff --git a/app/src/main/res/values/strings_library.xml b/app/src/main/res/values/strings_library.xml
index 8b7c6c2c6..21f700026 100644
--- a/app/src/main/res/values/strings_library.xml
+++ b/app/src/main/res/values/strings_library.xml
@@ -255,6 +255,10 @@
Reset tab order to the default?Reordering tabs…Drag handle
+ Visible Tabs
+ Removed Tabs
+ Remove tab
+ Add tabPick an Artist
diff --git a/app/src/main/res/values/strings_settings.xml b/app/src/main/res/values/strings_settings.xml
index 298046dec..4b41c0b7f 100644
--- a/app/src/main/res/values/strings_settings.xml
+++ b/app/src/main/res/values/strings_settings.xml
@@ -163,17 +163,6 @@
App LanguageChoose the language used across the app interface.System default
- English
- Español
- Deutsch
- Français
- Русский
- 简体中文
- Bahasa Indonesia
- Italiano
- Korean
- Norwegian (Bokmål)
- TürkçeApp ThemeSwitch between light, dark, or follow system appearance.Light Theme
@@ -633,6 +622,12 @@
Open source contributorsLive contributor list from GitHub.%1$d contrib.
+ GitHub
+ Repository
+ Telegram
+ Support
+ Open GitHub repository
+ Join Telegram communityOpen GitHub profileOpen TelegramAvatar of %1$s
diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml
index 12f03ebe0..933ea17f9 100644
--- a/app/src/main/res/xml/locales_config.xml
+++ b/app/src/main/res/xml/locales_config.xml
@@ -3,4 +3,5 @@
-
\ No newline at end of file
+
+
diff --git a/app/src/test/java/com/theveloper/pixelplay/data/ai/provider/AiClientFactoryTest.kt b/app/src/test/java/com/theveloper/pixelplay/data/ai/provider/AiClientFactoryTest.kt
new file mode 100644
index 000000000..e339075bc
--- /dev/null
+++ b/app/src/test/java/com/theveloper/pixelplay/data/ai/provider/AiClientFactoryTest.kt
@@ -0,0 +1,71 @@
+package com.theveloper.pixelplay.data.ai.provider
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+class AiClientFactoryTest {
+
+ private val factory = AiClientFactory()
+
+ @Test
+ fun `createClient returns GeminiAiClient for GEMINI`() {
+ val client = factory.createClient(AiProvider.GEMINI, "test-key")
+ assertThat(client).isInstanceOf(GeminiAiClient::class.java)
+ }
+
+ @Test
+ fun `createClient returns GenericOpenAiClient for DEEPSEEK`() {
+ val client = factory.createClient(AiProvider.DEEPSEEK, "test-key")
+ assertThat(client).isInstanceOf(GenericOpenAiClient::class.java)
+ }
+
+ @Test
+ fun `createClient returns GenericOpenAiClient for GROQ`() {
+ val client = factory.createClient(AiProvider.GROQ, "test-key")
+ assertThat(client).isInstanceOf(GenericOpenAiClient::class.java)
+ }
+
+ @Test
+ fun `createClient returns GenericOpenAiClient for MISTRAL`() {
+ val client = factory.createClient(AiProvider.MISTRAL, "test-key")
+ assertThat(client).isInstanceOf(GenericOpenAiClient::class.java)
+ }
+
+ @Test
+ fun `createClient returns GenericOpenAiClient for NVIDIA`() {
+ val client = factory.createClient(AiProvider.NVIDIA, "test-key")
+ assertThat(client).isInstanceOf(GenericOpenAiClient::class.java)
+ }
+
+ @Test
+ fun `createClient returns GenericOpenAiClient for OPENAI`() {
+ val client = factory.createClient(AiProvider.OPENAI, "test-key")
+ assertThat(client).isInstanceOf(GenericOpenAiClient::class.java)
+ }
+
+ @Test
+ fun `createClient returns GenericOpenAiClient for OPENROUTER`() {
+ val client = factory.createClient(AiProvider.OPENROUTER, "test-key")
+ assertThat(client).isInstanceOf(GenericOpenAiClient::class.java)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun `createClient throws for blank API key`() {
+ factory.createClient(AiProvider.GEMINI, "")
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun `createClient throws for whitespace-only API key`() {
+ factory.createClient(AiProvider.DEEPSEEK, " ")
+ }
+
+ @Test
+ fun `all providers return non-empty default model`() {
+ for (provider in AiProvider.entries) {
+ if (provider == AiProvider.CUSTOM) continue
+ val client = factory.createClient(provider, "test-key")
+ val defaultModel = client.getDefaultModel()
+ assertThat(defaultModel).isNotEmpty()
+ }
+ }
+}
diff --git a/app/src/test/java/com/theveloper/pixelplay/data/stream/CloudMusicUtilsTest.kt b/app/src/test/java/com/theveloper/pixelplay/data/stream/CloudMusicUtilsTest.kt
new file mode 100644
index 000000000..5883d54a1
--- /dev/null
+++ b/app/src/test/java/com/theveloper/pixelplay/data/stream/CloudMusicUtilsTest.kt
@@ -0,0 +1,85 @@
+package com.theveloper.pixelplay.data.stream
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+class CloudMusicUtilsTest {
+
+ @Test
+ fun `jsonToMap parses valid JSON object`() {
+ val result = CloudMusicUtils.jsonToMap("""{"key1":"val1","key2":"val2"}""")
+ assertThat(result).containsExactly("key1", "val1", "key2", "val2")
+ }
+
+ @Test
+ fun `jsonToMap handles empty object`() {
+ val result = CloudMusicUtils.jsonToMap("{}")
+ assertThat(result).isEmpty()
+ }
+
+ @Test
+ fun `jsonToMap returns empty string for null values`() {
+ val result = CloudMusicUtils.jsonToMap("""{"key":null}""")
+ assertThat(result).containsEntry("key", "")
+ }
+
+ @Test
+ fun `parseArtistNames splits on comma`() {
+ val result = CloudMusicUtils.parseArtistNames("Artist A, Artist B")
+ assertThat(result).containsExactly("Artist A", "Artist B")
+ }
+
+ @Test
+ fun `parseArtistNames splits on ampersand`() {
+ val result = CloudMusicUtils.parseArtistNames("A & B")
+ assertThat(result).containsExactly("A", "B")
+ }
+
+ @Test
+ fun `parseArtistNames splits on slash`() {
+ val result = CloudMusicUtils.parseArtistNames("A/B/C")
+ assertThat(result).containsExactly("A", "B", "C")
+ }
+
+ @Test
+ fun `parseArtistNames splits on semicolon`() {
+ val result = CloudMusicUtils.parseArtistNames("A;B")
+ assertThat(result).containsExactly("A", "B")
+ }
+
+ @Test
+ fun `parseArtistNames splits on CJK comma`() {
+ val result = CloudMusicUtils.parseArtistNames("A、B")
+ assertThat(result).containsExactly("A", "B")
+ }
+
+ @Test
+ fun `parseArtistNames deduplicates names`() {
+ val result = CloudMusicUtils.parseArtistNames("A, A, B")
+ assertThat(result).containsExactly("A", "B")
+ }
+
+ @Test
+ fun `parseArtistNames returns Unknown Artist for blank input`() {
+ assertThat(CloudMusicUtils.parseArtistNames("")).containsExactly("Unknown Artist")
+ assertThat(CloudMusicUtils.parseArtistNames(" ")).containsExactly("Unknown Artist")
+ }
+
+ @Test
+ fun `parseArtistNames returns single artist for simple name`() {
+ val result = CloudMusicUtils.parseArtistNames("Taylor Swift")
+ assertThat(result).containsExactly("Taylor Swift")
+ }
+
+ @Test
+ fun `parseArtistNames handles mixed delimiters`() {
+ val result = CloudMusicUtils.parseArtistNames("A, B & C/D")
+ assertThat(result).containsExactly("A", "B", "C", "D")
+ }
+
+ @Test
+ fun `parseArtistNames trims whitespace around names`() {
+ val result = CloudMusicUtils.parseArtistNames(" A , B ")
+ assertThat(result).containsExactly("A", "B")
+ }
+}
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 41c9b2494..7996811c6 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
@@ -180,7 +180,6 @@ class PlayerViewModelTest {
every { mockAiStateHolder.showAiPlaylistSheet } returns MutableStateFlow(false)
every { mockAiStateHolder.isGeneratingAiPlaylist } returns MutableStateFlow(false)
every { mockAiStateHolder.aiError } returns MutableStateFlow(null)
- every { mockAiStateHolder.isGeneratingMetadata } returns MutableStateFlow(false)
every { mockAiStateHolder.initialize(any(), any(), any(), any(), any(), any()) } just runs
every { mockCastStateHolder.castSession } returns _castSessionFlow
diff --git a/app/src/test/java/com/theveloper/pixelplay/utils/ServerUrlUtilsTest.kt b/app/src/test/java/com/theveloper/pixelplay/utils/ServerUrlUtilsTest.kt
new file mode 100644
index 000000000..62150c72c
--- /dev/null
+++ b/app/src/test/java/com/theveloper/pixelplay/utils/ServerUrlUtilsTest.kt
@@ -0,0 +1,104 @@
+package com.theveloper.pixelplay.utils
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+class ServerUrlUtilsTest {
+
+ @Test
+ fun `normalizeHttpUrl adds https scheme when absent`() {
+ val result = ServerUrlUtils.normalizeHttpUrl("music.example.com")
+ assertThat(result).isNotNull()
+ assertThat(result!!.scheme).isEqualTo("https")
+ assertThat(result.host).isEqualTo("music.example.com")
+ }
+
+ @Test
+ fun `normalizeHttpUrl preserves explicit http scheme`() {
+ val result = ServerUrlUtils.normalizeHttpUrl("http://192.168.1.100:8096")
+ assertThat(result).isNotNull()
+ assertThat(result!!.scheme).isEqualTo("http")
+ assertThat(result.host).isEqualTo("192.168.1.100")
+ assertThat(result.port).isEqualTo(8096)
+ }
+
+ @Test
+ fun `normalizeHttpUrl preserves https scheme`() {
+ val result = ServerUrlUtils.normalizeHttpUrl("https://music.example.com")
+ assertThat(result).isNotNull()
+ assertThat(result!!.scheme).isEqualTo("https")
+ }
+
+ @Test
+ fun `normalizeHttpUrl trims whitespace and trailing slashes`() {
+ val result = ServerUrlUtils.normalizeHttpUrl(" https://music.example.com/ ")
+ assertThat(result).isNotNull()
+ assertThat(result!!.host).isEqualTo("music.example.com")
+ }
+
+ @Test
+ fun `normalizeHttpUrl returns null for empty string`() {
+ assertThat(ServerUrlUtils.normalizeHttpUrl("")).isNull()
+ }
+
+ @Test
+ fun `normalizeServerUrl returns clean URL without trailing slash`() {
+ val result = ServerUrlUtils.normalizeServerUrl("https://music.example.com/")
+ assertThat(result).doesNotEndWith("/")
+ assertThat(result).startsWith("https://")
+ }
+
+ @Test
+ fun `normalizeServerUrl falls back to trimmed input for invalid URLs`() {
+ val result = ServerUrlUtils.normalizeServerUrl("")
+ assertThat(result).isEmpty()
+ }
+
+ @Test
+ fun `connectionValidationError returns null for valid https URL`() {
+ val error = ServerUrlUtils.connectionValidationError("https://music.example.com")
+ assertThat(error).isNull()
+ }
+
+ @Test
+ fun `connectionValidationError rejects embedded credentials`() {
+ val error = ServerUrlUtils.connectionValidationError("https://user:pass@music.example.com")
+ assertThat(error).contains("credentials")
+ }
+
+ @Test
+ fun `connectionValidationError rejects http on public hosts`() {
+ val error = ServerUrlUtils.connectionValidationError("http://music.example.com")
+ assertThat(error).contains("https")
+ }
+
+ @Test
+ fun `connectionValidationError allows http on local network`() {
+ val error = ServerUrlUtils.connectionValidationError("http://192.168.1.100:8096")
+ assertThat(error).isNull()
+ }
+
+ @Test
+ fun `connectionValidationError allows http on localhost`() {
+ val error = ServerUrlUtils.connectionValidationError("http://localhost:8096")
+ assertThat(error).isNull()
+ }
+
+ @Test
+ fun `connectionValidationError allows http on 127-0-0-1`() {
+ val error = ServerUrlUtils.connectionValidationError("http://127.0.0.1:4533")
+ assertThat(error).isNull()
+ }
+
+ @Test
+ fun `connectionValidationError uses serverLabel in message`() {
+ val error = ServerUrlUtils.connectionValidationError("http://public.example.com", "Jellyfin")
+ assertThat(error).contains("Jellyfin")
+ }
+
+ @Test
+ fun `connectionValidationError rejects invalid URL`() {
+ val error = ServerUrlUtils.connectionValidationError("not a url at all ://")
+ assertThat(error).contains("Invalid")
+ }
+}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index e653436ec..ec29af809 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -2,7 +2,7 @@
accompanistDrawablepainter = "0.37.3"
agp = "9.2.1"
app = "1.7.0"
-googleGenai = "1.58.0"
+googleGenai = "1.59.0"
googlePlayServicesCast = "22.3.1"
animation = "1.11.3"
appcompat = "1.7.1"
@@ -65,7 +65,7 @@ junit5 = "6.1.0"
kuromoji = "0.9.0"
pinyin4j = "2.5.1"
securityCrypto = "1.1.0"
-netty = "4.2.28.Final"
+netty = "4.2.15.Final"
bouncycastle = "1.84"
commons-lang3 = "3.20.0"
jdom2 = "2.0.6.1"