From e31c124221b20648782e4288bc36ce30a1612ad7 Mon Sep 17 00:00:00 2001 From: test01203 <104188844+test01203@users.noreply.github.com> Date: Wed, 7 Oct 2026 18:27:12 +0300 Subject: [PATCH] feat(ui): add floating top pill navigation bar layout option --- .../com/arflix/tv/ui/components/AppTopBar.kt | 38 ++- .../tv/ui/components/TopPillNavigationBar.kt | 323 ++++++++++++++++++ .../tv/ui/screens/settings/SettingsScreen.kt | 24 +- .../ui/screens/settings/SettingsViewModel.kt | 25 ++ app/src/main/res/values-iw/strings.xml | 6 + app/src/main/res/values/strings.xml | 4 + 6 files changed, 416 insertions(+), 4 deletions(-) create mode 100644 app/src/main/kotlin/com/arflix/tv/ui/components/TopPillNavigationBar.kt diff --git a/app/src/main/kotlin/com/arflix/tv/ui/components/AppTopBar.kt b/app/src/main/kotlin/com/arflix/tv/ui/components/AppTopBar.kt index a8cac80f1..0fff69247 100644 --- a/app/src/main/kotlin/com/arflix/tv/ui/components/AppTopBar.kt +++ b/app/src/main/kotlin/com/arflix/tv/ui/components/AppTopBar.kt @@ -89,6 +89,22 @@ fun topBarFocusedItem(focusedIndex: Int, hasProfile: Boolean): SidebarItem? { return NAV_ITEMS.getOrNull(itemIndex) } +@Composable +fun rememberTopBarStyle(defaultStyle: String = "Standard"): String { + val context = LocalContext.current + var resolvedStyle by remember(defaultStyle) { mutableStateOf(defaultStyle) } + LaunchedEffect(context, defaultStyle) { + runCatching { + val prefs = context.settingsDataStore.data.first() + val saved = prefs.asMap().entries + .firstOrNull { (key, _) -> key.name.endsWith("top_bar_style") } + ?.value as? String + resolvedStyle = saved ?: defaultStyle + } + } + return resolvedStyle +} + @OptIn(ExperimentalTvMaterial3Api::class) @Composable fun AppTopBar( @@ -100,8 +116,24 @@ fun AppTopBar( clockFormat: String = "24h", syncStatus: com.arflix.tv.data.repository.CloudSyncStatus = com.arflix.tv.data.repository.CloudSyncStatus.NOT_SIGNED_IN, hasUpdateBadge: Boolean = false, + topBarStyle: String? = null, modifier: Modifier = Modifier ) { + val currentStyle = topBarStyle ?: rememberTopBarStyle() + if (currentStyle.equals("Pill", ignoreCase = true)) { + TopPillNavigationBar( + selectedItem = selectedItem, + isFocused = isFocused, + focusedIndex = focusedIndex, + profile = profile, + profileCount = profileCount, + clockFormat = clockFormat, + hasUpdateBadge = hasUpdateBadge, + modifier = modifier + ) + return + } + val showProfile = profile != null val hasProfile = showProfile val currentTime = rememberTopBarTime(clockFormat) @@ -269,7 +301,7 @@ private fun TopBarNavChip( * of the top bar per the mockup. Receives focus/selection state for D-pad nav. */ @Composable -private fun TopBarSettingsGear( +internal fun TopBarSettingsGear( isFocused: Boolean, isSelected: Boolean, hasBadge: Boolean = false @@ -339,7 +371,7 @@ private fun TopBarSettingsGear( */ @OptIn(ExperimentalTvMaterial3Api::class) @Composable -private fun TopBarProfileAvatar( +internal fun TopBarProfileAvatar( profile: Profile, isFocused: Boolean ) { @@ -383,7 +415,7 @@ private fun TopBarProfileAvatar( } @Composable -private fun rememberTopBarTime(clockFormat: String): String { +internal fun rememberTopBarTime(clockFormat: String): String { val context = LocalContext.current var resolvedFormat by remember(clockFormat) { mutableStateOf(clockFormat) } var currentTime by remember(resolvedFormat) { mutableStateOf(topBarCurrentTime(resolvedFormat)) } diff --git a/app/src/main/kotlin/com/arflix/tv/ui/components/TopPillNavigationBar.kt b/app/src/main/kotlin/com/arflix/tv/ui/components/TopPillNavigationBar.kt new file mode 100644 index 000000000..aea55313c --- /dev/null +++ b/app/src/main/kotlin/com/arflix/tv/ui/components/TopPillNavigationBar.kt @@ -0,0 +1,323 @@ +package com.arflix.tv.ui.components + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +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.draw.drawWithCache +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.layout.onPlaced +import androidx.compose.ui.layout.positionInParent +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.tv.material3.ExperimentalTvMaterial3Api +import androidx.tv.material3.Text +import com.arflix.tv.R +import com.arflix.tv.data.model.Profile +import com.arflix.tv.ui.skin.resolveAccentColor +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch + +private val PillHeight = 54.dp +private val PillCorner = 27.dp +private val NAV_ITEMS = SidebarItem.entries.filter { it != SidebarItem.SETTINGS } + +/** + * Two independently sprung edges for a fluid liquid lens effect. + */ +@Stable +private class LiquidIndicator { + val left = Animatable(0f) + val right = Animatable(0f) + var placed by mutableStateOf(false) + private set + private var restWidth = 1f + + suspend fun moveTo(targetLeft: Float, targetRight: Float) { + restWidth = (targetRight - targetLeft).coerceAtLeast(1f) + if (!placed) { + left.snapTo(targetLeft) + right.snapTo(targetRight) + placed = true + return + } + val movingRight = targetLeft > left.value + val lead = spring(dampingRatio = 0.72f, stiffness = 520f) + val trail = spring(dampingRatio = 0.86f, stiffness = 210f) + coroutineScope { + launch { left.animateTo(targetLeft, if (movingRight) trail else lead) } + launch { right.animateTo(targetRight, if (movingRight) lead else trail) } + } + } + + fun stretch(): Float = ((right.value - left.value) / restWidth - 1f).coerceIn(0f, 1.5f) / 1.5f +} + +@OptIn(ExperimentalTvMaterial3Api::class) +@Composable +fun TopPillNavigationBar( + selectedItem: SidebarItem, + isFocused: Boolean, + focusedIndex: Int, + profile: Profile? = null, + profileCount: Int = 1, + clockFormat: String = "24h", + hasUpdateBadge: Boolean = false, + modifier: Modifier = Modifier +) { + val showProfile = profile != null + val hasProfile = showProfile + val currentTime = rememberTopBarTime(clockFormat) + val selectedIndex = remember(selectedItem, hasProfile) { topBarSelectedIndex(selectedItem, hasProfile) } + val settingsIndex = topBarMaxIndex(hasProfile) + val settingsFocused = isFocused && focusedIndex == settingsIndex + val settingsSelected = selectedItem == SidebarItem.SETTINGS + + val itemBounds = remember { mutableStateMapOf>() } + var rowOffset by remember { mutableStateOf(0f) } + val indicator = remember { LiquidIndicator() } + + // Animate fluid indicator lens to the focused or selected item + val activeIndex = if (isFocused) focusedIndex else selectedIndex + val targetBounds = itemBounds[activeIndex] + + LaunchedEffect(targetBounds, rowOffset) { + if (targetBounds != null) { + val start = rowOffset + targetBounds.first + val end = start + targetBounds.second + indicator.moveTo(start, end) + } + } + + Box( + modifier = modifier + .testTag("app-topbar-pill") + .fillMaxWidth() + .height(AppTopBarContentTopInset) + .padding(top = 16.dp, start = AppTopBarHorizontalPadding, end = AppTopBarHorizontalPadding), + contentAlignment = Alignment.TopCenter + ) { + // Floating Frosted Glass Capsule + Box( + modifier = Modifier + .height(PillHeight) + .clip(RoundedCornerShape(PillCorner)) + .background(Color(0xFF141418).copy(alpha = 0.82f)) + .drawWithCache { + val corner = PillCorner.toPx() + val radius = CornerRadius(corner) + val frost = Brush.verticalGradient( + 0f to Color.White.copy(alpha = 0.22f), + 0.5f to Color.White.copy(alpha = 0.08f), + 1f to Color.White.copy(alpha = 0.14f), + ) + val rim = Brush.linearGradient( + 0f to Color.White.copy(alpha = 0.65f), + 0.4f to Color.White.copy(alpha = 0.15f), + 0.8f to Color.White.copy(alpha = 0.08f), + 1f to Color.White.copy(alpha = 0.35f), + start = Offset.Zero, + end = Offset(size.width, size.height), + ) + val rimStroke = Stroke(width = 1.2.dp.toPx()) + val shadowStep = 1.dp.toPx() + val shadow = Color.Black.copy(alpha = 0.25f) + + onDrawBehind { + // Cast subtle drop shadow + for (step in 1..3) { + drawRoundRect(shadow, topLeft = Offset(0f, step * shadowStep), cornerRadius = radius) + } + // Liquid lens highlight + if (indicator.placed) { + val stretch = indicator.stretch() + val squash = 1f - 0.15f * stretch + val lensH = size.height * squash + val topY = (size.height - lensH) / 2f + val width = indicator.right.value - indicator.left.value + if (width > 0f) { + val lensFill = Brush.verticalGradient( + 0f to Color.White.copy(alpha = 0.32f), + 0.6f to Color.White.copy(alpha = 0.16f), + 1f to Color.White.copy(alpha = 0.22f), + ) + val lensRim = Brush.verticalGradient( + 0f to Color.White.copy(alpha = 0.75f), + 1f to Color.White.copy(alpha = 0.20f), + ) + drawRoundRect( + brush = lensFill, + topLeft = Offset(indicator.left.value, topY), + size = Size(width, lensH), + cornerRadius = CornerRadius(lensH / 2f), + ) + drawRoundRect( + brush = lensRim, + topLeft = Offset(indicator.left.value, topY), + size = Size(width, lensH), + cornerRadius = CornerRadius(lensH / 2f), + style = Stroke(width = 1.dp.toPx()), + ) + } + } + // Frosted sheen & specular border rim + drawRoundRect(frost, cornerRadius = radius) + drawRoundRect(rim, cornerRadius = radius, style = rimStroke) + } + } + .padding(horizontal = 14.dp), + contentAlignment = Alignment.Center + ) { + Row( + modifier = Modifier.onPlaced { + rowOffset = it.positionInParent().x + }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + // Profile Avatar (if present) + if (showProfile && profile != null) { + Box( + modifier = Modifier + .onPlaced { itemBounds[0] = it.positionInParent().x to it.size.width.toFloat() } + .padding(end = 4.dp), + contentAlignment = Alignment.Center + ) { + TopBarProfileAvatar( + profile = profile, + isFocused = isFocused && focusedIndex == 0 + ) + } + } + + // Nav Items (Search, Home, Watchlist, TV) + NAV_ITEMS.forEachIndexed { index, item -> + val itemFocusIndex = if (hasProfile) index + 1 else index + val isItemFocused = isFocused && focusedIndex == itemFocusIndex + val isItemSelected = selectedIndex == itemFocusIndex + + PillNavChip( + item = item, + isFocused = isItemFocused, + isSelected = isItemSelected, + modifier = Modifier.onPlaced { + itemBounds[itemFocusIndex] = it.positionInParent().x to it.size.width.toFloat() + } + ) + } + + // Settings Gear + Box( + modifier = Modifier.onPlaced { + itemBounds[settingsIndex] = it.positionInParent().x to it.size.width.toFloat() + }, + contentAlignment = Alignment.Center + ) { + TopBarSettingsGear( + isFocused = settingsFocused, + isSelected = settingsSelected, + hasBadge = hasUpdateBadge + ) + } + + // Clock divider & time display + Spacer( + modifier = Modifier + .width(1.dp) + .height(18.dp) + .background(Color.White.copy(alpha = 0.2f)) + ) + + Text( + text = currentTime, + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = Color.White.copy(alpha = 0.75f), + modifier = Modifier.padding(horizontal = 6.dp) + ) + } + } + } +} + +@Composable +private fun PillNavChip( + item: SidebarItem, + isFocused: Boolean, + isSelected: Boolean, + modifier: Modifier = Modifier +) { + val accent = resolveAccentColor(fallback = Color.White) + val label = if (item == SidebarItem.TV) stringResource(R.string.topbar_tv) else stringResource(item.labelRes) + + val contentAlpha by animateFloatAsState( + targetValue = if (isFocused || isSelected) 1f else 0.65f, + animationSpec = tween(120), + label = "pill_chip_alpha" + ) + + val scale by animateFloatAsState( + targetValue = if (isFocused) 1.05f else 1f, + animationSpec = spring(dampingRatio = 0.8f, stiffness = Spring.StiffnessMedium), + label = "pill_chip_scale" + ) + + Row( + modifier = modifier + .testTag("topbar-pill-item-${item.name}") + .graphicsLayer { + scaleX = scale + scaleY = scale + alpha = contentAlpha + } + .padding(horizontal = 14.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + imageVector = item.icon, + contentDescription = label, + tint = if (isSelected && !isFocused) accent else Color.White, + modifier = Modifier.size(18.dp) + ) + Text( + text = label, + fontSize = 14.sp, + fontWeight = if (isFocused || isSelected) FontWeight.SemiBold else FontWeight.Medium, + color = if (isSelected && !isFocused) accent else Color.White + ) + } +} diff --git a/app/src/main/kotlin/com/arflix/tv/ui/screens/settings/SettingsScreen.kt b/app/src/main/kotlin/com/arflix/tv/ui/screens/settings/SettingsScreen.kt index bacd0b850..c89361d4c 100644 --- a/app/src/main/kotlin/com/arflix/tv/ui/screens/settings/SettingsScreen.kt +++ b/app/src/main/kotlin/com/arflix/tv/ui/screens/settings/SettingsScreen.kt @@ -328,7 +328,7 @@ private fun tvGeneralRowsForSection(section: String): List { listOf(39, 9, 45) "ai_subtitles" -> listOf(28, 29, 30, 31, 32, 33) "playback" -> listOf(10, 11, 12, 43, 44, 13, 14, 34, 37, 16, 15, 40, 27) - "appearance" -> listOf(17, 18, 20, 21, 24, 23, 22, 41, 46, 36, 47) + "appearance" -> listOf(17, 49, 18, 20, 21, 24, 23, 22, 41, 46, 36, 47) "profiles" -> listOf(19) "network" -> listOf(25, 26, 35) else -> emptyList() @@ -1439,6 +1439,7 @@ fun SettingsScreen( 15 -> viewModel.cycleFrameRateMatchingMode() 16 -> showQualityFiltersModal = true 17 -> viewModel.toggleCardLayoutMode() + 49 -> viewModel.toggleTopBarStyle() 18 -> openUiModeWarningDialog() 19 -> viewModel.setSkipProfileSelection(!uiState.skipProfileSelection) 20 -> viewModel.setOledBlackBackground(!uiState.oledBlackBackground) @@ -2047,6 +2048,7 @@ fun SettingsScreen( defaultAudioLanguage = uiState.defaultAudioLanguage, dnsProvider = uiState.dnsProvider, cardLayoutMode = uiState.cardLayoutMode, + topBarStyle = uiState.topBarStyle, frameRateMatchingMode = uiState.frameRateMatchingMode, autoPlayNext = uiState.autoPlayNext, autoPlaySingleSource = uiState.autoPlaySingleSource, @@ -2071,6 +2073,7 @@ fun SettingsScreen( onSecondarySubtitleClick = openSecondarySubtitlePicker, onAudioLanguageClick = openAudioLanguagePicker, onCardLayoutToggle = { viewModel.toggleCardLayoutMode() }, + onTopBarStyleToggle = { viewModel.toggleTopBarStyle() }, onFrameRateMatchingClick = { viewModel.cycleFrameRateMatchingMode() }, onDnsProviderClick = openDnsProviderPicker, onAutoPlayToggle = { viewModel.setAutoPlayNext(it) }, @@ -5987,6 +5990,14 @@ private fun MobileSettingsSubPage( isFocused = false, onClick = { viewModel.toggleCardLayoutMode() } ) + MobileSettingsRow( + icon = Icons.Default.Tune, + title = stringResource(R.string.settings_top_bar_style), + subtitle = stringResource(R.string.settings_top_bar_style_desc), + value = if (uiState.topBarStyle == "Pill") stringResource(R.string.settings_top_bar_pill) else stringResource(R.string.settings_top_bar_standard), + isFocused = false, + onClick = { viewModel.toggleTopBarStyle() } + ) MobileSettingsRow( icon = Icons.Default.Palette, title = stringResource(R.string.oled_black_background), @@ -7122,6 +7133,7 @@ private fun TvGeneralSettingsRows( contentLanguage: String = "en-US", dnsProvider: String, cardLayoutMode: String, + topBarStyle: String = "Standard", frameRateMatchingMode: String, autoPlayNext: Boolean, autoPlaySingleSource: Boolean, @@ -7149,6 +7161,7 @@ private fun TvGeneralSettingsRows( onSecondarySubtitleClick: () -> Unit = {}, onAudioLanguageClick: () -> Unit, onCardLayoutToggle: () -> Unit, + onTopBarStyleToggle: () -> Unit = {}, onFrameRateMatchingClick: () -> Unit, onDnsProviderClick: () -> Unit, onAutoPlayToggle: (Boolean) -> Unit, @@ -7274,6 +7287,15 @@ private fun TvGeneralSettingsRows( 15 -> SettingsRow(Icons.Default.Movie, stringResource(R.string.frame_rate), stringResource(R.string.frame_rate_desc), frameRateMatchingMode, focusedIndex == localIndex, onFrameRateMatchingClick, Modifier.settingsFocusSlot(localIndex)) 16 -> SettingsRow(Icons.Default.HighQuality, stringResource(R.string.quality_filters), stringResource(R.string.quality_filters_desc), qualityFilterValue, focusedIndex == localIndex, onQualityFiltersClick, Modifier.settingsFocusSlot(localIndex)) 17 -> SettingsRow(Icons.Default.Widgets, stringResource(R.string.card_layout), stringResource(R.string.card_layout_desc), cardLayoutMode, focusedIndex == localIndex, onCardLayoutToggle, Modifier.settingsFocusSlot(localIndex)) + 49 -> SettingsRow( + icon = Icons.Default.Tune, + title = stringResource(R.string.settings_top_bar_style), + subtitle = stringResource(R.string.settings_top_bar_style_desc), + value = if (topBarStyle == "Pill") stringResource(R.string.settings_top_bar_pill) else stringResource(R.string.settings_top_bar_standard), + isFocused = focusedIndex == localIndex, + onClick = onTopBarStyleToggle, + modifier = Modifier.settingsFocusSlot(localIndex) + ) 18 -> SettingsRow( icon = Icons.Default.Settings, title = stringResource(R.string.ui_mode), diff --git a/app/src/main/kotlin/com/arflix/tv/ui/screens/settings/SettingsViewModel.kt b/app/src/main/kotlin/com/arflix/tv/ui/screens/settings/SettingsViewModel.kt index 46c87a288..07edd2ac0 100644 --- a/app/src/main/kotlin/com/arflix/tv/ui/screens/settings/SettingsViewModel.kt +++ b/app/src/main/kotlin/com/arflix/tv/ui/screens/settings/SettingsViewModel.kt @@ -211,6 +211,7 @@ data class SettingsUiState( val defaultAudioLanguage: String = "Auto (Original)", val audioLanguageOptions: List = emptyList(), val cardLayoutMode: String = CARD_LAYOUT_MODE_LANDSCAPE, + val topBarStyle: String = "Standard", val frameRateMatchingMode: String = "Off", val autoPlayNext: Boolean = true, val autoPlaySingleSource: Boolean = true, @@ -469,6 +470,8 @@ class SettingsViewModel @Inject constructor( private fun subtitleUsageKey() = profileManager.profileStringKey("subtitle_usage_v1") private fun cardLayoutModeKey() = profileManager.profileStringKey("card_layout_mode") private fun cardLayoutModeKeyFor(profileId: String) = profileManager.profileStringKeyFor(profileId, "card_layout_mode") + private fun topBarStyleKey() = profileManager.profileStringKey("top_bar_style") + private fun topBarStyleKeyFor(profileId: String) = profileManager.profileStringKeyFor(profileId, "top_bar_style") private fun frameRateMatchingModeKey() = profileManager.profileStringKey("frame_rate_matching_mode") private fun frameRateMatchingModeKeyFor(profileId: String) = profileManager.profileStringKeyFor(profileId, "frame_rate_matching_mode") private fun autoPlayNextKey() = profileManager.profileBooleanKey("auto_play_next") @@ -686,6 +689,7 @@ class SettingsViewModel @Inject constructor( var defaultSub = prefs[defaultSubtitleKey()] ?: "Off" val defaultAudio = prefs[defaultAudioLanguageKey()] ?: "Auto (Original)" val cardLayoutMode = normalizeCardLayoutMode(prefs[cardLayoutModeKey()]) + val topBarStyle = prefs[topBarStyleKey()] ?: "Standard" val frameRateMode = normalizeFrameRateMode(prefs[frameRateMatchingModeKey()]) val deviceModeOverride = prefs[com.arflix.tv.util.DEVICE_MODE_OVERRIDE_KEY] ?: "auto" val skipProfileSelection = prefs[com.arflix.tv.util.SKIP_PROFILE_SELECTION_KEY] ?: false @@ -820,6 +824,7 @@ class SettingsViewModel @Inject constructor( defaultAudioLanguage = defaultAudio, audioLanguageOptions = audioLanguageOptions, cardLayoutMode = cardLayoutMode, + topBarStyle = topBarStyle, frameRateMatchingMode = frameRateMode, autoPlayNext = autoPlay, autoPlaySingleSource = autoPlaySingleSource, @@ -1764,6 +1769,26 @@ class SettingsViewModel @Inject constructor( } } + fun toggleTopBarStyle() { + val next = if (_uiState.value.topBarStyle.equals("Pill", ignoreCase = true)) { + "Standard" + } else { + "Pill" + } + setTopBarStyle(next) + } + + fun setTopBarStyle(style: String) { + val normalized = if (style.equals("Pill", ignoreCase = true)) "Pill" else "Standard" + viewModelScope.launch { + context.settingsDataStore.edit { prefs -> + prefs[topBarStyleKey()] = normalized + } + _uiState.value = _uiState.value.copy(topBarStyle = normalized) + syncLocalStateToCloud(silent = true) + } + } + /** Set content/metadata language for TMDB (e.g. "en-US", "fr-FR", "nl-NL"). */ fun setContentLanguage(lang: String) { viewModelScope.launch { diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 8433939ae..dc96c338a 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -295,4 +295,10 @@ הוספת התוסף נכשלה קטלוג TMDB השתמש בכתובת של דף TMDB מסוג list, collection, company, network, person, keyword או genre + + + סגנון תפריט עליון + בחירה בין תפריט עליון רגיל לקפסולת זכוכית מרחפת + רגיל + קפסולה מרחפת diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 91552b4bf..b9719561e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -205,6 +205,10 @@ Off, Seamless, or Always Exclude quality tiers on this device Landscape or poster cards + Top Bar Style + Choose between standard top bar and floating frosted glass pill + Standard + Floating Pill Force TV, Tablet, or Phone Auto-load last used profile after restart Use pure black app backgrounds for OLED screens