From 8fe59df6f9a79d407120e72956da490a089bb495 Mon Sep 17 00:00:00 2001 From: Omni Devel Date: Tue, 23 Jun 2026 15:11:33 +0300 Subject: [PATCH] refactor + feat: updated UI and added cards search logic --- .../ru/omni_devel/cards/BackupActivity.kt | 5 + .../ru/omni_devel/cards/EditCardActivity.kt | 63 ++++---- .../main/java/ru/omni_devel/cards/Elements.kt | 49 +++++-- .../java/ru/omni_devel/cards/MainActivity.kt | 79 ++++++++++- .../ru/omni_devel/cards/SelectIconActivity.kt | 134 +++++++++++------- .../cards/SelectTemplateActivity.kt | 9 +- .../ru/omni_devel/cards/ShowCardActivity.kt | 8 +- mobile/src/main/res/values-ru/strings.xml | 1 + mobile/src/main/res/values/strings.xml | 1 + .../omni_devel/cards/presentation/Elements.kt | 2 +- 10 files changed, 255 insertions(+), 96 deletions(-) diff --git a/mobile/src/main/java/ru/omni_devel/cards/BackupActivity.kt b/mobile/src/main/java/ru/omni_devel/cards/BackupActivity.kt index 957f109..5f52b43 100644 --- a/mobile/src/main/java/ru/omni_devel/cards/BackupActivity.kt +++ b/mobile/src/main/java/ru/omni_devel/cards/BackupActivity.kt @@ -12,6 +12,9 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Save +import androidx.compose.material.icons.outlined.Restore import androidx.compose.material3.AlertDialog import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -135,6 +138,7 @@ fun BackupPage(innerPadding: PaddingValues, getDbFun: () -> DbHelper) { Page(stringResource(R.string.backup), innerPadding) { AdaptiveButton( modifier = Modifier.fillMaxWidth(), + icon = Icons.Outlined.Save, onClick = { createBackupLauncher.launch("omnicards-backup.json") } @@ -143,6 +147,7 @@ fun BackupPage(innerPadding: PaddingValues, getDbFun: () -> DbHelper) { } AdaptiveButton( modifier = Modifier.fillMaxWidth(), + icon = Icons.Outlined.Restore, onClick = { restoreFromBackupLauncher.launch("application/json") } diff --git a/mobile/src/main/java/ru/omni_devel/cards/EditCardActivity.kt b/mobile/src/main/java/ru/omni_devel/cards/EditCardActivity.kt index d6f15d7..83f44ea 100644 --- a/mobile/src/main/java/ru/omni_devel/cards/EditCardActivity.kt +++ b/mobile/src/main/java/ru/omni_devel/cards/EditCardActivity.kt @@ -30,6 +30,11 @@ import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Switch import androidx.compose.material3.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Palette +import androidx.compose.material.icons.outlined.QrCodeScanner +import androidx.compose.material.icons.outlined.TextFields +import androidx.compose.material.icons.outlined.Save import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue @@ -88,7 +93,6 @@ fun CardEditable( var isCodeTypeDropdownExpanded by rememberSaveable { mutableStateOf(false) } var isColorEnabled by rememberSaveable { mutableStateOf(color != null) } var currentColor by rememberSaveable { mutableStateOf(color) } - var currentIcon by rememberSaveable { mutableStateOf(icon) } val fieldModifier = Modifier.fillMaxWidth() @@ -151,32 +155,6 @@ fun CardEditable( label = { Text(stringResource(R.string.card_value)) } ) - AdaptiveButton( - modifier = Modifier.fillMaxWidth(), - onClick = { - scanLauncher.launch( - ScanOptions().apply { - setPrompt(context.getString(R.string.scan_your_card)) - setBeepEnabled(true) - setOrientationLocked(true) - } - ) - } - ) { - Text(stringResource(R.string.do_scan_card)) - } - - AdaptiveButton( - modifier = Modifier.fillMaxWidth(), - onClick = { - val intent = Intent(context, SelectTemplateActivity::class.java) - - selectTemplateLauncher.launch(intent) - } - ) { - Text(stringResource(R.string.card_from_template)) - } - Box( modifier = fieldModifier, ) { @@ -211,12 +189,41 @@ fun CardEditable( } } + AdaptiveButton( + modifier = Modifier.fillMaxWidth(), + icon = Icons.Outlined.QrCodeScanner, + onClick = { + scanLauncher.launch( + ScanOptions().apply { + setPrompt(context.getString(R.string.scan_your_card)) + setBeepEnabled(true) + setOrientationLocked(true) + } + ) + } + ) { + Text(stringResource(R.string.do_scan_card)) + } + + AdaptiveButton( + modifier = Modifier.fillMaxWidth(), + icon = Icons.Outlined.TextFields, + onClick = { + val intent = Intent(context, SelectTemplateActivity::class.java) + + selectTemplateLauncher.launch(intent) + } + ) { + Text(stringResource(R.string.card_from_template)) + } + Row( modifier = fieldModifier, horizontalArrangement = Arrangement.spacedBy(8.dp) ) { AdaptiveButton( modifier = Modifier.fillMaxWidth().weight(1.0f), + icon = Icons.Outlined.Palette, enabled = isColorEnabled, onClick = { val intent = Intent(context, ChooseColorActivity::class.java) @@ -298,6 +305,7 @@ fun AddCard(innerPadding: PaddingValues, getDbFun: () -> DbHelper) { AdaptiveButton( modifier = Modifier.fillMaxWidth(), + icon = Icons.Outlined.Save, onClick = { cardName = cardName.trim() @@ -350,6 +358,7 @@ fun EditCard(cardInfo: CardInfo, innerPadding: PaddingValues, getDbFun: () -> Db AdaptiveButton( modifier = Modifier.fillMaxWidth(), + icon = Icons.Outlined.Save, onClick = { cardName = cardName.trim() diff --git a/mobile/src/main/java/ru/omni_devel/cards/Elements.kt b/mobile/src/main/java/ru/omni_devel/cards/Elements.kt index 0e07426..2d7fba0 100644 --- a/mobile/src/main/java/ru/omni_devel/cards/Elements.kt +++ b/mobile/src/main/java/ru/omni_devel/cards/Elements.kt @@ -21,11 +21,13 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize 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.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material3.Button @@ -60,6 +62,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -73,12 +76,13 @@ import kotlinx.coroutines.launch fun Page( name: String, innerPadding: PaddingValues, + topBarAdditionalElements: @Composable () -> Unit = {}, floatingActionButton: @Composable (() -> Unit) = {}, content: @Composable () -> Unit ) { Scaffold( topBar = { - TopBar(name, innerPadding) + TopBar(name, innerPadding, topBarAdditionalElements) }, floatingActionButton = floatingActionButton ) { padding -> @@ -154,7 +158,11 @@ fun ExpandableFab( } @Composable -fun TopBar(text: String, innerPadding: PaddingValues) { +fun TopBar( + text: String, + innerPadding: PaddingValues, + topBarAdditionalElements: @Composable () -> Unit = {} +) { Row( modifier = Modifier .fillMaxWidth() @@ -162,7 +170,7 @@ fun TopBar(text: String, innerPadding: PaddingValues) { .padding(top = innerPadding.calculateTopPadding()) .padding(vertical = 12.dp, horizontal = 12.dp), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) + horizontalArrangement = Arrangement.SpaceBetween ) { Text( text, @@ -170,6 +178,8 @@ fun TopBar(text: String, innerPadding: PaddingValues) { fontSize = 24.sp, color = MaterialTheme.colorScheme.onPrimaryContainer ) + + topBarAdditionalElements() } } @@ -202,7 +212,7 @@ fun CardButton(cardInfo: CardInfo, onDeleteCard: (Int) -> Unit) { ) }, shape = RoundedCornerShape(12.dp), - color = cardColor.copy(0.425f), + color = MaterialTheme.colorScheme.primaryContainer.copy(0.625f), tonalElevation = 2.dp, ) { Row( @@ -296,25 +306,46 @@ fun AdaptiveButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, + icon: ImageVector? = null, elevation: ButtonElevation? = ButtonDefaults.buttonElevation(), border: BorderStroke? = null, contentPadding: PaddingValues = ButtonDefaults.ContentPadding, interactionSource: MutableInteractionSource? = null, + isSecondary: Boolean = false, content: @Composable RowScope.() -> Unit ) { + val colors = if (isSecondary) + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.secondaryContainer, + contentColor = MaterialTheme.colorScheme.onSecondaryContainer + ) + else + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer + ) + Button( onClick = onClick, modifier = modifier, enabled = enabled, shape = RoundedCornerShape(12.dp), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onPrimaryContainer - ), + colors = colors, elevation = elevation, border = border, contentPadding = contentPadding, interactionSource = interactionSource, - content = content + content = { + if (icon != null) { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + } + + content() + } ) } diff --git a/mobile/src/main/java/ru/omni_devel/cards/MainActivity.kt b/mobile/src/main/java/ru/omni_devel/cards/MainActivity.kt index 077c8e4..62ef091 100644 --- a/mobile/src/main/java/ru/omni_devel/cards/MainActivity.kt +++ b/mobile/src/main/java/ru/omni_devel/cards/MainActivity.kt @@ -5,22 +5,39 @@ import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Add import androidx.compose.material.icons.outlined.Sync import androidx.compose.material.icons.outlined.Backup import androidx.compose.material.icons.outlined.Code +import androidx.compose.material.icons.outlined.Search +import androidx.compose.material3.Icon +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource @@ -72,12 +89,25 @@ fun MainPage( val context = LocalContext.current val uriHandler = LocalUriHandler.current + var searchQuery by rememberSaveable { mutableStateOf("") } + var isSearchFieldShowed by rememberSaveable { mutableStateOf(false) } + Box( modifier = Modifier.fillMaxSize() ) { Page( stringResource(R.string.app_name), innerPadding, + topBarAdditionalElements = { + Icon( + Icons.Outlined.Search, + contentDescription = null, + modifier = Modifier.clickable { + isSearchFieldShowed = !isSearchFieldShowed + searchQuery = "" + } + ) + }, floatingActionButton = { ExpandableFab( items = listOf( @@ -100,17 +130,64 @@ fun MainPage( ) } ) { + var queryCleaned = searchQuery.lowercase() + + for ((from, to) in SEARCH_REPLACE_SYMBOLS) { + queryCleaned = queryCleaned.replace(from, to) + } + + val searchResults = cards.filter { cardInfo -> + if (queryCleaned.isEmpty()) { + return@filter true + } + + var cardNameCleaned = cardInfo.name.lowercase() + + for ((from, to) in SEARCH_REPLACE_SYMBOLS) { + cardNameCleaned = cardNameCleaned.replace(from, to) + } + + if (cardNameCleaned.contains(queryCleaned)) { + return@filter true + } + + return@filter false + } + LazyColumn( verticalArrangement = Arrangement.spacedBy(8.dp) ) { + item { + AnimatedVisibility( + visible = isSearchFieldShowed, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically() + ) { + OutlinedTextField( + value = searchQuery, + onValueChange = { + searchQuery = it + }, + modifier = Modifier.fillMaxWidth(), + label = { Text(stringResource(R.string.search)) } + ) + } + } + if (cards.isEmpty()) { item { Text( stringResource(R.string.add_card_to_get_started) ) } + } else if (searchResults.isEmpty()) { + item { + Text( + stringResource(R.string.cards_not_found) + ) + } } else { - for (card in cards) { + for (card in searchResults) { item { CardButton(card, onDeleteCard) } diff --git a/mobile/src/main/java/ru/omni_devel/cards/SelectIconActivity.kt b/mobile/src/main/java/ru/omni_devel/cards/SelectIconActivity.kt index d1d62cb..a4987d4 100644 --- a/mobile/src/main/java/ru/omni_devel/cards/SelectIconActivity.kt +++ b/mobile/src/main/java/ru/omni_devel/cards/SelectIconActivity.kt @@ -107,6 +107,32 @@ fun SelectIconPage(innerPadding: PaddingValues) { } } + var queryCleaned = searchQuery.lowercase() + + for ((from, to) in SEARCH_REPLACE_SYMBOLS) { + queryCleaned = queryCleaned.replace(from, to) + } + + val searchResults = iconsData!!.filter { iconData -> + if (queryCleaned.isEmpty()) { + return@filter true + } + + for (name in iconData.names.values) { + var nameCleaned = name.lowercase() + + for ((from, to) in SEARCH_REPLACE_SYMBOLS) { + nameCleaned = nameCleaned.replace(from, to) + } + + if (nameCleaned.contains(queryCleaned)) { + return@filter true + } + } + + return@filter false + } + Page(context.getString(R.string.choosing_icon), innerPadding) { OutlinedTextField( value = searchQuery, @@ -138,67 +164,69 @@ fun SelectIconPage(innerPadding: PaddingValues) { } } } else { - val searchResults = iconsData!!.filter { iconData -> - var queryCleaned = searchQuery.lowercase() - - if (queryCleaned.isEmpty()) { - return@filter true - } - - for (name in iconData.names.values) { - var nameCleaned = name.lowercase() - - for ((from, to) in SEARCH_REPLACE_SYMBOLS) { - nameCleaned = nameCleaned.replace(from, to) - queryCleaned = queryCleaned.replace(from, to) - } - - if (nameCleaned.contains(queryCleaned)) { - return@filter true - } - } - - return@filter false - } - if (searchResults.isEmpty()) { item { Text(stringResource(R.string.icons_not_found)) } } else { - for (iconData in searchResults) { + for (chunk in searchResults.chunked(2)) { item { - val loadedIcon = loadedIcons[iconData.iconId] - - AdaptiveButton( - modifier = Modifier.fillMaxWidth().height(64.dp), - contentPadding = PaddingValues(0.dp), - onClick = { - if (loadedIcon == null && iconData.iconId != null) { - Toast.makeText(context, context.getString(R.string.wait_for_icon_load), Toast.LENGTH_SHORT).show() - } else { - val intent = Intent() - - if (loadedIcon != null) { - intent.putExtra("icon", imageBitmapToBase64(loadedIcon)) - } - - activityContext!!.setResult(Activity.RESULT_OK, intent) - activityContext.finish() - } - } + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - Row( - modifier = Modifier - .fillMaxSize() - .background(Color.Transparent) - .padding(horizontal = 8.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - CardIcon(loadedIcon, MaterialTheme.colorScheme.primary, loadedIcon == null && iconData.iconId != null) + for (iconData in chunk) { + val loadedIcon = loadedIcons[iconData.iconId] - Text(selectItemNameViaLanguage(iconData.names, language)) + AdaptiveButton( + modifier = Modifier.weight(1f).height(64.dp), + contentPadding = PaddingValues(0.dp), + onClick = { + if (loadedIcon == null && iconData.iconId != null) { + Toast.makeText( + context, + context.getString(R.string.wait_for_icon_load), + Toast.LENGTH_SHORT + ).show() + } else { + val intent = Intent() + + if (loadedIcon != null) { + intent.putExtra( + "icon", + imageBitmapToBase64(loadedIcon) + ) + } + + activityContext!!.setResult( + Activity.RESULT_OK, + intent + ) + activityContext.finish() + } + } + ) { + Row( + modifier = Modifier + .fillMaxSize() + .background(Color.Transparent) + .padding(horizontal = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + CardIcon( + loadedIcon, + MaterialTheme.colorScheme.primary, + loadedIcon == null && iconData.iconId != null + ) + + Text( + selectItemNameViaLanguage( + iconData.names, + language + ) + ) + } + } } } } diff --git a/mobile/src/main/java/ru/omni_devel/cards/SelectTemplateActivity.kt b/mobile/src/main/java/ru/omni_devel/cards/SelectTemplateActivity.kt index fd06822..7bfaed0 100644 --- a/mobile/src/main/java/ru/omni_devel/cards/SelectTemplateActivity.kt +++ b/mobile/src/main/java/ru/omni_devel/cards/SelectTemplateActivity.kt @@ -129,9 +129,13 @@ fun SelectTemplatePage(innerPadding: PaddingValues) { } } } else { - val searchResults = currentTemplates.filter { template -> - var queryCleaned = searchQuery.lowercase() + var queryCleaned = searchQuery.lowercase() + for ((from, to) in SEARCH_REPLACE_SYMBOLS) { + queryCleaned = queryCleaned.replace(from, to) + } + + val searchResults = currentTemplates.filter { template -> if (queryCleaned.isEmpty()) { return@filter true } @@ -141,7 +145,6 @@ fun SelectTemplatePage(innerPadding: PaddingValues) { for ((from, to) in SEARCH_REPLACE_SYMBOLS) { nameCleaned = nameCleaned.replace(from, to) - queryCleaned = queryCleaned.replace(from, to) } if (nameCleaned.contains(queryCleaned)) { diff --git a/mobile/src/main/java/ru/omni_devel/cards/ShowCardActivity.kt b/mobile/src/main/java/ru/omni_devel/cards/ShowCardActivity.kt index f805a5a..b786d86 100644 --- a/mobile/src/main/java/ru/omni_devel/cards/ShowCardActivity.kt +++ b/mobile/src/main/java/ru/omni_devel/cards/ShowCardActivity.kt @@ -7,12 +7,14 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -20,6 +22,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.stringResource @@ -95,10 +99,10 @@ fun ShowCardPage(innerPadding: PaddingValues, cardInfo: CardInfo) { contentDescription = cardInfo.codeValue, modifier = Modifier .padding(bottom = 8.dp) - .fillMaxWidth() - .weight(1f, fill = false), + .clip(RoundedCornerShape(12.dp)), contentScale = ContentScale.Fit ) + Text( cardInfo.codeValue, modifier = Modifier.fillMaxWidth(), diff --git a/mobile/src/main/res/values-ru/strings.xml b/mobile/src/main/res/values-ru/strings.xml index 2849e5c..bd9c915 100644 --- a/mobile/src/main/res/values-ru/strings.xml +++ b/mobile/src/main/res/values-ru/strings.xml @@ -40,6 +40,7 @@ Ошибка в данных. Пожалуйста, заполните все необходимые (*) поля Шаблоны не найдены Поиск + Карты не найдены Выбор иконки Иконки не найдены Выбрать иконку diff --git a/mobile/src/main/res/values/strings.xml b/mobile/src/main/res/values/strings.xml index 9a787f4..7b2a555 100644 --- a/mobile/src/main/res/values/strings.xml +++ b/mobile/src/main/res/values/strings.xml @@ -45,6 +45,7 @@ Error in data. Please, fill all required (*) fields Templates not found Search + Cards not found Choosing icon Icons not found Select icon diff --git a/wear/src/main/java/ru/omni_devel/cards/presentation/Elements.kt b/wear/src/main/java/ru/omni_devel/cards/presentation/Elements.kt index 68c5094..f731386 100644 --- a/wear/src/main/java/ru/omni_devel/cards/presentation/Elements.kt +++ b/wear/src/main/java/ru/omni_devel/cards/presentation/Elements.kt @@ -44,7 +44,7 @@ fun CardButton(cardInfo: CardInfo) { Button( modifier = Modifier.fillMaxWidth().height(64.dp), colors = ButtonDefaults.buttonColors( - containerColor = cardColor.copy(0.425f) + containerColor = MaterialTheme.colorScheme.primaryContainer.copy(0.625f) ), shape = RoundedCornerShape(12.dp), contentPadding = PaddingValues(0.dp),