feat: added icons for cards

This commit is contained in:
2026-06-22 22:33:11 +03:00
parent d3ef728007
commit bb57ab007c
16 changed files with 455 additions and 80 deletions
+1 -1
View File
@@ -77,7 +77,7 @@ dependencies {
implementation("com.google.android.gms:play-services-wearable:18.1.0") implementation("com.google.android.gms:play-services-wearable:18.1.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
implementation(libs.compose.colorpicker) implementation(libs.compose.colorpicker)
implementation("org.burnoutcrew.composereorderable:reorderable:0.9.6")
implementation("io.ktor:ktor-client-core:3.5.0") implementation("io.ktor:ktor-client-core:3.5.0")
implementation("io.ktor:ktor-client-cio:3.5.0") implementation("io.ktor:ktor-client-cio:3.5.0")
implementation("androidx.compose.material:material-icons-extended")
} }
+5
View File
@@ -10,6 +10,11 @@
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.OmniCards"> android:theme="@style/Theme.OmniCards">
<activity
android:name=".SelectIconActivity"
android:exported="false"
android:label="@string/title_activity_select_icon"
android:theme="@style/Theme.OmniCards" />
<activity <activity
android:name=".FillTemplateActivity" android:name=".FillTemplateActivity"
android:exported="false" android:exported="false"
@@ -11,6 +11,7 @@ import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -0,0 +1,28 @@
package ru.omni_devel.cards
import kotlinx.serialization.Serializable
import org.json.JSONArray
@Serializable
class CardIcon(
val names: LinkedHashMap<String, String>,
val iconId: String? = null
) {
companion object {
fun parseIconsDataFromJson(json: String): List<CardIcon> {
val array = JSONArray(json)
val icons = mutableListOf<CardIcon>()
for (i in 0 until array.length()) {
val obj = array.getJSONObject(i)
icons.add(CardIcon(
names = jsonObjectToMap(obj.getJSONObject("names")),
iconId = if (obj.has("iconId")) obj.getString("iconId") else null
))
}
return icons
}
}
}
@@ -14,5 +14,6 @@ class CardInfo (
val name: String, val name: String,
val codeType: BarcodeFormat, val codeType: BarcodeFormat,
val codeValue: String, val codeValue: String,
val color: String? val color: String?,
val icon: String?
) {} ) {}
@@ -1,5 +1,15 @@
package ru.omni_devel.cards package ru.omni_devel.cards
const val TEMPLATES_URL = "https://gitea.omni-devel.ru/omni/OmniCardsRepo/raw/branch/main/templates.json" const val REPO_BASE_URL = "https://gitea.omni-devel.ru/omni/OmniCardsRepo/raw/branch/main"
val SPACE_SYMBOLS = listOf(" ", "-", "_") const val TEMPLATES_REPO_FILE = "templates.json"
const val ICONS_REPO_FILE = "icons.json"
const val ICONS_REPO_FOLDER = "icons"
val SEARCH_REPLACE_SYMBOLS = mapOf(
" " to "",
"-" to "",
"_" to "",
"ё" to "е"
)
@@ -4,11 +4,14 @@ import android.content.ContentValues
import android.content.Context import android.content.Context
import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper import android.database.sqlite.SQLiteOpenHelper
import androidx.core.database.getStringOrNull
import com.google.zxing.BarcodeFormat import com.google.zxing.BarcodeFormat
class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?) : SQLiteOpenHelper(context, "omni_cards", factory, 5) { class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?) : SQLiteOpenHelper(context, "omni_cards", factory, 6) {
override fun onCreate(db: SQLiteDatabase?) { override fun onCreate(db: SQLiteDatabase?) {
db!!.execSQL("CREATE TABLE IF NOT EXISTS cards (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, codeType TEXT, codeValue TEXT, color TEXT)") db!!.execSQL(
"CREATE TABLE IF NOT EXISTS cards (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, codeType TEXT, codeValue TEXT, color TEXT, icon TEXT)"
)
} }
override fun onUpgrade( override fun onUpgrade(
@@ -35,9 +38,13 @@ class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?)
db.execSQL("ALTER TABLE cards DROP COLUMN monochrome") db.execSQL("ALTER TABLE cards DROP COLUMN monochrome")
db.execSQL("ALTER TABLE cards DROP COLUMN position") db.execSQL("ALTER TABLE cards DROP COLUMN position")
} }
if (oldVersion < 6) {
db!!.execSQL("ALTER TABLE cards ADD COLUMN icon TEXT")
}
} }
fun addCard(name: String, codeValue: String, codeType: BarcodeFormat, color: String?) { fun addCard(name: String, codeValue: String, codeType: BarcodeFormat, color: String?, icon: String?) {
val db = this.writableDatabase val db = this.writableDatabase
val values = ContentValues() val values = ContentValues()
@@ -46,19 +53,21 @@ class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?)
values.put("codeValue", codeValue) values.put("codeValue", codeValue)
values.put("codeType", codeType.name) values.put("codeType", codeType.name)
values.put("color", color) values.put("color", color)
values.put("icon", icon)
db.insert("cards", null, values) db.insert("cards", null, values)
db.close() db.close()
} }
fun editCard(id: Int, name: String, codeValue: String, codeType: BarcodeFormat, color: String?) { fun editCard(id: Int, name: String, codeValue: String, codeType: BarcodeFormat, color: String?, icon: String?) {
val values = ContentValues() val values = ContentValues()
values.put("name", name) values.put("name", name)
values.put("codeValue", codeValue) values.put("codeValue", codeValue)
values.put("codeType", codeType.name) values.put("codeType", codeType.name)
values.put("color", color) values.put("color", color)
values.put("icon", icon)
val db = this.writableDatabase val db = this.writableDatabase
@@ -79,7 +88,8 @@ class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?)
name = cursor.getString(cursor.getColumnIndexOrThrow("name")), name = cursor.getString(cursor.getColumnIndexOrThrow("name")),
codeType = BarcodeFormat.valueOf(cursor.getString(cursor.getColumnIndexOrThrow("codeType"))), codeType = BarcodeFormat.valueOf(cursor.getString(cursor.getColumnIndexOrThrow("codeType"))),
codeValue = cursor.getString(cursor.getColumnIndexOrThrow("codeValue")), codeValue = cursor.getString(cursor.getColumnIndexOrThrow("codeValue")),
color = cursor.getString(cursor.getColumnIndexOrThrow("color")) color = cursor.getString(cursor.getColumnIndexOrThrow("color")),
icon = cursor.getStringOrNull(cursor.getColumnIndexOrThrow("icon"))
)) ))
} }
@@ -106,7 +116,8 @@ class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?)
name = cursor.getString(cursor.getColumnIndexOrThrow("name")), name = cursor.getString(cursor.getColumnIndexOrThrow("name")),
codeType = BarcodeFormat.valueOf(cursor.getString(cursor.getColumnIndexOrThrow("codeType"))), codeType = BarcodeFormat.valueOf(cursor.getString(cursor.getColumnIndexOrThrow("codeType"))),
codeValue = cursor.getString(cursor.getColumnIndexOrThrow("codeValue")), codeValue = cursor.getString(cursor.getColumnIndexOrThrow("codeValue")),
color = cursor.getString(cursor.getColumnIndexOrThrow("color")) color = cursor.getString(cursor.getColumnIndexOrThrow("color")),
icon = cursor.getStringOrNull(cursor.getColumnIndexOrThrow("icon"))
) )
cursor.close() cursor.close()
@@ -140,10 +151,11 @@ class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?)
values.put("codeValue", card.codeValue) values.put("codeValue", card.codeValue)
values.put("codeType", card.codeType.name) values.put("codeType", card.codeType.name)
values.put("color", card.color) values.put("color", card.color)
values.put("icon", card.icon)
db.insert("cards", null, values) db.insert("cards", null, values)
} }
db.close() db.close()
} }
} }
@@ -10,6 +10,7 @@ import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@@ -18,10 +19,13 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
@@ -31,7 +35,9 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -73,13 +79,16 @@ fun CardEditable(
codeType: BarcodeFormat, codeType: BarcodeFormat,
onCodeTypeChange: (BarcodeFormat) -> Unit, onCodeTypeChange: (BarcodeFormat) -> Unit,
color: String?, color: String?,
onColorChange: (String?) -> Unit onColorChange: (String?) -> Unit,
icon: String?,
onIconChange: (String?) -> Unit
) { ) {
val context = LocalContext.current val context = LocalContext.current
var isCodeTypeDropdownExpanded by rememberSaveable { mutableStateOf(false) } var isCodeTypeDropdownExpanded by rememberSaveable { mutableStateOf(false) }
var isColorEnabled by rememberSaveable { mutableStateOf(color != null) } var isColorEnabled by rememberSaveable { mutableStateOf(color != null) }
var currentColor by rememberSaveable { mutableStateOf(color) } var currentColor by rememberSaveable { mutableStateOf(color) }
var currentIcon by rememberSaveable { mutableStateOf(icon) }
val fieldModifier = Modifier.fillMaxWidth() val fieldModifier = Modifier.fillMaxWidth()
@@ -118,6 +127,16 @@ fun CardEditable(
} }
} }
val selectIconLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val icon = result.data?.getStringExtra("icon")
onIconChange(icon)
}
}
OutlinedTextField( OutlinedTextField(
value = cardName, value = cardName,
onValueChange = onCardNameChange, onValueChange = onCardNameChange,
@@ -221,6 +240,29 @@ fun CardEditable(
} }
) )
} }
AdaptiveButton(
modifier = Modifier.fillMaxWidth().height(64.dp),
contentPadding = PaddingValues(horizontal = 0.dp),
onClick = {
val intent = Intent(context, SelectIconActivity::class.java)
selectIconLauncher.launch(intent)
}
) {
Row(
modifier = Modifier
.fillMaxSize()
.background(Color.Transparent)
.padding(horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
CardIcon(if (icon == null) null else base64ToImageBitmap(icon), MaterialTheme.colorScheme.primary)
Text(stringResource(R.string.do_select_icon))
}
}
} }
@Composable @Composable
@@ -229,6 +271,7 @@ fun AddCard(innerPadding: PaddingValues, getDbFun: () -> DbHelper) {
var codeValue by rememberSaveable { mutableStateOf("") } var codeValue by rememberSaveable { mutableStateOf("") }
var codeType by rememberSaveable { mutableStateOf(BarcodeFormat.QR_CODE) } var codeType by rememberSaveable { mutableStateOf(BarcodeFormat.QR_CODE) }
var color by rememberSaveable { mutableStateOf<String?>(null) } var color by rememberSaveable { mutableStateOf<String?>(null) }
var icon by rememberSaveable { mutableStateOf<String?>(null) }
val context = LocalActivity.current val context = LocalActivity.current
@@ -248,7 +291,9 @@ fun AddCard(innerPadding: PaddingValues, getDbFun: () -> DbHelper) {
codeType = codeType, codeType = codeType,
onCodeTypeChange = { codeType = it }, onCodeTypeChange = { codeType = it },
color = color, color = color,
onColorChange = { color = it } onColorChange = { color = it },
icon = icon,
onIconChange = { icon = it }
) )
AdaptiveButton( AdaptiveButton(
@@ -259,7 +304,7 @@ fun AddCard(innerPadding: PaddingValues, getDbFun: () -> DbHelper) {
val err = checkCardData(cardName, codeValue) val err = checkCardData(cardName, codeValue)
if (err == null) { if (err == null) {
getDbFun().addCard(cardName, codeValue, codeType, color) getDbFun().addCard(cardName, codeValue, codeType, color, icon)
context!!.finish() context!!.finish()
} else { } else {
Toast.makeText(context, context!!.getString(err), Toast.LENGTH_SHORT).show() Toast.makeText(context, context!!.getString(err), Toast.LENGTH_SHORT).show()
@@ -278,6 +323,7 @@ fun EditCard(cardInfo: CardInfo, innerPadding: PaddingValues, getDbFun: () -> Db
var codeValue by rememberSaveable { mutableStateOf(cardInfo.codeValue) } var codeValue by rememberSaveable { mutableStateOf(cardInfo.codeValue) }
var codeType by rememberSaveable { mutableStateOf(cardInfo.codeType) } var codeType by rememberSaveable { mutableStateOf(cardInfo.codeType) }
var color by rememberSaveable { mutableStateOf(cardInfo.color) } var color by rememberSaveable { mutableStateOf(cardInfo.color) }
var icon by rememberSaveable { mutableStateOf(cardInfo.icon) }
val context = LocalActivity.current val context = LocalActivity.current
@@ -297,7 +343,9 @@ fun EditCard(cardInfo: CardInfo, innerPadding: PaddingValues, getDbFun: () -> Db
codeType = codeType, codeType = codeType,
onCodeTypeChange = { codeType = it }, onCodeTypeChange = { codeType = it },
color = color, color = color,
onColorChange = { color = it } onColorChange = { color = it },
icon = icon,
onIconChange = { icon = it }
) )
AdaptiveButton( AdaptiveButton(
@@ -308,7 +356,7 @@ fun EditCard(cardInfo: CardInfo, innerPadding: PaddingValues, getDbFun: () -> Db
val err = checkCardData(cardName, codeValue) val err = checkCardData(cardName, codeValue)
if (err == null) { if (err == null) {
getDbFun().editCard(cardInfo.id, cardName, codeValue, codeType, color) getDbFun().editCard(cardInfo.id, cardName, codeValue, codeType, color, icon)
context!!.finish() context!!.finish()
} else { } else {
Toast.makeText(context, context!!.getString(err), Toast.LENGTH_SHORT).show() Toast.makeText(context, context!!.getString(err), Toast.LENGTH_SHORT).show()
@@ -4,6 +4,7 @@ import android.content.Intent
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.activity.compose.LocalActivity import androidx.activity.compose.LocalActivity
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.detectTapGestures
@@ -11,24 +12,26 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonColors
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ButtonElevation import androidx.compose.material3.ButtonElevation
import androidx.compose.material3.DrawerValue import androidx.compose.material3.DrawerValue
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material.icons.outlined.QrCode
import androidx.compose.material.icons.outlined.DensityMedium
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalDrawerSheet import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.ModalNavigationDrawer import androidx.compose.material3.ModalNavigationDrawer
@@ -45,11 +48,10 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier.Companion
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@@ -105,15 +107,15 @@ fun Page(name: String, innerPadding: PaddingValues, pages: List<NavigationPageDa
topBar = { topBar = {
TopBar(name, innerPadding, if (pages != null) { TopBar(name, innerPadding, if (pages != null) {
{ {
Text( Icon(
"",
fontSize = 24.sp,
color = MaterialTheme.colorScheme.onPrimaryContainer,
modifier = Modifier.clickable() { modifier = Modifier.clickable() {
scope.launch { scope.launch {
drawerState.open() drawerState.open()
} }
} },
imageVector = Icons.Outlined.DensityMedium,
contentDescription = null,
tint = MaterialTheme.colorScheme.onPrimaryContainer
) )
} }
} else null) } else null)
@@ -167,6 +169,11 @@ fun CardButton(cardInfo: CardInfo, onDeleteCard: (Int) -> Unit) {
var menuExpanded by remember { mutableStateOf(false) } var menuExpanded by remember { mutableStateOf(false) }
val cardColor = if (cardInfo.color == null)
MaterialTheme.colorScheme.primaryContainer
else
Color(cardInfo.color.toColorInt())
Box(modifier = Modifier.fillMaxWidth()) { Box(modifier = Modifier.fillMaxWidth()) {
Surface( Surface(
modifier = Modifier modifier = Modifier
@@ -185,20 +192,27 @@ fun CardButton(cardInfo: CardInfo, onDeleteCard: (Int) -> Unit) {
) )
}, },
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(12.dp),
color = (if (cardInfo.color == null) MaterialTheme.colorScheme.primaryContainer else Color(cardInfo.color.toColorInt())).copy(0.425f), color = cardColor.copy(0.425f),
tonalElevation = 2.dp, tonalElevation = 2.dp,
) { ) {
Row( Row(
modifier = Modifier.fillMaxSize(), modifier = Modifier
verticalAlignment = Alignment.CenterVertically .fillMaxSize()
.background(Color.Transparent)
.padding(horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
CardIcon(
if (cardInfo.icon == null) null else base64ToImageBitmap(cardInfo.icon),
cardColor = cardColor
)
Text( Text(
cardInfo.name, cardInfo.name,
fontSize = 16.sp, fontSize = 16.sp,
color = MaterialTheme.colorScheme.onPrimaryContainer, color = MaterialTheme.colorScheme.onPrimaryContainer,
modifier = Modifier modifier = Modifier.fillMaxWidth(),
.padding(16.dp)
.fillMaxWidth(),
textAlign = TextAlign.Start textAlign = TextAlign.Start
) )
} }
@@ -232,12 +246,46 @@ fun CardButton(cardInfo: CardInfo, onDeleteCard: (Int) -> Unit) {
} }
} }
@Composable
fun CardIcon(icon: ImageBitmap?, cardColor: Color, isLoading: Boolean = false) {
Row(
modifier = Modifier
.background(
cardColor.copy(0.525f),
RoundedCornerShape(12.dp)
)
.padding(8.dp)
.size(32.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = MaterialTheme.colorScheme.primary
)
} else if (icon == null) {
Icon(
modifier = Modifier.size(24.dp),
imageVector = Icons.Outlined.QrCode,
tint = MaterialTheme.colorScheme.onPrimaryContainer,
contentDescription = null
)
} else {
Image(
bitmap = icon,
contentDescription = null,
modifier = Modifier.size(32.dp)
)
}
}
}
@Composable @Composable
fun AdaptiveButton( fun AdaptiveButton(
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
enabled: Boolean = true, enabled: Boolean = true,
shape: Shape = ButtonDefaults.shape,
elevation: ButtonElevation? = ButtonDefaults.buttonElevation(), elevation: ButtonElevation? = ButtonDefaults.buttonElevation(),
border: BorderStroke? = null, border: BorderStroke? = null,
contentPadding: PaddingValues = ButtonDefaults.ContentPadding, contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
@@ -248,7 +296,7 @@ fun AdaptiveButton(
onClick = onClick, onClick = onClick,
modifier = modifier, modifier = modifier,
enabled = enabled, enabled = enabled,
shape = shape, shape = RoundedCornerShape(12.dp),
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primaryContainer, containerColor = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.onPrimaryContainer contentColor = MaterialTheme.colorScheme.onPrimaryContainer
@@ -6,44 +6,22 @@ import android.os.Bundle
import android.widget.Toast import android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.LocalActivity import androidx.activity.compose.LocalActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.client.request.get
import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.bodyAsText
import kotlinx.serialization.json.Json
import ru.omni_devel.cards.ui.theme.OmniCardsTheme import ru.omni_devel.cards.ui.theme.OmniCardsTheme
class FillTemplateActivity : ComponentActivity() { class FillTemplateActivity : ComponentActivity() {
@@ -76,7 +54,7 @@ fun FillTemplatePage(template: Template, innerPadding: PaddingValues) {
}) } }) }
Page( Page(
selectTemplateNameViaLanguage(template.names, language), selectItemNameViaLanguage(template.names, language),
innerPadding innerPadding
) { ) {
for (field in template.fields) { for (field in template.fields) {
@@ -88,7 +66,7 @@ fun FillTemplatePage(template: Template, innerPadding: PaddingValues) {
} }
}, },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
label = { Text((if (field.maybeEmpty) "" else "*") + selectTemplateNameViaLanguage(field.names, language)) } label = { Text((if (field.maybeEmpty) "" else "*") + selectItemNameViaLanguage(field.names, language)) }
) )
} }
@@ -0,0 +1,210 @@
package ru.omni_devel.cards
import android.app.Activity
import android.content.Intent
import android.graphics.BitmapFactory
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.LocalActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.ActivityResult
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
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.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.QrCode
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.engine.cio.CIO
import io.ktor.client.request.get
import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.bodyAsText
import ru.omni_devel.cards.ui.theme.OmniCardsTheme
class SelectIconActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
OmniCardsTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
SelectIconPage(innerPadding)
}
}
}
}
}
@Composable
fun SelectIconPage(innerPadding: PaddingValues) {
val context = LocalContext.current
val activityContext = LocalActivity.current
val language = getLanguage(context)
var searchQuery by rememberSaveable { mutableStateOf("") }
var iconsData by remember { mutableStateOf<List<CardIcon>?>(null) }
val loadedIcons = remember { mutableStateMapOf<String, ImageBitmap?>() }
var loadingError by remember { mutableStateOf<String?>(null) }
LaunchedEffect(Unit) {
try {
val client = HttpClient(CIO)
val response: HttpResponse = client.get("$REPO_BASE_URL/$ICONS_REPO_FILE")
val loadedIconsData = CardIcon.parseIconsDataFromJson(response.bodyAsText())
iconsData = loadedIconsData
for (iconData in loadedIconsData) {
if (iconData.iconId != null) {
val response = client.get("$REPO_BASE_URL/icons/${iconData.iconId}.png")
val bytes: ByteArray = response.body()
BitmapFactory.decodeByteArray(bytes, 0, bytes.size)?.let { bitmap ->
loadedIcons[iconData.iconId] = bitmap.asImageBitmap()
}
}
}
} catch (e: Exception) {
loadingError = e.toString()
}
}
Page(context.getString(R.string.choosing_icon), innerPadding) {
OutlinedTextField(
value = searchQuery,
onValueChange = { newValue ->
searchQuery = newValue
},
modifier = Modifier.fillMaxWidth(),
label = { Text(stringResource(R.string.search)) }
)
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
if (loadingError != null) {
item {
Text(loadingError!!)
}
} else if (iconsData == null) {
item {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
CircularProgressIndicator(
modifier = Modifier.size(48.dp),
color = MaterialTheme.colorScheme.primary
)
}
}
} 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) {
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(
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))
}
}
}
}
}
}
}
}
}
@@ -71,7 +71,7 @@ fun SelectTemplatePage(innerPadding: PaddingValues) {
try { try {
val client = HttpClient(CIO) val client = HttpClient(CIO)
val response: HttpResponse = client.get(TEMPLATES_URL) val response: HttpResponse = client.get("$REPO_BASE_URL/$TEMPLATES_REPO_FILE")
templates = Template.parseTemplatesFromJson(response.bodyAsText()) templates = Template.parseTemplatesFromJson(response.bodyAsText())
} catch (e: Exception) { } catch (e: Exception) {
@@ -104,7 +104,7 @@ fun SelectTemplatePage(innerPadding: PaddingValues) {
searchQuery = newValue searchQuery = newValue
}, },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
label = { Text(stringResource(R.string.search_templates)) } label = { Text(stringResource(R.string.search)) }
) )
LazyColumn( LazyColumn(
@@ -139,9 +139,9 @@ fun SelectTemplatePage(innerPadding: PaddingValues) {
for (name in template.names.values) { for (name in template.names.values) {
var nameCleaned = name.lowercase() var nameCleaned = name.lowercase()
for (spaceSymbol in SPACE_SYMBOLS) { for ((from, to) in SEARCH_REPLACE_SYMBOLS) {
nameCleaned = nameCleaned.replace(spaceSymbol, "") nameCleaned = nameCleaned.replace(from, to)
queryCleaned = queryCleaned.replace(spaceSymbol, "") queryCleaned = queryCleaned.replace(from, to)
} }
if (nameCleaned.contains(queryCleaned)) { if (nameCleaned.contains(queryCleaned)) {
@@ -169,7 +169,7 @@ fun SelectTemplatePage(innerPadding: PaddingValues) {
fillTemplateLauncher.launch(intent) fillTemplateLauncher.launch(intent)
} }
) { ) {
Text(selectTemplateNameViaLanguage(template.names, language)) Text(selectItemNameViaLanguage(template.names, language))
} }
} }
} }
@@ -4,18 +4,6 @@ import kotlinx.serialization.Serializable
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
private fun jsonObjectToMap(obj: JSONObject): LinkedHashMap<String, String> {
val map = LinkedHashMap<String, String>()
val keys = obj.keys()
while (keys.hasNext()) {
val key = keys.next()
map[key] = obj.getString(key)
}
return map
}
@Serializable @Serializable
class Field( class Field(
val id: String, val id: String,
@@ -2,12 +2,19 @@ package ru.omni_devel.cards
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.LocaleList import android.os.LocaleList
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asAndroidBitmap
import com.google.android.gms.wearable.Wearable import com.google.android.gms.wearable.Wearable
import com.google.zxing.BarcodeFormat import com.google.zxing.BarcodeFormat
import com.google.zxing.EncodeHintType import com.google.zxing.EncodeHintType
import com.google.zxing.MultiFormatWriter import com.google.zxing.MultiFormatWriter
import com.journeyapps.barcodescanner.BarcodeEncoder import com.journeyapps.barcodescanner.BarcodeEncoder
import org.json.JSONObject
import java.io.ByteArrayOutputStream
import android.util.Base64
import androidx.compose.ui.graphics.asImageBitmap
fun generateCode(value: String, format: BarcodeFormat): Bitmap { fun generateCode(value: String, format: BarcodeFormat): Bitmap {
val writer = MultiFormatWriter() val writer = MultiFormatWriter()
@@ -81,6 +88,36 @@ fun getLanguage(context: Context): String {
return languageCode return languageCode
} }
fun selectTemplateNameViaLanguage(names: LinkedHashMap<String, String>, language: String): String { fun selectItemNameViaLanguage(names: LinkedHashMap<String, String>, language: String): String {
return names[language] ?: names.values.firstOrNull() ?: "???" return names[language] ?: names.values.firstOrNull() ?: "???"
} }
fun jsonObjectToMap(obj: JSONObject): LinkedHashMap<String, String> {
val map = LinkedHashMap<String, String>()
val keys = obj.keys()
while (keys.hasNext()) {
val key = keys.next()
map[key] = obj.getString(key)
}
return map
}
fun imageBitmapToBase64(imageBitmap: ImageBitmap): String {
val bitmap = imageBitmap.asAndroidBitmap()
val outputStream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
val byteArray = outputStream.toByteArray()
return Base64.encodeToString(byteArray, Base64.DEFAULT)
}
fun base64ToImageBitmap(base64String: String): ImageBitmap {
val decodedBytes = Base64.decode(base64String, Base64.DEFAULT)
val bitmap = BitmapFactory.decodeByteArray(decodedBytes, 0, decodedBytes.size)
return bitmap.asImageBitmap()
}
+5 -1
View File
@@ -39,7 +39,11 @@
<string name="choosing_template">Выбор шаблона</string> <string name="choosing_template">Выбор шаблона</string>
<string name="error_in_data">Ошибка в данных. Пожалуйста, заполните все необходимые (*) поля</string> <string name="error_in_data">Ошибка в данных. Пожалуйста, заполните все необходимые (*) поля</string>
<string name="templates_not_found">Шаблоны не найдены</string> <string name="templates_not_found">Шаблоны не найдены</string>
<string name="search_templates">Поиск шаблонов</string> <string name="search">Поиск</string>
<string name="choosing_icon">Выбор иконки</string>
<string name="icons_not_found">Иконки не найдены</string>
<string name="do_select_icon">Выбрать иконку</string>
<string name="wait_for_icon_load">Дождитесь загрузки иконки</string>
<string name="card_name_should_not_be_empty">Имя карты не должно быть пустым</string> <string name="card_name_should_not_be_empty">Имя карты не должно быть пустым</string>
<string name="card_value_should_not_be_empty">Значение карты не должно быть пустым</string> <string name="card_value_should_not_be_empty">Значение карты не должно быть пустым</string>
<string name="name_must_be_shorter_than_32_symbols">Имя карты должно быть короче 32 символов</string> <string name="name_must_be_shorter_than_32_symbols">Имя карты должно быть короче 32 символов</string>
+6 -1
View File
@@ -44,10 +44,15 @@
<string name="choosing_template">Choosing template</string> <string name="choosing_template">Choosing template</string>
<string name="error_in_data">Error in data. Please, fill all required (*) fields</string> <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="templates_not_found">Templates not found</string>
<string name="search_templates">Search templates</string> <string name="search">Search</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>
<string name="wait_for_icon_load">Wait for icon load</string>
<string name="card_name_should_not_be_empty">Card name must not be empty</string> <string name="card_name_should_not_be_empty">Card name must not be empty</string>
<string name="card_value_should_not_be_empty">Card value must not be empty</string> <string name="card_value_should_not_be_empty">Card value must not be empty</string>
<string name="name_must_be_shorter_than_32_symbols">Card name must be shorter than 32 characters</string> <string name="name_must_be_shorter_than_32_symbols">Card name must be shorter than 32 characters</string>
<string name="failed_to_generate_card_code">Failed to generate card code. Check the card data</string> <string name="failed_to_generate_card_code">Failed to generate card code. Check the card data</string>
<string name="title_activity_select_icon">SelectIconActivity</string>
</resources> </resources>