refactor + feat: updated UI and added cards search logic

This commit is contained in:
2026-06-23 15:11:33 +03:00
parent d3c194ce9c
commit 8fe59df6f9
10 changed files with 255 additions and 96 deletions
@@ -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")
}
@@ -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()
@@ -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()
}
)
}
@@ -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)
}
@@ -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
)
)
}
}
}
}
}
@@ -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)) {
@@ -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(),
@@ -40,6 +40,7 @@
<string name="error_in_data">Ошибка в данных. Пожалуйста, заполните все необходимые (*) поля</string>
<string name="templates_not_found">Шаблоны не найдены</string>
<string name="search">Поиск</string>
<string name="cards_not_found">Карты не найдены</string>
<string name="choosing_icon">Выбор иконки</string>
<string name="icons_not_found">Иконки не найдены</string>
<string name="do_select_icon">Выбрать иконку</string>
+1
View File
@@ -45,6 +45,7 @@
<string name="error_in_data">Error in data. Please, fill all required (*) fields</string>
<string name="templates_not_found">Templates not found</string>
<string name="search">Search</string>
<string name="cards_not_found">Cards not found</string>
<string name="choosing_icon">Choosing icon</string>
<string name="icons_not_found">Icons not found</string>
<string name="do_select_icon">Select icon</string>
@@ -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),