Compare commits

..
60 Commits
Author SHA1 Message Date
omni c593e06e4c refactor: cleaned code 2026-06-28 16:52:38 +03:00
omni 235afe61e7 update: updated versions 2026-06-25 12:35:41 +03:00
omni 8f79b60399 fix: disabled changing position when cards searching 2026-06-25 12:34:47 +03:00
omni 20a62b77b6 refactor: setted code scanner orientation to portrait 2026-06-24 22:11:06 +03:00
omni 4748a46156 feat: added cards reordering feature 2026-06-24 22:05:03 +03:00
omni c1a4414f10 feat: added color preview on card edit page 2026-06-24 20:29:04 +03:00
omni 2a538fdf1c feat: added sync with WearOS on card delete 2026-06-24 20:14:10 +03:00
omni ff214eb3d2 refactor: updated sync with WearOS logic 2026-06-24 20:11:19 +03:00
omni bdde3d8fbf update: updated versions 2026-06-24 07:20:49 +03:00
omni 5da00a1e88 fix: fixed card icon shape 2026-06-24 07:20:20 +03:00
omni 7e3a991548 update: updated versions 2026-06-23 21:13:26 +03:00
omni e8b572bf9a feat: added QrScript support for templates 2026-06-23 20:54:58 +03:00
omni d702928c28 update: setted corner radius for card icons 2026-06-23 18:18:20 +03:00
omni 28205df5e2 update: updated versions 2026-06-23 16:00:58 +03:00
omni 26069c19bd feat: added parallel icons loading 2026-06-23 15:27:05 +03:00
omni 17ac8d18a3 fix: fixed nullptr error 2026-06-23 15:23:41 +03:00
omni c80958f339 fix: removed card icons chunking 2026-06-23 15:15:59 +03:00
omni 8fe59df6f9 refactor + feat: updated UI and added cards search logic 2026-06-23 15:11:33 +03:00
omni d3c194ce9c fix: fixed top bar and added back handler for floating action button 2026-06-23 12:03:13 +03:00
omni 13d6ba0b52 refactor: replaced bottom actions list with floating action button 2026-06-23 11:53:30 +03:00
omni 3cf6b876f9 feat: added icons support for WearOS 2026-06-23 11:09:55 +03:00
omni bb57ab007c feat: added icons for cards 2026-06-22 22:33:11 +03:00
omni d3ef728007 fix: fixed backup buttons 2026-06-22 16:09:54 +03:00
omni 1fa90ff0b7 redesign: updated card buttons style on WearOS 2026-06-22 16:00:24 +03:00
omni 25c38418a3 redesign: updated card buttons style 2026-06-22 08:24:23 +03:00
omni 0a383573e2 fix: enabled minify 2026-06-21 21:53:54 +03:00
omni e6ebd574fd feat: added templates search 2026-06-21 21:35:36 +03:00
omni d7ff167e32 fix: setted UTF-8 charset for codes 2026-06-21 21:15:54 +03:00
omni 9d9b90deb1 feat: added localization for templates by language 2026-06-21 21:11:56 +03:00
omni 24927ba7ef fix 2026-06-21 20:44:34 +03:00
omni aea6607c29 fix: fixed wear database 2026-06-21 20:43:46 +03:00
omni 12b64766ef fix: fixed UI 2026-06-21 20:40:44 +03:00
omni 576642a920 refactor: updated UI 2026-06-21 20:14:41 +03:00
omni 2cc7a3a2c3 feat: added templates 2026-06-21 19:09:08 +03:00
omni 464094e6cb fix: fixed color picker 2026-06-21 15:25:05 +03:00
omni df95702247 feat: moved card color choosing to new screen 2026-06-21 14:07:12 +03:00
omni 1272e87ccd restore: restored older version 2026-06-21 12:27:54 +03:00
omni 123af530ed update: updated versions 2026-06-15 22:20:46 +03:00
omni 564245aa08 update: changed code size 2026-06-15 22:19:58 +03:00
omni 2277ae340c fix: fixed translations 2026-06-15 21:26:06 +03:00
omni edcaf6e02c fix: fixed closing action bar 2026-06-15 18:50:47 +03:00
omni da33a6813b feat: added animations 2026-06-15 18:43:03 +03:00
omni ca355f4135 update: updated versions 2026-06-15 17:34:59 +03:00
omni 4a5355e633 Merge pull request 'Added translations' (#1) from translations into main
Reviewed-on: #1
2026-06-15 14:33:00 +00:00
omni e50d0bf4ce fix: fixed translations 2026-06-15 17:31:53 +03:00
omni 4b840bf87d feat: added russian translations for WearOS 2026-06-15 17:30:05 +03:00
omni 36abc10972 feat: added russian translations 2026-06-15 17:18:36 +03:00
omni 5cf7e2b7b2 feat: added automatic generation for versionCode 2026-06-15 15:18:31 +03:00
omni 6f9490ea1c fix: fixed top bar paddings 2026-06-15 15:14:12 +03:00
omni 61090eefca update: updated versions 2026-06-14 20:24:23 +03:00
omni 52c485af64 feat: added backup and restored functionality 2026-06-14 20:21:38 +03:00
omni fba54e0243 feat: added trimming card name functionality 2026-06-14 19:36:33 +03:00
omni 7d6cf9dd7b fix: fixed card code image size 2026-06-14 19:31:38 +03:00
omni b18e99156a fix: fixed card values dropping at adding/editing after rotating phone 2026-06-14 19:21:20 +03:00
omni bb7807e2a4 feat: added closing card code after pause 2026-06-14 19:12:01 +03:00
omni 4d06e6cf0a update: replaced "About author" button with link to source code 2026-06-14 17:57:18 +03:00
omni 628a82c603 fix: fixed plugins 2026-06-14 17:54:05 +03:00
omni 0baa1eb019 fix: edited versions 2026-06-14 17:05:08 +03:00
omni 5593838a67 fix: edited versions 2026-06-14 16:55:12 +03:00
omni 97a9cb6582 refactor: cleaned code 2026-06-14 16:52:15 +03:00
40 changed files with 2161 additions and 506 deletions
+1
View File
@@ -2,4 +2,5 @@
plugins { plugins {
alias(libs.plugins.android.application) apply false alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.compose) apply false alias(libs.plugins.kotlin.compose) apply false
alias(libs.plugins.kotlin.serialization) apply false
} }
+1
View File
@@ -58,4 +58,5 @@ compose-colorpicker = { module = "com.github.skydoves:colorpicker-compose", vers
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
+22 -3
View File
@@ -1,6 +1,17 @@
plugins { plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.compose) alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlin.serialization)
}
fun generateVersionCode(): Int {
return try {
val process = ProcessBuilder("git", "rev-list", "--count", "HEAD").start()
"${process.inputStream.bufferedReader().readText().trim()}0".toInt() // 0 at end for mobile
} catch (e: Exception) {
1
}
} }
android { android {
@@ -11,15 +22,18 @@ android {
applicationId = "ru.omni_devel.cards" applicationId = "ru.omni_devel.cards"
minSdk = 30 minSdk = 30
targetSdk = 36 targetSdk = 36
versionCode = 40 // 0 at end for mobile versionCode = generateVersionCode()
versionName = "2.1" versionName = "4.3.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
buildTypes { buildTypes {
debug {
applicationIdSuffix = ".debug"
}
release { release {
isMinifyEnabled = false isMinifyEnabled = true
proguardFiles( proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro" "proguard-rules.pro"
@@ -33,6 +47,7 @@ android {
buildFeatures { buildFeatures {
compose = true compose = true
viewBinding = true viewBinding = true
buildConfig = true
} }
} }
@@ -63,4 +78,8 @@ 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("io.ktor:ktor-client-core:3.5.0")
implementation("io.ktor:ktor-client-cio:3.5.0")
implementation("androidx.compose.material:material-icons-extended")
implementation("sh.calvin.reorderable:reorderable:2.4.3")
} }
Binary file not shown.
Binary file not shown.
+31 -7
View File
@@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<application <application
android:allowBackup="true" android:allowBackup="true"
@@ -9,24 +12,45 @@
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.OmniCards"> android:theme="@style/Theme.OmniCards">
<activity <activity
android:name=".EditCardActivity" android:name="com.journeyapps.barcodescanner.CaptureActivity"
android:screenOrientation="portrait"
android:stateNotNeeded="true"
android:theme="@style/zxing_CaptureTheme"
android:windowSoftInputMode="stateAlwaysHidden"
tools:replace="android:screenOrientation" />
<activity
android:name=".SelectIconActivity"
android:exported="false" android:exported="false"
android:label="@string/title_activity_edit_card" android:label="@string/title_activity_select_icon"
android:theme="@style/Theme.OmniCards" /> android:theme="@style/Theme.OmniCards" />
<activity <activity
android:name=".AddCardActivity" android:name=".FillTemplateActivity"
android:exported="false"
android:theme="@style/Theme.OmniCards" />
<activity
android:name=".SelectTemplateActivity"
android:exported="false"
android:theme="@style/Theme.OmniCards" />
<activity
android:name=".ChooseColorActivity"
android:exported="false"
android:theme="@style/Theme.OmniCards" />
<activity
android:name=".BackupActivity"
android:exported="false"
android:theme="@style/Theme.OmniCards" />
<activity
android:name=".EditCardActivity"
android:exported="false" android:exported="false"
android:label="@string/title_activity_add_card"
android:theme="@style/Theme.OmniCards" /> android:theme="@style/Theme.OmniCards" />
<activity <activity
android:name=".ShowCardActivity" android:name=".ShowCardActivity"
android:exported="false" android:exported="false"
android:label="@string/title_activity_show_card"
android:theme="@style/Theme.OmniCards" /> android:theme="@style/Theme.OmniCards" />
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.OmniCards"> android:theme="@style/Theme.OmniCards">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
@@ -0,0 +1,158 @@
package ru.omni_devel.cards
import android.net.Uri
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.LocalActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
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
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import kotlinx.serialization.json.Json
import ru.omni_devel.cards.ui.theme.OmniCardsTheme
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource
class BackupActivity : ComponentActivity() {
private lateinit var db: DbHelper
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
db = DbHelper(this, null)
setContent {
OmniCardsTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
BackupPage(innerPadding = innerPadding, getDbFun = {
return@BackupPage db
})
}
}
}
}
}
@Composable
fun BackupPage(innerPadding: PaddingValues, getDbFun: () -> DbHelper) {
val context = LocalContext.current
val activityContext = LocalActivity.current
val showImportDialog = rememberSaveable { mutableStateOf(false) }
var pendingUri by rememberSaveable { mutableStateOf<Uri?>(null) }
val createBackupLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument("application/json")
) { uri ->
uri?.let {
try {
val jsonData = Json.encodeToString(getDbFun().getCards())
context.contentResolver.openOutputStream(it)?.use { outputStream ->
outputStream.write(jsonData.toByteArray())
Toast.makeText(context, context.getString(R.string.backup_successfully_saved), Toast.LENGTH_SHORT).show()
activityContext!!.finish()
}
} catch (e: Exception) {
Toast.makeText(context, context.getString(R.string.failed_to_save_backup, e), Toast.LENGTH_LONG).show()
}
}
}
val restoreFromBackupLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetContent()
) { uri ->
uri?.let {
pendingUri = it
showImportDialog.value = true
}
}
if (showImportDialog.value) {
AlertDialog(
title = { Text(stringResource(R.string.ask_do_restore)) },
text = { Text(stringResource(R.string.restore_caution)) },
confirmButton = {
TextButton(
onClick = {
showImportDialog.value = false
pendingUri?.let { uri ->
try {
context.contentResolver.openInputStream(uri)?.use { stream ->
val json = stream.bufferedReader().use { it.readText() }
val cards = Json.decodeFromString<List<CardInfo>>(json)
val db = getDbFun()
db.clearDatabase()
db.addCards(cards)
Toast.makeText(context, context.getString(R.string.backup_successfully_restored), Toast.LENGTH_SHORT).show()
activityContext!!.finish()
}
} catch (e: Exception) {
Toast.makeText(context, context.getString(R.string.failed_to_restore_backup, e), Toast.LENGTH_LONG).show()
}
}
}
) {
Text(stringResource(R.string.yes))
}
},
dismissButton = {
TextButton(
onClick = {
showImportDialog.value = false
}
) {
Text(stringResource(R.string.no))
}
},
onDismissRequest = { showImportDialog.value = false },
)
}
Page(stringResource(R.string.backup), innerPadding) {
AdaptiveButton(
modifier = Modifier.fillMaxWidth(),
icon = Icons.Outlined.Save,
onClick = {
createBackupLauncher.launch("omnicards-backup.json")
}
) {
Text(stringResource(R.string.do_backup))
}
AdaptiveButton(
modifier = Modifier.fillMaxWidth(),
icon = Icons.Outlined.Restore,
onClick = {
restoreFromBackupLauncher.launch("application/json")
}
) {
Text(stringResource(R.string.do_restore))
}
}
}
@@ -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
}
}
}
@@ -15,4 +15,6 @@ class CardInfo (
val codeType: BarcodeFormat, val codeType: BarcodeFormat,
val codeValue: String, val codeValue: String,
val color: String?, val color: String?,
val icon: String?,
val position: Int
) {} ) {}
@@ -0,0 +1,118 @@
package ru.omni_devel.cards
import android.app.Activity
import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.LocalActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
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.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
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.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.graphics.toColorInt
import com.github.skydoves.colorpicker.compose.ColorEnvelope
import com.github.skydoves.colorpicker.compose.HsvColorPicker
import com.github.skydoves.colorpicker.compose.rememberColorPickerController
import ru.omni_devel.cards.ui.theme.OmniCardsTheme
class ChooseColorActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
val currentColor = intent.getStringExtra("currentColor")
setContent {
OmniCardsTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
EditColorPage(innerPadding, currentColor)
}
}
}
}
}
@Composable
fun EditColorPage(innerPadding: PaddingValues, currentColor: String?) {
val activityContext = LocalActivity.current
val configuration = LocalConfiguration.current
val screenHeight = configuration.screenHeightDp.dp
val isPortrait = configuration.orientation == Configuration.ORIENTATION_PORTRAIT
var color by rememberSaveable { mutableStateOf(currentColor) }
val colorPickerController = rememberColorPickerController()
Page(
stringResource(R.string.choosing_color),
innerPadding
) {
Column(
modifier = Modifier.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
var colorPickerModifier = Modifier
.padding(10.dp)
if (isPortrait) {
colorPickerModifier = colorPickerModifier
.fillMaxWidth()
.height(450.dp)
} else {
val size = screenHeight * 0.5f
colorPickerModifier = colorPickerModifier
.height(size)
.width(size)
}
HsvColorPicker(
modifier = colorPickerModifier,
initialColor = color?.let { Color(it.toColorInt()) },
controller = colorPickerController,
onColorChanged = { colorEnvelope: ColorEnvelope ->
color = "#${colorEnvelope.hexCode.substring(2)}"
}
)
AdaptiveButton(
modifier = Modifier.fillMaxWidth(),
onClick = {
val resultIntent = Intent()
resultIntent.putExtra("color", color)
activityContext!!.setResult(Activity.RESULT_OK, resultIntent)
activityContext.finish()
}
) {
Text(stringResource(R.string.do_save))
}
}
}
}
@@ -0,0 +1,18 @@
package ru.omni_devel.cards
const val REPO_BASE_URL = "https://gitea.omni-devel.ru/omni/OmniCardsRepo/raw/branch/main"
val TEMPLATES_REPO_FILE = if (BuildConfig.DEBUG)
"templates.debug.json"
else
"templates.json"
const val ICONS_REPO_FILE = "icons.json"
const val ICONS_REPO_FOLDER = "icons"
val SEARCH_REPLACE_SYMBOLS = mapOf(
" " to "",
"-" to "",
"_" to "",
"ё" to "е"
)
@@ -2,13 +2,18 @@ package ru.omni_devel.cards
import android.content.ContentValues import android.content.ContentValues
import android.content.Context import android.content.Context
import android.database.Cursor
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 androidx.core.database.sqlite.transaction
import com.google.zxing.BarcodeFormat import com.google.zxing.BarcodeFormat
class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?) : SQLiteOpenHelper(context, "omni_cards", factory, 2) { class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?) : SQLiteOpenHelper(context, "omni_cards", factory, 8) {
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, position DEFAULT 0)"
)
} }
override fun onUpgrade( override fun onUpgrade(
@@ -19,30 +24,79 @@ class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?)
if (oldVersion < 2) { if (oldVersion < 2) {
db!!.execSQL("ALTER TABLE cards ADD COLUMN color TEXT") db!!.execSQL("ALTER TABLE cards ADD COLUMN color TEXT")
} }
if (oldVersion < 3) {
db!!.execSQL("ALTER TABLE cards ADD COLUMN position INTEGER DEFAULT 0")
db.execSQL("ALTER TABLE cards ADD COLUMN iconPath TEXT")
db.execSQL("UPDATE cards SET position = id")
}
if (oldVersion < 4) {
db!!.execSQL("ALTER TABLE cards ADD COLUMN monochrome INTEGER DEFAULT 0")
}
if (oldVersion < 5) {
db!!.execSQL("ALTER TABLE cards DROP COLUMN iconPath")
db.execSQL("ALTER TABLE cards DROP COLUMN monochrome")
db.execSQL("ALTER TABLE cards DROP COLUMN position")
}
if (oldVersion < 6) {
db!!.execSQL("ALTER TABLE cards ADD COLUMN icon TEXT")
}
if (oldVersion < 7) {
db!!.execSQL("ALTER TABLE cards ADD COLUMN position INTEGER DEFAULT 0")
db.execSQL("UPDATE cards SET position = id")
}
} }
fun addCard(name: String, codeValue: String, codeType: BarcodeFormat, color: String?) { private fun readCard(cursor: Cursor): CardInfo {
return CardInfo(
id = cursor.getInt(cursor.getColumnIndexOrThrow("id")),
name = cursor.getString(cursor.getColumnIndexOrThrow("name")),
codeType = BarcodeFormat.valueOf(cursor.getString(cursor.getColumnIndexOrThrow("codeType"))),
codeValue = cursor.getString(cursor.getColumnIndexOrThrow("codeValue")),
color = cursor.getString(cursor.getColumnIndexOrThrow("color")),
icon = cursor.getStringOrNull(cursor.getColumnIndexOrThrow("icon")),
position = cursor.getInt(cursor.getColumnIndexOrThrow("position"))
)
}
private fun nextPosition(db: SQLiteDatabase): Int {
val cursor = db.rawQuery("SELECT COALESCE(MAX(position), -1) + 1 FROM cards", null)
cursor.moveToFirst()
val next = cursor.getInt(0)
cursor.close()
return next
}
fun addCard(name: String, codeValue: String, codeType: BarcodeFormat, color: String?, icon: String?) {
val db = this.writableDatabase
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 values.put("position", nextPosition(db))
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
@@ -54,17 +108,11 @@ class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?)
fun getCards(): List<CardInfo> { fun getCards(): List<CardInfo> {
val db = this.readableDatabase val db = this.readableDatabase
val cursor = db.rawQuery("SELECT * FROM cards", null) val cursor = db.rawQuery("SELECT * FROM cards ORDER BY position ASC", null)
val cards = mutableListOf<CardInfo>() val cards = mutableListOf<CardInfo>()
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
cards.add(CardInfo( cards.add(readCard(cursor))
id = cursor.getInt(cursor.getColumnIndexOrThrow("id")),
name = cursor.getString(cursor.getColumnIndexOrThrow("name")),
codeType = BarcodeFormat.valueOf(cursor.getString(cursor.getColumnIndexOrThrow("codeType"))),
codeValue = cursor.getString(cursor.getColumnIndexOrThrow("codeValue")),
color = cursor.getString(cursor.getColumnIndexOrThrow("color"))
))
} }
cursor.close() cursor.close()
@@ -85,13 +133,7 @@ class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?)
return null return null
} }
val cardInfo = CardInfo( val cardInfo = readCard(cursor)
id = cursor.getInt(cursor.getColumnIndexOrThrow("id")),
name = cursor.getString(cursor.getColumnIndexOrThrow("name")),
codeType = BarcodeFormat.valueOf(cursor.getString(cursor.getColumnIndexOrThrow("codeType"))),
codeValue = cursor.getString(cursor.getColumnIndexOrThrow("codeValue")),
color = cursor.getString(cursor.getColumnIndexOrThrow("color"))
)
cursor.close() cursor.close()
db.close() db.close()
@@ -106,4 +148,48 @@ class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?)
db.close() db.close()
} }
fun clearDatabase() {
val db = this.writableDatabase
db.execSQL("DELETE FROM cards")
}
fun addCards(cards: List<CardInfo>) {
val db = this.writableDatabase
db.use { db ->
db.transaction {
for (card in cards) {
val values = ContentValues()
values.put("id", card.id)
values.put("name", card.name)
values.put("codeValue", card.codeValue)
values.put("codeType", card.codeType.name)
values.put("color", card.color)
values.put("icon", card.icon)
values.put("position", nextPosition(db))
db.insert("cards", null, values)
}
}
}
}
fun updatePositions(orderedIds: List<Int>) {
val db = this.writableDatabase
db.use { db ->
db.transaction {
orderedIds.forEachIndexed { index, id ->
val values = ContentValues()
values.put("position", index)
db.update("cards", values, "id = ?", arrayOf(id.toString()))
}
}
}
}
} }
@@ -1,6 +1,7 @@
package ru.omni_devel.cards package ru.omni_devel.cards
import android.app.Activity import android.app.Activity
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.widget.Toast import android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
@@ -8,6 +9,8 @@ import androidx.activity.compose.LocalActivity
import androidx.activity.compose.rememberLauncherForActivityResult 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.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
@@ -20,28 +23,30 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding 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.Button
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.IconButton 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
import androidx.compose.material3.Text 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.Composable
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.remember import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment 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.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.graphics.toColorInt import androidx.core.graphics.toColorInt
import com.github.skydoves.colorpicker.compose.ColorEnvelope
import com.github.skydoves.colorpicker.compose.HsvColorPicker
import com.github.skydoves.colorpicker.compose.rememberColorPickerController
import com.google.zxing.BarcodeFormat import com.google.zxing.BarcodeFormat
import com.journeyapps.barcodescanner.ScanContract import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanOptions import com.journeyapps.barcodescanner.ScanOptions
@@ -80,16 +85,23 @@ 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
) { ) {
var isCodeTypeDropdownExpanded by remember { mutableStateOf(false) } val context = LocalContext.current
var isColorPickerExpanded by remember { mutableStateOf(false) }
var isColorEnabled by remember { mutableStateOf(color != null) }
val colorPickerController = rememberColorPickerController() var isCodeTypeDropdownExpanded by rememberSaveable { mutableStateOf(false) }
var isColorEnabled by rememberSaveable { mutableStateOf(color != null) }
var currentColor by rememberSaveable { mutableStateOf(color) }
val fieldModifier = Modifier.fillMaxWidth() val fieldModifier = Modifier.fillMaxWidth()
val cardColor = if (color == null)
MaterialTheme.colorScheme.primary
else
Color(color.toColorInt())
val scanLauncher = rememberLauncherForActivityResult( val scanLauncher = rememberLauncherForActivityResult(
contract = ScanContract() contract = ScanContract()
) { result -> ) { result ->
@@ -99,41 +111,57 @@ fun CardEditable(
result.formatName?.let { formatName -> result.formatName?.let { formatName ->
try { try {
onCodeTypeChange(BarcodeFormat.valueOf(formatName)) onCodeTypeChange(BarcodeFormat.valueOf(formatName))
} catch (e: IllegalArgumentException) {} } catch (_: IllegalArgumentException) {}
} }
} }
} }
val editColorLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val color = result.data?.getStringExtra("color")
currentColor = color
onColorChange(color)
}
}
val selectTemplateLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val codeValue = result.data?.getStringExtra("codeValue")!!
val codeType = BarcodeFormat.valueOf(result.data?.getStringExtra("codeType")!!)
onCodeValueChange(codeValue)
onCodeTypeChange(codeType)
}
}
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,
modifier = fieldModifier, modifier = fieldModifier,
label = { Text("Name") } label = { Text(stringResource(R.string.card_name)) }
)
OutlinedTextField(
value = codeValue,
onValueChange = onCodeValueChange,
modifier = fieldModifier,
label = { Text(stringResource(R.string.card_value)) }
) )
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
OutlinedTextField(
value = codeValue,
onValueChange = onCodeValueChange,
modifier = Modifier.weight(1f),
label = { Text("Value") }
)
Button(
modifier = Modifier.align(Alignment.CenterVertically),
onClick = {
scanLauncher.launch(
ScanOptions().apply {
setPrompt("Scan your card")
setBeepEnabled(true)
setOrientationLocked(false)
}
)
}
) {
Text("Scan")
}
}
Box( Box(
modifier = fieldModifier, modifier = fieldModifier,
@@ -142,7 +170,7 @@ fun CardEditable(
value = codeType.name, value = codeType.name,
onValueChange = {}, onValueChange = {},
modifier = fieldModifier, modifier = fieldModifier,
label = { Text("Code type") }, label = { Text(stringResource(R.string.card_code_type)) },
readOnly = true, readOnly = true,
) )
Box( Box(
@@ -169,18 +197,53 @@ 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( Row(
modifier = fieldModifier, modifier = fieldModifier,
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
Button( AdaptiveButton(
modifier = Modifier.fillMaxWidth().weight(1.0f),
icon = Icons.Outlined.Palette,
enabled = isColorEnabled, enabled = isColorEnabled,
onClick = { onClick = {
isColorPickerExpanded = !isColorPickerExpanded val intent = Intent(context, ChooseColorActivity::class.java)
intent.putExtra("currentColor", currentColor)
editColorLauncher.launch(intent)
} }
) { ) {
Text("Choose color") Text(stringResource(R.string.do_choose_color))
} }
Switch( Switch(
checked = isColorEnabled, checked = isColorEnabled,
onCheckedChange = { onCheckedChange = {
@@ -193,32 +256,42 @@ fun CardEditable(
) )
} }
if (isColorPickerExpanded && isColorEnabled) { AdaptiveButton(
HsvColorPicker( 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 modifier = Modifier
.fillMaxWidth() .fillMaxSize()
.height(450.dp) .background(Color.Transparent)
.padding(10.dp), .padding(horizontal = 8.dp),
initialColor = color?.let { Color(it.toColorInt()) }, verticalAlignment = Alignment.CenterVertically,
controller = colorPickerController, horizontalArrangement = Arrangement.spacedBy(8.dp)
onColorChanged = { colorEnvelope: ColorEnvelope -> ) {
onColorChange("#${colorEnvelope.hexCode.substring(2)}") CardIcon(if (icon == null) null else base64ToImageBitmap(icon), cardColor)
}
) Text(stringResource(R.string.do_select_icon))
}
} }
} }
@Composable @Composable
fun AddCard(innerPadding: PaddingValues, getDbFun: () -> DbHelper) { fun AddCard(innerPadding: PaddingValues, getDbFun: () -> DbHelper) {
var cardName by remember { mutableStateOf("") } var cardName by rememberSaveable { mutableStateOf("") }
var codeValue by remember { mutableStateOf("") } var codeValue by rememberSaveable { mutableStateOf("") }
var codeType by remember { mutableStateOf(BarcodeFormat.QR_CODE) } var codeType by rememberSaveable { mutableStateOf(BarcodeFormat.QR_CODE) }
var color by remember { 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
Page( Page(
"New card", stringResource(R.string.new_card_title),
innerPadding innerPadding
) { ) {
Column( Column(
@@ -233,23 +306,28 @@ 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 }
) )
Button( AdaptiveButton(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
icon = Icons.Outlined.Save,
onClick = { onClick = {
cardName = cardName.trim()
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, err, Toast.LENGTH_SHORT).show() Toast.makeText(context, context!!.getString(err), Toast.LENGTH_SHORT).show()
} }
} }
) { ) {
Text("Save") Text(stringResource(R.string.do_save))
} }
} }
} }
@@ -257,15 +335,16 @@ fun AddCard(innerPadding: PaddingValues, getDbFun: () -> DbHelper) {
@Composable @Composable
fun EditCard(cardInfo: CardInfo, innerPadding: PaddingValues, getDbFun: () -> DbHelper) { fun EditCard(cardInfo: CardInfo, innerPadding: PaddingValues, getDbFun: () -> DbHelper) {
var cardName by remember { mutableStateOf(cardInfo.name) } var cardName by rememberSaveable { mutableStateOf(cardInfo.name) }
var codeValue by remember { mutableStateOf(cardInfo.codeValue) } var codeValue by rememberSaveable { mutableStateOf(cardInfo.codeValue) }
var codeType by remember { mutableStateOf(cardInfo.codeType) } var codeType by rememberSaveable { mutableStateOf(cardInfo.codeType) }
var color by remember { 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
Page( Page(
"Editing card id${cardInfo.id}", stringResource(R.string.editing_card_title, cardInfo.id),
innerPadding innerPadding
) { ) {
Column( Column(
@@ -280,23 +359,28 @@ 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 }
) )
Button( AdaptiveButton(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
icon = Icons.Outlined.Save,
onClick = { onClick = {
cardName = cardName.trim()
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, err, Toast.LENGTH_SHORT).show() Toast.makeText(context, context!!.getString(err), Toast.LENGTH_SHORT).show()
} }
} }
) { ) {
Text("Save") Text(stringResource(R.string.do_save))
} }
} }
} }
@@ -1,81 +1,206 @@
package ru.omni_devel.cards package ru.omni_devel.cards
import android.content.Intent import android.content.Intent
import androidx.activity.compose.BackHandler
import androidx.activity.compose.LocalActivity
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
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.gestures.detectTapGestures import androidx.compose.foundation.gestures.detectTapGestures
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.fillMaxHeight 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.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.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
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.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ButtonElevation
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.Add
import androidx.compose.material.icons.outlined.QrCode
import androidx.compose.material.icons.outlined.DensityMedium
import androidx.compose.material.icons.outlined.DragHandle
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.NavigationDrawerItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
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.draw.clip
import androidx.compose.ui.graphics.Color 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.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.core.graphics.toColorInt import androidx.core.graphics.toColorInt
import kotlinx.coroutines.launch
@Composable @Composable
fun Page(name: String, innerPadding: PaddingValues, content: @Composable () -> Unit) { fun Page(
Column() { name: String,
TopBar(name, innerPadding) innerPadding: PaddingValues,
topBarAdditionalElements: @Composable () -> Unit = {},
Column( floatingActionButton: @Composable (() -> Unit) = {},
content: @Composable () -> Unit
) {
Scaffold(
topBar = {
TopBar(name, innerPadding, topBarAdditionalElements)
},
floatingActionButton = floatingActionButton
) { padding ->
Box(
modifier = Modifier modifier = Modifier
.padding(horizontal = 16.dp) .padding(padding)
.fillMaxSize()
.background(MaterialTheme.colorScheme.background) .background(MaterialTheme.colorScheme.background)
.padding(horizontal = 16.dp)
.padding(top = 16.dp)
) { ) {
content() Column(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
content()
}
} }
} }
} }
@Composable @Composable
fun TopBar(text: String, innerPadding: PaddingValues) { fun ExpandableFab(
Row( items: List<NavigationPageData>
modifier = Modifier ) {
.fillMaxWidth() val activityContext = LocalActivity.current
.background(MaterialTheme.colorScheme.primaryContainer) var isExpanded by remember { mutableStateOf(false) }
.padding(innerPadding)
val rotation by animateFloatAsState(if (isExpanded) 45f else 0f)
BackHandler(
enabled = isExpanded
) { ) {
Text( isExpanded = false
text, }
modifier = Modifier.padding(vertical = 4.dp, horizontal = 12.dp),
fontSize = 24.sp, Column(
color = MaterialTheme.colorScheme.onPrimaryContainer horizontalAlignment = Alignment.End,
) verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items.forEach { page ->
AnimatedVisibility(
visible = isExpanded,
enter = fadeIn() + scaleIn(),
exit = fadeOut() + scaleOut()
) {
ExtendedFloatingActionButton(
text = { Text(page.name) },
icon = { Icon(page.icon, contentDescription = null) },
onClick = {
isExpanded = false
if (page.pageClass != null) {
val intent = Intent(activityContext, page.pageClass)
activityContext!!.startActivity(intent)
} else if (page.onClick != null) {
page.onClick()
}
}
)
}
}
FloatingActionButton(
onClick = { isExpanded = !isExpanded }
) {
Icon(
modifier = Modifier.graphicsLayer { rotationZ = rotation },
imageVector = Icons.Outlined.Add,
contentDescription = null
)
}
} }
} }
@Composable @Composable
fun CardButton(cardInfo: CardInfo, onDeleteCard: (Int) -> Unit) { fun TopBar(
text: String,
innerPadding: PaddingValues,
topBarAdditionalElements: @Composable () -> Unit = {}
) {
Row(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.primaryContainer)
.padding(top = innerPadding.calculateTopPadding())
.padding(vertical = 12.dp, horizontal = 12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text,
modifier = Modifier,
fontSize = 24.sp,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
topBarAdditionalElements()
}
}
@Composable
fun CardButton(cardInfo: CardInfo, onDeleteCard: (Int) -> Unit, reorderIcon: (@Composable () -> Unit)? = null) {
val context = LocalContext.current val context = LocalContext.current
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
.fillMaxWidth() .fillMaxWidth()
.height(64.dp)
.pointerInput(Unit) { .pointerInput(Unit) {
detectTapGestures( detectTapGestures(
onTap = { onTap = {
@@ -88,34 +213,40 @@ fun CardButton(cardInfo: CardInfo, onDeleteCard: (Int) -> Unit) {
} }
) )
}, },
shape = ButtonDefaults.shape, shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.primaryContainer, color = MaterialTheme.colorScheme.primaryContainer.copy(0.625f),
tonalElevation = 2.dp tonalElevation = 2.dp,
) { ) {
Row( Row(
modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Min), modifier = Modifier
.fillMaxSize()
.background(Color.Transparent)
.padding(horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
) { ) {
Box( Row(
modifier = Modifier modifier = Modifier.weight(1f),
.fillMaxHeight() verticalAlignment = Alignment.CenterVertically,
.width(32.dp) horizontalArrangement = Arrangement.spacedBy(8.dp)
.background( ) {
if (cardInfo.color == null) MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.625f) CardIcon(
else Color(cardInfo.color.toColorInt()).copy(alpha = 0.625f) 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) textAlign = TextAlign.Start
.fillMaxWidth() )
.weight(1f), }
textAlign = TextAlign.Start
) if (reorderIcon != null) {
reorderIcon()
}
} }
} }
@@ -124,7 +255,7 @@ fun CardButton(cardInfo: CardInfo, onDeleteCard: (Int) -> Unit) {
onDismissRequest = { menuExpanded = false } onDismissRequest = { menuExpanded = false }
) { ) {
DropdownMenuItem( DropdownMenuItem(
text = { Text("Edit") }, text = { Text(stringResource(R.string.do_edit_card)) },
onClick = { onClick = {
menuExpanded = false menuExpanded = false
@@ -136,7 +267,7 @@ fun CardButton(cardInfo: CardInfo, onDeleteCard: (Int) -> Unit) {
} }
) )
DropdownMenuItem( DropdownMenuItem(
text = { Text("Delete") }, text = { Text(stringResource(R.string.do_delete_card)) },
onClick = { onClick = {
menuExpanded = false menuExpanded = false
@@ -148,11 +279,84 @@ fun CardButton(cardInfo: CardInfo, onDeleteCard: (Int) -> Unit) {
} }
@Composable @Composable
fun FullWidthButton(onClick: () -> Unit, content: @Composable () -> Unit) { fun CardIcon(icon: ImageBitmap?, cardColor: Color, isLoading: Boolean = false) {
Button( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier
onClick = onClick .background(
cardColor.copy(0.525f),
RoundedCornerShape(12.dp)
)
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) { ) {
content() if (isLoading) {
CircularProgressIndicator(
modifier = Modifier.size(32.dp),
color = MaterialTheme.colorScheme.primary
)
} else if (icon == null) {
Icon(
modifier = Modifier.size(32.dp),
imageVector = Icons.Outlined.QrCode,
tint = MaterialTheme.colorScheme.onPrimaryContainer,
contentDescription = null
)
} else {
Image(
bitmap = icon,
contentDescription = null,
modifier = Modifier.size(36.dp).clip(RoundedCornerShape(12.dp))
)
}
} }
} }
@Composable
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 = colors,
elevation = elevation,
border = border,
contentPadding = contentPadding,
interactionSource = interactionSource,
content = {
if (icon != null) {
Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
Spacer(modifier = Modifier.width(8.dp))
}
content()
}
)
}
@@ -0,0 +1,110 @@
package ru.omni_devel.cards
import android.app.Activity
import android.content.Intent
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.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
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.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import ru.omni_devel.cards.ui.theme.OmniCardsTheme
class FillTemplateActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
val templateJson = intent.getStringExtra("templateJson") ?: ""
val template = Template.parseTemplateFromJson(templateJson)
setContent {
OmniCardsTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
FillTemplatePage(template, innerPadding)
}
}
}
}
}
@Composable
fun FillTemplatePage(template: Template, innerPadding: PaddingValues) {
val activityContext = LocalActivity.current
val context = LocalContext.current
val language = getLanguage(context)
var filledFields by rememberSaveable { mutableStateOf(template.fields.associate { field ->
field.id to (field.default ?: "")
}) }
Page(
selectItemNameViaLanguage(template.names, language),
innerPadding
) {
for (field in template.fields) {
OutlinedTextField(
value = filledFields[field.id]!!,
onValueChange = { newValue ->
filledFields = filledFields.toMutableMap().apply {
this[field.id] = newValue
}
},
modifier = Modifier.fillMaxWidth(),
label = { Text((if (field.maybeEmpty) "" else "*") + selectItemNameViaLanguage(field.names, language)) }
)
}
AdaptiveButton(
modifier = Modifier.fillMaxWidth(),
onClick = {
var isSuccess = true
for (field in template.fields) {
if (filledFields[field.id]!!.isEmpty() && !field.maybeEmpty) {
isSuccess = false
}
}
if (!isSuccess) {
Toast.makeText(context, context.getString(R.string.error_in_data), Toast.LENGTH_LONG).show()
return@AdaptiveButton
}
val result = try {
processQrScript(template.template, filledFields)
} catch (e: Exception) {
Toast.makeText(context, e.toString(), Toast.LENGTH_LONG).show()
return@AdaptiveButton
}
val resultIntent = Intent()
resultIntent.putExtra("codeValue", result)
resultIntent.putExtra("codeType", template.codeType.name)
activityContext!!.setResult(Activity.RESULT_OK, resultIntent)
activityContext.finish()
}
) {
Text(stringResource(R.string.do_save))
}
}
}
@@ -1,42 +1,54 @@
package ru.omni_devel.cards package ru.omni_devel.cards
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.widget.Toast import android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.background import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
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
import androidx.compose.foundation.layout.Column
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.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.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons
import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.outlined.Add
import androidx.compose.material3.Button 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.DragHandle
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
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.mutableStateOf import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.graphics.Color import androidx.compose.ui.draw.alpha
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import kotlinx.serialization.json.Json import androidx.compose.ui.res.stringResource
import ru.omni_devel.cards.ui.theme.OmniCardsTheme import ru.omni_devel.cards.ui.theme.OmniCardsTheme
import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyListState
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private lateinit var db: DbHelper private lateinit var db: DbHelper
@@ -54,12 +66,22 @@ class MainActivity : ComponentActivity() {
Scaffold( Scaffold(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { innerPadding -> ) { innerPadding ->
MainPage(innerPadding, cards, onDeleteCard = { cardId -> MainPage(
db.removeCard(cardId) innerPadding,
cards.removeIf { it.id == cardId } cards,
}, getDbFun = { onDeleteCard = { cardId ->
return@MainPage db db.removeCard(cardId)
}) cards.removeIf { it.id == cardId }
syncCardsWithWatch(this, db.getCards())
},
onReorderCards = {
db.updatePositions(it)
syncCardsWithWatch(this, db.getCards())
},
getDbFun = {
return@MainPage db
}
)
} }
} }
} }
@@ -70,108 +92,169 @@ class MainActivity : ComponentActivity() {
cards.clear() cards.clear()
cards.addAll(db.getCards()) cards.addAll(db.getCards())
syncCardsWithWatch(this, db.getCards())
} }
} }
@Composable @Composable
fun MainPage( fun MainPage(
innerPadding: PaddingValues, innerPadding: PaddingValues,
cards: List<CardInfo>, cards: SnapshotStateList<CardInfo>,
onDeleteCard: (Int) -> Unit, onDeleteCard: (Int) -> Unit,
onReorderCards: (List<Int>) -> Unit,
getDbFun: () -> DbHelper getDbFun: () -> DbHelper
) { ) {
val context = LocalContext.current val context = LocalContext.current
val uriHandler = LocalUriHandler.current val uriHandler = LocalUriHandler.current
var isActionBarShowed by remember { mutableStateOf(false) } var searchQuery by rememberSaveable { mutableStateOf("") }
var isSearchFieldShowed by rememberSaveable { mutableStateOf(false) }
val lazyListState = rememberLazyListState()
val reorderableLazyListState = rememberReorderableLazyListState(lazyListState) { from, to ->
val fromId = from.key as? Int ?: return@rememberReorderableLazyListState
val toId = to.key as? Int ?: return@rememberReorderableLazyListState
val fromIndex = cards.indexOfFirst { it.id == fromId }
val toIndex = cards.indexOfFirst { it.id == toId }
if (fromIndex != -1 && toIndex != -1) {
cards.add(toIndex, cards.removeAt(fromIndex))
}
}
Box( Box(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
Page( Page(
"OmniCards", stringResource(R.string.app_name),
innerPadding innerPadding,
topBarAdditionalElements = {
Icon(
Icons.Outlined.Search,
contentDescription = null,
modifier = Modifier.clickable {
isSearchFieldShowed = !isSearchFieldShowed
searchQuery = ""
}
)
},
floatingActionButton = {
ExpandableFab(
items = listOf(
NavigationPageData(stringResource(R.string.do_add_card), Icons.Outlined.Add, EditCardActivity::class.java),
NavigationPageData(stringResource(R.string.do_sync), Icons.Outlined.Sync) {
Toast.makeText(context, context.getString(R.string.sync_in_progress), Toast.LENGTH_SHORT).show()
val isSuccess = syncCardsWithWatch(context, getDbFun().getCards())
if (isSuccess) {
Toast.makeText(context, context.getString(R.string.sync_is_successful), Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, context.getString(R.string.sync_is_fail), Toast.LENGTH_LONG).show()
}
},
NavigationPageData(stringResource(R.string.open_backup_menu), Icons.Outlined.Backup, BackupActivity::class.java),
NavigationPageData(stringResource(R.string.source_code), Icons.Outlined.Code) {
uriHandler.openUri("https://github.com/omni-devel/OmniCards")
},
)
)
}
) { ) {
Column( var queryCleaned = searchQuery.lowercase()
modifier = Modifier.verticalScroll(rememberScrollState()).padding(bottom = 64.dp).background(Color.Transparent).padding(top = 16.dp),
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)
}
return@filter cardNameCleaned.contains(queryCleaned)
}
LazyColumn(
state = lazyListState,
verticalArrangement = Arrangement.spacedBy(8.dp) 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()) { if (cards.isEmpty()) {
Text("Add new card to get started") item {
Text(
stringResource(R.string.add_card_to_get_started)
)
}
} else if (searchResults.isEmpty()) {
item {
Text(
stringResource(R.string.cards_not_found)
)
}
} else { } else {
for (card in cards) { for (card in searchResults) {
CardButton(card, onDeleteCard) item(key = card.id) {
} ReorderableItem(reorderableLazyListState, key = card.id) { isDragging ->
} Row(
} verticalAlignment = Alignment.CenterVertically,
} modifier = Modifier
.fillMaxWidth()
.alpha(if (isDragging) 0.7f else 1f)
) {
var reorderIcon: (@Composable () -> Unit)? = null
if (isActionBarShowed) { if (queryCleaned.isEmpty()) {
Column( reorderIcon = {
modifier = Modifier Icon(
.align(Alignment.BottomEnd) imageVector = Icons.Outlined.DragHandle,
.padding(innerPadding) contentDescription = null,
.padding(16.dp) tint = MaterialTheme.colorScheme.onPrimaryContainer,
.background( modifier = Modifier
MaterialTheme.colorScheme.inversePrimary, .draggableHandle(
RoundedCornerShape(24.dp) onDragStopped = {
) onReorderCards(cards.map { it.id })
.padding(16.dp) }
.fillMaxWidth() )
) { .padding(8.dp)
FullWidthButton( )
onClick = { }
isActionBarShowed = false }
val intent = Intent(context, EditCardActivity::class.java) CardButton(
card,
context.startActivity(intent) onDeleteCard,
} reorderIcon
) { )
Text("Add card") }
}
FullWidthButton(
onClick = {
Toast.makeText(context, "Sync in progress...", Toast.LENGTH_SHORT).show()
sendToWatch(context, "/updateCards", Json.encodeToString(getDbFun().getCards())) { isSuccess ->
if (isSuccess) {
Toast.makeText(context, "Successfully synced!", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, "Sync failed! Please, check your connection", Toast.LENGTH_LONG).show()
} }
} }
isActionBarShowed = false
} }
) {
Text("Sync with WearOS")
} }
FullWidthButton(
onClick = {
uriHandler.openUri("https://omni-devel.ru")
}
) {
Text("About author")
}
FullWidthButton(
onClick = {
isActionBarShowed = false
}
) {
Text(">")
}
}
} else {
Button(
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(innerPadding).padding(32.dp),
onClick = {
isActionBarShowed = true
}
) {
Text("<")
} }
} }
} }
@@ -0,0 +1,11 @@
package ru.omni_devel.cards
import android.app.Activity
import androidx.compose.ui.graphics.vector.ImageVector
data class NavigationPageData(
val name: String,
val icon: ImageVector,
val pageClass: Class<out Activity>? = null,
val onClick: (() -> Unit)? = null
)
@@ -0,0 +1,237 @@
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 kotlinx.coroutines.launch
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) {
launch {
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()
}
}
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
} ?: emptyList()
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 {
if (searchResults.isEmpty()) {
item {
Text(stringResource(R.string.icons_not_found))
}
} else {
for (iconData in searchResults) {
val loadedIcon = loadedIcons[iconData.iconId]
item {
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
)
)
}
}
}
}
}
}
}
}
}
@@ -0,0 +1,185 @@
package ru.omni_devel.cards
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.LocalActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
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.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.CircularProgressIndicator
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.mutableStateOf
import androidx.compose.runtime.remember
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.platform.LocalContext
import androidx.compose.ui.res.stringResource
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
class SelectTemplateActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
OmniCardsTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
SelectTemplatePage(innerPadding)
}
}
}
}
}
@Composable
fun SelectTemplatePage(innerPadding: PaddingValues) {
val activityContext = LocalActivity.current
val context = LocalContext.current
val language = getLanguage(context)
var searchQuery by rememberSaveable { mutableStateOf("") }
var templates by remember { mutableStateOf<List<Template>?>(null) }
var loadingError by remember { mutableStateOf<String?>(null) }
LaunchedEffect(Unit) {
try {
val client = HttpClient(CIO)
val response: HttpResponse = client.get("$REPO_BASE_URL/$TEMPLATES_REPO_FILE")
templates = Template.parseTemplatesFromJson(response.bodyAsText())
} catch (e: Exception) {
loadingError = e.toString()
}
}
val fillTemplateLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val codeValue = result.data?.getStringExtra("codeValue")
val codeTypeString = result.data?.getStringExtra("codeType")
val resultIntent = Intent()
resultIntent.putExtra("codeValue", codeValue)
resultIntent.putExtra("codeType", codeTypeString)
activityContext!!.setResult(Activity.RESULT_OK, resultIntent)
activityContext.finish()
}
}
Page(
stringResource(R.string.choosing_template),
innerPadding
) {
OutlinedTextField(
value = searchQuery,
onValueChange = { newValue ->
searchQuery = newValue
},
modifier = Modifier.fillMaxWidth(),
label = { Text(stringResource(R.string.search)) }
)
LazyColumn(
horizontalAlignment = Alignment.CenterHorizontally
) {
val currentTemplates = templates
if (loadingError != null) {
item {
Text(loadingError!!)
}
} else if (currentTemplates == null) {
item {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
CircularProgressIndicator(
modifier = Modifier.size(48.dp),
color = MaterialTheme.colorScheme.primary
)
}
}
} else {
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
}
for (name in template.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
}
if (searchResults.isEmpty()) {
item {
Text(stringResource(R.string.templates_not_found))
}
} else {
for (template in searchResults) {
item {
AdaptiveButton(
modifier = Modifier.fillMaxWidth(),
onClick = {
val intent = Intent(context, FillTemplateActivity::class.java)
intent.putExtra("templateJson", Json.encodeToString(template))
fillTemplateLauncher.launch(intent)
}
) {
Text(selectItemNameViaLanguage(template.names, language))
}
}
}
}
}
}
}
}
@@ -7,21 +7,28 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column 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.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
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.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import ru.omni_devel.cards.ui.theme.OmniCardsTheme import ru.omni_devel.cards.ui.theme.OmniCardsTheme
class ShowCardActivity : ComponentActivity() { class ShowCardActivity : ComponentActivity() {
@@ -83,15 +90,19 @@ fun ShowCardPage(innerPadding: PaddingValues, cardInfo: CardInfo) {
Page(cardInfo.name, innerPadding) { Page(cardInfo.name, innerPadding) {
Column( Column(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) { ) {
if (bitmap != null) { if (bitmap != null) {
Image( Image(
bitmap = bitmap, bitmap = bitmap,
contentDescription = cardInfo.codeValue, contentDescription = cardInfo.codeValue,
modifier = Modifier.fillMaxWidth(), modifier = Modifier
contentScale = ContentScale.FillWidth .padding(bottom = 8.dp)
.clip(RoundedCornerShape(12.dp)),
contentScale = ContentScale.Fit
) )
Text( Text(
cardInfo.codeValue, cardInfo.codeValue,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@@ -99,7 +110,7 @@ fun ShowCardPage(innerPadding: PaddingValues, cardInfo: CardInfo) {
) )
} else { } else {
Text( Text(
"Failed to generate code. Check card data", stringResource(R.string.failed_to_generate_card_code),
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.error color = MaterialTheme.colorScheme.error
@@ -0,0 +1,90 @@
package ru.omni_devel.cards
import com.google.zxing.BarcodeFormat
import kotlinx.serialization.Serializable
import org.json.JSONArray
import org.json.JSONObject
@Serializable
class Field(
val id: String,
val names: LinkedHashMap<String, String>,
val default: String? = null,
val maybeEmpty: Boolean = false
)
@Serializable
class Template(
val names: LinkedHashMap<String, String>,
val fields: List<Field>,
val template: String,
val codeType: BarcodeFormat = BarcodeFormat.QR_CODE
) {
companion object {
fun parseTemplateFromJson(json: String): Template {
val obj = JSONObject(json)
val fieldsArray = obj.getJSONArray("fields")
val fields = mutableListOf<Field>()
for (fi in 0 until fieldsArray.length()) {
val fieldObj = fieldsArray.getJSONObject(fi)
fields.add(Field(
id = fieldObj.getString("id"),
names = jsonObjectToMap(fieldObj.getJSONObject("names")),
default = fieldObj.optString("default"),
maybeEmpty = fieldObj.optBoolean("maybeEmpty")
))
}
return Template(
names = jsonObjectToMap(obj.getJSONObject("names")),
fields = fields,
template = obj.getString("template"),
codeType = parseCodeType(obj)
)
}
fun parseTemplatesFromJson(json: String): List<Template> {
val array = JSONArray(json)
val templates = mutableListOf<Template>()
for (i in 0 until array.length()) {
val obj = array.getJSONObject(i)
val fieldsArray = obj.getJSONArray("fields")
val fields = mutableListOf<Field>()
for (fi in 0 until fieldsArray.length()) {
val fieldObj = fieldsArray.getJSONObject(fi)
fields.add(Field(
id = fieldObj.getString("id"),
names = jsonObjectToMap(fieldObj.getJSONObject("names")),
default = fieldObj.optString("default"),
maybeEmpty = fieldObj.optBoolean("maybeEmpty")
))
}
templates.add(Template(
names = jsonObjectToMap(obj.getJSONObject("names")),
fields = fields,
template = obj.getString("template"),
codeType = parseCodeType(obj)
))
}
return templates
}
private fun parseCodeType(obj: JSONObject): BarcodeFormat {
val raw = obj.optString("codeType", "")
return if (raw.isBlank()) {
BarcodeFormat.QR_CODE
} else {
BarcodeFormat.valueOf(raw)
}
}
}
}
+121 -10
View File
@@ -2,24 +2,47 @@ 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 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.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 android.widget.Toast
import androidx.compose.ui.graphics.asImageBitmap
import kotlinx.serialization.json.Json
fun generateCode(value: String, format: BarcodeFormat): Bitmap { fun generateCode(value: String, format: BarcodeFormat): Bitmap {
val writer = MultiFormatWriter() val writer = MultiFormatWriter()
val matrix = writer.encode(value, format, 800, 400)
val (width, height) = when (format) {
BarcodeFormat.QR_CODE, BarcodeFormat.DATA_MATRIX, BarcodeFormat.AZTEC -> 800 to 800
else -> 800 to 400
}
val hints = mapOf(
EncodeHintType.CHARACTER_SET to "UTF-8"
)
val matrix = writer.encode(value, format, width, height, hints)
return BarcodeEncoder().createBitmap(matrix) return BarcodeEncoder().createBitmap(matrix)
} }
fun sendToWatch(context: Context, path: String, message: String, onResult: (Boolean) -> Unit) { fun sendToWatch(context: Context, path: String, message: String): Boolean {
val nodeClient = Wearable.getNodeClient(context) val nodeClient = Wearable.getNodeClient(context)
var isSuccess = false
nodeClient.connectedNodes.addOnSuccessListener { nodes -> nodeClient.connectedNodes.addOnSuccessListener { nodes ->
if (nodes.isEmpty()) { if (nodes.isEmpty()) {
onResult(false) isSuccess = false
return@addOnSuccessListener return@addOnSuccessListener
} }
@@ -40,21 +63,109 @@ fun sendToWatch(context: Context, path: String, message: String, onResult: (Bool
} }
if (completedCount == nodes.size) { if (completedCount == nodes.size) {
onResult(isAnySuccess) isSuccess = isAnySuccess
} }
}.addOnFailureListener(context.mainExecutor) { }.addOnFailureListener(context.mainExecutor) {
onResult(false) isSuccess = false
} }
} }
} }
return isSuccess
} }
fun checkCardData(name: String, value: String): String? { fun syncCardsWithWatch(context: Context, cards: List<CardInfo>): Boolean {
if (name.trim().isEmpty()) { return sendToWatch(context, "/updateCards", Json.encodeToString(cards))
return "Name should not be empty" }
} else if (value.trim().isEmpty()) {
return "Code value should not be empty" fun checkCardData(name: String, value: String): Int? {
if (name.isEmpty()) {
return R.string.card_name_should_not_be_empty
} else if (value.isEmpty()) {
return R.string.card_value_should_not_be_empty
} else if (name.length > 32) {
return R.string.name_must_be_shorter_than_32_symbols
} }
return null return null
} }
fun getLanguage(context: Context): String {
val locales: LocaleList = context.resources.configuration.locales
val primaryLocale = locales[0]
val languageCode = primaryLocale.language
return languageCode
}
fun selectItemNameViaLanguage(names: LinkedHashMap<String, String>, language: String): String {
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()
}
fun processQrScript(input: String, fills: Map<String, String>): String {
val outerRegex = Regex("""\$(\w+)((?:\([^)]*\))*)\$""")
val innerRegex = Regex("""\(([^)]*)\)""")
var result = input
outerRegex.findAll(input).forEach { match ->
val fullBlock = match.value
val name = match.groupValues[1]
val contents = innerRegex.findAll(match.groupValues[2]).map { it.groupValues[1] }.toList()
var blockResult = fills[name] ?: return@forEach
contents.forEach { content ->
val args = content.split(" ")
val action = args[0]
when (action) {
"first" -> {
val count = args[1].toInt()
blockResult = blockResult.take(count)
}
"last" -> {
val count = args[1].toInt()
blockResult = blockResult.takeLast(count)
}
}
}
result = result.replace(fullBlock, blockResult)
}
return result
}
@@ -1,36 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".ShowCardActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:fitsSystemWindows="true"
>
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width = "match_parent"
android:layout_height = "?attr/actionBarSize" />
</com.google.android.material.appbar.AppBarLayout>
<include layout="@layout/content_show_card"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginEnd="@dimen/fab_margin"
android:layout_marginBottom="16dp"
app:srcCompat="@android:drawable/ic_dialog_email" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment_content_show_card"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -1,34 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".FirstFragment">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
<Button
android:id="@+id/button_first"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/next"
app:layout_constraintBottom_toTopOf="@id/textview_first"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textview_first"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/lorem_ipsum"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/button_first" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
@@ -1,34 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".SecondFragment">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
<Button
android:id="@+id/button_second"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/previous"
app:layout_constraintBottom_toTopOf="@id/textview_second"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textview_second"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/lorem_ipsum"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/button_second" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
+53
View File
@@ -0,0 +1,53 @@
<resources>
<string name="add_card_to_get_started">Добавьте новую карту для того, чтобы начать работу</string>
<string name="do_add_card">Добавить карту</string>
<string name="do_edit_card">Редактировать карту</string>
<string name="do_delete_card">Удалить карту</string>
<string name="source_code">Исходный код</string>
<string name="do_sync">Синхронизировать с WearOS</string>
<string name="sync_in_progress">Синхронизация в процессе…</string>
<string name="sync_is_successful">Успешная синхронизация</string>
<string name="sync_is_fail">Ошибка синхронизации. Проверьте подключение к часам</string>
<string name="backup">Резервное копирование</string>
<string name="open_backup_menu">Резервное копирование</string>
<string name="do_backup">Сохранить данные</string>
<string name="do_restore">Восстановить данные</string>
<string name="ask_do_restore">Восстановить данные?</string>
<string name="backup_successfully_saved">Данные успешно сохранены</string>
<string name="failed_to_save_backup">Не удалось сохранить данные: %s</string>
<string name="backup_successfully_restored">Данные успешно восстановлены</string>
<string name="failed_to_restore_backup">Не удалось восстановить данные: %s</string>
<string name="restore_caution">Это заменит существующие карты данными из файла резервной копии. Продолжить?</string>
<string name="yes">Да</string>
<string name="no">Нет</string>
<string name="do_save">Сохранить</string>
<string name="new_card_title">Добавление карты</string>
<string name="editing_card_title">Редактирование карты #%d</string>
<string name="card_name">Название карты</string>
<string name="card_value">Значение карты</string>
<string name="do_scan_card">Сканировать карту</string>
<string name="card_from_template">Из шаблона</string>
<string name="scan_your_card">Отсканируйте вашу карту</string>
<string name="card_code_type">Тип кода карты</string>
<string name="do_choose_color">Выбрать цвет</string>
<string name="choosing_color">Выбор цвета</string>
<string name="choosing_template">Выбор шаблона</string>
<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>
<string name="wait_for_icon_load">Дождитесь загрузки иконки</string>
<string name="card_name_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="failed_to_generate_card_code">Не удалось сгенерировать код карты. Проверьте данные</string>
</resources>
+56 -45
View File
@@ -1,48 +1,59 @@
<resources> <resources>
<string name="app_name">OmniCards</string> <string name="first_fragment_label" translatable="false">First Fragment</string>
<!-- Strings used for fragments for navigation --> <string name="second_fragment_label" translatable="false">Second Fragment</string>
<string name="first_fragment_label">First Fragment</string>
<string name="second_fragment_label">Second Fragment</string>
<string name="next">Next</string>
<string name="previous">Previous</string>
<string name="lorem_ipsum"> <string name="app_name" translatable="false">OmniCards</string>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam in scelerisque sem. Mauris
volutpat, dolor id interdum ullamcorper, risus dolor egestas lectus, sit amet mattis purus <string name="add_card_to_get_started">Add a new card to get started</string>
dui nec risus. Maecenas non sodales nisi, vel dictum dolor. Class aptent taciti sociosqu ad
litora torquent per conubia nostra, per inceptos himenaeos. Suspendisse blandit eleifend <string name="do_add_card">Add card</string>
diam, vel rutrum tellus vulputate quis. Aliquam eget libero aliquet, imperdiet nisl a, <string name="do_edit_card">Edit card</string>
ornare ex. Sed rhoncus est ut libero porta lobortis. Fusce in dictum tellus.\n\n <string name="do_delete_card">Delete card</string>
Suspendisse interdum ornare ante. Aliquam nec cursus lorem. Morbi id magna felis. Vivamus <string name="source_code">Source code</string>
egestas, est a condimentum egestas, turpis nisl iaculis ipsum, in dictum tellus dolor sed
neque. Morbi tellus erat, dapibus ut sem a, iaculis tincidunt dui. Interdum et malesuada <string name="do_sync">Sync with WearOS</string>
fames ac ante ipsum primis in faucibus. Curabitur et eros porttitor, ultricies urna vitae, <string name="sync_in_progress">Sync in progress…</string>
molestie nibh. Phasellus at commodo eros, non aliquet metus. Sed maximus nisl nec dolor <string name="sync_is_successful">Sync successful</string>
bibendum, vel congue leo egestas.\n\n <string name="sync_is_fail">Sync failed. Check your watch connection</string>
Sed interdum tortor nibh, in sagittis risus mollis quis. Curabitur mi odio, condimentum sit
amet auctor at, mollis non turpis. Nullam pretium libero vestibulum, finibus orci vel, <string name="backup">Backup</string>
molestie quam. Fusce blandit tincidunt nulla, quis sollicitudin libero facilisis et. Integer <string name="open_backup_menu">Backup</string>
interdum nunc ligula, et fermentum metus hendrerit id. Vestibulum lectus felis, dictum at <string name="do_backup">Save data</string>
lacinia sit amet, tristique id quam. Cras eu consequat dui. Suspendisse sodales nunc ligula, <string name="do_restore">Restore data</string>
in lobortis sem porta sed. Integer id ultrices magna, in luctus elit. Sed a pellentesque <string name="ask_do_restore">Restore data?</string>
est.\n\n <string name="backup_successfully_saved">Data saved successfully</string>
Aenean nunc velit, lacinia sed dolor sed, ultrices viverra nulla. Etiam a venenatis nibh. <string name="failed_to_save_backup">Failed to save data: %s</string>
Morbi laoreet, tortor sed facilisis varius, nibh orci rhoncus nulla, id elementum leo dui <string name="backup_successfully_restored">Data restored successfully</string>
non lorem. Nam mollis ipsum quis auctor varius. Quisque elementum eu libero sed commodo. In <string name="failed_to_restore_backup">Failed to restore data: %s</string>
eros nisl, imperdiet vel imperdiet et, scelerisque a mauris. Pellentesque varius ex nunc, <string name="restore_caution">This will replace your existing cards with data from the backup file. Continue?</string>
quis imperdiet eros placerat ac. Duis finibus orci et est auctor tincidunt. Sed non viverra
ipsum. Nunc quis augue egestas, cursus lorem at, molestie sem. Morbi a consectetur ipsum, a <string name="yes">Yes</string>
placerat diam. Etiam vulputate dignissim convallis. Integer faucibus mauris sit amet finibus <string name="no">No</string>
convallis.\n\n <string name="do_save">Save</string>
Phasellus in aliquet mi. Pellentesque habitant morbi tristique senectus et netus et
malesuada fames ac turpis egestas. In volutpat arcu ut felis sagittis, in finibus massa <string name="new_card_title">Add card</string>
gravida. Pellentesque id tellus orci. Integer dictum, lorem sed efficitur ullamcorper, <string name="editing_card_title">Editing card #%d</string>
libero justo consectetur ipsum, in mollis nisl ex sed nisl. Donec maximus ullamcorper <string name="card_name">Card name</string>
sodales. Praesent bibendum rhoncus tellus nec feugiat. In a ornare nulla. Donec rhoncus <string name="card_value">Card value</string>
libero vel nunc consequat, quis tincidunt nisl eleifend. Cras bibendum enim a justo luctus <string name="do_scan_card">Scan card</string>
vestibulum. Fusce dictum libero quis erat maximus, vitae volutpat diam dignissim. <string name="card_from_template">From template</string>
</string> <string name="scan_your_card">Scan your card</string>
<string name="title_activity_show_card">ShowCardActivity</string> <string name="card_code_type">Card code type</string>
<string name="title_activity_add_card">AddCardActivity</string> <string name="do_choose_color">Choose color</string>
<string name="title_activity_edit_card">EditCardActivity</string> <string name="choosing_color">Choosing color</string>
<string name="choosing_template">Choosing template</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="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>
<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_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="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>
+19 -3
View File
@@ -1,6 +1,17 @@
plugins { plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.compose) alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlin.serialization)
}
fun generateVersionCode(): Int {
return try {
val process = ProcessBuilder("git", "rev-list", "--count", "HEAD").start()
"${process.inputStream.bufferedReader().readText().trim()}1".toInt() // 0 at end for WearOS
} catch (e: Exception) {
1
}
} }
android { android {
@@ -11,18 +22,21 @@ android {
applicationId = "ru.omni_devel.cards" applicationId = "ru.omni_devel.cards"
minSdk = 30 minSdk = 30
targetSdk = 36 targetSdk = 36
versionCode = 41 // 1 at end for WearOS versionCode = generateVersionCode()
versionName = "1.1" versionName = "3.3.0"
} }
buildTypes { buildTypes {
release { release {
isMinifyEnabled = false isMinifyEnabled = true
proguardFiles( proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro" "proguard-rules.pro"
) )
} }
debug {
applicationIdSuffix = ".debug"
}
} }
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_11
@@ -57,4 +71,6 @@ dependencies {
implementation(libs.zxing.android) implementation(libs.zxing.android)
implementation(libs.material) implementation(libs.material)
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
implementation("androidx.compose.material:material-icons-extended")
implementation("androidx.lifecycle:lifecycle-process:2.8.7")
} }
Binary file not shown.
Binary file not shown.
@@ -9,5 +9,7 @@ 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?,
val position: Int
) )
@@ -2,13 +2,18 @@ package ru.omni_devel.cards.presentation
import android.content.ContentValues import android.content.ContentValues
import android.content.Context import android.content.Context
import android.database.Cursor
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 androidx.core.database.sqlite.transaction
import com.google.zxing.BarcodeFormat import com.google.zxing.BarcodeFormat
class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?) : SQLiteOpenHelper(context, "omni_cards", factory, 2) { 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(
@@ -19,6 +24,38 @@ class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?)
if (oldVersion < 2) { if (oldVersion < 2) {
db!!.execSQL("ALTER TABLE cards ADD COLUMN color TEXT") db!!.execSQL("ALTER TABLE cards ADD COLUMN color TEXT")
} }
if (oldVersion < 3) {
db!!.execSQL("ALTER TABLE cards ADD COLUMN position INTEGER DEFAULT 0")
db.execSQL("ALTER TABLE cards ADD COLUMN iconPath TEXT")
db.execSQL("UPDATE cards SET position = id")
}
if (oldVersion < 4) {
db!!.execSQL("ALTER TABLE cards DROP COLUMN position")
db.execSQL("ALTER TABLE cards DROP COLUMN iconPath")
}
if (oldVersion < 5) {
db!!.execSQL("ALTER TABLE cards ADD COLUMN icon TEXT")
}
if (oldVersion < 6) {
db!!.execSQL("ALTER TABLE cards ADD COLUMN position INTEGER DEFAULT 0")
db.execSQL("UPDATE cards SET position = id")
}
}
private fun readCard(cursor: Cursor): CardInfo {
return CardInfo(
id = cursor.getInt(cursor.getColumnIndexOrThrow("id")),
name = cursor.getString(cursor.getColumnIndexOrThrow("name")),
codeType = BarcodeFormat.valueOf(cursor.getString(cursor.getColumnIndexOrThrow("codeType"))),
codeValue = cursor.getString(cursor.getColumnIndexOrThrow("codeValue")),
color = cursor.getString(cursor.getColumnIndexOrThrow("color")),
icon = cursor.getStringOrNull(cursor.getColumnIndexOrThrow("icon")),
position = cursor.getInt(cursor.getColumnIndexOrThrow("position"))
)
} }
fun clearDatabase() { fun clearDatabase() {
@@ -32,35 +69,33 @@ class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?)
fun addCards(cards: List<CardInfo>) { fun addCards(cards: List<CardInfo>) {
val db = this.writableDatabase val db = this.writableDatabase
for (card in cards) { db.use { db ->
val values = ContentValues() db.transaction {
for (card in cards) {
val values = ContentValues()
values.put("id", card.id) values.put("id", card.id)
values.put("name", card.name) values.put("name", card.name)
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)
values.put("position", card.position)
db.insert("cards", null, values) db.insert("cards", null, values)
}
}
} }
db.close()
} }
fun getCards(): List<CardInfo> { fun getCards(): List<CardInfo> {
val db = this.readableDatabase val db = this.readableDatabase
val cursor = db.rawQuery("SELECT * FROM cards", null) val cursor = db.rawQuery("SELECT * FROM cards ORDER BY position", null)
val cards = mutableListOf<CardInfo>() val cards = mutableListOf<CardInfo>()
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
cards.add(CardInfo( cards.add(readCard(cursor))
id = cursor.getInt(cursor.getColumnIndexOrThrow("id")),
name = cursor.getString(cursor.getColumnIndexOrThrow("name")),
codeType = BarcodeFormat.valueOf(cursor.getString(cursor.getColumnIndexOrThrow("codeType"))),
codeValue = cursor.getString(cursor.getColumnIndexOrThrow("codeValue")),
color = cursor.getString(cursor.getColumnIndexOrThrow("color"))
))
} }
cursor.close() cursor.close()
@@ -81,13 +116,7 @@ class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?)
return null return null
} }
val cardInfo = CardInfo( val cardInfo = readCard(cursor)
id = cursor.getInt(cursor.getColumnIndexOrThrow("id")),
name = cursor.getString(cursor.getColumnIndexOrThrow("name")),
codeType = BarcodeFormat.valueOf(cursor.getString(cursor.getColumnIndexOrThrow("codeType"))),
codeValue = cursor.getString(cursor.getColumnIndexOrThrow("codeValue")),
color = cursor.getString(cursor.getColumnIndexOrThrow("color"))
)
cursor.close() cursor.close()
db.close() db.close()
@@ -1,21 +1,26 @@
package ru.omni_devel.cards.presentation package ru.omni_devel.cards.presentation
import android.content.Intent import android.content.Intent
import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
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.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.fillMaxHeight 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.material.icons.Icons
import androidx.compose.material.icons.outlined.QrCode
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -23,6 +28,7 @@ import androidx.compose.ui.unit.sp
import androidx.core.graphics.toColorInt import androidx.core.graphics.toColorInt
import androidx.wear.compose.material3.Button import androidx.wear.compose.material3.Button
import androidx.wear.compose.material3.ButtonDefaults import androidx.wear.compose.material3.ButtonDefaults
import androidx.wear.compose.material3.Icon
import androidx.wear.compose.material3.MaterialTheme import androidx.wear.compose.material3.MaterialTheme
import androidx.wear.compose.material3.Text import androidx.wear.compose.material3.Text
@@ -30,12 +36,18 @@ import androidx.wear.compose.material3.Text
fun CardButton(cardInfo: CardInfo) { fun CardButton(cardInfo: CardInfo) {
val context = LocalContext.current val context = LocalContext.current
val cardColor = if (cardInfo.color == null)
MaterialTheme.colorScheme.primaryContainer
else
Color(cardInfo.color.toColorInt())
Box(modifier = Modifier.fillMaxWidth()) { Box(modifier = Modifier.fillMaxWidth()) {
Button( Button(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth().height(64.dp),
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primaryContainer containerColor = MaterialTheme.colorScheme.primaryContainer.copy(0.625f)
), ),
shape = RoundedCornerShape(12.dp),
contentPadding = PaddingValues(0.dp), contentPadding = PaddingValues(0.dp),
onClick = { onClick = {
val intent = Intent(context, ShowCardActivity::class.java) val intent = Intent(context, ShowCardActivity::class.java)
@@ -47,32 +59,54 @@ fun CardButton(cardInfo: CardInfo) {
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxSize()
.height(IntrinsicSize.Min) .background(Color.Transparent)
.clip(ButtonDefaults.shape) .padding(horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
Box( CardIcon(
modifier = Modifier if (cardInfo.icon == null) null else base64ToImageBitmap(cardInfo.icon),
.fillMaxHeight() cardColor = cardColor
.width(32.dp)
.background(
if (cardInfo.color == null)
MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.625f)
else
Color(cardInfo.color.toColorInt()).copy(alpha = 0.625f)
)
) )
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()
.weight(1f),
textAlign = TextAlign.Start textAlign = TextAlign.Start
) )
} }
} }
} }
} }
@Composable
fun CardIcon(icon: ImageBitmap?, cardColor: Color) {
Row(
modifier = Modifier
.background(
cardColor.copy(0.525f),
RoundedCornerShape(12.dp)
)
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
if (icon == null) {
Icon(
modifier = Modifier.size(32.dp),
imageVector = Icons.Outlined.QrCode,
tint = MaterialTheme.colorScheme.onPrimaryContainer,
contentDescription = null
)
} else {
Image(
bitmap = icon,
contentDescription = null,
modifier = Modifier.size(36.dp).clip(RoundedCornerShape(12.dp))
)
}
}
}
@@ -4,37 +4,50 @@ import android.annotation.SuppressLint
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.compose.foundation.background
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.BoxWithConstraints import androidx.compose.foundation.layout.BoxWithConstraints
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.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.wear.compose.foundation.lazy.TransformingLazyColumn import androidx.wear.compose.foundation.lazy.TransformingLazyColumn
import androidx.wear.compose.material3.MaterialTheme
import androidx.wear.compose.material3.Text import androidx.wear.compose.material3.Text
import com.google.zxing.BarcodeFormat import ru.omni_devel.cards.R
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private lateinit var db: DbHelper private lateinit var db: DbHelper
private val cardsState = mutableStateOf<List<CardInfo>>(emptyList())
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
db = DbHelper(this, null) db = DbHelper(this, null)
cardsState.value = db.getCards()
setContent { setContent {
App(db.getCards()) App(cardsState.value)
} }
} }
override fun onResume() {
super.onResume()
cardsState.value = db.getCards()
}
} }
@SuppressLint("UnusedBoxWithConstraintsScope") @SuppressLint("UnusedBoxWithConstraintsScope")
@Composable @Composable
fun App(cards: List<CardInfo>) { fun App(cards: List<CardInfo>) {
BoxWithConstraints(modifier = Modifier.fillMaxSize()) { BoxWithConstraints(modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background)) {
val topBottomPadding = maxHeight * 0.4f val topBottomPadding = maxHeight * 0.4f
if (cards.isEmpty()) { if (cards.isEmpty()) {
@@ -42,7 +55,7 @@ fun App(cards: List<CardInfo>) {
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text("Sync with mobile APP to get started") Text(stringResource(R.string.sync_with_mobile_app_to_get_started))
} }
} else { } else {
TransformingLazyColumn( TransformingLazyColumn(
@@ -7,6 +7,9 @@ import android.widget.Toast
import com.google.android.gms.wearable.MessageEvent import com.google.android.gms.wearable.MessageEvent
import com.google.android.gms.wearable.WearableListenerService import com.google.android.gms.wearable.WearableListenerService
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import ru.omni_devel.cards.R
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.Lifecycle
class MessagesReceiver : WearableListenerService() { class MessagesReceiver : WearableListenerService() {
private lateinit var db: DbHelper private lateinit var db: DbHelper
@@ -23,17 +26,32 @@ class MessagesReceiver : WearableListenerService() {
val json = String(event.data, Charsets.UTF_8) val json = String(event.data, Charsets.UTF_8)
val cards = Json.decodeFromString<List<CardInfo>>(json) val cards = Json.decodeFromString<List<CardInfo>>(json)
val oldCards = db.getCards()
if (cards == oldCards) {
return
}
db.clearDatabase() db.clearDatabase()
db.addCards(cards) db.addCards(cards)
Handler(Looper.getMainLooper()).post { val isAppInForeground = ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(
Toast.makeText(this, "Database updated!", Toast.LENGTH_SHORT).show() Lifecycle.State.STARTED
)
val intent = Intent(this, MainActivity::class.java).apply { if (isAppInForeground) {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP Handler(Looper.getMainLooper()).post {
Toast.makeText(
this,
this.getString(R.string.sync_is_successful),
Toast.LENGTH_SHORT
).show()
val intent = Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
}
startActivity(intent)
} }
startActivity(intent)
} }
} }
} }
@@ -12,21 +12,18 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.wear.compose.material3.MaterialTheme import androidx.wear.compose.material3.MaterialTheme
import androidx.wear.compose.material3.Text import androidx.wear.compose.material3.Text
import ru.omni_devel.cards.R
const val CODE_WIDTH = 380
const val CODE_HEIGHT = 380
class ShowCardActivity : ComponentActivity() { class ShowCardActivity : ComponentActivity() {
private lateinit var db: DbHelper private lateinit var db: DbHelper
@@ -67,6 +64,8 @@ class ShowCardActivity : ComponentActivity() {
super.onPause() super.onPause()
setBrightness(WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE) setBrightness(WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE)
finish()
} }
private fun setBrightness(level: Float) { private fun setBrightness(level: Float) {
@@ -108,7 +107,7 @@ fun ShowCardPage(cardInfo: CardInfo) {
) )
} else { } else {
Text( Text(
"Failed to generate code", stringResource(R.string.failed_to_generate_card_code),
color = MaterialTheme.colorScheme.error, color = MaterialTheme.colorScheme.error,
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
@@ -1,6 +1,10 @@
package ru.omni_devel.cards.presentation package ru.omni_devel.cards.presentation
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.util.Base64
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import com.google.zxing.BarcodeFormat import com.google.zxing.BarcodeFormat
import com.google.zxing.MultiFormatWriter import com.google.zxing.MultiFormatWriter
import com.journeyapps.barcodescanner.BarcodeEncoder import com.journeyapps.barcodescanner.BarcodeEncoder
@@ -11,3 +15,10 @@ fun generateCode(value: String, format: BarcodeFormat, width: Int, height: Int):
return BarcodeEncoder().createBitmap(matrix) return BarcodeEncoder().createBitmap(matrix)
} }
fun base64ToImageBitmap(base64String: String): ImageBitmap {
val decodedBytes = Base64.decode(base64String, Base64.DEFAULT)
val bitmap = BitmapFactory.decodeByteArray(decodedBytes, 0, decodedBytes.size)
return bitmap.asImageBitmap()
}
+7
View File
@@ -0,0 +1,7 @@
<resources>
<string name="sync_with_mobile_app_to_get_started">Синхронизируйте с мобильным приложением для начала работы</string>
<string name="sync_is_successful">Успешная синхронизация</string>
<string name="failed_to_generate_card_code">Не удалось сгенерировать код карты. Проверьте данные</string>
</resources>
+7 -2
View File
@@ -1,4 +1,9 @@
<resources> <resources>
<string name="app_name">OmniCards</string> <string name="app_name" translatable="false">OmniCards</string>
<string name="hello_world">Hello, %1$s!</string>
<string name="sync_with_mobile_app_to_get_started">Sync with mobile APP to get started</string>
<string name="sync_is_successful">Successfully synced</string>
<string name="failed_to_generate_card_code">Failed to generate card code. Check the card data</string>
</resources> </resources>