Compare commits

..
19 Commits
Author SHA1 Message Date
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
omni 245c8540b7 feat: added color support for buttons on WearOS 2026-06-14 16:49:35 +03:00
omni 26fbbcf19f fix: fixed top bar on mobile 2026-06-14 16:08:56 +03:00
omni acb9cd55e4 feat: added color pick for cards 2026-06-14 15:55:41 +03:00
31 changed files with 623 additions and 311 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
} }
+5 -2
View File
@@ -9,9 +9,9 @@ playServicesWearable = "18.0.0"
material = "1.14.0" material = "1.14.0"
activityKtx = "1.13.0" activityKtx = "1.13.0"
constraintlayout = "2.2.1" constraintlayout = "2.2.1"
kotlin = "2.2.10" kotlin = "2.4.0"
composeBom = "2024.09.00" composeBom = "2024.09.00"
composeMaterial3 = "1.5.6" composeMaterial3 = "1.6.2"
composeFoundation = "1.5.6" composeFoundation = "1.5.6"
composeUiTooling = "1.5.6" composeUiTooling = "1.5.6"
wearToolingPreview = "1.0.0" wearToolingPreview = "1.0.0"
@@ -22,6 +22,7 @@ zxingAndroid = "4.3.0"
lifecycleRuntimeKtx = "2.6.1" lifecycleRuntimeKtx = "2.6.1"
navigationFragmentKtx = "2.6.0" navigationFragmentKtx = "2.6.0"
navigationUiKtx = "2.6.0" navigationUiKtx = "2.6.0"
colorpicker = "1.2.0"
[libraries] [libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -52,8 +53,10 @@ androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecyc
androidx-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-navigation-fragment-ktx = { group = "androidx.navigation", name = "navigation-fragment-ktx", version.ref = "navigationFragmentKtx" } androidx-navigation-fragment-ktx = { group = "androidx.navigation", name = "navigation-fragment-ktx", version.ref = "navigationFragmentKtx" }
androidx-navigation-ui-ktx = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "navigationUiKtx" } androidx-navigation-ui-ktx = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "navigationUiKtx" }
compose-colorpicker = { module = "com.github.skydoves:colorpicker-compose", version.ref = "colorpicker" }
[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" }
+14 -3
View File
@@ -1,7 +1,17 @@
plugins { plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.compose) alias(libs.plugins.kotlin.compose)
kotlin("plugin.serialization") version "2.2.20" 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 {
@@ -12,8 +22,8 @@ android {
applicationId = "ru.omni_devel.cards" applicationId = "ru.omni_devel.cards"
minSdk = 30 minSdk = 30
targetSdk = 36 targetSdk = 36
versionCode = 30 // 0 at end for mobile versionCode = generateVersionCode()
versionName = "2.0" versionName = "2.3.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
@@ -63,4 +73,5 @@ dependencies {
implementation(libs.zxing.android) implementation(libs.zxing.android)
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)
} }
Binary file not shown.
Binary file not shown.
+2 -6
View File
@@ -9,24 +9,20 @@
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.OmniCards"> android:theme="@style/Theme.OmniCards">
<activity <activity
android:name=".EditCardActivity" android:name=".BackupActivity"
android:exported="false" android:exported="false"
android:label="@string/title_activity_edit_card"
android:theme="@style/Theme.OmniCards" /> android:theme="@style/Theme.OmniCards" />
<activity <activity
android:name=".AddCardActivity" 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,150 @@
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.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("Backup", innerPadding) {
FullWidthButton(
onClick = {
createBackupLauncher.launch("omnicards-backup.json")
}
) {
Text(stringResource(R.string.do_backup))
}
FullWidthButton(
onClick = {
restoreFromBackupLauncher.launch("application/json")
}
) {
Text(stringResource(R.string.do_restore))
}
}
}
@@ -14,4 +14,5 @@ class CardInfo (
val name: String, val name: String,
val codeType: BarcodeFormat, val codeType: BarcodeFormat,
val codeValue: String, val codeValue: String,
) val color: String?,
) {}
@@ -6,25 +6,28 @@ import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper import android.database.sqlite.SQLiteOpenHelper
import com.google.zxing.BarcodeFormat import com.google.zxing.BarcodeFormat
class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?) : SQLiteOpenHelper(context, "omni_cards", factory, 1) { class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?) : SQLiteOpenHelper(context, "omni_cards", factory, 2) {
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)") db!!.execSQL("CREATE TABLE IF NOT EXISTS cards (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, codeType TEXT, codeValue TEXT, color TEXT)")
} }
override fun onUpgrade( override fun onUpgrade(
db: SQLiteDatabase?, db: SQLiteDatabase?,
p1: Int, oldVersion: Int,
p2: Int p2: Int
) { ) {
db!!.execSQL("DROP TABLE IF EXISTS cards") if (oldVersion < 2) {
db!!.execSQL("ALTER TABLE cards ADD COLUMN color TEXT")
}
} }
fun addCard(name: String, codeValue: String, codeType: BarcodeFormat) { fun addCard(name: String, codeValue: String, codeType: BarcodeFormat, color: 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)
val db = this.writableDatabase val db = this.writableDatabase
@@ -33,12 +36,13 @@ class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?)
db.close() db.close()
} }
fun editCard(id: Int, name: String, codeValue: String, codeType: BarcodeFormat) { fun editCard(id: Int, name: String, codeValue: String, codeType: BarcodeFormat, color: 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)
val db = this.writableDatabase val db = this.writableDatabase
@@ -58,7 +62,8 @@ class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?)
id = cursor.getInt(cursor.getColumnIndexOrThrow("id")), id = cursor.getInt(cursor.getColumnIndexOrThrow("id")),
name = cursor.getString(cursor.getColumnIndexOrThrow("name")), name = cursor.getString(cursor.getColumnIndexOrThrow("name")),
codeType = BarcodeFormat.valueOf(cursor.getString(cursor.getColumnIndexOrThrow("codeType"))), codeType = BarcodeFormat.valueOf(cursor.getString(cursor.getColumnIndexOrThrow("codeType"))),
codeValue = cursor.getString(cursor.getColumnIndexOrThrow("codeValue")) codeValue = cursor.getString(cursor.getColumnIndexOrThrow("codeValue")),
color = cursor.getString(cursor.getColumnIndexOrThrow("color"))
)) ))
} }
@@ -84,7 +89,8 @@ class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?)
id = cursor.getInt(cursor.getColumnIndexOrThrow("id")), id = cursor.getInt(cursor.getColumnIndexOrThrow("id")),
name = cursor.getString(cursor.getColumnIndexOrThrow("name")), name = cursor.getString(cursor.getColumnIndexOrThrow("name")),
codeType = BarcodeFormat.valueOf(cursor.getString(cursor.getColumnIndexOrThrow("codeType"))), codeType = BarcodeFormat.valueOf(cursor.getString(cursor.getColumnIndexOrThrow("codeType"))),
codeValue = cursor.getString(cursor.getColumnIndexOrThrow("codeValue")) codeValue = cursor.getString(cursor.getColumnIndexOrThrow("codeValue")),
color = cursor.getString(cursor.getColumnIndexOrThrow("color"))
) )
cursor.close() cursor.close()
@@ -100,4 +106,28 @@ 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
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)
db.insert("cards", null, values)
}
db.close()
}
} }
@@ -1,9 +1,9 @@
package ru.omni_devel.cards package ru.omni_devel.cards
import android.app.Activity
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.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
@@ -15,22 +15,32 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button 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.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.Text import androidx.compose.material3.Text
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.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 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
@@ -65,11 +75,19 @@ fun CardEditable(
cardName: String, cardName: String,
onCardNameChange: (String) -> Unit, onCardNameChange: (String) -> Unit,
codeValue: String, codeValue: String,
oncodeValueChange: (String) -> Unit, onCodeValueChange: (String) -> Unit,
codeType: BarcodeFormat, codeType: BarcodeFormat,
onCodeTypeChange: (BarcodeFormat) -> Unit onCodeTypeChange: (BarcodeFormat) -> Unit,
color: String?,
onColorChange: (String?) -> Unit
) { ) {
var isCodeTypeDropdownExpanded by remember { mutableStateOf(false) } val context = LocalContext.current
var isCodeTypeDropdownExpanded by rememberSaveable { mutableStateOf(false) }
var isColorPickerExpanded by rememberSaveable { mutableStateOf(false) }
var isColorEnabled by rememberSaveable { mutableStateOf(color != null) }
val colorPickerController = rememberColorPickerController()
val fieldModifier = Modifier.fillMaxWidth() val fieldModifier = Modifier.fillMaxWidth()
@@ -77,7 +95,7 @@ fun CardEditable(
contract = ScanContract() contract = ScanContract()
) { result -> ) { result ->
if (result.contents != null) { if (result.contents != null) {
oncodeValueChange(result.contents) onCodeValueChange(result.contents)
result.formatName?.let { formatName -> result.formatName?.let { formatName ->
try { try {
@@ -91,30 +109,30 @@ fun CardEditable(
value = cardName, value = cardName,
onValueChange = onCardNameChange, onValueChange = onCardNameChange,
modifier = fieldModifier, modifier = fieldModifier,
label = { Text("Name") } label = { Text(stringResource(R.string.card_name)) }
) )
Row( Row(
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
OutlinedTextField( OutlinedTextField(
value = codeValue, value = codeValue,
onValueChange = oncodeValueChange, onValueChange = onCodeValueChange,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
label = { Text("Value") } label = { Text(stringResource(R.string.card_value)) }
) )
Button( Button(
modifier = Modifier.align(Alignment.CenterVertically), modifier = Modifier.align(Alignment.CenterVertically),
onClick = { onClick = {
scanLauncher.launch( scanLauncher.launch(
ScanOptions().apply { ScanOptions().apply {
setPrompt("Scan your card") setPrompt(context.getString(R.string.scan_your_card))
setBeepEnabled(true) setBeepEnabled(true)
setOrientationLocked(false) setOrientationLocked(false)
} }
) )
} }
) { ) {
Text("Scan") Text(stringResource(R.string.do_scan_card))
} }
} }
@@ -125,7 +143,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(
@@ -151,46 +169,90 @@ fun CardEditable(
} }
} }
} }
Row(
modifier = fieldModifier,
horizontalArrangement = Arrangement.SpaceBetween
) {
Button(
enabled = isColorEnabled,
onClick = {
isColorPickerExpanded = !isColorPickerExpanded
}
) {
Text(stringResource(R.string.do_choose_color))
}
Switch(
checked = isColorEnabled,
onCheckedChange = {
isColorEnabled = it
if (!isColorEnabled) {
onColorChange(null)
}
}
)
}
if (isColorPickerExpanded && isColorEnabled) {
HsvColorPicker(
modifier = Modifier
.fillMaxWidth()
.height(450.dp)
.padding(10.dp),
initialColor = color?.let { Color(it.toColorInt()) },
controller = colorPickerController,
onColorChanged = { colorEnvelope: ColorEnvelope ->
onColorChange("#${colorEnvelope.hexCode.substring(2)}")
}
)
}
} }
@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 rememberSaveable { mutableStateOf<String?>(null) }
val context = LocalContext.current as Activity val context = LocalActivity.current
Page( Page(
"New card", stringResource(R.string.new_card_title),
innerPadding innerPadding
) { ) {
Column( Column(
modifier = Modifier.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
CardEditable( CardEditable(
cardName = cardName, cardName = cardName,
onCardNameChange = { cardName = it }, onCardNameChange = { cardName = it },
codeValue = codeValue, codeValue = codeValue,
oncodeValueChange = { codeValue = it }, onCodeValueChange = { codeValue = it },
codeType = codeType, codeType = codeType,
onCodeTypeChange = { codeType = it } onCodeTypeChange = { codeType = it },
color = color,
onColorChange = { color = it }
) )
Button( Button(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
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) getDbFun().addCard(cardName, codeValue, codeType, color)
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))
} }
} }
} }
@@ -198,42 +260,48 @@ 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 rememberSaveable { mutableStateOf(cardInfo.color) }
val context = LocalContext.current as Activity val context = LocalActivity.current
Page( Page(
"Editing card id${cardInfo.id}", stringResource(R.string.editing_card_title, cardInfo.id),
innerPadding innerPadding
) { ) {
Column( Column(
modifier = Modifier.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
CardEditable( CardEditable(
cardName = cardName, cardName = cardName,
onCardNameChange = { cardName = it }, onCardNameChange = { cardName = it },
codeValue = codeValue, codeValue = codeValue,
oncodeValueChange = { codeValue = it }, onCodeValueChange = { codeValue = it },
codeType = codeType, codeType = codeType,
onCodeTypeChange = { codeType = it } onCodeTypeChange = { codeType = it },
color = color,
onColorChange = { color = it }
) )
Button( Button(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
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) getDbFun().editCard(cardInfo.id, cardName, codeValue, codeType, color)
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))
} }
} }
} }
@@ -2,14 +2,19 @@ package ru.omni_devel.cards
import android.content.Intent import android.content.Intent
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.detectTapGestures
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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
@@ -23,21 +28,25 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.Color
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
@Composable @Composable
fun Page(name: String, innerPadding: PaddingValues, content: @Composable () -> Unit) { fun Page(name: String, innerPadding: PaddingValues, content: @Composable () -> Unit) {
Column( Column() {
modifier = Modifier.padding(innerPadding) TopBar(name, innerPadding)
) {
TopBar(name)
Column( Column(
modifier = Modifier.padding(horizontal = 16.dp) modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp)
.background(MaterialTheme.colorScheme.background)
) { ) {
content() content()
} }
@@ -45,12 +54,18 @@ fun Page(name: String, innerPadding: PaddingValues, content: @Composable () -> U
} }
@Composable @Composable
fun TopBar(text: String) { fun TopBar(text: String, innerPadding: PaddingValues) {
Row( Row(
modifier = Modifier.fillMaxWidth().background(MaterialTheme.colorScheme.primaryContainer).padding(horizontal = 12.dp, vertical = 16.dp) modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.primaryContainer)
.padding(top = innerPadding.calculateTopPadding())
.padding(horizontal = 8.dp)
.padding(bottom = 8.dp)
) { ) {
Text( Text(
text, text,
modifier = Modifier.padding(vertical = 4.dp, horizontal = 12.dp),
fontSize = 24.sp, fontSize = 24.sp,
color = MaterialTheme.colorScheme.onPrimaryContainer color = MaterialTheme.colorScheme.onPrimaryContainer
) )
@@ -58,7 +73,7 @@ fun TopBar(text: String) {
} }
@Composable @Composable
fun CardButton(cardInfo: CardInfo, onDeleteCard: (Int) -> Unit, getDbFun: () -> DbHelper) { fun CardButton(cardInfo: CardInfo, onDeleteCard: (Int) -> Unit) {
val context = LocalContext.current val context = LocalContext.current
var menuExpanded by remember { mutableStateOf(false) } var menuExpanded by remember { mutableStateOf(false) }
@@ -82,15 +97,31 @@ fun CardButton(cardInfo: CardInfo, onDeleteCard: (Int) -> Unit, getDbFun: () ->
color = MaterialTheme.colorScheme.primaryContainer, color = MaterialTheme.colorScheme.primaryContainer,
tonalElevation = 2.dp tonalElevation = 2.dp
) { ) {
Text( Row(
cardInfo.name, modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Min),
fontSize = 16.sp, horizontalArrangement = Arrangement.SpaceBetween
color = MaterialTheme.colorScheme.onPrimaryContainer, ) {
modifier = Modifier Box(
.padding(vertical = 16.dp, horizontal = 16.dp) modifier = Modifier
.fillMaxWidth(), .fillMaxHeight()
textAlign = TextAlign.Start .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(
cardInfo.name,
fontSize = 16.sp,
color = MaterialTheme.colorScheme.onPrimaryContainer,
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
.weight(1f),
textAlign = TextAlign.Start
)
}
} }
DropdownMenu( DropdownMenu(
@@ -98,7 +129,7 @@ fun CardButton(cardInfo: CardInfo, onDeleteCard: (Int) -> Unit, getDbFun: () ->
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
@@ -110,7 +141,7 @@ fun CardButton(cardInfo: CardInfo, onDeleteCard: (Int) -> Unit, getDbFun: () ->
} }
) )
DropdownMenuItem( DropdownMenuItem(
text = { Text("Delete") }, text = { Text(stringResource(R.string.do_delete_card)) },
onClick = { onClick = {
menuExpanded = false menuExpanded = false
@@ -11,11 +11,9 @@ 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.PaddingValues import androidx.compose.foundation.layout.PaddingValues
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.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
@@ -35,6 +33,7 @@ import androidx.compose.ui.Alignment
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.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import ru.omni_devel.cards.ui.theme.OmniCardsTheme import ru.omni_devel.cards.ui.theme.OmniCardsTheme
@@ -89,7 +88,7 @@ fun MainPage(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
Page( Page(
"OmniCards", stringResource(R.string.app_name),
innerPadding innerPadding
) { ) {
Column( Column(
@@ -97,10 +96,12 @@ fun MainPage(
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
if (cards.isEmpty()) { if (cards.isEmpty()) {
Text("Add new card to get started") Text(
stringResource(R.string.add_card_to_get_started)
)
} else { } else {
for (card in cards) { for (card in cards) {
CardButton(card, onDeleteCard, getDbFun) CardButton(card, onDeleteCard)
} }
} }
} }
@@ -128,31 +129,44 @@ fun MainPage(
context.startActivity(intent) context.startActivity(intent)
} }
) { ) {
Text("Add card") Text(stringResource(R.string.do_add_card))
} }
FullWidthButton( FullWidthButton(
onClick = { onClick = {
Toast.makeText(context, "Sync in progress...", Toast.LENGTH_SHORT).show() Toast.makeText(context, context.getString(R.string.sync_in_progress), Toast.LENGTH_SHORT).show()
sendToWatch(context, "/updateCards", Json.encodeToString(getDbFun().getCards())) { isSuccess -> sendToWatch(context, "/updateCards", Json.encodeToString(getDbFun().getCards())) { isSuccess ->
if (isSuccess) { if (isSuccess) {
Toast.makeText(context, "Successfully synced!", Toast.LENGTH_SHORT).show() Toast.makeText(context, context.getString(R.string.sync_is_successful), Toast.LENGTH_SHORT).show()
} else { } else {
Toast.makeText(context, "Sync failed! Please, check your connection", Toast.LENGTH_LONG).show() Toast.makeText(context, context.getString(R.string.sync_is_fail), Toast.LENGTH_LONG).show()
} }
} }
isActionBarShowed = false isActionBarShowed = false
} }
) { ) {
Text("Sync with WearOS") Text(stringResource(R.string.do_sync))
} }
FullWidthButton( FullWidthButton(
onClick = { onClick = {
uriHandler.openUri("https://omni-devel.ru") val intent = Intent(context, BackupActivity::class.java)
context.startActivity(intent)
isActionBarShowed = false
} }
) { ) {
Text("About author") Text(stringResource(R.string.open_backup_menu))
}
FullWidthButton(
onClick = {
uriHandler.openUri("https://github.com/omni-devel/OmniCards")
isActionBarShowed = false
}
) {
Text(stringResource(R.string.source_code))
} }
FullWidthButton( FullWidthButton(
onClick = { onClick = {
@@ -12,16 +12,19 @@ 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.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.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,14 +86,18 @@ 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)
.fillMaxWidth()
.weight(1f, fill = false),
contentScale = ContentScale.Fit
) )
Text( Text(
cardInfo.codeValue, cardInfo.codeValue,
@@ -99,7 +106,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
@@ -2,6 +2,7 @@ package ru.omni_devel.cards
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import androidx.compose.ui.platform.LocalContext
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.MultiFormatWriter import com.google.zxing.MultiFormatWriter
@@ -49,11 +50,13 @@ fun sendToWatch(context: Context, path: String, message: String, onResult: (Bool
} }
} }
fun checkCardData(name: String, value: String): String? { fun checkCardData(name: String, value: String): Int? {
if (name.trim().isEmpty()) { if (name.isEmpty()) {
return "Name should not be empty" return R.string.card_name_should_not_be_empty
} else if (value.trim().isEmpty()) { } else if (value.isEmpty()) {
return "Code value should not be empty" 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
@@ -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>
+41
View File
@@ -0,0 +1,41 @@
<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="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="scan_your_card">Отсканируйте вашу карту</string>
<string name="card_code_type">Тип кода карты</string>
<string name="do_choose_color">Выбрать цвет</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>
+43 -45
View File
@@ -1,48 +1,46 @@
<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="open_backup_menu">Backup / Restore</string>
molestie quam. Fusce blandit tincidunt nulla, quis sollicitudin libero facilisis et. Integer <string name="do_backup">Save data</string>
interdum nunc ligula, et fermentum metus hendrerit id. Vestibulum lectus felis, dictum at <string name="do_restore">Restore data</string>
lacinia sit amet, tristique id quam. Cras eu consequat dui. Suspendisse sodales nunc ligula, <string name="ask_do_restore">Restore data?</string>
in lobortis sem porta sed. Integer id ultrices magna, in luctus elit. Sed a pellentesque <string name="backup_successfully_saved">Data saved successfully</string>
est.\n\n <string name="failed_to_save_backup">Failed to save data: %s</string>
Aenean nunc velit, lacinia sed dolor sed, ultrices viverra nulla. Etiam a venenatis nibh. <string name="backup_successfully_restored">Data restored successfully</string>
Morbi laoreet, tortor sed facilisis varius, nibh orci rhoncus nulla, id elementum leo dui <string name="failed_to_restore_backup">Failed to restore data: %s</string>
non lorem. Nam mollis ipsum quis auctor varius. Quisque elementum eu libero sed commodo. In <string name="restore_caution">This will replace your existing cards with data from the backup file. Continue?</string>
eros nisl, imperdiet vel imperdiet et, scelerisque a mauris. Pellentesque varius ex nunc,
quis imperdiet eros placerat ac. Duis finibus orci et est auctor tincidunt. Sed non viverra <string name="yes">Yes</string>
ipsum. Nunc quis augue egestas, cursus lorem at, molestie sem. Morbi a consectetur ipsum, a <string name="no">No</string>
placerat diam. Etiam vulputate dignissim convallis. Integer faucibus mauris sit amet finibus <string name="do_save">Save</string>
convallis.\n\n
Phasellus in aliquet mi. Pellentesque habitant morbi tristique senectus et netus et <string name="new_card_title">Add card</string>
malesuada fames ac turpis egestas. In volutpat arcu ut felis sagittis, in finibus massa <string name="editing_card_title">Editing card #%d</string>
gravida. Pellentesque id tellus orci. Integer dictum, lorem sed efficitur ullamcorper, <string name="card_name">Card name</string>
libero justo consectetur ipsum, in mollis nisl ex sed nisl. Donec maximus ullamcorper <string name="card_value">Card value</string>
sodales. Praesent bibendum rhoncus tellus nec feugiat. In a ornare nulla. Donec rhoncus <string name="do_scan_card">Scan card</string>
libero vel nunc consequat, quis tincidunt nisl eleifend. Cras bibendum enim a justo luctus <string name="scan_your_card">Scan your card</string>
vestibulum. Fusce dictum libero quis erat maximus, vitae volutpat diam dignissim. <string name="card_code_type">Card code type</string>
</string> <string name="do_choose_color">Choose color</string>
<string name="title_activity_show_card">ShowCardActivity</string> <string name="card_name_should_not_be_empty">Card name must not be empty</string>
<string name="title_activity_add_card">AddCardActivity</string> <string name="card_value_should_not_be_empty">Card value must not be empty</string>
<string name="title_activity_edit_card">EditCardActivity</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>
</resources> </resources>
+13 -4
View File
@@ -1,7 +1,17 @@
plugins { plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.compose) alias(libs.plugins.kotlin.compose)
kotlin("plugin.serialization") version "2.2.20" 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 {
@@ -12,9 +22,8 @@ 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 = "1.3.0"
} }
buildTypes { buildTypes {
Binary file not shown.
Binary file not shown.
@@ -9,4 +9,5 @@ class CardInfo (
val name: String, val name: String,
val codeType: BarcodeFormat, val codeType: BarcodeFormat,
val codeValue: String, val codeValue: String,
val color: String?
) )
@@ -6,17 +6,19 @@ import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper import android.database.sqlite.SQLiteOpenHelper
import com.google.zxing.BarcodeFormat import com.google.zxing.BarcodeFormat
class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?) : SQLiteOpenHelper(context, "omni_cards", factory, 1) { class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?) : SQLiteOpenHelper(context, "omni_cards", factory, 2) {
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)") db!!.execSQL("CREATE TABLE IF NOT EXISTS cards (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, codeType TEXT, codeValue TEXT, color TEXT)")
} }
override fun onUpgrade( override fun onUpgrade(
db: SQLiteDatabase?, db: SQLiteDatabase?,
p1: Int, oldVersion: Int,
p2: Int p2: Int
) { ) {
db!!.execSQL("DROP TABLE IF EXISTS cards") if (oldVersion < 2) {
db!!.execSQL("ALTER TABLE cards ADD COLUMN color TEXT")
}
} }
fun clearDatabase() { fun clearDatabase() {
@@ -37,6 +39,7 @@ class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?)
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)
db.insert("cards", null, values) db.insert("cards", null, values)
} }
@@ -55,7 +58,8 @@ class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?)
id = cursor.getInt(cursor.getColumnIndexOrThrow("id")), id = cursor.getInt(cursor.getColumnIndexOrThrow("id")),
name = cursor.getString(cursor.getColumnIndexOrThrow("name")), name = cursor.getString(cursor.getColumnIndexOrThrow("name")),
codeType = BarcodeFormat.valueOf(cursor.getString(cursor.getColumnIndexOrThrow("codeType"))), codeType = BarcodeFormat.valueOf(cursor.getString(cursor.getColumnIndexOrThrow("codeType"))),
codeValue = cursor.getString(cursor.getColumnIndexOrThrow("codeValue")) codeValue = cursor.getString(cursor.getColumnIndexOrThrow("codeValue")),
color = cursor.getString(cursor.getColumnIndexOrThrow("color"))
)) ))
} }
@@ -81,7 +85,8 @@ class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?)
id = cursor.getInt(cursor.getColumnIndexOrThrow("id")), id = cursor.getInt(cursor.getColumnIndexOrThrow("id")),
name = cursor.getString(cursor.getColumnIndexOrThrow("name")), name = cursor.getString(cursor.getColumnIndexOrThrow("name")),
codeType = BarcodeFormat.valueOf(cursor.getString(cursor.getColumnIndexOrThrow("codeType"))), codeType = BarcodeFormat.valueOf(cursor.getString(cursor.getColumnIndexOrThrow("codeType"))),
codeValue = cursor.getString(cursor.getColumnIndexOrThrow("codeValue")) codeValue = cursor.getString(cursor.getColumnIndexOrThrow("codeValue")),
color = cursor.getString(cursor.getColumnIndexOrThrow("color"))
) )
cursor.close() cursor.close()
@@ -0,0 +1,77 @@
package ru.omni_devel.cards.presentation
import android.content.Intent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
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.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.graphics.toColorInt
import androidx.wear.compose.material3.Button
import androidx.wear.compose.material3.ButtonDefaults
import androidx.wear.compose.material3.MaterialTheme
import androidx.wear.compose.material3.Text
@Composable
fun CardButton(cardInfo: CardInfo) {
val context = LocalContext.current
Box(modifier = Modifier.fillMaxWidth()) {
Button(
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primaryContainer
),
contentPadding = PaddingValues(0.dp),
onClick = {
val intent = Intent(context, ShowCardActivity::class.java)
intent.putExtra("cardId", cardInfo.id)
context.startActivity(intent)
}
) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(IntrinsicSize.Min)
.clip(ButtonDefaults.shape)
) {
Box(
modifier = Modifier
.fillMaxHeight()
.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(
cardInfo.name,
fontSize = 16.sp,
color = MaterialTheme.colorScheme.onPrimaryContainer,
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
.weight(1f),
textAlign = TextAlign.Start
)
}
}
}
}
@@ -1,27 +1,22 @@
package ru.omni_devel.cards.presentation package ru.omni_devel.cards.presentation
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent
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.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.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.padding
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
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.Button
import androidx.wear.compose.material3.Text import androidx.wear.compose.material3.Text
import ru.omni_devel.cards.R
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private lateinit var db: DbHelper private lateinit var db: DbHelper
@@ -40,8 +35,6 @@ class MainActivity : ComponentActivity() {
@SuppressLint("UnusedBoxWithConstraintsScope") @SuppressLint("UnusedBoxWithConstraintsScope")
@Composable @Composable
fun App(cards: List<CardInfo>) { fun App(cards: List<CardInfo>) {
val context = LocalContext.current
BoxWithConstraints(modifier = Modifier.fillMaxSize()) { BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
val topBottomPadding = maxHeight * 0.4f val topBottomPadding = maxHeight * 0.4f
@@ -50,7 +43,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(
@@ -63,22 +56,7 @@ fun App(cards: List<CardInfo>) {
) { ) {
for (card in cards) { for (card in cards) {
item { item {
Button( CardButton(card)
modifier = Modifier.fillMaxWidth(),
onClick = {
val intent = Intent(context, ShowCardActivity::class.java)
intent.putExtra("cardId", card.id)
context.startActivity(intent)
}
) {
Text(
text = card.name,
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center
)
}
} }
} }
} }
@@ -7,6 +7,7 @@ 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
class MessagesReceiver : WearableListenerService() { class MessagesReceiver : WearableListenerService() {
private lateinit var db: DbHelper private lateinit var db: DbHelper
@@ -28,7 +29,7 @@ class MessagesReceiver : WearableListenerService() {
db.addCards(cards) db.addCards(cards)
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
Toast.makeText(this, "Database updated!", Toast.LENGTH_SHORT).show() Toast.makeText(this, this.getString(R.string.sync_is_successful), Toast.LENGTH_SHORT).show()
val intent = Intent(this, MainActivity::class.java).apply { val intent = Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
@@ -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
) )
+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>