From e076f1706151bff052380dcc230778d4cdd43943 Mon Sep 17 00:00:00 2001
From: beyond
Date: Mon, 31 Aug 2026 20:51:37 +0800
Subject: [PATCH] =?UTF-8?q?=E5=B0=8F=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../com/example/myapplication/MainApp.kt | 92 ++++----
.../myapplication/navigation/AppNavHost.kt | 49 +++--
.../myapplication/screens/HomeScreen.kt | 89 ++++----
.../myapplication/screens/LibraryScreen.kt | 78 ++++---
.../myapplication/screens/RecordsScreen.kt | 88 +++++---
.../myapplication/screens/SearchScreen.kt | 37 ++--
.../myapplication/screens/SettingsScreen.kt | 2 +-
.../example/myapplication/ui/AppBottomBar.kt | 196 ++----------------
.../example/myapplication/ui/LiquidGlass.kt | 44 +++-
.../ui/liquidglass/LiquidBottomTab.kt | 10 +-
.../ui/liquidglass/LiquidBottomTabs.kt | 64 +++---
11 files changed, 336 insertions(+), 413 deletions(-)
diff --git a/app/src/main/kotlin/com/example/myapplication/MainApp.kt b/app/src/main/kotlin/com/example/myapplication/MainApp.kt
index 5c763e7..75151aa 100644
--- a/app/src/main/kotlin/com/example/myapplication/MainApp.kt
+++ b/app/src/main/kotlin/com/example/myapplication/MainApp.kt
@@ -195,52 +195,56 @@ fun MainApp() {
Box(
Modifier
.fillMaxSize()
- .then(if (showBottomBar && glassBackdrop != null) Modifier.layerBackdrop(glassBackdrop) else Modifier),
+ // Keep the page background inside the same capture layer
+ // used by the bottom bar. This makes transparent page
+ // regions render consistently through Backdrop as well.
+ .background(backgroundBrush)
+ .then(if (glassBackdrop != null) Modifier.layerBackdrop(glassBackdrop) else Modifier),
) {
androidx.compose.runtime.CompositionLocalProvider(LocalGlassBackdrop provides null) {
- AppNavHost(
- navController = navController,
- servers = servers,
- selectedServer = selectedServer,
- iconLibraries = iconLibraries,
- onSelectServer = selectServer,
- onAddServer = addServer,
- onDeleteServer = { id ->
- coroutineScope.launch { ServerRepository.deleteServer(id) }
- if (selectedServerId == id) selectedServerId = ""
- },
- onUpdateServer = updateServer,
- changeIconTarget = changeIconTarget,
- onDismissChangeIcon = { changeIconTarget = null },
- onPickIcon = { server, iconUrl -> ServerRepository.setRuntimeIcon(server.id, iconUrl) },
- onRequestChangeIcon = { server -> changeIconTarget = server },
- onSwitchServerAddress = { serverId, addressId ->
- coroutineScope.launch {
- ServerRepository.setActiveAddress(serverId, addressId)
- ServerRepository.checkServerOnline(serverId)
- }
- },
- onAddIconLibrary = { name, url, icons, isDefault ->
- val library = IconLibrary(
- id = "lib_${System.currentTimeMillis()}",
- name = name,
- sourceUrl = url,
- iconCount = icons.size,
- isDefault = isDefault,
- icons = icons,
- )
- coroutineScope.launch { IconLibraryRepository.addLibrary(library) }
- },
- onUpdateIconLibrary = { library ->
- coroutineScope.launch { IconLibraryRepository.updateLibrary(library) }
- },
- onDeleteIconLibrary = { id ->
- coroutineScope.launch { IconLibraryRepository.removeLibrary(id) }
- },
- onRefreshIconLibrary = { id ->
- coroutineScope.launch { IconLibraryRepository.refreshLibrary(id) }
- },
- )
+ AppNavHost(
+ navController = navController,
+ servers = servers,
+ selectedServer = selectedServer,
+ iconLibraries = iconLibraries,
+ onSelectServer = selectServer,
+ onAddServer = addServer,
+ onDeleteServer = { id ->
+ coroutineScope.launch { ServerRepository.deleteServer(id) }
+ if (selectedServerId == id) selectedServerId = ""
+ },
+ onUpdateServer = updateServer,
+ changeIconTarget = changeIconTarget,
+ onDismissChangeIcon = { changeIconTarget = null },
+ onPickIcon = { server, iconUrl -> ServerRepository.setRuntimeIcon(server.id, iconUrl) },
+ onRequestChangeIcon = { server -> changeIconTarget = server },
+ onSwitchServerAddress = { serverId, addressId ->
+ coroutineScope.launch {
+ ServerRepository.setActiveAddress(serverId, addressId)
+ ServerRepository.checkServerOnline(serverId)
+ }
+ },
+ onAddIconLibrary = { name, url, icons, isDefault ->
+ val library = IconLibrary(
+ id = "lib_${System.currentTimeMillis()}",
+ name = name,
+ sourceUrl = url,
+ iconCount = icons.size,
+ isDefault = isDefault,
+ icons = icons,
+ )
+ coroutineScope.launch { IconLibraryRepository.addLibrary(library) }
+ },
+ onUpdateIconLibrary = { library ->
+ coroutineScope.launch { IconLibraryRepository.updateLibrary(library) }
+ },
+ onDeleteIconLibrary = { id ->
+ coroutineScope.launch { IconLibraryRepository.removeLibrary(id) }
+ },
+ onRefreshIconLibrary = { id ->
+ coroutineScope.launch { IconLibraryRepository.refreshLibrary(id) }
+ },
+ )
}
}
diff --git a/app/src/main/kotlin/com/example/myapplication/navigation/AppNavHost.kt b/app/src/main/kotlin/com/example/myapplication/navigation/AppNavHost.kt
index c774d88..ac4662b 100644
--- a/app/src/main/kotlin/com/example/myapplication/navigation/AppNavHost.kt
+++ b/app/src/main/kotlin/com/example/myapplication/navigation/AppNavHost.kt
@@ -4,6 +4,8 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -16,6 +18,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
@@ -45,6 +48,9 @@ import com.example.myapplication.screens.SettingsScreen
import com.example.myapplication.screens.SettingsTopBar
import com.example.myapplication.screens.VideoPlayerScreen
import com.example.myapplication.ui.design.LumaLoading
+import com.example.myapplication.ui.design.LumaColors
+import com.example.myapplication.ui.design.rememberLumaLayerBackdrop
+import com.kyant.backdrop.backdrops.layerBackdrop
import kotlinx.coroutines.CancellationException
import top.yukonga.miuix.kmp.basic.Text
@@ -117,21 +123,36 @@ fun AppNavHost(
composable(AppRoute.Records.pattern) {
var segment by rememberSaveable { mutableStateOf(0) }
- Column(Modifier.fillMaxSize()) {
- RecordsTopBar(selected = segment, onSelect = { segment = it })
- Box(Modifier.weight(1f)) {
- RecordsScreen(
- segment = segment,
- servers = servers,
- activeServer = selectedServer,
- onOpenDetail = { serverId, itemId -> openDetail(serverId, itemId) },
- onViewAll = { serverId, type ->
- navController.navigate(AppRoute.FavoritesAll.create(serverId, type)) {
- launchSingleTop = true
- }
- },
- )
+ val recordsBackdrop = rememberLumaLayerBackdrop()
+ Box(Modifier.fillMaxSize()) {
+ Box(
+ Modifier
+ .fillMaxSize()
+ .background(LumaColors.background)
+ .then(if (recordsBackdrop != null) Modifier.layerBackdrop(recordsBackdrop) else Modifier),
+ ) {
+ Column(Modifier.fillMaxSize()) {
+ Spacer(Modifier.height(120.dp))
+ Box(Modifier.weight(1f)) {
+ RecordsScreen(
+ segment = segment,
+ servers = servers,
+ activeServer = selectedServer,
+ onOpenDetail = { serverId, itemId -> openDetail(serverId, itemId) },
+ onViewAll = { serverId, type ->
+ navController.navigate(AppRoute.FavoritesAll.create(serverId, type)) {
+ launchSingleTop = true
+ }
+ },
+ )
+ }
+ }
}
+ RecordsTopBar(
+ selected = segment,
+ backdrop = recordsBackdrop,
+ onSelect = { segment = it },
+ )
}
}
diff --git a/app/src/main/kotlin/com/example/myapplication/screens/HomeScreen.kt b/app/src/main/kotlin/com/example/myapplication/screens/HomeScreen.kt
index e513115..3a9c9d1 100644
--- a/app/src/main/kotlin/com/example/myapplication/screens/HomeScreen.kt
+++ b/app/src/main/kotlin/com/example/myapplication/screens/HomeScreen.kt
@@ -77,6 +77,7 @@ import com.example.myapplication.data.EmbyServer
import com.example.myapplication.ui.GlassDialog
import com.example.myapplication.ui.GlassTokens
import com.example.myapplication.ui.LiquidGlassPopupOverlay
+import com.example.myapplication.ui.LumaEmptyStateContainer
import com.example.myapplication.ui.PosterImmersivePalette
import com.example.myapplication.ui.rememberPosterImmersiveBackground
import com.example.myapplication.ui.bottomBarContentPadding
@@ -84,7 +85,7 @@ import com.example.myapplication.ui.design.LumaIcon
import com.example.myapplication.ui.design.LumaIconView
import com.example.myapplication.ui.design.LumaAsyncImage
import com.example.myapplication.ui.design.LumaMediaCard
-import com.example.myapplication.ui.design.LumaEmptyState
+import com.example.myapplication.ui.design.LumaColors
import com.example.myapplication.ui.design.LumaErrorState
import com.example.myapplication.ui.design.LumaLoading
import com.example.myapplication.ui.design.LumaMotion
@@ -730,6 +731,8 @@ fun HomeScreen(
val homeListState = rememberLazyListState()
val activeHeroItem = heroItems.getOrNull(heroPagerState.currentPage)
val immersiveBackground = rememberPosterImmersiveBackground(heroImageUrl(activeHeroItem, activeServer?.fullUrl.orEmpty()))
+ val hasHomeContent = heroItems.isNotEmpty() || resumeItems.isNotEmpty() || libraries.isNotEmpty()
+ val useImmersiveHomeBackground = activeServer != null && hasHomeContent
LaunchedEffect(homeKey, reloadToken) {
val server = activeServer
@@ -779,9 +782,32 @@ fun HomeScreen(
}
}
- val adaptiveText = if (immersiveBackground.isDark) Color.White else Color.Black
- val adaptiveSecondary = adaptiveText.copy(alpha = 0.7f)
- val adaptiveAccent = if (immersiveBackground.isDark) Color(0xFF6EA8FF) else Color(0xFF0061A4)
+ val adaptiveText = if (useImmersiveHomeBackground) {
+ if (immersiveBackground.isDark) Color.White else Color.Black
+ } else {
+ LumaColors.text
+ }
+ val adaptiveSecondary = if (useImmersiveHomeBackground) adaptiveText.copy(alpha = 0.7f) else LumaColors.textSecondary
+ val adaptiveAccent = if (useImmersiveHomeBackground) {
+ if (immersiveBackground.isDark) Color(0xFF6EA8FF) else Color(0xFF0061A4)
+ } else {
+ LumaColors.accent
+ }
+ val homeBackground = if (useImmersiveHomeBackground) {
+ Brush.verticalGradient(
+ 0f to immersiveBackground.pageTop,
+ 0.54f to immersiveBackground.pageTop,
+ 1f to immersiveBackground.pageBottom,
+ )
+ } else {
+ Brush.linearGradient(
+ listOf(
+ LumaColors.background,
+ LumaColors.backgroundElevated,
+ LumaColors.background,
+ ),
+ )
+ }
CompositionLocalProvider(
LocalHomeContentColor provides adaptiveText,
@@ -796,13 +822,7 @@ fun HomeScreen(
Box(
Modifier
.fillMaxSize()
- .background(
- Brush.verticalGradient(
- 0f to immersiveBackground.pageTop,
- 0.54f to immersiveBackground.pageTop,
- 1f to immersiveBackground.pageBottom,
- )
- )
+ .background(homeBackground)
.then(if (homeBackdrop != null) Modifier.layerBackdrop(homeBackdrop) else Modifier),
) {
AnimatedContent(
@@ -813,9 +833,11 @@ fun HomeScreen(
label = "HomeLoadingTransition"
) { isLoading ->
if (activeServer == null) {
- Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
- EmptyHomeState()
- }
+ LumaEmptyStateContainer(
+ icon = LumaIcon.Server,
+ title = stringResource(Res.string.empty_home_no_server),
+ description = stringResource(Res.string.empty_home_no_server_sub),
+ )
} else if (isLoading) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
LumaLoading(label = stringResource(Res.string.loading_home))
@@ -829,12 +851,18 @@ fun HomeScreen(
onRetry = { reloadToken++ },
)
}
+ } else if (!hasHomeContent) {
+ LumaEmptyStateContainer(
+ icon = LumaIcon.Media,
+ title = stringResource(Res.string.empty_home_no_data),
+ description = stringResource(Res.string.empty_home_no_data_sub),
+ )
} else {
val server = activeServer
LazyColumn(
state = homeListState,
modifier = Modifier.fillMaxSize(),
- contentPadding = PaddingValues(bottom = bottomBarContentPadding(90.dp).calculateBottomPadding())
+ contentPadding = PaddingValues(bottom = bottomBarContentPadding().calculateBottomPadding())
) {
if (heroItems.isNotEmpty()) {
item {
@@ -856,18 +884,6 @@ fun HomeScreen(
.padding(start = LumaSpacing.md, end = LumaSpacing.md, top = LumaSpacing.md),
verticalArrangement = Arrangement.spacedBy(LumaSpacing.xl),
) {
- if (resumeItems.isEmpty() && latestItems.isEmpty() && libraries.isEmpty()) {
- Box(
- Modifier.fillMaxWidth().padding(vertical = LumaSpacing.xxl),
- contentAlignment = Alignment.Center,
- ) {
- LumaEmptyState(
- icon = LumaIcon.Media,
- title = stringResource(Res.string.empty_home_no_data),
- description = stringResource(Res.string.empty_home_no_data_sub),
- )
- }
- }
if (resumeItems.isNotEmpty()) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
SectionTitle(title = stringResource(Res.string.continue_watching))
@@ -954,25 +970,6 @@ fun HomeScreen(
}
}
-@Composable
-private fun EmptyHomeState() {
- Column(
- Modifier.fillMaxWidth().padding(horizontal = 24.dp),
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- Box(
- Modifier.size(64.dp).clip(CircleShape).background(homeSecondaryColor().copy(alpha = 0.16f)),
- contentAlignment = Alignment.Center,
- ) {
- LumaIconView(LumaIcon.Media, contentDescription = null, tint = homeSecondaryColor(), modifier = Modifier.size(32.dp))
- }
- Spacer(Modifier.height(16.dp))
- Text(stringResource(Res.string.empty_home_no_server), color = homeSecondaryColor(), fontSize = 15.sp)
- Spacer(Modifier.height(6.dp))
- Text(stringResource(Res.string.empty_home_no_server_sub), color = homeSecondaryColor().copy(alpha = 0.72f), fontSize = 12.sp)
- }
-}
-
@Composable
private fun HomeOverlayTopBar(
server: EmbyServer?,
diff --git a/app/src/main/kotlin/com/example/myapplication/screens/LibraryScreen.kt b/app/src/main/kotlin/com/example/myapplication/screens/LibraryScreen.kt
index b87f9e5..62cb6a2 100644
--- a/app/src/main/kotlin/com/example/myapplication/screens/LibraryScreen.kt
+++ b/app/src/main/kotlin/com/example/myapplication/screens/LibraryScreen.kt
@@ -16,6 +16,7 @@ import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@@ -46,6 +47,7 @@ import com.example.myapplication.ui.GlassSheet
import com.example.myapplication.ui.GlassInputField
import com.example.myapplication.ui.GlassPopupItem
import com.example.myapplication.ui.LiquidGlassButton
+import com.example.myapplication.ui.LumaEmptyStateContainer
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
@@ -100,7 +102,6 @@ import com.example.myapplication.ui.bottomBarContentPadding
import com.example.myapplication.ui.design.LumaColors
import com.example.myapplication.ui.design.LumaGlassRuntime
import com.example.myapplication.ui.design.rememberLumaLayerBackdrop
-import com.example.myapplication.ui.design.LumaEmptyState
import com.example.myapplication.ui.design.LumaIcon
import com.example.myapplication.ui.design.LumaIconView
import com.example.myapplication.ui.design.LumaAsyncImage
@@ -119,6 +120,7 @@ private val serverTints = listOf(
)
private val onlineGreen = Color(0xFF34C759)
private val offlineRed = Color(0xFFFF3B30)
+private val iconLibraryControlHeight = 48.dp
@Composable
fun LibraryTopBar(
@@ -382,25 +384,16 @@ fun LibraryScreen(
)
CompositionLocalProvider(LocalGlassBackdrop provides libraryBackdrop) {
if (servers.isEmpty()) {
- Box(
- Modifier.fillMaxSize().padding(bottom = bottomBarContentPadding(90.dp).calculateBottomPadding()),
- contentAlignment = Alignment.Center,
- ) {
- Column(
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- LumaEmptyState(
- icon = LumaIcon.Server,
- title = stringResource(Res.string.empty_servers),
- description = stringResource(Res.string.empty_servers_sub),
- )
- }
- }
+ LumaEmptyStateContainer(
+ icon = LumaIcon.Server,
+ title = stringResource(Res.string.empty_servers),
+ description = stringResource(Res.string.empty_servers_sub),
+ )
} else {
LazyVerticalGrid(
columns = if (isGrid) GridCells.Adaptive(minSize = 150.dp) else GridCells.Fixed(1),
modifier = Modifier.fillMaxSize().statusBarsPadding().padding(horizontal = 16.dp).padding(top = 78.dp),
- contentPadding = PaddingValues(bottom = bottomBarContentPadding(86.dp).calculateBottomPadding()),
+ contentPadding = PaddingValues(bottom = bottomBarContentPadding().calculateBottomPadding()),
verticalArrangement = Arrangement.spacedBy(if (isGrid) 12.dp else 10.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
@@ -536,12 +529,6 @@ fun LibraryScreen(
}
}
-private fun managementDialogSurfaceColor(): Color = if (GlassTokens.isDark) {
- Color(0xFF111923).copy(alpha = 0.92f)
-} else {
- Color(0xFFDDE7F1).copy(alpha = 0.94f)
-}
-
@Composable
private fun ServerAddressPickerDialog(
server: EmbyServer,
@@ -726,6 +713,9 @@ internal fun IconLibraryManagerScreenContent(
Box(Modifier.width(86.dp)) {
Box(
Modifier.size(48.dp)
+ .clip(CircleShape)
+ .background(LumaColors.floatingTint)
+ .border(0.6.dp, LumaColors.glassBorder, CircleShape)
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() },
@@ -733,13 +723,7 @@ internal fun IconLibraryManagerScreenContent(
) { onBack() },
contentAlignment = Alignment.Center,
) {
- Box(
- Modifier.size(34.dp)
- .lumaGlass(radius = 17.dp, level = com.example.myapplication.ui.design.LumaGlassLevel.Floating),
- contentAlignment = Alignment.Center,
- ) {
- LumaIconView(LumaIcon.Close, contentDescription = stringResource(Res.string.back), tint = GlassTokens.text, modifier = Modifier.size(19.dp))
- }
+ LumaIconView(LumaIcon.Back, contentDescription = stringResource(Res.string.back), tint = GlassTokens.text, modifier = Modifier.size(20.dp))
}
}
Box(Modifier.weight(1f), contentAlignment = Alignment.Center) {
@@ -958,7 +942,7 @@ private fun ManageIconLibraryDialog(
backdrop = backdrop,
onDismissRequest = onDismiss,
modifier = Modifier.width(320.dp),
- surfaceOverlay = managementDialogSurfaceColor(),
+ surfaceOverlay = Color.White.copy(alpha = 0.94f),
) {
Column(
modifier = Modifier.fillMaxWidth()
@@ -988,8 +972,8 @@ private fun ManageIconLibraryDialog(
}
// Editable library metadata
- GlassInputField(label = stringResource(Res.string.icon_lib_name_label), value = editName, placeholder = stringResource(Res.string.icon_lib_name_label), onValueChange = { editName = it }, containerTint = Color.Transparent, useBackdropGlass = false)
- GlassInputField(label = stringResource(Res.string.icon_lib_url_label), value = editUrl, placeholder = "https://...", onValueChange = { editUrl = it }, containerTint = Color.Transparent, useBackdropGlass = false)
+ GlassInputField(label = stringResource(Res.string.icon_lib_name_label), value = editName, placeholder = stringResource(Res.string.icon_lib_name_label), onValueChange = { editName = it }, containerTint = Color.Transparent, useBackdropGlass = false, fieldHeight = iconLibraryControlHeight)
+ GlassInputField(label = stringResource(Res.string.icon_lib_url_label), value = editUrl, placeholder = "https://...", onValueChange = { editUrl = it }, containerTint = Color.Transparent, useBackdropGlass = false, fieldHeight = iconLibraryControlHeight)
// Default library switch
Row(
@@ -998,7 +982,8 @@ private fun ManageIconLibraryDialog(
.background(if (setAsDefault) LumaColors.accentSoft else Color.Transparent)
.border(0.7.dp, if (setAsDefault) GlassTokens.accent.copy(alpha = 0.35f) else LumaColors.glassBorder, RoundedCornerShape(16.dp))
.clickable { setAsDefault = !setAsDefault }
- .padding(horizontal = 14.dp, vertical = 10.dp),
+ .height(iconLibraryControlHeight)
+ .padding(horizontal = 14.dp, vertical = 7.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Column(Modifier.weight(1f)) {
@@ -1113,7 +1098,7 @@ private fun AddIconLibraryDialog(
backdrop = backdrop,
onDismissRequest = onDismiss,
modifier = Modifier.width(320.dp),
- surfaceOverlay = managementDialogSurfaceColor(),
+ surfaceOverlay = Color.White.copy(alpha = 0.94f),
) {
Box(Modifier.fillMaxWidth()) {
Column(
@@ -1139,6 +1124,7 @@ private fun AddIconLibraryDialog(
onValueChange = { name = it },
containerTint = Color.Transparent,
useBackdropGlass = false,
+ fieldHeight = iconLibraryControlHeight,
)
// Default library switch
@@ -1148,7 +1134,8 @@ private fun AddIconLibraryDialog(
.background(if (isDefault) LumaColors.accentSoft else Color.Transparent)
.border(0.7.dp, if (isDefault) GlassTokens.accent.copy(alpha = 0.35f) else LumaColors.glassBorder, RoundedCornerShape(16.dp))
.clickable { isDefault = !isDefault }
- .padding(horizontal = 14.dp, vertical = 10.dp),
+ .height(iconLibraryControlHeight)
+ .padding(horizontal = 14.dp, vertical = 7.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Column(Modifier.weight(1f)) {
@@ -1179,6 +1166,7 @@ private fun AddIconLibraryDialog(
},
containerTint = Color.Transparent,
useBackdropGlass = false,
+ fieldHeight = iconLibraryControlHeight,
)
LiquidGlassButton(
@@ -1295,7 +1283,13 @@ private fun IconPickerDialog(
}
}
- GlassSheet(backdrop = backdrop, onDismissRequest = onDismiss, modifier = Modifier.widthIn(max = 420.dp)) {
+ GlassSheet(
+ backdrop = backdrop,
+ onDismissRequest = onDismiss,
+ modifier = Modifier.widthIn(max = 420.dp),
+ surfaceOverlay = Color.White.copy(alpha = 0.94f),
+ horizontalMargin = 16.dp,
+ ) {
Box(Modifier.fillMaxWidth()) {
Column(
verticalArrangement = Arrangement.spacedBy(12.dp),
@@ -1316,11 +1310,12 @@ private fun IconPickerDialog(
if (!isLoading && allIcons.isNotEmpty()) {
Box(
Modifier.fillMaxWidth()
+ .height(iconLibraryControlHeight)
.lumaGlass(radius = 14.dp, level = LumaGlassLevel.Regular, shadowElevation = 0.dp)
- .padding(horizontal = 12.dp, vertical = 9.dp),
+ .padding(horizontal = 12.dp, vertical = 6.dp),
) {
Row(
- modifier = Modifier.fillMaxWidth(),
+ modifier = Modifier.fillMaxSize(),
verticalAlignment = Alignment.CenterVertically,
) {
LumaIconView(
@@ -1330,7 +1325,10 @@ private fun IconPickerDialog(
modifier = Modifier.size(18.dp),
)
Spacer(Modifier.width(8.dp))
- Box(Modifier.weight(1f)) {
+ Box(
+ Modifier.weight(1f).fillMaxHeight(),
+ contentAlignment = Alignment.CenterStart,
+ ) {
if (query.text.isEmpty()) {
Text(
text = stringResource(Res.string.icon_picker_search_hint),
@@ -1351,7 +1349,7 @@ private fun IconPickerDialog(
Spacer(Modifier.width(8.dp))
Box(
Modifier
- .size(48.dp)
+ .size(36.dp)
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() },
diff --git a/app/src/main/kotlin/com/example/myapplication/screens/RecordsScreen.kt b/app/src/main/kotlin/com/example/myapplication/screens/RecordsScreen.kt
index 964997f..98ccd35 100644
--- a/app/src/main/kotlin/com/example/myapplication/screens/RecordsScreen.kt
+++ b/app/src/main/kotlin/com/example/myapplication/screens/RecordsScreen.kt
@@ -62,22 +62,29 @@ import com.example.myapplication.data.WatchlistEntity
import com.example.myapplication.data.WatchlistRepository
import com.example.myapplication.ui.GlassTokens
import com.example.myapplication.ui.LiquidGlassTopBar
-import com.example.myapplication.ui.SegmentedControl
+import com.example.myapplication.ui.LumaEmptyStateContainer
import com.example.myapplication.ui.bottomBarContentPadding
import com.example.myapplication.ui.design.LumaColors
-import com.example.myapplication.ui.design.LumaEmptyState
import com.example.myapplication.ui.design.LumaIcon
import com.example.myapplication.ui.design.LumaIconView
import com.example.myapplication.ui.design.LumaLoading
import com.example.myapplication.ui.design.LumaShapes
+import com.example.myapplication.ui.liquidglass.LiquidBottomTab
+import com.example.myapplication.ui.liquidglass.LiquidBottomTabs
+import com.kyant.backdrop.Backdrop
import top.yukonga.miuix.kmp.basic.Text
@Composable
fun RecordsTopBar(
selected: Int,
+ backdrop: Backdrop? = null,
modifier: Modifier = Modifier,
onSelect: (Int) -> Unit,
) {
+ val labels = listOf(
+ stringResource(Res.string.records_fav),
+ stringResource(Res.string.records_calendar),
+ )
LiquidGlassTopBar(modifier) {
Box(
Modifier
@@ -85,12 +92,41 @@ fun RecordsTopBar(
.padding(horizontal = 16.dp, vertical = 12.dp),
contentAlignment = Alignment.Center,
) {
- SegmentedControl(
- items = listOf(stringResource(Res.string.records_fav), stringResource(Res.string.records_calendar)),
- selected = selected,
- onSelect = onSelect,
+ LiquidBottomTabs(
+ selectedTabIndex = { selected.coerceIn(0, labels.lastIndex) },
+ onTabSelected = onSelect,
+ backdrop = backdrop,
+ tabsCount = labels.size,
modifier = Modifier.width(220.dp),
- )
+ height = 56.dp,
+ surfaceAlpha = 0.16f,
+ pressHighlight = false,
+ ghostContent = {},
+ ) {
+ labels.forEachIndexed { index, label ->
+ LiquidBottomTab(onClick = { onSelect(index) }) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ LumaIconView(
+ icon = if (index == 0) LumaIcon.Favorite else LumaIcon.Calendar,
+ selected = selected == index,
+ tint = if (selected == index) LumaColors.accent else LumaColors.textSecondary,
+ contentDescription = null,
+ modifier = Modifier.size(18.dp),
+ )
+ Spacer(Modifier.width(6.dp))
+ Text(
+ text = label,
+ color = if (selected == index) LumaColors.accent else LumaColors.textSecondary,
+ fontSize = 14.sp,
+ fontWeight = if (selected == index) FontWeight.SemiBold else FontWeight.Medium,
+ maxLines = 1,
+ )
+ }
+ }
+ }
+ }
}
}
}
@@ -133,12 +169,6 @@ fun RecordsScreen(
) { selectedSegment ->
if (selectedSegment == 1) {
CalendarTab(onEpisodeClick = onOpenDetail)
- } else if (servers.isEmpty()) {
- RecordsEmptyState(
- icon = LumaIcon.Search,
- title = stringResource(Res.string.search_no_server),
- subtitle = stringResource(Res.string.search_no_server_sub),
- )
} else if (favoritesByServer.isEmpty()) {
RecordsEmptyState(
icon = LumaIcon.Favorite,
@@ -151,7 +181,7 @@ fun RecordsScreen(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(
top = 8.dp,
- bottom = bottomBarContentPadding(86.dp).calculateBottomPadding(),
+ bottom = bottomBarContentPadding().calculateBottomPadding(),
),
) {
favoritesByServer.forEach { (server, serverFavs) ->
@@ -399,14 +429,12 @@ private fun RecordsEmptyState(
title: String,
subtitle: String,
) {
- Box(
- Modifier
- .fillMaxSize()
- .padding(bottom = bottomBarContentPadding(90.dp).calculateBottomPadding()),
- contentAlignment = Alignment.Center,
- ) {
- LumaEmptyState(icon = icon, title = title, description = subtitle)
- }
+ LumaEmptyStateContainer(
+ icon = icon,
+ title = title,
+ description = subtitle,
+ verticalOffset = (-60).dp,
+ )
}
// ─── Calendar Tab ───────────────────────────────────────────────────────────
@@ -475,20 +503,18 @@ private fun CalendarTab(onEpisodeClick: (String, String) -> Unit = { _, _ -> })
calendarEpisodes
}
if (filteredEpisodes.isEmpty()) {
- Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
- LumaEmptyState(
- icon = LumaIcon.Calendar,
- title = stringResource(Res.string.empty_calendar),
- description = stringResource(Res.string.empty_calendar_sub),
- showIconBackground = false,
- )
- }
+ LumaEmptyStateContainer(
+ icon = LumaIcon.Calendar,
+ title = stringResource(Res.string.empty_calendar),
+ description = stringResource(Res.string.empty_calendar_sub),
+ verticalOffset = (-60).dp,
+ )
} else {
val groupedByDate = filteredEpisodes.groupBy { it.airDate }.toSortedMap()
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(
- bottom = 16.dp + bottomBarContentPadding(86.dp).calculateBottomPadding(),
+ bottom = 16.dp + bottomBarContentPadding().calculateBottomPadding(),
),
) {
groupedByDate.forEach { (dateStr, episodes) ->
diff --git a/app/src/main/kotlin/com/example/myapplication/screens/SearchScreen.kt b/app/src/main/kotlin/com/example/myapplication/screens/SearchScreen.kt
index 375f78b..6d44de1 100644
--- a/app/src/main/kotlin/com/example/myapplication/screens/SearchScreen.kt
+++ b/app/src/main/kotlin/com/example/myapplication/screens/SearchScreen.kt
@@ -57,9 +57,9 @@ import com.example.myapplication.data.EmbyServer
import com.example.myapplication.ui.GlassSearchField
import com.example.myapplication.ui.GlassTokens
import com.example.myapplication.ui.LiquidGlassTopBar
+import com.example.myapplication.ui.LumaEmptyStateContainer
import com.example.myapplication.ui.bottomBarContentPadding
import com.example.myapplication.ui.design.LumaColors
-import com.example.myapplication.ui.design.LumaEmptyState
import com.example.myapplication.ui.design.LumaErrorState
import com.example.myapplication.ui.design.LumaIcon
import com.example.myapplication.ui.design.LumaIconView
@@ -328,8 +328,7 @@ fun SearchScreen(
Box(
Modifier
- .weight(1f)
- .padding(bottom = bottomBarContentPadding(90.dp).calculateBottomPadding()),
+ .weight(1f),
) {
when {
// No servers configured
@@ -367,14 +366,12 @@ fun SearchScreen(
}
// Search complete: all servers returned empty
allEmpty -> {
- Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
- LumaEmptyState(
- icon = LumaIcon.Search,
- title = stringResource(Res.string.search_no_result_title),
- description = stringResource(Res.string.search_no_result_prefix) + query.trim() + stringResource(Res.string.search_no_result_suffix),
- showIconBackground = false,
- )
- }
+ LumaEmptyStateContainer(
+ icon = LumaIcon.Search,
+ title = stringResource(Res.string.search_no_result_title),
+ description = stringResource(Res.string.search_no_result_prefix) + query.trim() + stringResource(Res.string.search_no_result_suffix),
+ verticalOffset = (-58).dp,
+ )
}
// Has results (even if some are still loading or errored)
else -> ServerResultsList(
@@ -410,7 +407,7 @@ private fun SearchHistoryList(
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
- contentPadding = PaddingValues(top = 8.dp, bottom = bottomBarContentPadding(86.dp).calculateBottomPadding()),
+ contentPadding = PaddingValues(top = 8.dp, bottom = bottomBarContentPadding().calculateBottomPadding()),
) {
item(key = "header") {
Row(
@@ -493,7 +490,7 @@ private fun ServerResultsList(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(
top = 8.dp,
- bottom = bottomBarContentPadding(86.dp).calculateBottomPadding(),
+ bottom = bottomBarContentPadding().calculateBottomPadding(),
),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
@@ -718,14 +715,12 @@ private fun ServerErrorSection(server: EmbyServer) {
@Composable
private fun SearchEmptyState(title: String, subtitle: String) {
- Box(
- Modifier
- .fillMaxSize()
- .padding(bottom = bottomBarContentPadding(90.dp).calculateBottomPadding()),
- contentAlignment = Alignment.Center,
- ) {
- LumaEmptyState(icon = LumaIcon.Search, title = title, description = subtitle, showIconBackground = false)
- }
+ LumaEmptyStateContainer(
+ icon = LumaIcon.Search,
+ title = title,
+ description = subtitle,
+ verticalOffset = (-58).dp,
+ )
}
// ─── Media Poster Card ──────────────────────────────────────────────────────
diff --git a/app/src/main/kotlin/com/example/myapplication/screens/SettingsScreen.kt b/app/src/main/kotlin/com/example/myapplication/screens/SettingsScreen.kt
index 78533fc..1fcfa5b 100644
--- a/app/src/main/kotlin/com/example/myapplication/screens/SettingsScreen.kt
+++ b/app/src/main/kotlin/com/example/myapplication/screens/SettingsScreen.kt
@@ -110,7 +110,7 @@ fun SettingsScreen(
Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
- .padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = bottomBarContentPadding(86.dp).calculateBottomPadding()),
+ .padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = bottomBarContentPadding().calculateBottomPadding()),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
LumaSectionLabel(stringResource(R.string.settings_appearance_section))
diff --git a/app/src/main/kotlin/com/example/myapplication/ui/AppBottomBar.kt b/app/src/main/kotlin/com/example/myapplication/ui/AppBottomBar.kt
index bc29bca..d1d161a 100644
--- a/app/src/main/kotlin/com/example/myapplication/ui/AppBottomBar.kt
+++ b/app/src/main/kotlin/com/example/myapplication/ui/AppBottomBar.kt
@@ -1,9 +1,5 @@
package com.example.myapplication.ui
-import androidx.compose.foundation.border
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -12,45 +8,21 @@ import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
-import androidx.compose.runtime.remember
-import androidx.compose.animation.core.animateFloatAsState
-import androidx.compose.animation.core.spring
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.alpha
-import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.ColorFilter
-import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.myapplication.ui.liquidglass.LiquidBottomTab
import com.example.myapplication.ui.liquidglass.LiquidBottomTabs
import com.example.myapplication.ui.design.LumaColors
-import com.example.myapplication.ui.design.LumaGlassRuntime
import com.example.myapplication.ui.design.LumaIcon
import com.example.myapplication.ui.design.LumaAnimatedIcon
-import com.example.myapplication.ui.design.LumaIconView
import com.example.myapplication.ui.design.LumaSpacing
-import com.example.myapplication.ui.design.lumaFallbackGlass
import com.kyant.backdrop.Backdrop
-import com.kyant.backdrop.backdrops.layerBackdrop
-import com.kyant.backdrop.backdrops.rememberCombinedBackdrop
-import com.kyant.backdrop.backdrops.rememberLayerBackdrop
-import com.kyant.backdrop.drawBackdrop
-import com.kyant.backdrop.effects.blur
-import com.kyant.backdrop.effects.lens
-import com.kyant.backdrop.effects.vibrancy
-import com.kyant.backdrop.highlight.Highlight
-import com.kyant.backdrop.shadow.InnerShadow
-import com.kyant.backdrop.shadow.Shadow
-import com.kyant.shapes.Capsule
import top.yukonga.miuix.kmp.basic.Text
@Composable
@@ -72,15 +44,12 @@ fun LiquidAppBottomBar(
Box(modifier = Modifier.weight(1f)) {
LiquidBottomTabs(
selectedTabIndex = { selectedTab.coerceIn(0, 3) },
- onTabSelected = { index ->
- if (selectedTab < 4) onSelect(index)
- },
+ onTabSelected = onSelect,
backdrop = backdrop,
tabsCount = 4,
showSelection = { selectedTab < 4 },
) {
- val tabsCount = mainTabs.size
- for (index in 0 until tabsCount) {
+ for (index in mainTabs.indices) {
val tab = mainTabs[index]
LiquidBottomTab(onClick = { onSelect(index) }) {
LumaAnimatedIcon(
@@ -105,156 +74,23 @@ fun LiquidAppBottomBar(
Spacer(Modifier.width(LumaSpacing.sm))
- val searchSelected = selectedTab == 4
- val safeBackdrop = backdrop?.takeIf { LumaGlassRuntime.useNativeBackdrop }
- val searchContentBackdrop = rememberLayerBackdrop()
- val searchInteraction = remember { MutableInteractionSource() }
- val searchPressed by searchInteraction.collectIsPressedAsState()
- val searchPressProgress by animateFloatAsState(
- targetValue = if (searchPressed) 1f else 0f,
- animationSpec = spring(dampingRatio = 0.62f, stiffness = 420f),
- label = "bottomSearchPressProgress",
- )
- val searchScale by animateFloatAsState(
- targetValue = if (searchPressed) 1.12f else 1f,
- animationSpec = spring(dampingRatio = 0.72f, stiffness = 650f),
- label = "bottomSearchPress",
- )
- Box(
- modifier = Modifier
- .size(width = 64.dp, height = 64.dp)
- .graphicsLayer {
- scaleX = searchScale
- scaleY = searchScale
- }
- .clip(CircleShape)
- .then(
- if (safeBackdrop != null) {
- Modifier.drawBackdrop(
- backdrop = safeBackdrop,
- shape = { Capsule() },
- effects = {
- vibrancy()
- blur(8f.dp.toPx())
- lens(24f.dp.toPx(), 24f.dp.toPx())
- },
- highlight = { Highlight.Default.copy(alpha = searchPressProgress) },
- shadow = { Shadow(alpha = searchPressProgress) },
- onDrawSurface = {
- drawRect(
- if (LumaColors.isDark) {
- Color(0xFF121212).copy(alpha = 0.40f)
- } else {
- Color(0xFFFAFAFA).copy(alpha = 0.40f)
- },
- )
- },
- )
- } else {
- Modifier.lumaFallbackGlass(
- shape = CircleShape,
- selected = searchSelected,
- neutralVeil = if (LumaColors.isDark) {
- Color(0xFF1C2430).copy(alpha = 0.72f)
- } else if (AppTheme.glassMode == GlassMode.Liquid) {
- Color(0xFFD7E3F0).copy(alpha = 0.58f)
- } else {
- Color(0xFFD4D9DF).copy(alpha = 0.84f)
- },
- )
- },
- )
- .then(
- if (safeBackdrop != null) {
- Modifier.border(
- 0.6.dp,
- if (searchSelected) LumaColors.glassBorder else LumaColors.glassBorder.copy(alpha = 0.72f),
- CircleShape,
- )
- } else {
- Modifier
- },
- )
- .clickable(indication = null, interactionSource = searchInteraction) { onSelect(4) },
- contentAlignment = Alignment.Center,
+ LiquidBottomTabs(
+ selectedTabIndex = { 0 },
+ onTabSelected = { onSelect(4) },
+ backdrop = backdrop,
+ tabsCount = 1,
+ showSelection = { selectedTab == 4 },
+ modifier = Modifier.size(64.dp),
) {
- if (safeBackdrop != null) {
- Box(
- Modifier
- .clearAndSetSemantics {}
- .alpha(0f)
- .layerBackdrop(searchContentBackdrop)
- .size(56.dp)
- .graphicsLayer(colorFilter = ColorFilter.tint(active)),
- contentAlignment = Alignment.Center,
- ) {
- LumaIconView(
- icon = LumaIcon.Search,
- contentDescription = null,
- tint = active,
- modifier = Modifier.size(27.dp),
- )
- }
- }
- if (searchSelected) {
- Box(
- Modifier
- .size(56.dp)
- .clip(CircleShape)
- .then(
- if (safeBackdrop != null) {
- Modifier.drawBackdrop(
- backdrop = rememberCombinedBackdrop(safeBackdrop, searchContentBackdrop),
- shape = { Capsule() },
- effects = {
- lens(
- 10f.dp.toPx() * searchPressProgress,
- 14f.dp.toPx() * searchPressProgress,
- chromaticAberration = true,
- )
- },
- highlight = { Highlight.Default.copy(alpha = searchPressProgress) },
- shadow = { Shadow(alpha = searchPressProgress) },
- innerShadow = {
- InnerShadow(
- radius = 8.dp * searchPressProgress,
- alpha = searchPressProgress,
- )
- },
- layerBlock = {
- val selectedScale = 1f + (78f / 56f - 1f) * searchPressProgress
- scaleX = selectedScale
- scaleY = selectedScale
- },
- onDrawSurface = {
- drawRect(
- if (LumaColors.isDark) Color.White.copy(alpha = 0.10f)
- else Color.Black.copy(alpha = 0.10f),
- alpha = 1f - searchPressProgress,
- )
- drawRect(Color.Black.copy(alpha = 0.03f * searchPressProgress))
- },
- )
- } else {
- Modifier.lumaFallbackGlass(
- shape = CircleShape,
- selected = true,
- neutralVeil = if (LumaColors.isDark) {
- Color(0xFF26313E).copy(alpha = 0.62f)
- } else {
- Color(0xFFC9D8E8).copy(alpha = 0.60f)
- },
- )
- },
- )
+ LiquidBottomTab(onClick = { onSelect(4) }) {
+ LumaAnimatedIcon(
+ icon = LumaIcon.Search,
+ selected = selectedTab == 4,
+ contentDescription = stringResource(com.example.myapplication.R.string.search),
+ tint = if (selectedTab == 4) active else inactive,
+ modifier = Modifier.size(27.dp),
)
}
- LumaIconView(
- icon = LumaIcon.Search,
- contentDescription = stringResource(com.example.myapplication.R.string.search),
- tint = if (searchSelected) active else inactive,
- modifier = Modifier.size(27.dp),
- )
}
}
}
diff --git a/app/src/main/kotlin/com/example/myapplication/ui/LiquidGlass.kt b/app/src/main/kotlin/com/example/myapplication/ui/LiquidGlass.kt
index 9a38a7c..e0b3494 100644
--- a/app/src/main/kotlin/com/example/myapplication/ui/LiquidGlass.kt
+++ b/app/src/main/kotlin/com/example/myapplication/ui/LiquidGlass.kt
@@ -101,6 +101,7 @@ import com.example.myapplication.ui.liquidglass.DampedDragAnimation
import com.example.myapplication.ui.design.LumaColors
import com.example.myapplication.ui.design.LumaIcon
import com.example.myapplication.ui.design.LumaIconView
+import com.example.myapplication.ui.design.LumaEmptyState
import com.example.myapplication.ui.design.LumaGlassLevel
import com.example.myapplication.ui.design.LumaMotion
import com.example.myapplication.ui.design.LumaShapes
@@ -123,12 +124,41 @@ internal val mainTabs = listOf(
val LocalGlassBackdrop = staticCompositionLocalOf { null }
+object AppBottomBarMetrics {
+ val contentInset = 90.dp
+}
+
@Composable
-fun bottomBarContentPadding(extra: Dp = 86.dp): PaddingValues {
+fun bottomBarContentPadding(extra: Dp = AppBottomBarMetrics.contentInset): PaddingValues {
val systemInset = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
return PaddingValues(bottom = systemInset + extra)
}
+@Composable
+fun LumaEmptyStateContainer(
+ icon: LumaIcon,
+ title: String,
+ description: String,
+ modifier: Modifier = Modifier,
+ showIconBackground: Boolean = false,
+ verticalOffset: Dp = 0.dp,
+) {
+ Box(
+ modifier
+ .fillMaxSize()
+ .padding(bottom = bottomBarContentPadding().calculateBottomPadding())
+ .offset(y = verticalOffset),
+ contentAlignment = Alignment.Center,
+ ) {
+ LumaEmptyState(
+ icon = icon,
+ title = title,
+ description = description,
+ showIconBackground = showIconBackground,
+ )
+ }
+}
+
object GlassTokens {
val isDark get() = AppTheme.isDark
val text: Color get() = LumaColors.text
@@ -748,6 +778,8 @@ fun GlassSheet(
onDismissRequest: () -> Unit,
backdrop: Backdrop? = null,
modifier: Modifier = Modifier,
+ surfaceOverlay: Color? = null,
+ horizontalMargin: Dp = 0.dp,
content: @Composable ColumnScope.() -> Unit,
) {
Dialog(
@@ -769,6 +801,7 @@ fun GlassSheet(
) {
Column(
modifier
+ .padding(horizontal = horizontalMargin)
.fillMaxWidth()
.widthIn(max = 520.dp)
.lumaGlass(
@@ -778,6 +811,11 @@ fun GlassSheet(
borderColor = GlassTokens.glassBorder,
shadowElevation = 12.dp,
)
+ .then(
+ surfaceOverlay?.let {
+ Modifier.background(it, RoundedCornerShape(28.dp))
+ } ?: Modifier,
+ )
.padding(20.dp),
content = content,
)
@@ -831,6 +869,7 @@ fun GlassInputField(
focusRequester: FocusRequester? = null,
containerTint: Color = GlassTokens.glassFillSoft,
useBackdropGlass: Boolean = true,
+ fieldHeight: Dp = 48.dp,
) {
val backdrop = LocalGlassBackdrop.current
Column(modifier, verticalArrangement = Arrangement.spacedBy(4.dp)) {
@@ -838,7 +877,7 @@ fun GlassInputField(
val shape = RoundedCornerShape(12.dp)
Box(
Modifier.fillMaxWidth()
- .heightIn(min = 48.dp)
+ .heightIn(min = fieldHeight)
.then(
if (useBackdropGlass) {
Modifier.lumaGlass(
@@ -854,6 +893,7 @@ fun GlassInputField(
)
.border(0.7.dp, if (isError) GlassTokens.danger else GlassTokens.glassBorder, shape)
.padding(horizontal = 14.dp, vertical = 12.dp),
+ contentAlignment = Alignment.Center,
) {
BasicTextField(
value = value,
diff --git a/app/src/main/kotlin/com/example/myapplication/ui/liquidglass/LiquidBottomTab.kt b/app/src/main/kotlin/com/example/myapplication/ui/liquidglass/LiquidBottomTab.kt
index 5007e98..582216f 100644
--- a/app/src/main/kotlin/com/example/myapplication/ui/liquidglass/LiquidBottomTab.kt
+++ b/app/src/main/kotlin/com/example/myapplication/ui/liquidglass/LiquidBottomTab.kt
@@ -16,8 +16,8 @@ import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
import com.kyant.shapes.Capsule
-internal val LocalLiquidBottomTabScale =
- staticCompositionLocalOf { { 1f } }
+internal val LocalLiquidBottomTabsGhostLayer = staticCompositionLocalOf { false }
+internal val LocalLiquidBottomTabScale = staticCompositionLocalOf { { 1f } }
@Composable
fun RowScope.LiquidBottomTab(
@@ -44,9 +44,9 @@ fun RowScope.LiquidBottomTab(
.fillMaxHeight()
.weight(1f)
.graphicsLayer {
- val scale = scale()
- scaleX = scale
- scaleY = scale
+ val currentScale = scale()
+ scaleX = currentScale
+ scaleY = currentScale
},
verticalArrangement = Arrangement.spacedBy(2f.dp, Alignment.CenterVertically),
horizontalAlignment = Alignment.CenterHorizontally,
diff --git a/app/src/main/kotlin/com/example/myapplication/ui/liquidglass/LiquidBottomTabs.kt b/app/src/main/kotlin/com/example/myapplication/ui/liquidglass/LiquidBottomTabs.kt
index fb8251f..23c0478 100644
--- a/app/src/main/kotlin/com/example/myapplication/ui/liquidglass/LiquidBottomTabs.kt
+++ b/app/src/main/kotlin/com/example/myapplication/ui/liquidglass/LiquidBottomTabs.kt
@@ -23,19 +23,18 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
-import androidx.compose.runtime.staticCompositionLocalOf
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.geometry.Offset
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastCoerceIn
import androidx.compose.ui.util.fastRoundToInt
@@ -64,9 +63,6 @@ import kotlinx.coroutines.launch
import kotlin.math.abs
import kotlin.math.sign
-/** true = the hidden blue content-capture layer, false = the visible layer. */
-val LocalLiquidBottomTabsGhostLayer = staticCompositionLocalOf { false }
-
@Composable
fun LiquidBottomTabs(
selectedTabIndex: () -> Int,
@@ -75,14 +71,18 @@ fun LiquidBottomTabs(
tabsCount: Int,
showSelection: () -> Boolean = { true },
modifier: Modifier = Modifier,
+ height: Dp = 64.dp,
+ surfaceAlpha: Float = 0.40f,
+ pressHighlight: Boolean = true,
+ ghostContent: (@Composable RowScope.() -> Unit)? = null,
content: @Composable RowScope.() -> Unit,
) {
val isLightTheme = !LumaColors.isDark
- val accentColor = if (isLightTheme) Color(0xFF0088FF) else Color(0xFF0091FF)
+ val pressHighlightAlpha = if (pressHighlight) 1f else 0f
val containerColor = if (isLightTheme) {
- Color(0xFFFAFAFA).copy(alpha = 0.40f)
+ Color(0xFFFAFAFA).copy(alpha = surfaceAlpha)
} else {
- Color(0xFF121212).copy(alpha = 0.40f)
+ Color(0xFF121212).copy(alpha = surfaceAlpha)
}
val liquidFallback = AppTheme.glassMode == GlassMode.Liquid
val fallbackContainerVeil = if (isLightTheme) {
@@ -99,7 +99,7 @@ fun LiquidBottomTabs(
}
val nativeBackdrop = backdrop?.takeIf { LumaGlassRuntime.useNativeBackdrop }
val selectionVisible = showSelection()
- val tabsBackdrop = rememberLayerBackdrop()
+ val tabsBackdrop = if (nativeBackdrop != null) rememberLayerBackdrop() else null
val currentOnTabSelected by rememberUpdatedState(onTabSelected)
BoxWithConstraints(modifier, contentAlignment = Alignment.CenterStart) {
@@ -167,7 +167,7 @@ fun LiquidBottomTabs(
}
}
- val interactiveHighlight = remember(animationScope, isLtr, tabWidth) {
+ val interactiveHighlight = remember(animationScope) {
InteractiveHighlight(
animationScope = animationScope,
position = { size, _ ->
@@ -184,7 +184,7 @@ fun LiquidBottomTabs(
Box(
Modifier
.graphicsLayer { translationX = panelOffset }
- .height(64.dp)
+ .height(height)
.fillMaxWidth()
.lumaFallbackGlass(
shape = Capsule(),
@@ -200,7 +200,7 @@ fun LiquidBottomTabs(
if (isLtr) dampedDragAnimation.value * tabWidth + panelOffset
else size.width - (dampedDragAnimation.value + 1f) * tabWidth + panelOffset
}
- .height(56.dp)
+ .height(height - 8.dp)
.width(with(density) { tabWidth.toDp() })
.lumaFallbackGlass(
shape = Capsule(),
@@ -212,11 +212,11 @@ fun LiquidBottomTabs(
Row(
Modifier
.graphicsLayer { translationX = panelOffset }
- .then(interactiveHighlight.modifier)
+ .then(if (pressHighlight) interactiveHighlight.modifier else Modifier)
.then(interactiveHighlight.gestureModifier)
.then(dampedDragAnimation.modifier)
.clip(Capsule())
- .height(64.dp)
+ .height(height)
.fillMaxWidth()
.padding(LumaSpacing.xxs),
verticalAlignment = Alignment.CenterVertically,
@@ -242,11 +242,11 @@ fun LiquidBottomTabs(
},
onDrawSurface = { drawRect(containerColor) },
)
- .then(interactiveHighlight.modifier)
+ .then(if (pressHighlight) interactiveHighlight.modifier else Modifier)
+ .border(0.6.dp, LumaColors.glassBorder.copy(alpha = 0.72f), Capsule())
+ .height(height)
.then(interactiveHighlight.gestureModifier)
.then(dampedDragAnimation.modifier)
- .border(0.6.dp, LumaColors.glassBorder.copy(alpha = 0.72f), Capsule())
- .height(64.dp)
.fillMaxWidth()
.padding(4.dp),
verticalAlignment = Alignment.CenterVertically,
@@ -256,12 +256,17 @@ fun LiquidBottomTabs(
}
}
- CompositionLocalProvider(LocalLiquidBottomTabsGhostLayer provides true) {
+ CompositionLocalProvider(
+ LocalLiquidBottomTabsGhostLayer provides true,
+ LocalLiquidBottomTabScale provides {
+ lerp(1f, 1.2f, dampedDragAnimation.pressProgress)
+ },
+ ) {
Row(
Modifier
.clearAndSetSemantics {}
.alpha(0f)
- .layerBackdrop(tabsBackdrop)
+ .layerBackdrop(tabsBackdrop!!)
.graphicsLayer { translationX = panelOffset }
.drawBackdrop(
backdrop = nativeBackdrop,
@@ -273,17 +278,16 @@ fun LiquidBottomTabs(
lens(24f.dp.toPx() * progress, 24f.dp.toPx() * progress)
},
highlight = {
- Highlight.Default.copy(alpha = dampedDragAnimation.pressProgress)
+ Highlight.Default.copy(alpha = dampedDragAnimation.pressProgress * pressHighlightAlpha)
},
onDrawSurface = { drawRect(containerColor) },
)
- .then(interactiveHighlight.modifier)
- .height(56.dp)
- .fillMaxWidth()
- .padding(horizontal = 4.dp)
- .graphicsLayer(colorFilter = ColorFilter.tint(accentColor)),
+ .then(if (pressHighlight) interactiveHighlight.modifier else Modifier)
+ .height(height - 8.dp)
+ .fillMaxWidth()
+ .padding(horizontal = 4.dp),
verticalAlignment = Alignment.CenterVertically,
- content = content,
+ content = ghostContent ?: content,
)
}
@@ -296,8 +300,10 @@ fun LiquidBottomTabs(
if (isLtr) dampedDragAnimation.value * tabWidth + panelOffset
else size.width - (dampedDragAnimation.value + 1f) * tabWidth + panelOffset
}
+ .then(interactiveHighlight.gestureModifier)
+ .then(dampedDragAnimation.modifier)
.drawBackdrop(
- backdrop = rememberCombinedBackdrop(nativeBackdrop, tabsBackdrop),
+ backdrop = rememberCombinedBackdrop(nativeBackdrop, tabsBackdrop!!),
shape = { Capsule() },
effects = {
val progress = dampedDragAnimation.pressProgress
@@ -308,7 +314,7 @@ fun LiquidBottomTabs(
)
},
highlight = {
- Highlight.Default.copy(alpha = dampedDragAnimation.pressProgress)
+ Highlight.Default.copy(alpha = dampedDragAnimation.pressProgress * pressHighlightAlpha)
},
shadow = { Shadow(alpha = dampedDragAnimation.pressProgress) },
innerShadow = {
@@ -332,7 +338,7 @@ fun LiquidBottomTabs(
drawRect(Color.Black.copy(alpha = 0.03f * progress))
},
)
- .height(56.dp)
+ .height(height - 8.dp)
.fillMaxWidth(1f / tabsCount),
)
}