小优化

This commit is contained in:
beyond committed 2026-08-31 20:51:37 +08:00
1 parent 6cb94b7793
commit e076f17061
11 files changed
+336 -413

No files matched your search

@@ -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) }
},
)
}
}
@@ -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 },
)
}
}
@@ -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?,
@@ -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() },
@@ -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) ->
@@ -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 ──────────────────────────────────────────────────────
@@ -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))
@@ -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),
)
}
}
}
@@ -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<Backdrop?> { 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,
@@ -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,
@@ -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),
)
}