小优化
This commit is contained in:
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),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user