diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index b2793cfd8..2a8c09bbc 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 @@ -47,9 +47,10 @@ jobs: with: languages: java-kotlin build-mode: manual + cache: true - name: Build with Gradle - run: ${{ github.workspace }}/gradlew :app:assembleDebug :wear:assembleDebug --no-daemon + run: ${{ github.workspace }}/gradlew :app:assembleDebug :wear:assembleDebug --no-daemon --no-build-cache - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v4 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 c932c1186..000000000 --- a/.idea/deploymentTargetSelector.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 04d49b171..604e3beea 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -247,6 +247,8 @@ dependencies { implementation(libs.androidx.media3.exoplayer.ffmpeg) implementation(libs.androidx.media3.exoplayer.midi) implementation(libs.androidx.media3.transformer) + implementation(libs.androidx.media3.common) + implementation(libs.androidx.media3.common.ktx) implementation(libs.androidx.mediarouter) implementation(libs.androidx.media) implementation(libs.coil.compose) @@ -298,6 +300,8 @@ dependencies { exclude(group = "androidx.compose.runtime") exclude(group = "androidx.compose.ui") } + implementation(libs.haze) + implementation(libs.haze.materials) // Projects implementation(project(":shared")) diff --git a/app/src/main/java/com/theveloper/pixelplay/MainActivity.kt b/app/src/main/java/com/theveloper/pixelplay/MainActivity.kt index 503ac7a21..fbed5d40a 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,22 @@ 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 androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.Color 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 +132,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 @@ -152,6 +163,14 @@ import com.theveloper.pixelplay.presentation.utils.AppHapticsConfig import com.theveloper.pixelplay.presentation.utils.LocalAppHapticsConfig import com.theveloper.pixelplay.presentation.utils.NoOpHapticFeedback import com.theveloper.pixelplay.utils.CrashLogData +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.HazeStyle +import dev.chrisbanes.haze.HazeTint +import dev.chrisbanes.haze.haze +import dev.chrisbanes.haze.hazeChild +import dev.chrisbanes.haze.hazeEffect +import dev.chrisbanes.haze.hazeSource +import dev.chrisbanes.haze.materials.HazeMaterials import javax.annotation.concurrent.Immutable import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map @@ -193,6 +212,12 @@ class MainActivity : ComponentActivity() { // Handle the result in onResume } + companion object { + val LocalHazeState = staticCompositionLocalOf { + error("No HazeState provided") + } + } + @CallSuper override fun attachBaseContext(newBase: Context) { super.attachBaseContext(AppLocaleManager.wrapContext(newBase)) @@ -306,7 +331,9 @@ class MainActivity : ComponentActivity() { } Surface( - modifier = Modifier.fillMaxSize().graphicsLayer { alpha = contentAlpha }, + modifier = Modifier + .fillMaxSize() + .graphicsLayer { alpha = contentAlpha }, color = MaterialTheme.colorScheme.background ) { if (showSetupScreen == null) { @@ -608,6 +635,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), @@ -681,7 +713,7 @@ class MainActivity : ComponentActivity() { val scopedHapticFeedback = remember(platformHapticFeedback, appHapticsConfig.enabled) { if (appHapticsConfig.enabled) platformHapticFeedback else NoOpHapticFeedback } - + val hazeState = remember { HazeState() } val systemNavBarInset = sanitizeNavigationBarBottomInset( WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() ) @@ -703,8 +735,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, @@ -774,7 +812,8 @@ class MainActivity : ComponentActivity() { CompositionLocalProvider( LocalAppHapticsConfig provides appHapticsConfig, - LocalHapticFeedback provides scopedHapticFeedback + LocalHapticFeedback provides scopedHapticFeedback, + LocalHazeState provides hazeState ) { AppSidebarDrawer( drawerState = drawerState, @@ -798,7 +837,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 +878,7 @@ class MainActivity : ComponentActivity() { Box( modifier = Modifier .fillMaxWidth() + .widthIn(max = 540.dp) .height(navBarOccupiedHeight) .clipToBounds() ) { @@ -850,6 +890,7 @@ class MainActivity : ComponentActivity() { modifier = Modifier .align(Alignment.BottomCenter) .fillMaxWidth() + .widthIn(max = 540.dp) .padding(bottom = bottomBarPadding) .onSizeChanged { componentHeightPx = it.height } .graphicsLayer { @@ -857,38 +898,62 @@ class MainActivity : ComponentActivity() { // hide and the route-based hide as a pure translation, // so child items never resize or get clipped/squished. val expansionHide = if (showPlayerContentArea) { - playerViewModel.playerContentExpansionFraction.value.coerceIn(0f, 1f) + playerViewModel.playerContentExpansionFraction.value.coerceIn( + 0f, + 1f + ) } else { 0f } - val routeHide = (1f - navBarVisibilityProgressState.value).coerceIn(0f, 1f) + val routeHide = + (1f - navBarVisibilityProgressState.value).coerceIn( + 0f, + 1f + ) val hideFraction = maxOf(expansionHide, routeHide) - translationY = (componentHeightPx + shadowOverflowPx + bottomBarPaddingPx) * hideFraction + translationY = + (componentHeightPx + shadowOverflowPx + bottomBarPaddingPx) * hideFraction alpha = 1f } .height(navBarHeight) .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 fraction = + playerViewModel.playerContentExpansionFraction.value val safeFraction = fraction.coerceIn(0f, 1f) val topDp = when { navBarStyle == NavBarStyle.DEFAULT -> animatedDefaultTopCornerRadius.value - navBarStyle == NavBarStyle.FULL_WIDTH -> lerp(navBarCornerRadius.dp, 26.dp, safeFraction) + navBarStyle == NavBarStyle.FULL_WIDTH -> lerp( + navBarCornerRadius.dp, + 26.dp, + safeFraction + ) + showPlayerContentArea -> if (fraction < 0.2f) { - lerp(navBarCornerRadius.dp, 26.dp, (fraction / 0.2f).coerceIn(0f, 1f)) + lerp( + navBarCornerRadius.dp, + 26.dp, + (fraction / 0.2f).coerceIn(0f, 1f) + ) } else { 26.dp } + else -> navBarCornerRadius.dp } val bottomDp = when (navBarStyle) { NavBarStyle.FULL_WIDTH -> 0.dp else -> animatedNavBarCornerRadius.value } - shape = navBarShapeCache.get(this, topDp.toPx(), bottomDp.toPx(), useSmoothCorners) + shape = navBarShapeCache.get( + this, + topDp.toPx(), + bottomDp.toPx(), + useSmoothCorners + ) clip = true shadowElevation = navBarElevationPx }, @@ -902,20 +967,62 @@ class MainActivity : ComponentActivity() { compactMode = navBarCompactMode, bottomBarPadding = bottomBarPadding, onSearchIconDoubleTap = onSearchIconDoubleTap, - modifier = Modifier.fillMaxSize() + modifier = Modifier + .fillMaxSize() + .widthIn(max = 540.dp) + .hazeEffect( + state = LocalHazeState.current, + style = HazeMaterials.ultraThin() + ) ) } } } } ) { 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 +1032,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 @@ -948,17 +1078,23 @@ class MainActivity : ComponentActivity() { Box( modifier = Modifier .fillMaxSize() +// .hazeSource(hazeState) .graphicsLayer { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (disableBlurAllOver) { renderEffect = null } else { val expansion = expansionFractionProvider() - val fraction = (expansion * (1f - predictiveBackCollapseFraction)).coerceIn(0f, 1f) + val fraction = + (expansion * (1f - predictiveBackCollapseFraction)).coerceIn( + 0f, + 1f + ) // Quantize to 2px steps: rebuild the RenderEffect only // when the blur crosses a step, reuse the cached object // every other frame. - val quantizedBlurPx = (fraction * 120f / 2f).roundToInt() * 2f + val quantizedBlurPx = + (fraction * 120f / 2f).roundToInt() * 2f renderEffect = blurEffectCache.get(quantizedBlurPx) } } @@ -967,7 +1103,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 +1115,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 +1144,9 @@ class MainActivity : ComponentActivity() { hideMiniPlayer = shouldHideMiniPlayer, containerHeight = containerHeight, navController = navController, - isNavBarHidden = isNavBarEffectivelyHidden + isNavBarHidden = isNavBarEffectivelyHidden, + isNavRailHidden = useNavigationRail, + hazeState = LocalHazeState.current ) val dismissUndoBarSlice by remember { @@ -1028,7 +1166,7 @@ class MainActivity : ComponentActivity() { { playerViewModel.hideDismissUndoBar() } } - AnimatedVisibility( + androidx.compose.animation.AnimatedVisibility( visible = dismissUndoBarSlice.isVisible, enter = slideInVertically(initialOffsetY = { it }) + fadeIn(), exit = slideOutVertically(targetOffsetY = { -it }) + fadeOut(), @@ -1041,7 +1179,11 @@ class MainActivity : ComponentActivity() { modifier = Modifier .fillMaxWidth() .height(MiniPlayerHeight) - .padding(horizontal = 14.dp), + .padding(horizontal = 14.dp) + .hazeEffect( + state = LocalHazeState.current, + style = HazeMaterials.regular() + ), onUndo = onUndoDismissPlaylist, onClose = onCloseDismissUndoBar, durationMillis = dismissUndoBarSlice.durationMillis @@ -1061,6 +1203,7 @@ class MainActivity : ComponentActivity() { } } } + } } Trace.endSection() 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/media/AudioRmsSink.kt b/app/src/main/java/com/theveloper/pixelplay/data/media/AudioRmsSink.kt new file mode 100644 index 000000000..0e6dcdb5b --- /dev/null +++ b/app/src/main/java/com/theveloper/pixelplay/data/media/AudioRmsSink.kt @@ -0,0 +1,75 @@ +package com.theveloper.pixelplay.data.media + +import androidx.media3.common.C +import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.audio.TeeAudioProcessor +import java.nio.ByteBuffer +import kotlin.math.sqrt + +@UnstableApi +class AudioRmsSink( + private val onRmsChanged: (Float) -> Unit +) : TeeAudioProcessor.AudioBufferSink { + + // 给一个合理的初始底噪下限,防止刚开始静音时被无限放大 + private var maxRms = 1000f + private var currentEncoding = C.ENCODING_PCM_16BIT + + override fun flush(sampleRateHz: Int, channelCount: Int, encoding: Int) { + currentEncoding = encoding + maxRms = 1000f + onRmsChanged(0f) + } + + override fun handleBuffer(buffer: ByteBuffer) { + if (!buffer.hasRemaining()) return + + var sumSquares = 0.0 + var sampleCount = 0 + + when (currentEncoding) { + C.ENCODING_PCM_16BIT -> { + val shortBuffer = buffer.asShortBuffer() + sampleCount = shortBuffer.remaining() + if (sampleCount == 0) return + while (shortBuffer.hasRemaining()) { + val sample = shortBuffer.get().toDouble() + sumSquares += sample * sample + } + } + C.ENCODING_PCM_FLOAT -> { + val floatBuffer = buffer.asFloatBuffer() + sampleCount = floatBuffer.remaining() + if (sampleCount == 0) return + while (floatBuffer.hasRemaining()) { + val sample = floatBuffer.get().toDouble() + // Float 范围是 -1.0 到 1.0,乘以 32768 对齐到 16-bit 级别,保证计算口径统一 + val scaled = sample * 32768.0 + sumSquares += scaled * scaled + } + } + else -> return // 其他非常规编码直接忽略 + } + + if (sampleCount == 0) return + + val rms = sqrt(sumSquares / sampleCount).toFloat() + + // 【核心修复】不仅要记录最大值,还要让它缓慢衰减 + if (rms > maxRms) { + maxRms = rms + } else { + // 每次缓冲平滑衰减,使其能适应接下来的低潮片段 + maxRms *= 0.995f + } + + // 钳制最低基准,防止将纯静音里的微弱底噪放大成强烈的跳动 + maxRms = maxRms.coerceAtLeast(1000f) + + // 归一化,并加入一个极小的死区(低于 5% 视作静音停止跳动) + var normalizedRms = (rms / maxRms).coerceIn(0f, 1f) + if (normalizedRms < 0.05f) normalizedRms = 0f + + onRmsChanged(normalizedRms) + } +} \ No newline at end of file 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/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/UserPreferencesRepository.kt b/app/src/main/java/com/theveloper/pixelplay/data/preferences/UserPreferencesRepository.kt index 4598f1a86..ba9a61a0c 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") @@ -221,6 +222,7 @@ class UserPreferencesRepository @Inject constructor( longPreferencesKey("advanced_performance_diagnostics_expires_at_epoch_ms") val IMMERSIVE_LYRICS_ENABLED = booleanPreferencesKey("immersive_lyrics_enabled") val IMMERSIVE_LYRICS_TIMEOUT = longPreferencesKey("immersive_lyrics_timeout") + val CONTROLS_BUTTONS_ENABLED = booleanPreferencesKey("controls_button_enabled") val USE_ANIMATED_LYRICS = booleanPreferencesKey("use_animated_lyrics") val ANIMATED_LYRICS_BLUR_ENABLED = booleanPreferencesKey("animated_lyrics_blur_enabled") val ANIMATED_LYRICS_BLUR_STRENGTH = androidx.datastore.preferences.core.floatPreferencesKey("animated_lyrics_blur_strength") @@ -887,8 +889,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() { @@ -1113,6 +1125,13 @@ suspend fun markDirectoryRulesVersionApplied(version: Int) { dataStore.edit { it[PreferencesKeys.IMMERSIVE_LYRICS_TIMEOUT] = timeout } } + val controlsButtonEnabledFlow: Flow = + pref { it[PreferencesKeys.CONTROLS_BUTTONS_ENABLED] ?: true } + + suspend fun setControlsButtonEnabled(enabled: Boolean) { + dataStore.edit { it[PreferencesKeys.CONTROLS_BUTTONS_ENABLED] = enabled } + } + val useAnimatedLyricsFlow: Flow = pref { it[PreferencesKeys.USE_ANIMATED_LYRICS] ?: false } 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/MusicService.kt b/app/src/main/java/com/theveloper/pixelplay/data/service/MusicService.kt index adab461ca..2324d629c 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/service/MusicService.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/service/MusicService.kt @@ -97,6 +97,7 @@ import coil.size.Precision import javax.inject.Inject import androidx.core.net.toUri +import com.theveloper.pixelplay.presentation.viewmodel.PlaybackStateHolder // Acciones personalizadas para compatibilidad con el widget existente @@ -144,6 +145,8 @@ class MusicService : MediaLibraryService() { @Inject lateinit var engine: DualPlayerEngine @Inject + lateinit var playbackStateHolder: PlaybackStateHolder + @Inject lateinit var controller: TransitionController @Inject lateinit var musicRepository: MusicRepository @@ -437,6 +440,10 @@ class MusicService : MediaLibraryService() { registerSystemVolumeObserver() // Handle player swaps (crossfade) to keep MediaSession in sync + engine.setOnAmplitudeUpdateListener { amplitude -> + playbackStateHolder.updateAudioAmplitude(amplitude) + } + engine.setOnPlayerAboutToBeReleasedListener { oldPlayer -> oldPlayer.removeListener(playerListener) } diff --git a/app/src/main/java/com/theveloper/pixelplay/data/service/player/DualPlayerEngine.kt b/app/src/main/java/com/theveloper/pixelplay/data/service/player/DualPlayerEngine.kt index c8f05dc0a..e2e9d3ec7 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/service/player/DualPlayerEngine.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/service/player/DualPlayerEngine.kt @@ -62,7 +62,9 @@ import com.theveloper.pixelplay.data.netease.NeteaseStreamProxy import com.theveloper.pixelplay.data.navidrome.NavidromeStreamProxy import com.theveloper.pixelplay.data.qqmusic.QqMusicStreamProxy import androidx.core.net.toUri +import androidx.media3.exoplayer.audio.TeeAudioProcessor import com.theveloper.pixelplay.data.diagnostics.AdvancedPerformanceDiagnostics +import com.theveloper.pixelplay.data.media.AudioRmsSink data class ActiveDecoderInfo( val name: String, @@ -267,6 +269,12 @@ class DualPlayerEngine @Inject constructor( private val onTransitionDisplayPlayerListeners = mutableListOf<(Player) -> Unit>() private val onTransitionFinishedListeners = mutableListOf<() -> Unit>() + private var onAmplitudeUpdateListener: ((Float) -> Unit)? = null + + fun setOnAmplitudeUpdateListener(listener: ((Float) -> Unit)?) { + onAmplitudeUpdateListener = listener + } + private var onPlayerAboutToBeReleasedListener: ((Player) -> Unit)? = null fun setOnPlayerAboutToBeReleasedListener(listener: (Player) -> Unit) { @@ -348,6 +356,11 @@ class DualPlayerEngine @Inject constructor( */ var incomingTrackReplayGainVolume: Float? = null + val rmsSink = AudioRmsSink { amplitude -> + // amplitude 是 0.0f 到 1.0f 的值 + onAmplitudeUpdateListener?.invoke(amplitude) + } + private val focusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange -> when (focusChange) { AudioManager.AUDIOFOCUS_LOSS -> { @@ -1029,6 +1042,7 @@ class DualPlayerEngine @Inject constructor( } private fun buildPlayer(): ExoPlayer { + val teeAudioProcessor = TeeAudioProcessor(rmsSink) val mediaCodecSelector = MediaCodecSelector { mimeType, requiresSecureDecoder, requiresTunnelingDecoder -> val decoderInfos = MediaCodecSelector.DEFAULT.getDecoderInfos( mimeType, @@ -1049,6 +1063,7 @@ class DualPlayerEngine @Inject constructor( .setEnableAudioOutputPlaybackParameters(enableAudioOutputPlaybackParams) .setAudioProcessorChain( DefaultAudioSink.DefaultAudioProcessorChain( + teeAudioProcessor, HiResSampleRateCapAudioProcessor(), SurroundDownmixProcessor() ) 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/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 98eaa91b3..ef27e0acd 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 @@ -26,6 +26,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -350,6 +351,7 @@ private fun DailyMixSongList( onMoreOptionsClick = onMoreOptionsClick, customShape = RoundedCornerShape(10.dp), showAlbumArt = false, + stablePlayerStateFlow = playerViewModel.stablePlayerState, onClick = { playerViewModel.showAndPlaySong( song = song, 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/GradientTopBar.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/GradientTopBar.kt index 8d8ef3597..992fbaf99 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/GradientTopBar.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/GradientTopBar.kt @@ -41,6 +41,9 @@ import com.theveloper.pixelplay.R import com.theveloper.pixelplay.ui.theme.GoogleSansRounded import com.theveloper.pixelplay.ui.theme.PixelPlayStatusBarStyle import androidx.compose.ui.res.stringResource +import com.theveloper.pixelplay.MainActivity.Companion.LocalHazeState +import dev.chrisbanes.haze.hazeEffect +import dev.chrisbanes.haze.materials.HazeMaterials @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -116,7 +119,12 @@ fun HomeGradientTopBar( ) TopAppBar( - modifier = Modifier.background(surfaceContainerHigh.copy(alpha = animatedAlpha)), + modifier = Modifier + .background(surfaceContainerHigh.copy(alpha = animatedAlpha * 0.4f)) + .hazeEffect( + state = LocalHazeState.current, + style = HazeMaterials.regular() // 可以根据喜好换成 thin() 或 ultraThin() + ), title = { /* nada, usamos solo acciones */ }, navigationIcon = { Row( diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/LyricsSheet.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/LyricsSheet.kt index 9787826e8..afccb057e 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/LyricsSheet.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/LyricsSheet.kt @@ -136,8 +136,12 @@ import androidx.compose.material3.FilledTonalIconButton import androidx.compose.material3.HorizontalDivider import androidx.compose.ui.text.style.TextGeometricTransform import androidx.compose.ui.text.style.TextOverflow -import com.theveloper.pixelplay.presentation.components.subcomps.PlayingEqIcon +import com.theveloper.pixelplay.MainActivity.Companion.LocalHazeState +import com.theveloper.pixelplay.presentation.components.subcomps.PlayingEqIconV2 import com.theveloper.pixelplay.utils.MultiLangRomanizer +import dev.chrisbanes.haze.hazeEffect +import dev.chrisbanes.haze.hazeSource +import dev.chrisbanes.haze.materials.HazeMaterials internal data class LyricsSheetColors( val container: Color, @@ -244,6 +248,7 @@ fun LyricsSheet( immersiveLyricsTimeout: Long, isImmersiveTemporarilyDisabled: Boolean, onSetImmersiveTemporarilyDisabled: (Boolean) -> Unit, + controlsButtonEnabled: Boolean, onSaveLyricsToFile: (Song, Lyrics, Boolean) -> Unit, onTranslateViaAi: () -> Unit, // BottomToggleRow Params @@ -453,6 +458,7 @@ fun LyricsSheet( // Immersive Mode State var immersiveMode by remember { mutableStateOf(false) } var lastInteractionTime by remember { mutableLongStateOf(System.currentTimeMillis()) } + var controlsButtonVisible by remember { mutableStateOf(true) } var showMoreSheet by remember { mutableStateOf(false) } val moreSheetState = rememberModalBottomSheetState( skipPartiallyExpanded = true @@ -487,12 +493,22 @@ fun LyricsSheet( } // Auto-hide controls logic - LaunchedEffect(immersiveLyricsEnabled, lastInteractionTime, showSyncedLyrics, isImmersiveTemporarilyDisabled) { - if (immersiveLyricsEnabled && showSyncedLyrics == true && !isImmersiveTemporarilyDisabled) { - delay(immersiveLyricsTimeout) - immersiveMode = true + LaunchedEffect( + immersiveLyricsEnabled, + lastInteractionTime, + controlsButtonEnabled, + showSyncedLyrics, + isImmersiveTemporarilyDisabled + ) { + if (controlsButtonEnabled) { + if (immersiveLyricsEnabled && showSyncedLyrics == true && !isImmersiveTemporarilyDisabled) { + delay(immersiveLyricsTimeout) + immersiveMode = true + } else { + immersiveMode = false + } } else { - immersiveMode = false + immersiveMode = true } } @@ -636,16 +652,16 @@ fun LyricsSheet( }, onDragEnd = { isSwipeActive = false - val committed = abs(dragOffset) > swipeThresholdPx && !hasTriggeredAction - + val committed = abs(dragOffset) > swipeThresholdPx && !hasTriggeredAction + if (committed) { if (dragOffset > 0) onPrev() else onNext() hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) } coroutineScope.launch { - swipeProgress.animateTo(0f, tween(200)) - dragOffset = 0f + swipeProgress.animateTo(0f, tween(200)) + dragOffset = 0f } }, onDragCancel = { @@ -658,11 +674,11 @@ fun LyricsSheet( onDrag = { change, dragAmount -> change.consume() resetImmersiveTimer() - + if (!hasTriggeredAction) { dragOffset += dragAmount.x val progress = (abs(dragOffset) / swipeThresholdPx).coerceIn(0f, 1f) - + coroutineScope.launch { swipeProgress.snapTo(progress) } @@ -670,7 +686,7 @@ fun LyricsSheet( } ) }, - containerColor = containerColor, +// containerColor = containerColor, contentColor = contentColor, // Removed TopBar and FAB ) { paddingValues -> @@ -678,13 +694,13 @@ fun LyricsSheet( Column( modifier = Modifier .fillMaxSize() - .padding(top = paddingValues.calculateTopPadding()) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = null - ) { - resetImmersiveTimer() - } +// .padding(top = paddingValues.calculateTopPadding()) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { + resetImmersiveTimer() + } ) { val initialSyncedLineIndex = remember(lyrics, playbackPositionFlow, lyricsSyncOffset) { resolveCurrentLineIndex( @@ -708,14 +724,15 @@ fun LyricsSheet( AnimatedContent( targetState = currentSong, transitionSpec = { - (fadeIn(animationSpec = tween(300)) + + (fadeIn(animationSpec = tween(300)) + scaleIn(initialScale = 0.9f, animationSpec = tween(300))) .togetherWith(fadeOut(animationSpec = tween(300))) }, modifier = Modifier .align(Alignment.TopStart) .zIndex(2f) - .wrapContentWidth(), + .wrapContentWidth() + .padding(top = paddingValues.calculateTopPadding()), label = "headerAnimation" ) { song -> LyricsTrackInfo( @@ -725,132 +742,140 @@ fun LyricsSheet( .padding( top = 4.dp, bottom = 24.dp, start = 18.dp, end = 18.dp ) + .clip(CircleShape) .background( - color = backgroundColor, - shape = CircleShape + color = backgroundColor.copy(0.6f), ) .wrapContentWidth() .animateContentSize(), // Animate width changes - backgroundColor = backgroundColor, // Distinct solid background - contentColor = onBackgroundColor, - isPlaying = isPlaying + backgroundColor = backgroundColor.copy(0.7f), // Distinct solid background + containerColor = containerColor, + contentColor = contentColor, + isPlaying = isPlaying, + stablePlayerStateFlow = stablePlayerStateFlow ) } - when (showSyncedLyrics) { - null -> { - LazyColumn( - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(top = 110.dp, bottom = 24.dp, start = 24.dp, end = 24.dp) - ) { - item(key = "loader_or_empty") { - Box( - modifier = Modifier - .fillParentMaxSize(), - contentAlignment = Alignment.Center - ) { - if (isLoadingLyrics) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text( - text = stringResource(R.string.lyrics_loading), - style = MaterialTheme.typography.titleMedium - ) - Spacer(modifier = Modifier.height(8.dp)) - LinearWavyProgressIndicator( - trackColor = accentColor.copy(alpha = 0.4f), - color = accentColor, - modifier = Modifier.width(100.dp) - ) + Box(modifier = Modifier + .fillMaxSize() + .background(containerColor) + ) { + when (showSyncedLyrics) { + null -> { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(top = 110.dp, bottom = 24.dp, start = 24.dp, end = 24.dp) + ) { + item(key = "loader_or_empty") { + Box( + modifier = Modifier + .fillParentMaxSize(), + contentAlignment = Alignment.Center + ) { + if (isLoadingLyrics) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text( + text = stringResource(R.string.lyrics_loading), + style = MaterialTheme.typography.titleMedium + ) + Spacer(modifier = Modifier.height(8.dp)) + LinearWavyProgressIndicator( + trackColor = accentColor.copy(alpha = 0.4f), + color = accentColor, + modifier = Modifier.width(100.dp) + ) + } } } } } } - } - true -> { - lyrics?.synced?.let { synced -> - SyncedLyricsList( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 24.dp), - contentPadding = PaddingValues(top = 130.dp, bottom = 100.dp), - lines = synced, - listState = syncedListState, - playbackPositionFlow = playbackPositionFlow, - lyricsSyncOffset = lyricsSyncOffset, - positionOverrideMs = previewSeekPositionMs, - accentColor = lyricHighlightColor, - textStyle = scaledTextStyle, - onLineClick = { syncedLine -> - onSeekTo( - resolveSeekPositionMs( - lineTimeMs = syncedLine.time.toLong(), - lyricsSyncOffsetMs = lyricsSyncOffset - ) - ) - resetImmersiveTimer() - }, - highlightZoneFraction = highlightZoneFraction, - highlightOffsetDp = highlightOffsetDp, - autoscrollAnimationSpec = resolvedAutoscrollSpec, - useAnimatedLyrics = useAnimatedLyrics, - animatedLyricsBlurEnabled = animatedLyricsBlurEnabled && !disableBlurAllOver, - animatedLyricsBlurStrength = animatedLyricsBlurStrength, - immersiveMode = immersiveMode, - lyricsAlignment = lyricsAlignment, - showTranslation = showLyricsTranslation, - showRomanization = showLyricsRomanization, - footer = { - if (lyrics?.areFromRemote == true) { - item(key = "provider_text") { - ProviderText( - providerText = stringResource(R.string.lyrics_provided_by), - uri = stringResource(R.string.lyrics_lrclib_uri), - textAlign = TextAlign.Center, - accentColor = lyricHighlightColor, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp) + true -> { + lyrics?.synced?.let { synced -> + SyncedLyricsList( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 24.dp), + contentPadding = PaddingValues(top = 130.dp, bottom = 100.dp), + lines = synced, + listState = syncedListState, + playbackPositionFlow = playbackPositionFlow, + lyricsSyncOffset = lyricsSyncOffset, + positionOverrideMs = previewSeekPositionMs, + accentColor = lyricHighlightColor, + textStyle = scaledTextStyle, + onLineClick = { syncedLine -> + onSeekTo( + resolveSeekPositionMs( + lineTimeMs = syncedLine.time.toLong(), + lyricsSyncOffsetMs = lyricsSyncOffset ) + ) + resetImmersiveTimer() + }, + highlightZoneFraction = highlightZoneFraction, + highlightOffsetDp = highlightOffsetDp, + autoscrollAnimationSpec = resolvedAutoscrollSpec, + useAnimatedLyrics = useAnimatedLyrics, + animatedLyricsBlurEnabled = animatedLyricsBlurEnabled && !disableBlurAllOver, + animatedLyricsBlurStrength = animatedLyricsBlurStrength, + immersiveMode = immersiveMode, + lyricsAlignment = lyricsAlignment, + showTranslation = showLyricsTranslation, + showRomanization = showLyricsRomanization, + footer = { + if (lyrics?.areFromRemote == true) { + item(key = "provider_text") { + ProviderText( + providerText = stringResource(R.string.lyrics_provided_by), + uri = stringResource(R.string.lyrics_lrclib_uri), + textAlign = TextAlign.Center, + accentColor = lyricHighlightColor, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp) + ) + } } } - } - ) + ) + } } - } - false -> { - lyrics?.plain?.let { plain -> - LazyColumn( - modifier = Modifier.fillMaxSize(), - state = staticListState, - contentPadding = PaddingValues( - start = 24.dp, - end = 24.dp, - top = 130.dp, - bottom = 24.dp - ) - ) { - itemsIndexed( - items = plain, - key = { index, line -> "$index-$line" } - ) { _, line -> - PlainLyricsLine( - line = line, - style = lyricsTextStyle, - lyricsAlignment = lyricsAlignment, - showTranslation = if (hasTranslatedLyrics) showLyricsTranslation else true, - showRomanization = if (hasRomanizedLyrics) showLyricsRomanization else true, - modifier = Modifier.fillMaxWidth() + false -> { + lyrics?.plain?.let { plain -> + LazyColumn( + modifier = Modifier.fillMaxSize(), + state = staticListState, + contentPadding = PaddingValues( + start = 24.dp, + end = 24.dp, + top = 130.dp, + bottom = 24.dp ) - Spacer(modifier = Modifier.height(16.dp)) + ) { + itemsIndexed( + items = plain, + key = { index, line -> "$index-$line" } + ) { _, line -> + PlainLyricsLine( + line = line, + style = lyricsTextStyle, + lyricsAlignment = lyricsAlignment, + showTranslation = if (hasTranslatedLyrics) showLyricsTranslation else true, + showRomanization = if (hasRomanizedLyrics) showLyricsRomanization else true, + modifier = Modifier.fillMaxWidth() + ) + Spacer(modifier = Modifier.height(16.dp)) + } } } } } } - + + // Top Gradient for fade Box( modifier = Modifier @@ -880,7 +905,7 @@ fun LyricsSheet( // Controls Section (Auto-hide in immersive mode) AnimatedVisibility( - visible = !immersiveMode, + visible = if (controlsButtonEnabled) { !immersiveMode } else false, enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(), exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut() ) { @@ -888,36 +913,40 @@ fun LyricsSheet( modifier = Modifier .fillMaxWidth() .background(containerColor) - .padding(bottom = paddingValues.calculateBottomPadding() + 10.dp, end = 16.dp, start = 16.dp) + .padding( + bottom = paddingValues.calculateBottomPadding() + 10.dp, + end = 16.dp, + start = 16.dp + ) .pointerInput(Unit) { awaitPointerEventScope { while (true) { val event = awaitPointerEvent() // Reset timer on any touch down or move in this area if (event.changes.any { it.pressed }) { - resetImmersiveTimer() + resetImmersiveTimer() } } } } ) { - AnimatedVisibility( - visible = showSyncedLyrics == true && lyrics?.synced != null && showSyncControls, - enter = expandVertically() + fadeIn(), - exit = shrinkVertically() + fadeOut() - ) { - LyricsSyncControls( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 8.dp), - offsetMillis = lyricsSyncOffset, - onOffsetChange = onLyricsSyncOffsetChange, - backgroundColor = backgroundColor, - accentColor = sheetColors.syncButtonContainer, - onAccentColor = sheetColors.syncButtonContent, - onBackgroundColor = onBackgroundColor - ) - } + AnimatedVisibility( + visible = showSyncedLyrics == true && lyrics?.synced != null && showSyncControls, + enter = expandVertically() + fadeIn(), + exit = shrinkVertically() + fadeOut() + ) { + LyricsSyncControls( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + offsetMillis = lyricsSyncOffset, + onOffsetChange = onLyricsSyncOffsetChange, + backgroundColor = backgroundColor, + accentColor = sheetColors.syncButtonContainer, + onAccentColor = sheetColors.syncButtonContent, + onBackgroundColor = onBackgroundColor + ) + } // Playback Controls Row Row( @@ -1071,6 +1100,7 @@ fun LyricsSheet( } }, immersiveLyricsEnabled = immersiveLyricsEnabled, + controlsButtonEnabled = controlsButtonVisible, isShuffleEnabled = isShuffleEnabled, repeatMode = repeatMode, isFavoriteProvider = isFavoriteProvider, @@ -1092,7 +1122,7 @@ fun LyricsSheet( // Show Controls Button (Overlay) AnimatedVisibility( - visible = immersiveMode, + visible = if (controlsButtonEnabled) { immersiveMode } else false, enter = fadeIn() + slideInVertically { it / 2 }, exit = fadeOut() + slideOutVertically { it / 2 }, modifier = Modifier @@ -1125,25 +1155,25 @@ fun LyricsSheet( .align(overlayAlignment) .size(100.dp) // Base size .padding( - start = if(isNext) 0.dp else 6.dp, - end = if(isNext) 6.dp else 0.dp + start = if (isNext) 0.dp else 6.dp, + end = if (isNext) 6.dp else 0.dp ) .graphicsLayer { - val widthPx = size.width - val initialOffset = if(isNext) widthPx else -widthPx - translationX = initialOffset * (1f - swipeProgress.value) + val widthPx = size.width + val initialOffset = if (isNext) widthPx else -widthPx + translationX = initialOffset * (1f - swipeProgress.value) - scaleX = 0.8f + (swipeProgress.value * 0.2f) - scaleY = 0.8f + (swipeProgress.value * 0.2f) + scaleX = 0.8f + (swipeProgress.value * 0.2f) + scaleY = 0.8f + (swipeProgress.value * 0.2f) } .background( - color = accentColor, // No alpha modulation - shape = RoundedCornerShape( - topStart = if(isNext) 360.dp else 8.dp, - bottomStart = if(isNext) 360.dp else 8.dp, - topEnd = if(isNext) 8.dp else 360.dp, - bottomEnd = if(isNext) 8.dp else 360.dp - ) + color = accentColor, // No alpha modulation + shape = RoundedCornerShape( + topStart = if (isNext) 360.dp else 8.dp, + bottomStart = if (isNext) 360.dp else 8.dp, + topEnd = if (isNext) 8.dp else 360.dp, + bottomEnd = if (isNext) 8.dp else 360.dp + ) ), contentAlignment = Alignment.Center ) { @@ -1987,8 +2017,10 @@ private fun LyricsTrackInfo( song: Song?, modifier: Modifier = Modifier, backgroundColor: Color, + containerColor: Color, contentColor: Color, - isPlaying: Boolean + isPlaying: Boolean, + stablePlayerStateFlow: StateFlow ) { if (song == null) return @@ -2073,12 +2105,13 @@ private fun LyricsTrackInfo( ) } - PlayingEqIcon( + PlayingEqIconV2( modifier = Modifier .padding(start = 8.dp, end = 18.dp) .size(width = 18.dp, height = 16.dp), - color = contentColor, - isPlaying = isPlaying + color = containerColor, + isPlaying = isPlaying, + stablePlayerStateFlow = stablePlayerStateFlow ) } } 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/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/QueueBottomSheet.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/QueueBottomSheet.kt index a3f564732..514a0ca93 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/QueueBottomSheet.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/QueueBottomSheet.kt @@ -143,7 +143,6 @@ import com.theveloper.pixelplay.R import com.theveloper.pixelplay.data.model.Song import com.theveloper.pixelplay.presentation.components.AutoScrollingText import com.theveloper.pixelplay.presentation.components.SmartImage -import com.theveloper.pixelplay.presentation.components.subcomps.PlayingEqIcon import com.theveloper.pixelplay.presentation.components.player.AnimatedPlaybackControls import com.theveloper.pixelplay.presentation.viewmodel.PlayerUiState import com.theveloper.pixelplay.presentation.viewmodel.PlayerViewModel @@ -192,6 +191,12 @@ import kotlinx.coroutines.flow.map import java.util.RandomAccess import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource +import com.theveloper.pixelplay.presentation.components.subcomps.PlayingEqIconV2 +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.HazeStyle +import dev.chrisbanes.haze.hazeEffect +import dev.chrisbanes.haze.hazeSource +import dev.chrisbanes.haze.materials.HazeMaterials import kotlin.math.abs private data class QueueUndoBarProjection( @@ -254,6 +259,7 @@ fun QueueBottomSheet( modifier: Modifier = Modifier, tonalElevation: Dp = 10.dp, shape: RoundedCornerShape = RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp), + hazeState: HazeState ) { val colors = MaterialTheme.colorScheme var showTimerOptions by rememberSaveable { mutableStateOf(false) } @@ -758,7 +764,7 @@ fun QueueBottomSheet( Box( modifier = Modifier.fillMaxSize() ) { - Column { + Column(modifier = Modifier.hazeSource(hazeState)) { val headerTopPadding = WindowInsets.statusBars .asPaddingValues() .calculateTopPadding() + 10.dp @@ -892,6 +898,7 @@ fun QueueBottomSheet( swipeStateIdentity = itemStableKey, onDismissSong = { onRemoveSong(song.id) }, isFromPlaylist = true, + playerViewModel = viewModel, onMoreOptionsClick = { onSongInfoClick(song) }, dragHandle = { IconButton( @@ -937,7 +944,8 @@ fun QueueBottomSheet( .padding( top = 24.dp, end = 14.dp, - bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 14.dp + bottom = WindowInsets.navigationBars.asPaddingValues() + .calculateBottomPadding() + 14.dp ) ) } @@ -954,6 +962,19 @@ fun QueueBottomSheet( label = "fabRotation" ) + val fabShape = remember { + AbsoluteSmoothCornerShape( + cornerRadiusTR = 50.dp, + smoothnessAsPercentTR = 60, + cornerRadiusTL = 8.dp, + smoothnessAsPercentTL = 60, + cornerRadiusBR = 50.dp, + smoothnessAsPercentBR = 60, + cornerRadiusBL = 8.dp, + smoothnessAsPercentBL = 60 + ) + } + val navigationBarHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() Row( @@ -974,6 +995,7 @@ fun QueueBottomSheet( isTimerActive = isTimerActiveDerived, onToggleShuffle = onToggleShuffle, onToggleRepeat = onToggleRepeat, + hazeState = hazeState, onTimerClick = { showTimerOptions = true } ) @@ -1409,30 +1431,36 @@ private fun QueueControlsToolbar( onToggleShuffle: () -> Unit, onToggleRepeat: () -> Unit, onTimerClick: () -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + hazeState: HazeState ) { val activeColors = IconButtonDefaults.filledIconButtonColors( - containerColor = MaterialTheme.colorScheme.primary, + containerColor = MaterialTheme.colorScheme.primary.copy(0.4f), contentColor = MaterialTheme.colorScheme.onPrimary ) val inactiveColors = IconButtonDefaults.filledTonalIconButtonColors( - containerColor = MaterialTheme.colorScheme.surfaceContainer, + containerColor = MaterialTheme.colorScheme.surfaceContainer.copy(0.4f), contentColor = MaterialTheme.colorScheme.onSurfaceVariant ) + val shape = AbsoluteSmoothCornerShape( + cornerRadiusTR = 8.dp, + smoothnessAsPercentTR = 60, + cornerRadiusTL = 50.dp, + smoothnessAsPercentTL = 60, + cornerRadiusBR = 8.dp, + smoothnessAsPercentBR = 60, + cornerRadiusBL = 50.dp, + smoothnessAsPercentBL = 60 + ) + Surface( - modifier = modifier.fillMaxHeight(), - shape = AbsoluteSmoothCornerShape( - cornerRadiusTR = 8.dp, - smoothnessAsPercentTR = 60, - cornerRadiusTL = 50.dp, - smoothnessAsPercentTL = 60, - cornerRadiusBR = 8.dp, - smoothnessAsPercentBR = 60, - cornerRadiusBL = 50.dp, - smoothnessAsPercentBL = 60 - ), - color = MaterialTheme.colorScheme.surfaceContainerHighest, + modifier = modifier + .fillMaxHeight() + .clip(shape) + .hazeEffect(state = hazeState, HazeMaterials.ultraThin()), + shape = shape, + color = MaterialTheme.colorScheme.surfaceContainerHighest.copy(0.4f), shadowElevation = 0.dp ) { Row( @@ -1876,6 +1904,7 @@ fun SaveQueueAsPlaylistSheet( } } +@androidx.annotation.OptIn(UnstableApi::class) @Composable fun QueuePlaylistSongItem( modifier: Modifier = Modifier, @@ -1893,7 +1922,8 @@ fun QueuePlaylistSongItem( enableSwipeToDismiss: Boolean = false, swipeStateIdentity: Long = 0L, onDismissSong: () -> Unit = {}, - isFromPlaylist: Boolean + isFromPlaylist: Boolean, + playerViewModel: PlayerViewModel ) { val colors = MaterialTheme.colorScheme @@ -2085,12 +2115,13 @@ fun QueuePlaylistSongItem( if (isCurrentSong) { if (isPlaying != null) { - PlayingEqIcon( + PlayingEqIconV2( modifier = Modifier .padding(start = 8.dp) .size(width = 18.dp, height = 16.dp), color = colors.secondary, - isPlaying = isPlaying + isPlaying = isPlaying, + stablePlayerStateFlow = playerViewModel.stablePlayerState ) Spacer(Modifier.width(4.dp)) if (!isRemoveButtonVisible) { 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/UnifiedPlayerOverlaysLayer.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerOverlaysLayer.kt index 2cb218cce..7ebfee22b 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 @@ -40,6 +40,7 @@ import com.theveloper.pixelplay.data.model.Song import com.theveloper.pixelplay.presentation.viewmodel.PlayerViewModel import com.theveloper.pixelplay.presentation.viewmodel.PlaylistViewModel import com.theveloper.pixelplay.presentation.viewmodel.StablePlayerState +import dev.chrisbanes.haze.HazeState import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.map import kotlin.math.roundToInt @@ -90,7 +91,8 @@ internal fun UnifiedPlayerQueueLayer( onQueueDrag: (Float) -> Unit, onQueueRelease: (Float, Float) -> Unit, queuePredictiveBackProgress: Animatable, - queuePredictiveBackSwipeEdge: State + queuePredictiveBackSwipeEdge: State, + hazeState: HazeState ) { if (!shouldRenderLayer) return @@ -167,7 +169,8 @@ internal fun UnifiedPlayerQueueLayer( onQueueRelease = onQueueRelease, predictiveBackProgress = queuePredictiveBackProgress, predictiveBackSwipeEdge = queuePredictiveBackSwipeEdge, - queueSheetOffset = queueSheetOffset + queueSheetOffset = queueSheetOffset, + hazeState = hazeState ) } } @@ -304,7 +307,8 @@ internal fun UnifiedPlayerQueueAndSongInfoHost( onNavigateToArtist: (Song) -> Unit, onNavigateToGenre: (Song) -> Unit, queuePredictiveBackProgress: Animatable, - queuePredictiveBackSwipeEdge: State + queuePredictiveBackSwipeEdge: State, + hazeState: HazeState ) { if (!shouldRenderHost) return @@ -437,7 +441,8 @@ internal fun UnifiedPlayerQueueAndSongInfoHost( onQueueDrag = onQueueDrag, onQueueRelease = onQueueRelease, queuePredictiveBackProgress = queuePredictiveBackProgress, - queuePredictiveBackSwipeEdge = queuePredictiveBackSwipeEdge + queuePredictiveBackSwipeEdge = queuePredictiveBackSwipeEdge, + hazeState = hazeState ) UnifiedPlayerSongInfoLayer( 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..eb211dd56 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 @@ -21,6 +22,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.layout import androidx.compose.ui.unit.Dp @@ -39,6 +41,9 @@ import com.theveloper.pixelplay.presentation.components.scoped.rememberFullPlaye import com.theveloper.pixelplay.presentation.viewmodel.PlayerSheetState import com.theveloper.pixelplay.presentation.viewmodel.PlayerViewModel import com.theveloper.pixelplay.presentation.viewmodel.StablePlayerState +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.hazeEffect +import dev.chrisbanes.haze.materials.HazeMaterials @OptIn(UnstableApi::class) @Composable @@ -54,6 +59,7 @@ internal fun BoxScope.UnifiedPlayerMiniAndFullLayers( bottomSheetOpenFraction: Float, fullPlayerVisualState: FullPlayerVisualState, containerHeight: Dp, + containerColor: Color, currentQueueSourceName: String, currentSheetContentState: PlayerSheetState, carouselStyle: String, @@ -63,13 +69,15 @@ internal fun BoxScope.UnifiedPlayerMiniAndFullLayers( currentPositionProvider: () -> Long, isFavorite: Boolean, shouldRenderFullPlayer: Boolean = true, + hazeState: HazeState, currentHorizontalPaddingStartPxProvider: () -> Float, currentHorizontalPaddingEndPxProvider: () -> Float, onShowQueueClicked: () -> Unit, onQueueDragStart: () -> Unit, onQueueDrag: (Float) -> Unit, onQueueRelease: (Float, Float) -> Unit, - onShowCastClicked: () -> Unit + onShowCastClicked: () -> Unit, + isNavRailHidden: Boolean ) { currentSong?.let { currentSongNonNull -> miniPlayerScheme?.let { readyScheme -> @@ -116,7 +124,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 +135,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 + ).hazeEffect(state = hazeState, HazeMaterials.ultraThin(containerColor = containerColor)) ) } } 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..03ceee683 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 @@ -36,6 +37,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue +import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clipToBounds @@ -88,6 +90,10 @@ import com.theveloper.pixelplay.presentation.viewmodel.PlayerSheetState import com.theveloper.pixelplay.presentation.viewmodel.PlayerViewModel import com.theveloper.pixelplay.presentation.viewmodel.StablePlayerState import com.theveloper.pixelplay.ui.theme.LocalPixelPlayDarkTheme +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.LocalHazeStyle +import dev.chrisbanes.haze.hazeEffect +import dev.chrisbanes.haze.materials.HazeMaterials import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map @@ -115,13 +121,15 @@ fun UnifiedPlayerSheetV2( collapsedStateHorizontalPadding: Dp = 12.dp, navController: NavHostController, hideMiniPlayer: Boolean = false, - isNavBarHidden: Boolean = false + isNavBarHidden: Boolean = false, + isNavRailHidden: Boolean = false, + hazeState: HazeState + ) { val context = LocalContext.current val lifecycleOwner = LocalLifecycleOwner.current val latestContext by rememberUpdatedState(context) var showNoInternetDialog by remember { mutableStateOf(false) } - // MediaStore write-permission launcher (for metadata editing without MANAGE_EXTERNAL_STORAGE) val writePermissionLauncher = androidx.activity.compose.rememberLauncherForActivityResult( androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult() @@ -620,13 +628,15 @@ fun UnifiedPlayerSheetV2( Surface( modifier = Modifier .fillMaxWidth() + .widthIn(max = 450.dp) .layout { measurable, constraints -> val translationY = visualSheetTranslationYProvider().roundToInt() - val overshoot = if (currentSheetContentState == PlayerSheetState.EXPANDED && !isDragging) { - -translationY - } else { - if (translationY < 0) -translationY else 0 - } + val overshoot = + if (currentSheetContentState == PlayerSheetState.EXPANDED && !isDragging) { + -translationY + } else { + if (translationY < 0) -translationY else 0 + } val targetHeight = constraints.maxHeight + overshoot val placeable = measurable.measure( constraints.copy( @@ -669,9 +679,10 @@ fun UnifiedPlayerSheetV2( .toInt().coerceAtLeast(0) val endPaddingPx = currentHorizontalPaddingEndPxProvider() .toInt().coerceAtLeast(0) - val innerWidth = (constraints.maxWidth - startPaddingPx - endPaddingPx) - .coerceAtLeast(0) - + val innerWidth = + (constraints.maxWidth - startPaddingPx - endPaddingPx) + .coerceAtLeast(0) + val placeable = measurable.measure( constraints.copy( minWidth = innerWidth, @@ -694,11 +705,19 @@ fun UnifiedPlayerSheetV2( shape = sheetInteractionState.playerShadowShape, clip = false ) + .clip(sheetInteractionState.playerShadowShape) + // 1. 在背景之上应用 Haze 模糊特效 +// .hazeEffect( +// state = hazeState, +// style = HazeMaterials.ultraThin() +// ) + // 2. 将原本的背景颜色设为透明或半透明 .background( + // 如果想要完全通透的模糊效果,使用 Color.Transparent + // 如果想保留原本的专辑主题色/深色模式色调,可以保留 playerAreaBackground 但降低透明度 color = playerAreaBackground, shape = sheetInteractionState.playerShadowShape ) - .clip(sheetInteractionState.playerShadowShape) // innerLayout: // Measures the actual player content with full screen height targetContentHeightPx // so that it can render correctly, while reporting targetHeightPx to the outer @@ -757,6 +776,7 @@ fun UnifiedPlayerSheetV2( bottomSheetOpenFraction = bottomSheetOpenFraction, fullPlayerVisualState = fullPlayerVisualState, containerHeight = containerHeight, + containerColor = playerAreaBackground, currentQueueSourceName = currentQueueSourceName, currentSheetContentState = currentSheetContentState, carouselStyle = carouselStyle, @@ -766,13 +786,15 @@ fun UnifiedPlayerSheetV2( currentPositionProvider = positionToDisplayProvider, isFavorite = isFavorite, shouldRenderFullPlayer = shouldRenderFullPlayer, + hazeState = hazeState, currentHorizontalPaddingStartPxProvider = currentHorizontalPaddingStartPxProvider, currentHorizontalPaddingEndPxProvider = currentHorizontalPaddingEndPxProvider, onShowQueueClicked = sheetActionHandlers.openQueueSheet, onQueueDragStart = sheetActionHandlers.beginQueueDrag, onQueueDrag = sheetActionHandlers.dragQueueBy, onQueueRelease = sheetActionHandlers.endQueueDrag, - onShowCastClicked = castSheetState.openCastSheet + onShowCastClicked = castSheetState.openCastSheet, + isNavRailHidden = isNavRailHidden, ) } } @@ -815,6 +837,7 @@ fun UnifiedPlayerSheetV2( ) queuePredictiveBackSwipeEdge = null } + } } catch (_: kotlin.coroutines.cancellation.CancellationException) { scope.launch { @@ -832,6 +855,7 @@ fun UnifiedPlayerSheetV2( } } + val queuePredictiveBackSwipeEdgeState = rememberUpdatedState(queuePredictiveBackSwipeEdge) UnifiedPlayerQueueAndSongInfoHost( @@ -862,7 +886,8 @@ fun UnifiedPlayerSheetV2( onNavigateToArtist = sheetActionHandlers.onNavigateToArtist, onNavigateToGenre = sheetActionHandlers.onNavigateToGenre, queuePredictiveBackProgress = queuePredictiveBackProgress, - queuePredictiveBackSwipeEdge = queuePredictiveBackSwipeEdgeState + queuePredictiveBackSwipeEdge = queuePredictiveBackSwipeEdgeState, + hazeState = hazeState ) } } diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/player/FullPlayerContent.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/player/FullPlayerContent.kt index cb6e3d400..c80667b74 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/player/FullPlayerContent.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/player/FullPlayerContent.kt @@ -256,6 +256,7 @@ fun FullPlayerContent( val immersiveLyricsEnabled = fullPlayerSlice.immersiveLyricsEnabled val immersiveLyricsTimeout = fullPlayerSlice.immersiveLyricsTimeout val isImmersiveTemporarilyDisabled = fullPlayerSlice.isImmersiveTemporarilyDisabled + val controlsButtonEnabled = fullPlayerSlice.controlsButtonEnabled val isRemotePlaybackActive = fullPlayerSlice.isRemotePlaybackActive val selectedRouteName = fullPlayerSlice.selectedRouteName val isBluetoothEnabled = fullPlayerSlice.isBluetoothEnabled @@ -970,6 +971,7 @@ fun FullPlayerContent( immersiveLyricsEnabled = immersiveLyricsEnabled, immersiveLyricsTimeout = immersiveLyricsTimeout, isImmersiveTemporarilyDisabled = isImmersiveTemporarilyDisabled, + controlsButtonEnabled = controlsButtonEnabled, onSetImmersiveTemporarilyDisabled = { playerViewModel.setImmersiveTemporarilyDisabled(it) }, isShuffleEnabled = isShuffleEnabled, repeatMode = repeatMode, @@ -1152,6 +1154,23 @@ private fun FullPlayerControlsSection( Column( horizontalAlignment = Alignment.CenterHorizontally ) { + + BottomToggleRow( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 48.dp, max = 48.dp) + .padding(horizontal = 26.dp, vertical = 0.dp), + isShuffleEnabled = isShuffleEnabledProvider(), + isShuffleTransitionInProgress = shuffleTransitionInProgress, + repeatMode = repeatModeProvider(), + isFavoriteProvider = isFavoriteProvider, + onShuffleToggle = onShuffleToggle, + onRepeatToggle = onRepeatToggle, + onFavoriteToggle = onFavoriteToggle + ) + + Spacer(modifier = Modifier.height(14.dp)) + AnimatedPlaybackControls( modifier = Modifier .padding(horizontal = 12.dp, vertical = 8.dp), @@ -1172,22 +1191,6 @@ private fun FullPlayerControlsSection( tintNextIcon = transportSkipColors.content ) - Spacer(modifier = Modifier.height(14.dp)) - - BottomToggleRow( - modifier = Modifier - .fillMaxWidth() - .heightIn(min = 66.dp, max = 86.dp) - .padding(horizontal = 26.dp, vertical = 0.dp) - .padding(bottom = 6.dp), - isShuffleEnabled = isShuffleEnabledProvider(), - isShuffleTransitionInProgress = shuffleTransitionInProgress, - repeatMode = repeatModeProvider(), - isFavoriteProvider = isFavoriteProvider, - onShuffleToggle = onShuffleToggle, - onRepeatToggle = onRepeatToggle, - onFavoriteToggle = onFavoriteToggle - ) } } } 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/EnhancedSongListItem.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/subcomps/EnhancedSongListItem.kt index 30abec587..b2e53dda5 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/subcomps/EnhancedSongListItem.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/subcomps/EnhancedSongListItem.kt @@ -54,6 +54,8 @@ import com.theveloper.pixelplay.presentation.components.ShimmerBox import androidx.compose.ui.res.stringResource import com.theveloper.pixelplay.R import com.theveloper.pixelplay.presentation.components.SmartImage +import com.theveloper.pixelplay.presentation.viewmodel.StablePlayerState +import kotlinx.coroutines.flow.StateFlow @Immutable private data class EnhancedSongAnimationTarget( @@ -97,6 +99,7 @@ fun EnhancedSongListItem( selectionIndex: Int? = null, isSelectionMode: Boolean = false, showMoreOptionsButton: Boolean = true, + stablePlayerStateFlow: StateFlow, onLongPress: () -> Unit = {}, onMoreOptionsClick: (Song) -> Unit, onClick: () -> Unit @@ -362,12 +365,13 @@ fun EnhancedSongListItem( val showTrailingAction = showMoreOptionsButton && !isSelectionMode if (showPlayingIndicator) { - PlayingEqIcon( + PlayingEqIconV2( modifier = Modifier .padding(start = 8.dp) .size(width = 18.dp, height = 16.dp), color = contentColor, - isPlaying = isPlaying + isPlaying = isPlaying, + stablePlayerStateFlow = stablePlayerStateFlow ) } diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/subcomps/LyricsMoreBottomSheet.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/subcomps/LyricsMoreBottomSheet.kt index 61a717c8b..5926e24be 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/subcomps/LyricsMoreBottomSheet.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/subcomps/LyricsMoreBottomSheet.kt @@ -85,6 +85,7 @@ fun LyricsMoreBottomSheet( onShowTranslationChange: (Boolean) -> Unit, onShowRomanizationChange: (Boolean) -> Unit, immersiveLyricsEnabled: Boolean, + controlsButtonEnabled: Boolean, // BottomToggleRow params isShuffleEnabled: Boolean, repeatMode: Int, @@ -331,7 +332,7 @@ fun LyricsMoreBottomSheet( val isSyncVisible = showSyncedLyrics val isRomanizationVisible = hasRomanizedLyrics val isTranslationVisible = hasTranslatedLyrics - val isImmersiveVisible = showSyncedLyrics && immersiveLyricsEnabled + val isImmersiveVisible = showSyncedLyrics && (immersiveLyricsEnabled || controlsButtonEnabled) val isKeepScreenOnVisible = true if (isSyncVisible || isRomanizationVisible || isTranslationVisible || isKeepScreenOnVisible) { diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/subcomps/PlayingEqIconV2.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/subcomps/PlayingEqIconV2.kt new file mode 100644 index 000000000..97abd963a --- /dev/null +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/subcomps/PlayingEqIconV2.kt @@ -0,0 +1,77 @@ +package com.theveloper.pixelplay.presentation.components.subcomps + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.media3.common.util.UnstableApi +import com.theveloper.pixelplay.presentation.viewmodel.PlayerViewModel +import com.theveloper.pixelplay.presentation.viewmodel.StablePlayerState +import kotlinx.coroutines.flow.StateFlow + +@androidx.annotation.OptIn(UnstableApi::class) +@Composable +fun PlayingEqIconV2( + modifier: Modifier = Modifier, + color: Color, + isPlaying: Boolean, + stablePlayerStateFlow: StateFlow +) { + + val stablePlayerState by stablePlayerStateFlow.collectAsStateWithLifecycle() + + val audioAmplitude = stablePlayerState.audioAmplitude + // 使用 tween 加快反应速度,使动画紧跟音乐节奏,同时保持一定的平滑过渡 + val animatedAmp by animateFloatAsState( + targetValue = if (isPlaying) audioAmplitude else 0.1f, + animationSpec = tween(durationMillis = 80), + label = "eq_amplitude" + ) + + // 通过简单的数学偏移,让一根基础振幅数据变成三根看起来独立的跳动柱子 + // 保证它们最小有 0.1f 的高度(不会完全消失),最大不超过 1f + val bar1Height = (animatedAmp * 0.7f + 0.1f).coerceIn(0.1f, 1f) + val bar2Height = (animatedAmp * 1.0f).coerceIn(0.2f, 1f) // 中间这根最高 + val bar3Height = (animatedAmp * 0.5f + 0.3f).coerceIn(0.1f, 1f) + + Row( + modifier = modifier, + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Bottom + ) { + Box( + modifier = Modifier + .weight(1f) + .fillMaxHeight(bar1Height) + .clip(RoundedCornerShape(topStart = 2.dp, topEnd = 2.dp)) + .background(color) + ) + Spacer(modifier = Modifier.width(2.dp)) + Box( + modifier = Modifier + .weight(1f) + .fillMaxHeight(bar2Height) + .clip(RoundedCornerShape(topStart = 2.dp, topEnd = 2.dp)) + .background(color) + ) + Spacer(modifier = Modifier.width(2.dp)) + Box( + modifier = Modifier + .weight(1f) + .fillMaxHeight(bar3Height) + .clip(RoundedCornerShape(topStart = 2.dp, topEnd = 2.dp)) + .background(color) + ) + } +} \ No newline at end of file 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 459e68aaf..1c02a688a 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 @@ -99,6 +99,7 @@ import androidx.navigation.NavController import coil.compose.AsyncImagePainter import coil.request.ImageRequest import coil.size.Size +import com.theveloper.pixelplay.MainActivity.Companion.LocalHazeState import com.theveloper.pixelplay.R import com.theveloper.pixelplay.data.github.GitHubContributorService import com.theveloper.pixelplay.presentation.components.CollapsibleCommonTopBar @@ -107,6 +108,7 @@ import com.theveloper.pixelplay.presentation.components.SmartImage import com.theveloper.pixelplay.presentation.navigation.Screen import com.theveloper.pixelplay.presentation.navigation.navigateSafely import com.theveloper.pixelplay.presentation.viewmodel.PlayerViewModel +import dev.chrisbanes.haze.hazeSource import kotlinx.coroutines.launch import racra.compose.smooth_corner_rect_library.AbsoluteSmoothCornerShape import timber.log.Timber @@ -360,7 +362,7 @@ fun AboutScreen( .asPaddingValues() .calculateBottomPadding() + 12.dp, ), - modifier = Modifier.fillMaxSize(), + modifier = Modifier.fillMaxSize().hazeSource(LocalHazeState.current), horizontalAlignment = Alignment.CenterHorizontally, ) { item(key = "hero_card") { diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/AccountsScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/AccountsScreen.kt index b5aeee48a..8a739bed7 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/AccountsScreen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/AccountsScreen.kt @@ -75,6 +75,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import com.theveloper.pixelplay.MainActivity.Companion.LocalHazeState import com.theveloper.pixelplay.R import com.theveloper.pixelplay.presentation.components.CollapsibleCommonTopBar import com.theveloper.pixelplay.presentation.components.MiniPlayerHeight @@ -87,6 +88,7 @@ import com.theveloper.pixelplay.presentation.telegram.auth.TelegramLoginActivity import com.theveloper.pixelplay.presentation.viewmodel.AccountsViewModel import com.theveloper.pixelplay.presentation.viewmodel.ExternalAccountUiModel import com.theveloper.pixelplay.presentation.viewmodel.ExternalServiceAccount +import dev.chrisbanes.haze.hazeSource import kotlin.math.roundToInt import kotlinx.coroutines.launch import racra.compose.smooth_corner_rect_library.AbsoluteSmoothCornerShape @@ -172,7 +174,7 @@ fun AccountsScreen( LazyColumn( state = lazyListState, - modifier = Modifier.fillMaxSize(), + modifier = Modifier.fillMaxSize().hazeSource(LocalHazeState.current), contentPadding = PaddingValues( top = currentTopBarHeightDp + 8.dp, start = 16.dp, 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 ce578abd5..8e17235bd 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 @@ -104,6 +104,8 @@ import com.theveloper.pixelplay.utils.shapes.RoundedStarShape import kotlinx.coroutines.launch import kotlin.math.roundToInt import androidx.compose.ui.res.stringResource +import com.theveloper.pixelplay.MainActivity.Companion.LocalHazeState +import dev.chrisbanes.haze.hazeSource private const val UseSharedCollapsibleTopBarProbe = true @@ -302,7 +304,7 @@ fun AlbumDetailScreen( val extraHeight = (topBarHeight.value - minTopBarHeightPx).roundToInt() IntOffset(0, extraHeight) - }, + }.hazeSource(LocalHazeState.current), contentPadding = PaddingValues( top = minTopBarHeight + 8.dp, start = 16.dp, @@ -334,6 +336,7 @@ fun AlbumDetailScreen( isCurrentSong = stablePlayerState.currentSong?.id == song.id, isPlaying = stablePlayerState.isPlaying, showAlbumArt = false, + stablePlayerStateFlow = playerViewModel.stablePlayerState, onMoreOptionsClick = { playerViewModel.selectSongForInfo(song) showSongInfoBottomSheet = true 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 f8f85fcfe..eda7df24e 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 @@ -100,6 +100,8 @@ import coil.size.Size import racra.compose.smooth_corner_rect_library.AbsoluteSmoothCornerShape import androidx.compose.ui.res.stringResource import com.theveloper.pixelplay.R +import com.theveloper.pixelplay.presentation.viewmodel.StablePlayerState +import kotlinx.coroutines.flow.StateFlow private const val UseSharedCollapsibleTopBarProbe = true @@ -367,6 +369,7 @@ fun ArtistDetailScreen( songCount = section.songs.size, isCurrentSong = stablePlayerState.currentSong?.id == song.id, isPlaying = stablePlayerState.isPlaying, + stablePlayerStateFlow = playerViewModel.stablePlayerState, onSongClick = { playerViewModel.showAndPlaySong(song, section.songs) }, @@ -663,6 +666,7 @@ private fun ArtistAlbumSectionSongItem( songCount: Int, isCurrentSong: Boolean, isPlaying: Boolean, + stablePlayerStateFlow: StateFlow, onSongClick: () -> Unit, onMoreOptionsClick: () -> Unit ) { @@ -714,6 +718,7 @@ private fun ArtistAlbumSectionSongItem( isPlaying = isPlaying, showAlbumArt = false, customShape = songItemShape, + stablePlayerStateFlow = stablePlayerStateFlow, onMoreOptionsClick = { onMoreOptionsClick() }, onClick = onSongClick ) 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 eabba3c12..629dcd921 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 @@ -94,6 +94,8 @@ import com.theveloper.pixelplay.presentation.components.subcomps.EnhancedSongLis import com.theveloper.pixelplay.presentation.components.subcomps.TightWrapText import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource +import com.theveloper.pixelplay.MainActivity.Companion.LocalHazeState +import dev.chrisbanes.haze.hazeSource @androidx.annotation.OptIn(UnstableApi::class) @OptIn(ExperimentalMaterial3ExpressiveApi::class) @@ -252,6 +254,7 @@ fun DailyMixScreen( modifier = Modifier .fillMaxSize() .background(backgroundBrush) + .hazeSource(LocalHazeState.current) ) { if (dailyMixSongs.isEmpty()) { Box( @@ -356,6 +359,7 @@ fun DailyMixScreen( song = song, isCurrentSong = stablePlayerState.currentSong?.id == song.id, isPlaying = currentSongId == song.id && isPlaying, + stablePlayerStateFlow = playerViewModel.stablePlayerState, onClick = { playerViewModel.showAndPlaySong(song, dailyMixSongs, dailyMixTitle, isVoluntaryPlay = false) }, onMoreOptionsClick = { playerViewModel.selectSongForInfo(song) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/DeviceCapabilitiesScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/DeviceCapabilitiesScreen.kt index a9d376f9c..b5b6b641b 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/DeviceCapabilitiesScreen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/DeviceCapabilitiesScreen.kt @@ -91,6 +91,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.media3.common.util.UnstableApi import androidx.navigation.NavController +import com.theveloper.pixelplay.MainActivity.Companion.LocalHazeState import com.theveloper.pixelplay.R import com.theveloper.pixelplay.presentation.components.CollapsibleCommonTopBar import com.theveloper.pixelplay.presentation.components.MiniPlayerHeight @@ -104,6 +105,7 @@ import com.theveloper.pixelplay.presentation.viewmodel.LocalMusicStorageSummary import com.theveloper.pixelplay.presentation.viewmodel.MemorySummary import com.theveloper.pixelplay.presentation.viewmodel.PlaybackCompatibilitySummary import com.theveloper.pixelplay.presentation.viewmodel.PlayerViewModel +import dev.chrisbanes.haze.hazeSource import java.text.SimpleDateFormat import java.util.Date import java.util.Locale @@ -232,7 +234,7 @@ private fun DeviceCapabilitiesContent( bottom = MiniPlayerHeight + WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 16.dp ), verticalArrangement = Arrangement.spacedBy(12.dp), - modifier = modifier + modifier = modifier.hazeSource(LocalHazeState.current) ) { item { PlaybackReadinessCard( diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/EqualizerScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/EqualizerScreen.kt index a223cea51..31361e21f 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/EqualizerScreen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/EqualizerScreen.kt @@ -151,6 +151,8 @@ import androidx.compose.material.icons.automirrored.rounded.VolumeUp import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource +import com.theveloper.pixelplay.MainActivity.Companion.LocalHazeState +import dev.chrisbanes.haze.hazeSource @androidx.annotation.OptIn(UnstableApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @@ -297,7 +299,7 @@ fun EqualizerScreen( top = currentTopBarHeightDp + 8.dp, bottom = MiniPlayerHeight + WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 20.dp ), - modifier = Modifier.fillMaxSize(), + modifier = Modifier.fillMaxSize().hazeSource(LocalHazeState.current), verticalArrangement = Arrangement.spacedBy(6.dp) ) { // Preset Tabs 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 2fe1dd6ad..3bc0afbf4 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 @@ -90,7 +90,10 @@ import kotlinx.coroutines.launch import racra.compose.smooth_corner_rect_library.AbsoluteSmoothCornerShape import kotlin.math.roundToInt import androidx.compose.ui.res.stringResource +import com.theveloper.pixelplay.MainActivity.Companion.LocalHazeState import com.theveloper.pixelplay.presentation.components.subcomps.TightWrapText +import dev.chrisbanes.haze.hazeSource +import kotlinx.coroutines.flow.StateFlow // --- Data Models & Helpers --- @@ -323,7 +326,7 @@ fun GenreDetailScreen( // Offset the entire list down by the current "expansion" of the top bar val extraHeight = (topBarHeight.value - minTopBarHeightPx).roundToInt() IntOffset(0, extraHeight) - } + }.hazeSource(LocalHazeState.current) ) { // Optimization: Limit rendered items during the navigation transition // to ensure the slide-in animation remains smooth. @@ -356,7 +359,7 @@ fun GenreDetailScreen( val selectionIndex = multiSelectionState.getSelectionIndex(item.song.id) GenreSongItemWrapper( item = item, - stablePlayerState = stablePlayerState, + stablePlayerStateFlow = playerViewModel.stablePlayerState, onSongClick = { song -> playerViewModel.showAndPlaySong(song, uiState.sortedSongs, genreDisplayName) }, @@ -998,7 +1001,7 @@ fun GenreAlbumHeader( @Composable fun GenreSongItemWrapper( item: com.theveloper.pixelplay.presentation.viewmodel.GenreDetailListItem.SongItem, - stablePlayerState: StablePlayerState, + stablePlayerStateFlow: StateFlow, onSongClick: (Song) -> Unit, onMoreOptionsClick: (Song) -> Unit, isSelectionMode: Boolean = false, @@ -1012,6 +1015,8 @@ fun GenreSongItemWrapper( val isLastAlbumInSection = item.isLastAlbumInSection val useArtistStyle = item.useArtistStyle + val stablePlayerState by stablePlayerStateFlow.collectAsStateWithLifecycle() + // Optimization: Cache shapes to avoid reallocation during scroll val songItemShape = remember(isFirstInAlbum, isLastInAlbum) { when { @@ -1060,7 +1065,8 @@ fun GenreSongItemWrapper( isSelected = isSelected, selectionIndex = selectionIndex, isSelectionMode = isSelectionMode, - onLongPress = onLongPress + onLongPress = onLongPress, + stablePlayerStateFlow = stablePlayerStateFlow, ) if (isLastInAlbum) Spacer(Modifier.height(8.dp)) 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..f9a6396a1 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 @@ -103,7 +105,6 @@ import com.theveloper.pixelplay.presentation.components.StatsOverviewCard import com.theveloper.pixelplay.presentation.components.resolveMainScreenBottomGradientHeight import com.theveloper.pixelplay.presentation.model.collectRecentlyPlayedSongIds import com.theveloper.pixelplay.presentation.model.mapRecentlyPlayedSongs -import com.theveloper.pixelplay.presentation.components.subcomps.PlayingEqIcon import com.theveloper.pixelplay.presentation.navigation.Screen import com.theveloper.pixelplay.presentation.components.StreamingProviderSheet import com.theveloper.pixelplay.presentation.telegram.auth.TelegramLoginActivity @@ -111,6 +112,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 @@ -119,8 +121,19 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import racra.compose.smooth_corner_rect_library.AbsoluteSmoothCornerShape import androidx.compose.ui.res.stringResource +import com.theveloper.pixelplay.MainActivity.Companion.LocalHazeState +import com.theveloper.pixelplay.presentation.components.subcomps.PlayingEqIconV2 +import com.theveloper.pixelplay.presentation.viewmodel.StablePlayerState +import dev.chrisbanes.haze.hazeSource +import kotlinx.coroutines.flow.StateFlow 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 +318,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 +450,77 @@ fun HomeScreen( ) } ) { innerPadding -> - LazyColumn( - state = listState, - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background), - contentPadding = PaddingValues( - top = innerPadding.calculateTopPadding(), - bottom = paddingValuesParent.calculateBottomPadding() - + 38.dp + bottomPadding - ), - verticalArrangement = Arrangement.spacedBy(24.dp) - ) { - if (yourMixSongs.isEmpty()) { - item( - key = "your_mix_placeholder", - contentType = "your_mix_placeholder" - ) { - if (shouldShowYourMixLoadingPlaceholder) { - YourMixLoadingPlaceholder() - } else { - YourMixEmptyPlaceholder( - onRefresh = { - homePlaceholderRefreshGeneration++ - settingsViewModel.refreshLibrary() - playerViewModel.forceUpdateDailyMix() - } - ) + BoxWithConstraints( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .hazeSource(LocalHazeState.current), + ) { + 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 +617,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() { @@ -766,6 +892,7 @@ fun SongListItemFavs( albumArtUrl: String?, isPlaying: Boolean, isCurrentSong: Boolean, + stablePlayerStateFlow: StateFlow, onClick: () -> Unit ) { val colors = MaterialTheme.colorScheme @@ -817,13 +944,14 @@ fun SongListItemFavs( } Spacer(Modifier.width(16.dp)) if (isCurrentSong) { - PlayingEqIcon( + PlayingEqIconV2( modifier = Modifier .weight(0.1f) .padding(start = 8.dp) .size(width = 18.dp, height = 16.dp), // similar al tamaño del ícono color = colors.primary, - isPlaying = isPlaying // o conectalo a tu estado real de reproducción + isPlaying = isPlaying, // o conectalo a tu estado real de reproducción + stablePlayerStateFlow = stablePlayerStateFlow ) } } @@ -856,6 +984,7 @@ fun SongListItemFavsWrapper( albumArtUrl = song.albumArtUriString, isPlaying = stablePlayerState.isPlaying, isCurrentSong = song.id == stablePlayerState.currentSong?.id, + stablePlayerStateFlow = playerViewModel.stablePlayerState, onClick = onClick ) } diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibraryPlaybackAwareSongItem.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibraryPlaybackAwareSongItem.kt index 5c7da0dff..096fe0e93 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibraryPlaybackAwareSongItem.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibraryPlaybackAwareSongItem.kt @@ -55,6 +55,7 @@ internal fun LibraryPlaybackAwareSongItem( isSelected = isSelected, selectionIndex = selectionIndex, isSelectionMode = isSelectionMode, + stablePlayerStateFlow = playerViewModel.stablePlayerState, onLongPress = onLongPress, onMoreOptionsClick = onMoreOptionsClick, onClick = onClick diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibraryScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibraryScreen.kt index 5d37d029e..87c7093be 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 @@ -229,7 +229,6 @@ import com.theveloper.pixelplay.presentation.components.AutoScrollingTextOnDeman import com.theveloper.pixelplay.presentation.screens.CreatePlaylistDialog import com.theveloper.pixelplay.presentation.components.PlaylistBottomSheet import com.theveloper.pixelplay.presentation.components.PlaylistContainer -import com.theveloper.pixelplay.presentation.components.subcomps.PlayingEqIcon import com.theveloper.pixelplay.ui.theme.GoogleSansRounded import java.util.Locale import androidx.compose.ui.platform.LocalContext @@ -241,12 +240,14 @@ import kotlinx.coroutines.flow.first import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.itemContentType import androidx.paging.LoadState +import com.theveloper.pixelplay.MainActivity.Companion.LocalHazeState import com.theveloper.pixelplay.presentation.components.ExpressiveScrollBar import com.theveloper.pixelplay.ui.theme.LocalShowScrollbar import com.theveloper.pixelplay.presentation.components.LibrarySortBottomSheet import com.theveloper.pixelplay.presentation.components.subcomps.EnhancedSongListItem import com.theveloper.pixelplay.data.service.wear.PhoneWatchTransferState import com.theveloper.pixelplay.shared.WearTransferProgress +import dev.chrisbanes.haze.hazeSource import java.io.File import kotlin.math.abs @@ -839,7 +840,8 @@ fun LibraryScreen( val headerContainerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.4f) Scaffold( - modifier = Modifier.background(brush = gradientBrush), + modifier = Modifier.background(brush = gradientBrush) + .hazeSource(LocalHazeState.current), topBar = { Column( modifier = Modifier.background(headerContainerColor) @@ -2072,10 +2074,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/LibrarySongsAndFavoritesTabs.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibrarySongsAndFavoritesTabs.kt index c245e6506..02a49b9f1 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibrarySongsAndFavoritesTabs.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibrarySongsAndFavoritesTabs.kt @@ -268,6 +268,7 @@ fun LibraryFavoritesTab( isPlaying = false, isLoading = true, isCurrentSong = false, + stablePlayerStateFlow = playerViewModel.stablePlayerState, onMoreOptionsClick = {}, onClick = {} ) @@ -331,6 +332,7 @@ fun LibrarySongsTabPaginated( isPlaying = false, isLoading = true, isCurrentSong = false, + stablePlayerStateFlow = playerViewModel.stablePlayerState, onMoreOptionsClick = {}, onClick = {} ) @@ -448,6 +450,7 @@ fun LibrarySongsTabPaginated( isPlaying = isPlayingThisSong, isCurrentSong = stablePlayerState.currentSong?.id == song.id, isLoading = false, + stablePlayerStateFlow = playerViewModel.stablePlayerState, onMoreOptionsClick = rememberedOnMoreOptionsClick, onClick = rememberedOnClick ) @@ -457,6 +460,7 @@ fun LibrarySongsTabPaginated( isPlaying = false, isLoading = true, isCurrentSong = false, + stablePlayerStateFlow = playerViewModel.stablePlayerState, onMoreOptionsClick = {}, onClick = {} ) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibrarySongsTab.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibrarySongsTab.kt index 5fdab7ddb..7e8d3953f 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibrarySongsTab.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibrarySongsTab.kt @@ -266,6 +266,7 @@ fun LibrarySongsTab( isPlaying = false, isLoading = true, isCurrentSong = false, + stablePlayerStateFlow = playerViewModel.stablePlayerState, onMoreOptionsClick = {}, onClick = {} ) @@ -359,6 +360,7 @@ fun LibrarySongsTab( isPlaying = false, isLoading = true, isCurrentSong = false, + stablePlayerStateFlow = playerViewModel.stablePlayerState, onMoreOptionsClick = {}, onClick = {} ) 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 a2b8c0c67..8f73874a1 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 @@ -112,6 +112,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.media3.common.util.UnstableApi import androidx.navigation.NavController import coil.size.Size +import com.theveloper.pixelplay.MainActivity.Companion.LocalHazeState import com.theveloper.pixelplay.R import com.theveloper.pixelplay.data.model.Song import com.theveloper.pixelplay.presentation.components.MiniPlayerHeight @@ -140,6 +141,7 @@ import sh.calvin.reorderable.rememberReorderableLazyListState import com.theveloper.pixelplay.presentation.components.LibrarySortBottomSheet import com.theveloper.pixelplay.data.model.SortOption import com.theveloper.pixelplay.data.model.PlaylistShapeType +import dev.chrisbanes.haze.hazeSource import kotlinx.coroutines.launch @androidx.annotation.OptIn(UnstableApi::class) @@ -661,7 +663,8 @@ fun PlaylistDetailScreen( if (localReorderableSongs.isEmpty()) { Box(Modifier .fillMaxSize() - .weight(1f), Alignment.Center) { + .weight(1f) + .hazeSource(LocalHazeState.current), Alignment.Center) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Icon(Icons.Filled.MusicOff, null, Modifier.size(48.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant) Spacer(Modifier.height(8.dp)) @@ -693,7 +696,8 @@ fun PlaylistDetailScreen( smoothnessAsPercentTL = 60, ) ) - .background(color = MaterialTheme.colorScheme.surfaceContainerHigh), + .background(color = MaterialTheme.colorScheme.surfaceContainerHigh) + .hazeSource(LocalHazeState.current), verticalArrangement = Arrangement.spacedBy(8.dp), contentPadding = PaddingValues( top = 12.dp, @@ -750,6 +754,7 @@ fun PlaylistDetailScreen( } }, isFromPlaylist = true, + playerViewModel = playerViewModel, isReorderModeEnabled = isReorderModeEnabled, isDragHandleVisible = isReorderModeEnabled, isRemoveButtonVisible = isRemoveModeEnabled, 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 5ebf94a74..6066c3c8f 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 @@ -96,6 +96,8 @@ import java.util.Locale import android.text.format.DateFormat as AndroidDateFormat import racra.compose.smooth_corner_rect_library.AbsoluteSmoothCornerShape import androidx.compose.ui.res.stringResource +import com.theveloper.pixelplay.MainActivity.Companion.LocalHazeState +import dev.chrisbanes.haze.hazeSource @androidx.annotation.OptIn(UnstableApi::class) @OptIn(ExperimentalMaterial3ExpressiveApi::class) @@ -179,6 +181,7 @@ fun RecentlyPlayedScreen( modifier = Modifier .fillMaxSize() .background(backgroundBrush) + .hazeSource(LocalHazeState.current) ) { if (recentlyPlayedSourceSongs == null) { Box( @@ -255,6 +258,7 @@ fun RecentlyPlayedScreen( song = item.song, isCurrentSong = currentSongId == item.song.id, isPlaying = currentSongId == item.song.id && isPlaying, + stablePlayerStateFlow = playerViewModel.stablePlayerState, onClick = { playerViewModel.playSongs( songsToPlay = queueSongs, 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 eab38137e..b10f21e7d 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 @@ -148,6 +148,11 @@ import racra.compose.smooth_corner_rect_library.AbsoluteSmoothCornerShape import timber.log.Timber import com.theveloper.pixelplay.presentation.components.subcomps.EnhancedSongListItem import androidx.compose.ui.res.stringResource +import com.theveloper.pixelplay.MainActivity.Companion.LocalHazeState +import dev.chrisbanes.haze.HazeStyle +import dev.chrisbanes.haze.hazeEffect +import dev.chrisbanes.haze.hazeSource +import dev.chrisbanes.haze.materials.HazeMaterials private const val MAX_ALBUM_MULTI_SELECTION = 6 @@ -434,7 +439,7 @@ fun SearchScreen( label = "search_mode_transition" ) { isGenreMode -> if (isGenreMode) { - Column(modifier = Modifier.fillMaxSize()) { + Column(modifier = Modifier.fillMaxSize().hazeSource(LocalHazeState.current)) { if (isGenreSelectionMode) { SelectionActionRow( selectedCount = selectedGenres.size, @@ -495,7 +500,7 @@ fun SearchScreen( Column( modifier = Modifier .fillMaxSize() - .padding(horizontal = 16.dp) + .padding(horizontal = 16.dp).hazeSource(LocalHazeState.current) ) { if (anySelectionMode) { val count = when { @@ -1220,6 +1225,7 @@ fun SearchResultsList( isSelected = isSelected, selectionIndex = selectionIndex, isSelectionMode = isSelectionMode, + stablePlayerStateFlow = playerViewModel.stablePlayerState, onLongPress = { onSongLongPress(item.song) } ) } diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsCategoryScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsCategoryScreen.kt index dd1672dbc..bac24907d 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 @@ -146,6 +146,7 @@ import androidx.core.net.toUri import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.media3.common.util.UnstableApi import androidx.navigation.NavController +import com.theveloper.pixelplay.MainActivity.Companion.LocalHazeState import kotlin.math.roundToInt import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest @@ -177,6 +178,7 @@ import com.theveloper.pixelplay.presentation.viewmodel.LyricsRefreshProgress import com.theveloper.pixelplay.presentation.viewmodel.PlayerViewModel import com.theveloper.pixelplay.presentation.viewmodel.SettingsViewModel import com.theveloper.pixelplay.ui.theme.GoogleSansRounded +import dev.chrisbanes.haze.hazeSource @androidx.annotation.OptIn(UnstableApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @@ -395,13 +397,15 @@ fun SettingsCategoryScreen( Box( modifier = - Modifier.nestedScroll(nestedScrollConnection).fillMaxSize() + Modifier + .nestedScroll(nestedScrollConnection) + .fillMaxSize() ) { val currentTopBarHeightDp = with(density) { topBarHeight.value.toDp() } LazyColumn( state = lazyListState, - modifier = Modifier.fillMaxSize(), + modifier = Modifier.fillMaxSize().hazeSource(LocalHazeState.current), contentPadding = PaddingValues( top = currentTopBarHeightDp + 8.dp, start = 16.dp, @@ -703,6 +707,21 @@ fun SettingsCategoryScreen( leadingIcon = { Icon(Icons.Rounded.Timer, null, tint = MaterialTheme.colorScheme.secondary) } ) } + + SwitchSettingItem( + title = stringResource(R.string.settings_hide_controls_button_title), + subtitle = stringResource(R.string.settings_hide_controls_button_subtitle), + checked = !uiState.controlsButtonEnabled, + onCheckedChange = { settingsViewModel.setControlsButtonEnabled(!it) }, + leadingIcon = { + Icon( + painterResource(R.drawable.rounded_lyrics_24), + null, + tint = MaterialTheme.colorScheme.secondary + ) + } + ) + } SettingsSubsection( @@ -1436,7 +1455,7 @@ fun SettingsCategoryScreen( Box(modifier = Modifier .fillMaxSize() .pointerInput(Unit) { - awaitPointerEventScope { + awaitPointerEventScope { while (true) { awaitPointerEvent() } diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsScreen.kt index fa40ecb26..c71fbec9a 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsScreen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsScreen.kt @@ -85,7 +85,9 @@ import kotlinx.coroutines.launch import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.media3.common.util.UnstableApi import androidx.navigation.NavController +import com.theveloper.pixelplay.MainActivity.Companion.LocalHazeState import com.theveloper.pixelplay.data.preferences.LaunchTab +import dev.chrisbanes.haze.hazeSource // SettingsTopBar removed, replaced by CollapsibleCommonTopBar @@ -208,7 +210,7 @@ fun SettingsScreen( bottom = MiniPlayerHeight + WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 8.dp ), verticalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize().hazeSource(LocalHazeState.current) ) { item { val isDark = MaterialTheme.colorScheme.surface.luminance() < 0.5f 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/StatsScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/StatsScreen.kt index 0cbace0d4..15cb211db 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/StatsScreen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/StatsScreen.kt @@ -132,7 +132,9 @@ import androidx.compose.material.icons.outlined.Album import com.theveloper.pixelplay.utils.shapes.RoundedStarShape import androidx.compose.material.icons.outlined.MusicNote import androidx.compose.material.icons.outlined.PlayCircleOutline +import com.theveloper.pixelplay.MainActivity.Companion.LocalHazeState import com.theveloper.pixelplay.ui.theme.ExpTitleTypography +import dev.chrisbanes.haze.hazeSource private const val PULL_TO_REFRESH_MIN_DURATION_MS = 3500L @@ -275,7 +277,8 @@ fun StatsScreen( LazyColumn( state = lazyListState, modifier = Modifier - .fillMaxSize(), + .fillMaxSize() + .hazeSource(LocalHazeState.current), contentPadding = PaddingValues( top = currentTopBarHeightDp + tabsHeight + tabIndicatorExtraSpacing + tabContentSpacing + 0.dp, bottom = MiniPlayerHeight + WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 16.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/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/PlaybackStateHolder.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlaybackStateHolder.kt index 1ef56cb28..d8b34e532 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlaybackStateHolder.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlaybackStateHolder.kt @@ -94,6 +94,37 @@ class PlaybackStateHolder @Inject constructor( _sliderUiMounted.value = mounted } + // 用于节流的时间戳 + private var lastAmplitudeUpdateTimeMs = 0L + + /* -------------------------------------------------------------------------- */ + /* Audio EQ Visualizer */ + /* -------------------------------------------------------------------------- */ + + /** + * 高频调用的音频振幅更新通道。 + * 注意:此方法会被 ExoPlayer 的后台音频线程高频调用,绝对不能包含任何 IPC + * 或复杂的耗时逻辑! + */ + fun updateAudioAmplitude(amplitude: Float) { + val nowMs = SystemClock.elapsedRealtime() + + // 节流:限制更新频率约为 50ms 一次 (20 FPS),避免 StateFlow 频繁发射导致 UI 掉帧 + if (nowMs - lastAmplitudeUpdateTimeMs >= 50L) { + lastAmplitudeUpdateTimeMs = nowMs + + // 直接操作底层的 _stablePlayerState,避开 updateStablePlayerState 中的耗时逻辑 + _stablePlayerState.update { current -> + // 只有当振幅发生有意义的改变时才更新,避免无意义的重组 + if (current.audioAmplitude != amplitude) { + current.copy(audioAmplitude = amplitude) + } else { + current + } + } + } + } + // Internal State private var isSeeking = false private var remoteSeekUnlockJob: Job? = null @@ -670,11 +701,13 @@ class PlaybackStateHolder @Inject constructor( } _stablePlayerState.update { state -> + val finalAmplitude = if (isRemotePlaying || mediaController?.isPlaying == true) state.audioAmplitude else 0f if ( state.totalDuration == duration && state.isPlaying == isRemotePlaying && state.playWhenReady == remotePlayWhenReady && - state.isBuffering == (remotePlayback?.isBuffering ?: false) + state.isBuffering == (remotePlayback?.isBuffering ?: false) && + state.audioAmplitude == finalAmplitude ) { state } else { @@ -682,7 +715,8 @@ class PlaybackStateHolder @Inject constructor( totalDuration = duration, isPlaying = isRemotePlaying, playWhenReady = remotePlayWhenReady, - isBuffering = remotePlayback?.isBuffering ?: false + isBuffering = remotePlayback?.isBuffering ?: false, + audioAmplitude = finalAmplitude, ) } } diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt index 4ddadbede..9a5af27d7 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 @@ -608,6 +608,13 @@ class PlayerViewModel @Inject constructor( _isImmersiveTemporarilyDisabled.value = disabled } + val controlsButtonEnabled: StateFlow = userPreferencesRepository.controlsButtonEnabledFlow + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = true + ) + val albumArtQuality: StateFlow = userPreferencesRepository.albumArtQualityFlow .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), AlbumArtQuality.MEDIUM) @@ -1059,19 +1066,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 } } - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), listOf("SONGS", "ALBUMS", "ARTIST", "PLAYLISTS", "FOLDERS", "LIKED")) + + // 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 } + } + + 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 @@ -1234,6 +1268,7 @@ class PlayerViewModel @Inject constructor( val immersiveLyricsEnabled: Boolean = false, val immersiveLyricsTimeout: Long = 4000L, val isImmersiveTemporarilyDisabled: Boolean = false, + val controlsButtonEnabled: Boolean = true, val isRemotePlaybackActive: Boolean = false, val selectedRouteName: String? = null, val isBluetoothEnabled: Boolean = false, @@ -1260,15 +1295,20 @@ class PlayerViewModel @Inject constructor( // Intermediate combine #2: remaining flows (≤5 for Kotlin type inference) private val fullPlayerSlicePart2 = combine( - immersiveLyricsEnabled, - immersiveLyricsTimeout, - isImmersiveTemporarilyDisabled, + combine( + immersiveLyricsEnabled, + immersiveLyricsTimeout, + isImmersiveTemporarilyDisabled, + + ) { immersive, immersiveTimeout, immersiveDisabled -> + Triple(immersive, immersiveTimeout, immersiveDisabled) }, + controlsButtonEnabled, isRemotePlaybackActive, combine(selectedRouteName, bluetoothSlice) { route, bt -> route to bt } - ) { immersive: Boolean, immersiveTimeout: Long, immersiveDisabled: Boolean, - remotePb: Boolean, routeAndBt: Pair -> + ) { immersive: Triple, controlsEnable: Boolean, remotePb: Boolean, routeAndBt: Pair -> + val (immersiveEnabled, immersiveTimeout, isImmersiveDisabled) = immersive val (routeName, bt) = routeAndBt - FullPlayerSlicePart2(immersive, immersiveTimeout, immersiveDisabled, remotePb, routeName, bt.enabled, bt.name) + FullPlayerSlicePart2(immersiveEnabled, immersiveTimeout, isImmersiveDisabled, controlsEnable, remotePb, routeName, bt.enabled, bt.name) } private data class FullPlayerSlicePart1( @@ -1283,6 +1323,7 @@ class PlayerViewModel @Inject constructor( val immersiveLyricsEnabled: Boolean, val immersiveLyricsTimeout: Long, val isImmersiveTemporarilyDisabled: Boolean, + val controlsButtonEnabled: Boolean, val isRemotePlaybackActive: Boolean, val selectedRouteName: String?, val isBluetoothEnabled: Boolean, @@ -1302,6 +1343,7 @@ class PlayerViewModel @Inject constructor( immersiveLyricsEnabled = p2.immersiveLyricsEnabled, immersiveLyricsTimeout = p2.immersiveLyricsTimeout, isImmersiveTemporarilyDisabled = p2.isImmersiveTemporarilyDisabled, + controlsButtonEnabled = p2.controlsButtonEnabled, isRemotePlaybackActive = p2.isRemotePlaybackActive, selectedRouteName = p2.selectedRouteName, isBluetoothEnabled = p2.isBluetoothEnabled, @@ -2806,6 +2848,12 @@ class PlayerViewModel @Inject constructor( } } + fun saveLibraryHiddenTabs(hiddenTabs: Set) { + viewModelScope.launch { + userPreferencesRepository.setLibraryHiddenTabs(hiddenTabs) + } + } + fun resetLibraryTabsOrder() { viewModelScope.launch { userPreferencesRepository.resetLibraryTabsOrder() 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 abba7eace..c8e97eca7 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 @@ -94,6 +94,7 @@ data class SettingsUiState( val hapticsEnabled: Boolean = true, val immersiveLyricsEnabled: Boolean = false, val immersiveLyricsTimeout: Long = 4000L, + val controlsButtonEnabled: Boolean = true, val useAnimatedLyrics: Boolean = false, val animatedLyricsBlurEnabled: Boolean = true, val animatedLyricsBlurStrength: Float = 2.5f, @@ -169,6 +170,7 @@ private sealed interface SettingsUiUpdate { val hapticsEnabled: Boolean, val immersiveLyricsEnabled: Boolean, val immersiveLyricsTimeout: Long, + val controlsButtonEnabled: Boolean, val animatedLyricsBlurEnabled: Boolean, val animatedLyricsBlurStrength: Float, val disableBlurAllOver: Boolean, @@ -662,6 +664,7 @@ class SettingsViewModel @Inject constructor( userPreferencesRepository.hapticsEnabledFlow, userPreferencesRepository.immersiveLyricsEnabledFlow, userPreferencesRepository.immersiveLyricsTimeoutFlow, + userPreferencesRepository.controlsButtonEnabledFlow, userPreferencesRepository.animatedLyricsBlurEnabledFlow, userPreferencesRepository.animatedLyricsBlurStrengthFlow, userPreferencesRepository.disableBlurAllOverFlow, @@ -684,10 +687,11 @@ class SettingsViewModel @Inject constructor( hapticsEnabled = values[13] as Boolean, immersiveLyricsEnabled = values[14] as Boolean, immersiveLyricsTimeout = values[15] as Long, - animatedLyricsBlurEnabled = values[16] as Boolean, - animatedLyricsBlurStrength = values[17] as Float, - disableBlurAllOver = values[18] as Boolean, - showScrollbar = values[19] as Boolean + controlsButtonEnabled = values[16] as Boolean, + animatedLyricsBlurEnabled = values[17] as Boolean, + animatedLyricsBlurStrength = values[18] as Float, + disableBlurAllOver = values[19] as Boolean, + showScrollbar = values[20] as Boolean ) }.collect { update -> _uiState.update { state -> @@ -708,6 +712,7 @@ class SettingsViewModel @Inject constructor( hapticsEnabled = update.hapticsEnabled, immersiveLyricsEnabled = update.immersiveLyricsEnabled, immersiveLyricsTimeout = update.immersiveLyricsTimeout, + controlsButtonEnabled = update.controlsButtonEnabled, animatedLyricsBlurEnabled = update.animatedLyricsBlurEnabled, animatedLyricsBlurStrength = update.animatedLyricsBlurStrength, disableBlurAllOver = update.disableBlurAllOver, @@ -1188,6 +1193,12 @@ class SettingsViewModel @Inject constructor( } } + fun setControlsButtonEnabled(enabled: Boolean) { + viewModelScope.launch { + userPreferencesRepository.setControlsButtonEnabled(enabled) + } + } + /** * Completely rebuilds the database from scratch. * Clears all data including user edits (lyrics, favorites) and rescans. 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/StablePlayerState.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/StablePlayerState.kt index d93cf924c..d76010cb6 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/StablePlayerState.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/StablePlayerState.kt @@ -17,5 +17,6 @@ data class StablePlayerState( val repeatMode: Int = Player.REPEAT_MODE_OFF, val isLoadingLyrics: Boolean = false, val lyrics: Lyrics? = null, - val isBuffering: Boolean = false + val isBuffering: Boolean = false, + val audioAmplitude: Float = 0f ) 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_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_settings.xml b/app/src/main/res/values-de/strings_settings.xml index e6fcda843..63f98277d 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 @@ Sprache Sprache der App-Oberfläche festlegen. Systemstandard - English - Español - Deutsch - Français - Русский - 简体中文 - Bahasa Indonesia - Italiano - Koreanisch - Norwegisch Bokmål - Türkisch App-Design Hell, Dunkel oder System-Design – ganz nach Geschmack. Hell 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_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 reloj Ajustes Editar - Reorder pestañas + Reordenar pestañas Expandir 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ña Elige 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 79a7e6257..d5af6f1f5 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 app Elige 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) - Turco Tema de la app Cambia entre claro, oscuro o seguir el sistema. Tema claro diff --git a/app/src/main/res/values-fr/strings_settings.xml b/app/src/main/res/values-fr/strings_settings.xml index 5932d3935..0ef9490cc 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\'application Choisissez 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) - Turc Thème de l\'application Passer du mode clair au mode sombre, ou suivre l\'apparence du système. Thème clair 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 @@ Lirik Pengaturan Sampul Album - Daftar Putar + Playlist Trek tidak dikenal Artis tidak dikenal Album 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_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 ALBUM terpilih Batas: %1$d album per pilihan. - Antrekan + putar menghormati urutan pilihan Anda. + Antrekan + putar berdasarkan urutan pilihan Anda. %1$d GENRE terpilih Lakukan operasi batch pada semua lagu dalam genre ini. @@ -499,7 +499,7 @@ Gagal mengekspor: %1$s Musik/Ekspor PixelPlayer Silakan konfigurasi API key Gemini Anda di Pengaturan. - Daftar putar dipulihkan + Playlist dipulihkan Membagikan %d playlist Membagikan %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 cabbc3ab0..342a18b2a 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 Aplikasi Pilih bahasa yang digunakan di seluruh antarmuka aplikasi. Default sistem - Inggris - Spanyol - Jerman - Prancis - Rusia - Tionghoa (Sederhana) - Indonesia - Italia - Korea - Norwegia (Bokmål) - Turki Tema Aplikasi Beralih antara terang, gelap, atau ikuti tampilan sistem. Tema Terang diff --git a/app/src/main/res/values-it/strings_settings.xml b/app/src/main/res/values-it/strings_settings.xml index 24ad47887..7c6a6115d 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 app Scegli la lingua usata nell\'interfaccia dell\'app. Predefinita sistema - Inglese - Spagnolo - Tedesco - Francese - Russo - Cinese semplificato - Indonesiano - Italiano - Coreano - Norvegese (Bokmål) - Turco Tema app Passa tra chiaro, scuro o segui l\'aspetto di sistema. Tema chiaro 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_settings.xml b/app/src/main/res/values-ko/strings_settings.xml index 4b4fe3a87..ad1975ddb 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) - 터키어 앱 테마 밝은 테마, 어두운 테마 또는 시스템 설정 따르기 중에서 선택하세요. 밝은 테마 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_settings.xml b/app/src/main/res/values-nb/strings_settings.xml index d8e2abe1f..b839bf1b1 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åk Velg språket som skal brukes i appen. Systemstandard - Engelsk - Spansk - Tysk - Fransk - Russisk - Kinesisk (forenklet) - Indonesisk - Italiensk - Koreansk - Norsk bokmål - Tyrkisk App-tema Bytt mellom lyst, mørkt eller følg systemets utseende. Lyst tema 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_settings.xml b/app/src/main/res/values-ru/strings_settings.xml index c49784964..7a2368588 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) - Турецкий Тема приложения Светлая, тёмная тема или настройки системы. Светлая тема 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_settings.xml b/app/src/main/res/values-tr/strings_settings.xml index 71b05a38e..97f5981af 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 Dili Uygulama 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çe Uygulama Teması Açık, koyu tema arasında geçiş yapın veya sistem görünümünü takip edin. Açık Tema 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_settings.xml b/app/src/main/res/values-zh-rCN/strings_settings.xml index daf7d4eac..4f96e4f43 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) - 土耳其语 应用主题 在浅色、深色之间切换,或跟随系统外观。 浅色主题 @@ -215,6 +204,8 @@ 歌词界面 沉浸式歌词 自动隐藏控件并放大文本。 + 隐藏控制按钮 + 隐藏多余控制按钮 自动隐藏延迟 控件隐藏前的时间。 3秒 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 tab Pick an Artist diff --git a/app/src/main/res/values/strings_settings.xml b/app/src/main/res/values/strings_settings.xml index ea3602815..0689787a0 100644 --- a/app/src/main/res/values/strings_settings.xml +++ b/app/src/main/res/values/strings_settings.xml @@ -163,17 +163,6 @@ App Language Choose the language used across the app interface. System default - English - Español - Deutsch - Français - Русский - 简体中文 - Bahasa Indonesia - Italiano - Korean - Norwegian (Bokmål) - Türkçe App Theme Switch between light, dark, or follow system appearance. Light Theme @@ -215,6 +204,8 @@ Lyrics Screen Immersive Lyrics Auto-hide controls and enlarge text. + 隐藏控制按钮 + 隐藏多余控制按钮 Auto-hide Delay Time before controls hide. 3s 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 index bfd35a313..3d91d6e93 100644 --- a/app/src/test/java/com/theveloper/pixelplay/data/stream/CloudMusicUtilsTest.kt +++ b/app/src/test/java/com/theveloper/pixelplay/data/stream/CloudMusicUtilsTest.kt @@ -5,6 +5,24 @@ import org.junit.jupiter.api.Test class CloudMusicUtilsTest { + @Test + fun `jsonToMap parses valid JSON object`() { + val result = CloudMusicUtils.jsonToMap("""{"key1":"val1","key2":"val2"}""") + assertEquals(mapOf("key1" to "val1", "key2" to "val2"), result) + } + + @Test + fun `jsonToMap handles empty object`() { + val result = CloudMusicUtils.jsonToMap("{}") + assertEquals(emptyMap(), result) + } + + @Test + fun `jsonToMap returns empty string for null values`() { + val result = CloudMusicUtils.jsonToMap("""{"key":null}""") + assertEquals(mapOf("key" to ""), result) + } + @Test fun `parseArtistNames preserves common punctuation in artist names by default`() { assertEquals(listOf("W&W"), CloudMusicUtils.parseArtistNames("W&W")) @@ -20,4 +38,22 @@ class CloudMusicUtilsTest { CloudMusicUtils.parseArtistNames("Artist One; Artist Two") ) } + + @Test + fun `parseArtistNames returns Unknown Artist for blank input`() { + assertEquals(listOf("Unknown Artist"), CloudMusicUtils.parseArtistNames("")) + assertEquals(listOf("Unknown Artist"), CloudMusicUtils.parseArtistNames(" ")) + } + + @Test + fun `parseArtistNames returns single artist for simple name`() { + val result = CloudMusicUtils.parseArtistNames("Taylor Swift") + assertEquals(listOf("Taylor Swift"), result) + } + + @Test + fun `parseArtistNames trims whitespace around names`() { + val result = CloudMusicUtils.parseArtistNames(" Artist A ; Artist B ") + assertEquals(listOf("Artist A", "Artist B"), result) + } } 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..9dd6910ea 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" @@ -20,6 +20,7 @@ foundation = "1.11.3" glance = "1.3.0-alpha01" graphicsShapes = "1.1.0" gson = "2.14.0" +haze = "1.7.2" hiltAndroid = "2.59.2" hiltNavigationCompose = "1.3.0" ktor = "3.5.0" @@ -65,7 +66,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" @@ -112,6 +113,8 @@ playServicesWearable = "20.0.1" [libraries] accompanist-drawablepainter = { module = "com.google.accompanist:accompanist-drawablepainter", version.ref = "accompanistDrawablepainter" } +haze = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" } +haze-materials = { module = "dev.chrisbanes.haze:haze-materials", version.ref = "haze" } lifecycleprocess = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycleRuntimeKtx" } junitplatformlauncher = { group = "org.junit.platform", name = "junit-platform-launcher", version.ref = "junitJupiter" } worktesting = { group = "androidx.work", name = "work-testing", version.ref = "workRuntimeKtx" } @@ -147,6 +150,8 @@ androidx-media3-exoplayer-midi = { module = "androidx.media3:media3-exoplayer-mi androidx-media3-session = { module = "androidx.media3:media3-session", version.ref = "media3Session" } androidx-media3-transformer = { module = "androidx.media3:media3-transformer", version.ref = "media3Transformer" } androidx-media3-ui = { module = "androidx.media3:media3-ui", version.ref = "media3Session" } +androidx-media3-common = { module = "androidx.media3:media3-common", version.ref = "media3Session" } +androidx-media3-common-ktx = { module = "androidx.media3:media3-common-ktx", version.ref = "media3Session" } androidx-media3-exoplayer-ffmpeg = { module = "org.jellyfin.media3:media3-ffmpeg-decoder", version = "1.9.0+1" } androidx-media-router = { module = "androidx.mediarouter:mediarouter", version.ref = "mediaRouter" } androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 116d9acdb..dcf577fe3 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=bafc141b619ad6350fd975fc903156dd5c151998cc8b058e8c1044ab5f7b031f -distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip +distributionSha256Sum=bbaeb2fef8710818cf0e261201dab964c572f92b942812df0c3620d62a529a01 +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-bin.zip networkTimeout=10000 retries=0 retryBackOffMs=500 diff --git a/gradlew b/gradlew index b9bb139f7..249efbb03 100755 --- a/gradlew +++ b/gradlew @@ -20,7 +20,7 @@ ############################################################################## # -# Gradle start up script for POSIX generated by Gradle. +# gradlew start up script for POSIX generated by Gradle. # # Important for running: # @@ -29,7 +29,7 @@ # bash, then to run this script, type that shell name before the whole # command line, like: # -# ksh Gradle +# ksh gradlew # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: diff --git a/gradlew.bat b/gradlew.bat index aa5f10b06..8508ef684 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -19,7 +19,7 @@ @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem -@rem Gradle startup script for Windows +@rem gradlew startup script for Windows @rem @rem ########################################################################## @@ -72,7 +72,7 @@ echo location of your Java installation. 1>&2 -@rem Execute Gradle +@rem Execute gradlew @rem endlocal doesn't take effect until after the line is parsed and variables are expanded @rem which allows us to clear the local environment before executing the java command endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel