Красивый UI

This commit is contained in:
2026-05-21 01:10:55 +03:00
parent 184edc0b67
commit 9c38da76d2
19 changed files with 557 additions and 213 deletions

View File

@@ -4,17 +4,15 @@ import android.content.Intent
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.List
import androidx.compose.material.icons.rounded.Menu
import androidx.compose.material.icons.rounded.Settings
import androidx.compose.material.icons.rounded.Sync
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
@@ -22,16 +20,16 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.navDeepLink
import com.github.nullptroma.wallenc.ui.elements.FloatingWallencNavigationBar
import com.github.nullptroma.wallenc.ui.navigation.NavBarItemData
import com.github.nullptroma.wallenc.ui.navigation.WallencDeepLinks
import com.github.nullptroma.wallenc.ui.navigation.matchesWallencDeepLink
import com.github.nullptroma.wallenc.ui.elements.NavigationBarMarqueeText
import com.github.nullptroma.wallenc.ui.navigation.rememberNavigationState
import com.github.nullptroma.wallenc.ui.screens.main.MainRoute
import com.github.nullptroma.wallenc.ui.screens.main.MainScreen
@@ -108,57 +106,52 @@ fun WallencNavRoot(
)
}
val navBackStackEntry by navState.navHostController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
Scaffold(bottomBar = {
NavigationBar(modifier = Modifier.wrapContentHeight()) {
val navBackStackEntry by navState.navHostController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
topLevelNavBarItems.forEach {
val routeClassName = it.key
val navBarItemData = it.value
NavigationBarItem(
modifier = Modifier.wrapContentHeight(),
icon = {
if (navBarItemData.icon != null) Icon(
navBarItemData.icon,
contentDescription = stringResource(navBarItemData.iconContentDescriptionResourceId),
)
},
label = {
NavigationBarMarqueeText(
text = stringResource(navBarItemData.nameStringResourceId),
)
Scaffold(
bottomBar = {
Box(
modifier = Modifier
.navigationBarsPadding()
.padding(horizontal = 12.dp)
.padding(top = 4.dp, bottom = 6.dp),
) {
FloatingWallencNavigationBar(
items = topLevelNavBarItems,
routes = topLevelRoutes,
currentRoute = currentRoute,
onNavigate = { item ->
val route = topLevelRoutes[item.screenRouteClass]
?: error("Route ${item.screenRouteClass} not found")
if (currentRoute?.startsWith(item.screenRouteClass) != true) {
navState.changeTop(route)
}
},
selected = currentRoute?.startsWith(routeClassName) == true,
onClick = {
val route = topLevelRoutes[navBarItemData.screenRouteClass]
if (route == null)
throw NullPointerException("Route $route not found")
if (currentRoute?.startsWith(routeClassName) != true) navState.changeTop(
route
)
}
)
}
}
}) { innerPaddings ->
},
) { innerPaddings ->
NavHost(
navState.navHostController,
startDestination = topLevelRoutes[MainRoute::class.qualifiedName]!!
startDestination = topLevelRoutes[MainRoute::class.qualifiedName]!!,
modifier = Modifier.padding(innerPaddings),
) {
composable<MainRoute>(
deepLinks = listOf(
navDeepLink { uriPattern = WallencDeepLinks.MAIN_URI_PATTERN },
),
enterTransition = {
fadeIn(tween(200))
}, exitTransition = {
fadeOut(tween(200))
}) {
fadeIn(tween(200))
},
exitTransition = {
fadeOut(tween(200))
},
) {
MainScreen(
modifier = Modifier.padding(innerPaddings),
modifier = Modifier,
navState = mainNavState,
viewModel = mainViewModel
viewModel = mainViewModel,
)
}
composable<SettingsRoute>(
@@ -166,23 +159,27 @@ fun WallencNavRoot(
navDeepLink { uriPattern = WallencDeepLinks.SETTINGS_URI_PATTERN },
),
enterTransition = {
fadeIn(tween(200))
}, exitTransition = {
fadeOut(tween(200))
}) {
SettingsScreen(Modifier.padding(innerPaddings), settingsViewModel)
fadeIn(tween(200))
},
exitTransition = {
fadeOut(tween(200))
},
) {
SettingsScreen(Modifier, settingsViewModel)
}
composable<StorageSyncRoute>(
deepLinks = listOf(
navDeepLink { uriPattern = WallencDeepLinks.SYNC_URI_PATTERN },
),
enterTransition = {
fadeIn(tween(200))
}, exitTransition = {
fadeOut(tween(200))
}) {
fadeIn(tween(200))
},
exitTransition = {
fadeOut(tween(200))
},
) {
StorageSyncScreen(
modifier = Modifier.padding(innerPaddings),
modifier = Modifier,
viewModel = storageSyncViewModel,
)
}
@@ -191,13 +188,13 @@ fun WallencNavRoot(
navDeepLink { uriPattern = WallencDeepLinks.TASKS_URI_PATTERN },
),
enterTransition = {
fadeIn(tween(200))
}, exitTransition = {
fadeOut(tween(200))
}) {
TaskPipelineScreen(
modifier = Modifier.padding(innerPaddings)
)
fadeIn(tween(200))
},
exitTransition = {
fadeOut(tween(200))
},
) {
TaskPipelineScreen(modifier = Modifier)
}
}
}

View File

@@ -0,0 +1,75 @@
package com.github.nullptroma.wallenc.ui.elements
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.github.nullptroma.wallenc.ui.R
private const val BackButtonAnimMillis = 200
private val backButtonEnter = fadeIn(tween(BackButtonAnimMillis)) +
slideInVertically(
animationSpec = tween(BackButtonAnimMillis),
initialOffsetY = { fullHeight -> fullHeight / 2 },
)
private val backButtonExit = fadeOut(tween(BackButtonAnimMillis)) +
slideOutVertically(
animationSpec = tween(BackButtonAnimMillis),
targetOffsetY = { fullHeight -> -fullHeight / 2 },
)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun FloatingBackButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Surface(
onClick = onClick,
modifier = modifier.size(44.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.surfaceContainerHigh,
shadowElevation = 4.dp,
tonalElevation = 2.dp,
contentColor = MaterialTheme.colorScheme.onSurface,
) {
Icon(
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = stringResource(R.string.nav_cd_back),
modifier = Modifier.padding(10.dp),
)
}
}
@Composable
fun AnimatedFloatingBackButton(
visible: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
AnimatedVisibility(
visible = visible,
modifier = modifier,
enter = backButtonEnter,
exit = backButtonExit,
) {
FloatingBackButton(onClick = onClick)
}
}

View File

@@ -0,0 +1,180 @@
package com.github.nullptroma.wallenc.ui.elements
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import com.github.nullptroma.wallenc.ui.navigation.NavBarItemData
/** Вертикальный зазор между вложенной и корневой плавающими панелями навигации. */
val WallencNestedNavBarGap = 2.dp
@Composable
fun FloatingWallencNavigationBar(
items: Map<String, NavBarItemData>,
routes: Map<String, *>,
currentRoute: String?,
onNavigate: (NavBarItemData) -> Unit,
modifier: Modifier = Modifier,
compact: Boolean = false,
) {
val haptic = LocalHapticFeedback.current
val barHeight = if (compact) 48.dp else 56.dp
val barShape = if (compact) RoundedCornerShape(22.dp) else RoundedCornerShape(28.dp)
val barHorizontalPadding = if (compact) 4.dp else 6.dp
val barSurface: @Composable () -> Unit = {
Surface(
modifier = Modifier
.then(
if (compact) {
Modifier
.widthIn(max = 300.dp)
.fillMaxWidth(0.68f)
} else {
Modifier.fillMaxWidth()
},
),
shape = barShape,
color = MaterialTheme.colorScheme.surfaceContainerHigh,
shadowElevation = 6.dp,
tonalElevation = 2.dp,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(barHeight)
.padding(horizontal = barHorizontalPadding, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
items.forEach { (routeClassName, navBarItemData) ->
val iconVector = navBarItemData.icon ?: return@forEach
val selected = currentRoute?.startsWith(routeClassName) == true
val enabled = routes[navBarItemData.screenRouteClass] != null
FloatingNavItem(
modifier = Modifier
.weight(1f)
.fillMaxWidth(),
icon = iconVector,
label = stringResource(navBarItemData.nameStringResourceId),
contentDescription = stringResource(navBarItemData.iconContentDescriptionResourceId),
selected = selected,
enabled = enabled,
compact = compact,
onClick = {
if (!selected && enabled) {
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
onNavigate(navBarItemData)
}
},
)
}
}
}
}
if (compact) {
Box(
modifier = modifier.fillMaxWidth(),
contentAlignment = Alignment.Center,
) {
barSurface()
}
} else {
Box(modifier = modifier.fillMaxWidth()) {
barSurface()
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun FloatingNavItem(
icon: ImageVector,
label: String,
contentDescription: String,
selected: Boolean,
enabled: Boolean,
compact: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val iconSize = if (compact) 22.dp else 24.dp
val itemPaddingH = if (compact) 6.dp else 8.dp
val itemPaddingV = if (compact) 6.dp else 8.dp
val labelStyle = if (compact) {
MaterialTheme.typography.labelSmall
} else {
MaterialTheme.typography.labelMedium
}
val labelVelocity = if (compact) 24.dp else 28.dp
val itemShape = if (compact) RoundedCornerShape(16.dp) else RoundedCornerShape(20.dp)
val containerColor = if (selected) {
MaterialTheme.colorScheme.secondaryContainer
} else {
MaterialTheme.colorScheme.surfaceContainerHigh
}
val contentColor = if (selected) {
MaterialTheme.colorScheme.onSecondaryContainer
} else {
MaterialTheme.colorScheme.onSurfaceVariant
}
Surface(
onClick = onClick,
enabled = enabled,
modifier = modifier
.padding(horizontal = if (compact) 1.dp else 2.dp)
.semantics {
role = Role.Tab
this.contentDescription = contentDescription
},
shape = itemShape,
color = containerColor,
contentColor = contentColor,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = itemPaddingH, vertical = itemPaddingV),
horizontalArrangement = if (selected) Arrangement.Start else Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.size(iconSize),
)
if (selected) {
NavigationBarMarqueeText(
text = label,
modifier = Modifier
.weight(1f)
.padding(start = if (compact) 4.dp else 6.dp),
style = labelStyle,
velocity = labelVelocity,
)
}
}
}
}

View File

@@ -7,25 +7,33 @@ import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
/**
* Однострочная подпись таба нижней навигации: при нехватке ширины текст
* прокручивается (marquee), без переноса последних букв на вторую строку.
* Однострочная подпись таба: при нехватке ширины текст циклически прокручивается.
*/
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun NavigationBarMarqueeText(
text: String,
modifier: Modifier = Modifier,
style: TextStyle = LocalTextStyle.current,
velocity: Dp = 28.dp,
) {
Text(
text = text,
modifier = modifier
.fillMaxWidth()
.basicMarquee(),
style = LocalTextStyle.current,
.basicMarquee(
iterations = Int.MAX_VALUE,
repeatDelayMillis = 1_200,
velocity = velocity,
),
style = style,
maxLines = 1,
softWrap = false,
overflow = TextOverflow.Clip,

View File

@@ -0,0 +1,49 @@
package com.github.nullptroma.wallenc.ui.elements
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.FabPosition
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun WallencScreenScaffold(
modifier: Modifier = Modifier,
snackbarHostState: SnackbarHostState? = null,
floatingActionButton: @Composable () -> Unit = {},
content: @Composable (PaddingValues) -> Unit,
) {
Scaffold(
modifier = modifier,
contentWindowInsets = WindowInsets(0.dp),
snackbarHost = {
if (snackbarHostState != null) {
SnackbarHost(snackbarHostState)
}
},
floatingActionButton = floatingActionButton,
floatingActionButtonPosition = FabPosition.End,
content = content,
)
}
@Composable
fun WallencScreenContentPadding(
innerPadding: PaddingValues,
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
) {
Box(
modifier = modifier
.padding(innerPadding)
.padding(horizontal = 16.dp, vertical = 12.dp),
) {
content()
}
}

View File

@@ -0,0 +1,25 @@
package com.github.nullptroma.wallenc.ui.navigation
import com.github.nullptroma.wallenc.ui.screens.main.screens.remotes.RemoteVaultsRoute
import com.github.nullptroma.wallenc.ui.screens.main.screens.vault.LocalVaultRoute
import com.github.nullptroma.wallenc.ui.screens.shared.TextEditRoute
private val mainTopLevelRoutePrefixes: Set<String> = setOf(
LocalVaultRoute::class.qualifiedName!!,
RemoteVaultsRoute::class.qualifiedName!!,
)
fun isMainTopLevelRoute(route: String?): Boolean {
if (route == null) return true
return mainTopLevelRoutePrefixes.any { route.startsWith(it) }
}
fun isTextEditDestination(route: String?): Boolean {
val qualified = TextEditRoute::class.qualifiedName ?: return false
return route?.startsWith(qualified) == true
}
fun shouldShowMainFloatingBack(route: String?): Boolean {
if (route == null) return false
return !isMainTopLevelRoute(route)
}

View File

@@ -24,6 +24,10 @@ class NavigationState(
restoreState = true
}
}
fun pop(): Boolean = navHostController.popBackStack()
fun canPop(): Boolean = navHostController.previousBackStackEntry != null
}
@Composable

View File

@@ -3,34 +3,38 @@ package com.github.nullptroma.wallenc.ui.screens.main
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Cloud
import androidx.compose.material.icons.outlined.Folder
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.toRoute
import com.github.nullptroma.wallenc.ui.elements.NavigationBarMarqueeText
import com.github.nullptroma.wallenc.ui.R
import com.github.nullptroma.wallenc.ui.elements.AnimatedFloatingBackButton
import com.github.nullptroma.wallenc.ui.elements.FloatingWallencNavigationBar
import com.github.nullptroma.wallenc.ui.elements.WallencNestedNavBarGap
import com.github.nullptroma.wallenc.ui.navigation.NavBarItemData
import com.github.nullptroma.wallenc.ui.navigation.NavigationState
import com.github.nullptroma.wallenc.ui.navigation.isMainTopLevelRoute
import com.github.nullptroma.wallenc.ui.navigation.isTextEditDestination
import com.github.nullptroma.wallenc.ui.navigation.shouldShowMainFloatingBack
import com.github.nullptroma.wallenc.ui.navigation.rememberNavigationState
import com.github.nullptroma.wallenc.ui.screens.main.screens.remotes.RemoteVaultsRoute
import com.github.nullptroma.wallenc.ui.screens.main.screens.remotes.RemoteVaultsScreen
@@ -54,13 +58,8 @@ import com.github.nullptroma.wallenc.ui.screens.main.screens.vault.VaultBrowserS
import com.github.nullptroma.wallenc.ui.screens.shared.TextEditRoute
import com.github.nullptroma.wallenc.ui.screens.shared.TextEditScreen
private fun isTextEditDestination(route: String?): Boolean {
val q = TextEditRoute::class.qualifiedName ?: return false
return route?.startsWith(q) == true
}
@OptIn(ExperimentalMaterial3Api::class)
@androidx.compose.runtime.Composable
@Composable
fun MainScreen(
modifier: Modifier = Modifier,
viewModel: MainViewModel = hiltViewModel(),
@@ -72,8 +71,12 @@ fun MainScreen(
val remoteVaultsViewModel: RemoteVaultsViewModel = hiltViewModel()
val childBackStackEntry by navState.navHostController.currentBackStackEntryAsState()
val showWorkStatusBar = !isTextEditDestination(childBackStackEntry?.destination?.route)
val childRoute = childBackStackEntry?.destination?.route
val showWorkStatusBar = !isTextEditDestination(childRoute)
val showMainBottomNav = isMainTopLevelRoute(childRoute)
val showFloatingBack = shouldShowMainFloatingBack(childRoute) && navState.canPop()
val workStatus = mainUi.workStatus
val onBack: () -> Unit = { navState.pop() }
val topLevelNavBarItems = remember {
mapOf(
@@ -101,50 +104,37 @@ fun MainScreen(
}
},
bottomBar = {
Column {
NavigationBar(windowInsets = WindowInsets(0)) {
val navBackStackEntry by navState.navHostController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
topLevelNavBarItems.forEach {
val routeClassName = it.key
val navBarItemData = it.value
val iconVector = navBarItemData.icon
?: error("Main tab requires icon")
NavigationBarItem(
modifier = Modifier.weight(1f),
icon = {
Icon(
imageVector = iconVector,
contentDescription = stringResource(navBarItemData.iconContentDescriptionResourceId),
)
},
label = {
NavigationBarMarqueeText(
text = stringResource(navBarItemData.nameStringResourceId),
)
},
selected = currentRoute?.startsWith(routeClassName) == true,
onClick = {
val route = routes[navBarItemData.screenRouteClass]
?: throw NullPointerException("Route ${navBarItemData.screenRouteClass} not found")
if (currentRoute?.startsWith(routeClassName) != true) {
navState.changeTop(route)
}
},
)
}
if (showMainBottomNav) {
Box(
modifier = Modifier
.padding(horizontal = 16.dp)
.padding(top = WallencNestedNavBarGap, bottom = WallencNestedNavBarGap),
) {
FloatingWallencNavigationBar(
compact = true,
items = topLevelNavBarItems,
routes = routes,
currentRoute = childRoute,
onNavigate = { item ->
val route = routes[item.screenRouteClass]
?: error("Route ${item.screenRouteClass} not found")
navState.changeTop(route)
},
)
}
HorizontalDivider()
}
},
) { innerPaddings ->
NavHost(
navController = navState.navHostController,
startDestination = routes[LocalVaultRoute::class.qualifiedName]!!,
Box(
modifier = Modifier
.fillMaxSize()
.padding(innerPaddings),
) {
NavHost(
navController = navState.navHostController,
startDestination = routes[LocalVaultRoute::class.qualifiedName]!!,
modifier = Modifier.fillMaxSize(),
) {
composable<LocalVaultRoute>(
enterTransition = { fadeIn(tween(200)) },
exitTransition = { fadeOut(tween(200)) },
@@ -242,9 +232,7 @@ fun MainScreen(
),
)
},
onDeleted = {
navState.navHostController.popBackStack()
},
onDeleted = { navState.pop() },
)
}
composable<TextSecretEditRoute>(
@@ -255,7 +243,7 @@ fun MainScreen(
TextSecretEditScreen(
onSaved = { savedSecretId ->
val editingExisting = route.secretId != null
navState.navHostController.popBackStack()
navState.pop()
if (!editingExisting) {
navState.push(
TextSecretDetailsRoute(
@@ -269,8 +257,18 @@ fun MainScreen(
}
composable<TextEditRoute> {
val route: TextEditRoute = it.toRoute()
TextEditScreen(route.text)
TextEditScreen(text = route.text)
}
}
AnimatedFloatingBackButton(
visible = showFloatingBack,
onClick = onBack,
modifier = Modifier
.zIndex(1f)
.align(Alignment.BottomStart)
.navigationBarsPadding()
.padding(start = 12.dp, bottom = 12.dp),
)
}
}
}

View File

@@ -4,7 +4,6 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@@ -16,7 +15,6 @@ import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -27,6 +25,8 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.github.nullptroma.wallenc.ui.R
import com.github.nullptroma.wallenc.ui.elements.WallencScreenContentPadding
import com.github.nullptroma.wallenc.ui.elements.WallencScreenScaffold
import com.github.nullptroma.wallenc.ui.resources.resolveText
@Composable
@@ -38,15 +38,10 @@ fun StorageHomeScreen(
) {
val uiState by viewModel.state.collectAsStateWithLifecycle()
Scaffold(
modifier = modifier,
contentWindowInsets = WindowInsets(0.dp),
) { innerPadding ->
WallencScreenScaffold(modifier = modifier) { innerPadding ->
WallencScreenContentPadding(innerPadding) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
.padding(16.dp),
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
if (uiState.isLoading) {
@@ -110,6 +105,7 @@ fun StorageHomeScreen(
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
}
}

View File

@@ -4,7 +4,6 @@ import android.content.ClipData
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@@ -19,7 +18,6 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
@@ -33,6 +31,8 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.github.nullptroma.wallenc.ui.R
import com.github.nullptroma.wallenc.ui.elements.WallencScreenContentPadding
import com.github.nullptroma.wallenc.ui.elements.WallencScreenScaffold
import com.github.nullptroma.wallenc.ui.resources.resolveText
import kotlinx.coroutines.launch
@@ -48,15 +48,10 @@ fun TextSecretDetailsScreen(
val clipboard = LocalClipboard.current
val scope = rememberCoroutineScope()
Scaffold(
modifier = modifier,
contentWindowInsets = WindowInsets(0.dp),
) { innerPadding ->
WallencScreenScaffold(modifier = modifier) { innerPadding ->
WallencScreenContentPadding(innerPadding) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
.padding(16.dp),
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
uiState.errorNotification?.let { notification ->
@@ -144,5 +139,6 @@ fun TextSecretDetailsScreen(
Text(stringResource(R.string.remove))
}
}
}
}
}

View File

@@ -3,7 +3,6 @@ package com.github.nullptroma.wallenc.ui.screens.main.screens.storage.secrets
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@@ -16,7 +15,6 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
@@ -33,6 +31,8 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.github.nullptroma.wallenc.domain.datatypes.TextSecretEntryRecord
import com.github.nullptroma.wallenc.ui.R
import com.github.nullptroma.wallenc.ui.elements.WallencScreenContentPadding
import com.github.nullptroma.wallenc.ui.elements.WallencScreenScaffold
import com.github.nullptroma.wallenc.ui.resources.resolveText
@Composable
@@ -58,15 +58,10 @@ fun TextSecretEditScreen(
}
}
Scaffold(
modifier = modifier,
contentWindowInsets = WindowInsets(0.dp),
) { innerPadding ->
WallencScreenScaffold(modifier = modifier) { innerPadding ->
WallencScreenContentPadding(innerPadding) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
.padding(16.dp),
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
Text(
@@ -155,5 +150,6 @@ fun TextSecretEditScreen(
}
}
}
}
}
}

View File

@@ -1,7 +1,6 @@
package com.github.nullptroma.wallenc.ui.screens.main.screens.storage.secrets
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
@@ -10,7 +9,6 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
@@ -21,6 +19,8 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.github.nullptroma.wallenc.domain.datatypes.TextSecretRecord
import com.github.nullptroma.wallenc.ui.R
import com.github.nullptroma.wallenc.ui.elements.WallencScreenContentPadding
import com.github.nullptroma.wallenc.ui.elements.WallencScreenScaffold
@Composable
fun TextSecretsScreen(
@@ -31,9 +31,8 @@ fun TextSecretsScreen(
) {
val uiState by viewModel.state.collectAsStateWithLifecycle()
Scaffold(
WallencScreenScaffold(
modifier = modifier,
contentWindowInsets = WindowInsets(0.dp),
floatingActionButton = {
FloatingActionButton(
onClick = {
@@ -47,11 +46,10 @@ fun TextSecretsScreen(
}
},
) { innerPadding ->
WallencScreenContentPadding(innerPadding) {
TextSecretsScreenContent(
uiState = uiState,
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
modifier = Modifier.fillMaxSize(),
) {
LazyColumn(verticalArrangement = Arrangement.spacedBy(10.dp)) {
items(uiState.items) { secret ->
@@ -63,5 +61,6 @@ fun TextSecretsScreen(
}
}
}
}
}
}

View File

@@ -79,6 +79,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.github.nullptroma.wallenc.domain.datatypes.TwoFaTokenRecord
import com.github.nullptroma.wallenc.ui.R
import com.github.nullptroma.wallenc.ui.elements.QrScannerDialog
import com.github.nullptroma.wallenc.ui.elements.WallencScreenContentPadding
import com.github.nullptroma.wallenc.ui.elements.WallencScreenScaffold
import com.github.nullptroma.wallenc.usecases.TwoFaCodeState
import com.github.nullptroma.wallenc.usecases.buildTwoFaCodeState
import kotlinx.coroutines.delay
@@ -103,9 +105,8 @@ fun TwoFaTokensScreen(
var editingToken by remember { mutableStateOf<TwoFaTokenRecord?>(null) }
var creating by remember { mutableStateOf(false) }
Scaffold(
WallencScreenScaffold(
modifier = modifier,
contentWindowInsets = WindowInsets(0.dp),
floatingActionButton = {
FloatingActionButton(
onClick = {
@@ -119,11 +120,10 @@ fun TwoFaTokensScreen(
}
},
) { innerPadding ->
WallencScreenContentPadding(innerPadding) {
TwoFaTokensScreenContent(
uiState = uiState,
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
modifier = Modifier.fillMaxSize(),
) {
LazyColumn(verticalArrangement = Arrangement.spacedBy(10.dp)) {
items(uiState.items) { item ->
@@ -244,6 +244,7 @@ fun TwoFaTokensScreen(
}
}
}
}
}
}

View File

@@ -27,9 +27,7 @@ fun TwoFaTokensScreenContent(
tokenList: @Composable () -> Unit = {},
) {
Column(
modifier = modifier
.fillMaxSize()
.padding(16.dp),
modifier = modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
if (uiState.isLoading) {

View File

@@ -80,32 +80,29 @@ fun VaultBrowserScreen(
val showEmptyState = uiState.storagesList.isEmpty() && !uiState.storagesRefreshing
val isUuidBusy: (UUID) -> Boolean = { uuid -> uuid in uiState.busyStorageUuids }
Box {
Scaffold(
modifier = modifier,
contentWindowInsets = WindowInsets(0.dp),
floatingActionButton = {
FloatingActionButton(
onClick = {
if (fabEnabled && !fabBusy) {
viewModel.createStorage()
}
},
modifier = Modifier.alpha(if (fabEnabled && !fabBusy) 1f else 0.38f),
) {
Icon(
Icons.Filled.Add,
contentDescription = stringResource(
when {
!fabEnabled -> R.string.vault_fab_add_storage_disabled_cd
fabBusy -> R.string.vault_fab_add_storage_busy_cd
else -> R.string.vault_fab_add_storage_cd
},
),
)
val addFab: @Composable () -> Unit = {
FloatingActionButton(
onClick = {
if (fabEnabled && !fabBusy) {
viewModel.createStorage()
}
},
) { innerPadding ->
modifier = Modifier.alpha(if (fabEnabled && !fabBusy) 1f else 0.38f),
) {
Icon(
Icons.Filled.Add,
contentDescription = stringResource(
when {
!fabEnabled -> R.string.vault_fab_add_storage_disabled_cd
fabBusy -> R.string.vault_fab_add_storage_busy_cd
else -> R.string.vault_fab_add_storage_cd
},
),
)
}
}
val vaultContent: @Composable (androidx.compose.foundation.layout.PaddingValues) -> Unit = { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
@@ -178,7 +175,14 @@ fun VaultBrowserScreen(
}
}
}
}
}
Box(modifier = modifier) {
Scaffold(
contentWindowInsets = WindowInsets(0.dp),
floatingActionButton = addFab,
content = vaultContent,
)
if (showFullscreenLoader) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {

View File

@@ -1,19 +1,25 @@
package com.github.nullptroma.wallenc.ui.screens.shared
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.github.nullptroma.wallenc.ui.R
import com.github.nullptroma.wallenc.ui.elements.WallencScreenContentPadding
import com.github.nullptroma.wallenc.ui.elements.WallencScreenScaffold
@Composable
fun TextEditScreen(text: String) {
Text(
text = stringResource(R.string.text_edit_screen_placeholder, text),
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(16.dp),
)
fun TextEditScreen(
text: String,
modifier: Modifier = Modifier,
) {
WallencScreenScaffold(modifier = modifier) { innerPadding ->
WallencScreenContentPadding(innerPadding) {
Text(
text = stringResource(R.string.text_edit_screen_placeholder, text),
style = MaterialTheme.typography.bodyLarge,
)
}
}
}

View File

@@ -1,6 +1,7 @@
package com.github.nullptroma.wallenc.ui.screens.sync
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
@@ -39,6 +40,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
@@ -46,6 +48,7 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.github.nullptroma.wallenc.ui.R
import com.github.nullptroma.wallenc.ui.elements.AnimatedFloatingBackButton
import com.github.nullptroma.wallenc.ui.resources.UserNotification
import com.github.nullptroma.wallenc.domain.interfaces.StorageSyncGroupEncryptionKind
import java.util.UUID
@@ -441,28 +444,21 @@ private fun StoragePickerScreen(
contentWindowInsets = WindowInsets(0.dp),
snackbarHost = { SnackbarHost(snackbarHostState) },
) { inner ->
Column(
Box(
modifier = Modifier
.padding(inner)
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
.fillMaxSize(),
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
IconButton(onClick = onBack) {
Icon(
Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = stringResource(R.string.sync_cd_picker_back),
)
}
Text(
text = stringResource(id = R.string.sync_picker_title, groupId),
style = MaterialTheme.typography.titleMedium,
style = MaterialTheme.typography.titleLarge,
)
}
LazyColumn(
modifier = Modifier.fillMaxSize(),
@@ -539,6 +535,15 @@ private fun StoragePickerScreen(
}
}
}
}
AnimatedFloatingBackButton(
visible = true,
onClick = onBack,
modifier = Modifier
.align(Alignment.BottomStart)
.navigationBarsPadding()
.padding(start = 12.dp, bottom = 12.dp),
)
}
}
}

View File

@@ -7,6 +7,7 @@
<string name="nav_label_main">Главная</string>
<string name="nav_label_sync">Синхронизация</string>
<string name="nav_label_settings">Настройки</string>
<string name="nav_cd_back">Назад</string>
<string name="main_work_status_label">Статус:</string>
<string name="main_status_multiple_tasks">Выполняется задач: %1$d</string>
<string name="main_status_vault_scanning_storages">Сканирование vault: загрузка списка хранилищ…</string>

View File

@@ -7,6 +7,12 @@
<string name="nav_label_main">Home</string>
<string name="nav_label_sync">Sync</string>
<string name="nav_label_settings">Settings</string>
<string name="nav_cd_back">Go back</string>
<string name="screen_title_remote_vault">Remote vault</string>
<string name="screen_title_storage">Storage</string>
<string name="screen_title_two_fa">2FA tokens</string>
<string name="screen_title_text_secrets">Text secrets</string>
<string name="screen_title_text_edit">Text</string>
<string name="main_work_status_label">Status:</string>
<string name="main_status_multiple_tasks">Running tasks: %1$d</string>
<string name="main_status_vault_scanning_storages">Scanning vault: loading storage list…</string>