diff --git a/app/src/main/java/cn/airnan/a2fair/ui/tokens/TokenDetailSheet.kt b/app/src/main/java/cn/airnan/a2fair/ui/tokens/TokenDetailSheet.kt index 77b3bc3..42832bc 100644 --- a/app/src/main/java/cn/airnan/a2fair/ui/tokens/TokenDetailSheet.kt +++ b/app/src/main/java/cn/airnan/a2fair/ui/tokens/TokenDetailSheet.kt @@ -40,6 +40,8 @@ fun TokenDetailSheet( var nextCode by remember { mutableStateOf("") } var progress by remember { mutableStateOf(1f) } var remainingSeconds by remember { mutableStateOf(0) } + val showNextPreview = remainingSeconds in 0..5 && + (token.tokenType == TokenType.TOTP || token.tokenType == TokenType.STEAM) val secretBytes = remember(token.secret.value) { try { @@ -64,7 +66,7 @@ fun TokenDetailSheet( secretBytes, time, token.period, token.digits, token.algorithm.ordinal ) nextCode = NativeLib.generateTOTP( - secretBytes, time + 1, token.period, token.digits, token.algorithm.ordinal + secretBytes, time + token.period, token.period, token.digits, token.algorithm.ordinal ) val remaining = periodMs - (now % periodMs) progress = remaining.toFloat() / periodMs.toFloat() @@ -73,7 +75,7 @@ fun TokenDetailSheet( TokenType.STEAM -> { val time = now / 1000L currentCode = NativeLib.generateSteam(secretBytes, time) - nextCode = NativeLib.generateSteam(secretBytes, time + 1) + nextCode = NativeLib.generateSteam(secretBytes, time + 30) val periodMs = 30000L val remaining = periodMs - (now % periodMs) progress = remaining.toFloat() / periodMs.toFloat() @@ -133,12 +135,17 @@ fun TokenDetailSheet( color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold ) - if (remainingSeconds <= 5 && (token.tokenType == TokenType.TOTP || token.tokenType == TokenType.STEAM)) { - Text( - text = formatCode(nextCode), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) - ) + Box( + modifier = Modifier.height(20.dp), + contentAlignment = Alignment.Center + ) { + if (showNextPreview) { + Text( + text = formatCode(nextCode), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) + ) + } } } @@ -150,7 +157,7 @@ fun TokenDetailSheet( CircularProgressIndicator( progress = { progress }, modifier = Modifier.fillMaxSize(), - color = if (remainingSeconds <= 5) warningColor else MaterialTheme.colorScheme.primary, + color = if (showNextPreview) warningColor else MaterialTheme.colorScheme.primary, trackColor = MaterialTheme.colorScheme.surfaceVariant, strokeWidth = 3.dp, ) @@ -158,7 +165,7 @@ fun TokenDetailSheet( text = "$remainingSeconds", style = MaterialTheme.typography.labelMedium, fontWeight = FontWeight.Bold, - color = if (remainingSeconds <= 5) warningColor else MaterialTheme.colorScheme.onSurface + color = if (showNextPreview) warningColor else MaterialTheme.colorScheme.onSurface ) } } diff --git a/app/src/main/java/cn/airnan/a2fair/ui/tokens/TokensScreen.kt b/app/src/main/java/cn/airnan/a2fair/ui/tokens/TokensScreen.kt index 4aee2bc..9042e25 100644 --- a/app/src/main/java/cn/airnan/a2fair/ui/tokens/TokensScreen.kt +++ b/app/src/main/java/cn/airnan/a2fair/ui/tokens/TokensScreen.kt @@ -25,7 +25,11 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight @@ -48,6 +52,15 @@ fun TokensScreen( val uiState by viewModel.uiState.collectAsStateWithLifecycle() var showAddBottomSheet by remember { mutableStateOf(false) } var isSearchExpanded by remember { mutableStateOf(false) } + var searchFieldHasFocus by remember { mutableStateOf(false) } + val searchFocusRequester = remember { FocusRequester() } + val focusManager = LocalFocusManager.current + + LaunchedEffect(isSearchExpanded) { + if (isSearchExpanded) { + searchFocusRequester.requestFocus() + } + } Scaffold( topBar = { @@ -62,7 +75,9 @@ fun TokensScreen( } AnimatedVisibility( visible = isSearchExpanded, - modifier = Modifier.weight(1f) + modifier = Modifier + .weight(1f) + .padding(end = 16.dp) ) { Surface( shape = MaterialTheme.shapes.large, @@ -72,12 +87,21 @@ fun TokensScreen( TextField( value = uiState.searchQuery, onValueChange = { viewModel.updateSearchQuery(it) }, - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .focusRequester(searchFocusRequester) + .onFocusChanged { focusState -> + if (searchFieldHasFocus && !focusState.isFocused) { + isSearchExpanded = false + } + searchFieldHasFocus = focusState.isFocused + }, placeholder = { Text(stringResource(R.string.search_tokens)) }, leadingIcon = { Icon(Icons.Default.Search, contentDescription = stringResource(R.string.search)) }, trailingIcon = { - IconButton(onClick = { + IconButton(onClick = { viewModel.updateSearchQuery("") + focusManager.clearFocus(force = true) isSearchExpanded = false }) { Icon(Icons.Default.Close, contentDescription = "Clear search") @@ -211,6 +235,8 @@ fun TokenCard( var remainingSeconds by remember { mutableStateOf(0) } var showDetailSheet by remember { mutableStateOf(false) } val context = LocalContext.current + val showNextPreview = remainingSeconds in 0..5 && + (token.tokenType == TokenType.TOTP || token.tokenType == TokenType.STEAM) val secretBytes = remember(token.secret.value) { try { @@ -240,7 +266,7 @@ fun TokenCard( ) nextCode = NativeLib.generateTOTP( secretBytes, - time + 1, + time + token.period, token.period, token.digits, token.algorithm.ordinal @@ -257,7 +283,7 @@ fun TokenCard( ) nextCode = NativeLib.generateSteam( secretBytes, - time + 1 + time + 30 ) val periodMs = 30000L val remaining = periodMs - (now % periodMs) @@ -318,12 +344,17 @@ fun TokenCard( color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold ) - if (remainingSeconds <= 5 && (token.tokenType == TokenType.TOTP || token.tokenType == TokenType.STEAM)) { - Text( - text = formatCode(nextCode), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) - ) + Box( + modifier = Modifier.height(20.dp), + contentAlignment = Alignment.CenterStart + ) { + if (showNextPreview) { + Text( + text = formatCode(nextCode), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) + ) + } } } @@ -339,7 +370,7 @@ fun TokenCard( CircularProgressIndicator( progress = { progress }, modifier = Modifier.fillMaxSize(), - color = if (remainingSeconds <= 5) warningColor else MaterialTheme.colorScheme.primary, + color = if (showNextPreview) warningColor else MaterialTheme.colorScheme.primary, trackColor = MaterialTheme.colorScheme.surfaceVariant, strokeWidth = 3.dp, ) @@ -347,7 +378,7 @@ fun TokenCard( text = "$remainingSeconds", style = MaterialTheme.typography.labelMedium, fontWeight = FontWeight.Bold, - color = if (remainingSeconds <= 5) warningColor else MaterialTheme.colorScheme.onSurface + color = if (showNextPreview) warningColor else MaterialTheme.colorScheme.onSurface ) } }