Compare commits

..
8 Commits
13 changed files with 282 additions and 111 deletions
+2 -1
View File
@@ -23,7 +23,7 @@ android {
minSdk = 30 minSdk = 30
targetSdk = 36 targetSdk = 36
versionCode = generateVersionCode() versionCode = generateVersionCode()
versionName = "4.2.1" versionName = "4.3.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
@@ -81,4 +81,5 @@ dependencies {
implementation("io.ktor:ktor-client-core:3.5.0") implementation("io.ktor:ktor-client-core:3.5.0")
implementation("io.ktor:ktor-client-cio:3.5.0") implementation("io.ktor:ktor-client-cio:3.5.0")
implementation("androidx.compose.material:material-icons-extended") implementation("androidx.compose.material:material-icons-extended")
implementation("sh.calvin.reorderable:reorderable:2.4.3")
} }
+10 -1
View File
@@ -1,5 +1,6 @@
<?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" /> <uses-permission android:name="android.permission.INTERNET" />
@@ -10,6 +11,14 @@
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.OmniCards"> android:theme="@style/Theme.OmniCards">
<activity
android:name="com.journeyapps.barcodescanner.CaptureActivity"
android:screenOrientation="portrait"
android:stateNotNeeded="true"
android:theme="@style/zxing_CaptureTheme"
android:windowSoftInputMode="stateAlwaysHidden"
tools:replace="android:screenOrientation" />
<activity <activity
android:name=".SelectIconActivity" android:name=".SelectIconActivity"
android:exported="false" android:exported="false"
@@ -15,5 +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 icon: String?,
val position: Int
) {} ) {}
@@ -2,15 +2,17 @@ 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.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, 6) { 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( db!!.execSQL(
"CREATE TABLE IF NOT EXISTS cards (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, codeType TEXT, codeValue TEXT, color TEXT, icon TEXT)" "CREATE TABLE IF NOT EXISTS cards (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, codeType TEXT, codeValue TEXT, color TEXT, icon TEXT, position DEFAULT 0)"
) )
} }
@@ -42,6 +44,32 @@ class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?)
if (oldVersion < 6) { if (oldVersion < 6) {
db!!.execSQL("ALTER TABLE cards ADD COLUMN icon TEXT") 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")
}
}
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?) { fun addCard(name: String, codeValue: String, codeType: BarcodeFormat, color: String?, icon: String?) {
@@ -54,6 +82,7 @@ class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?)
values.put("codeType", codeType.name) values.put("codeType", codeType.name)
values.put("color", color) values.put("color", color)
values.put("icon", icon) values.put("icon", icon)
values.put("position", nextPosition(db))
db.insert("cards", null, values) db.insert("cards", null, values)
@@ -79,18 +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 ORDER BY id ASC", 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")),
icon = cursor.getStringOrNull(cursor.getColumnIndexOrThrow("icon"))
))
} }
cursor.close() cursor.close()
@@ -111,14 +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")),
icon = cursor.getStringOrNull(cursor.getColumnIndexOrThrow("icon"))
)
cursor.close() cursor.close()
db.close() db.close()
@@ -143,19 +158,38 @@ 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("icon", card.icon)
values.put("position", nextPosition(db))
db.insert("cards", null, values) db.insert("cards", null, values)
}
}
} }
}
db.close() 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()))
}
}
}
} }
} }
@@ -46,6 +46,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.graphics.toColorInt
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
@@ -96,6 +97,11 @@ fun CardEditable(
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 ->
@@ -267,7 +273,7 @@ fun CardEditable(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
CardIcon(if (icon == null) null else base64ToImageBitmap(icon), MaterialTheme.colorScheme.primary) CardIcon(if (icon == null) null else base64ToImageBitmap(icon), cardColor)
Text(stringResource(R.string.do_select_icon)) Text(stringResource(R.string.do_select_icon))
} }
@@ -40,6 +40,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material.icons.outlined.Add import androidx.compose.material.icons.outlined.Add
import androidx.compose.material.icons.outlined.QrCode import androidx.compose.material.icons.outlined.QrCode
import androidx.compose.material.icons.outlined.DensityMedium import androidx.compose.material.icons.outlined.DensityMedium
import androidx.compose.material.icons.outlined.DragHandle
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButton
@@ -185,7 +186,7 @@ fun TopBar(
} }
@Composable @Composable
fun CardButton(cardInfo: CardInfo, onDeleteCard: (Int) -> Unit) { 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) }
@@ -222,20 +223,30 @@ fun CardButton(cardInfo: CardInfo, onDeleteCard: (Int) -> Unit) {
.background(Color.Transparent) .background(Color.Transparent)
.padding(horizontal = 8.dp), .padding(horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.SpaceBetween
) { ) {
CardIcon( Row(
if (cardInfo.icon == null) null else base64ToImageBitmap(cardInfo.icon), modifier = Modifier.weight(1f),
cardColor = cardColor verticalAlignment = Alignment.CenterVertically,
) horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
CardIcon(
if (cardInfo.icon == null) null else base64ToImageBitmap(cardInfo.icon),
cardColor = cardColor
)
Text( Text(
cardInfo.name, cardInfo.name,
fontSize = 16.sp, fontSize = 16.sp,
color = MaterialTheme.colorScheme.onPrimaryContainer, color = MaterialTheme.colorScheme.onPrimaryContainer,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Start textAlign = TextAlign.Start
) )
}
if (reorderIcon != null) {
reorderIcon()
}
} }
} }
@@ -9,23 +9,26 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.animation.shrinkVertically import androidx.compose.animation.shrinkVertically
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
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.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Add import androidx.compose.material.icons.outlined.Add
import androidx.compose.material.icons.outlined.Sync import androidx.compose.material.icons.outlined.Sync
import androidx.compose.material.icons.outlined.Backup import androidx.compose.material.icons.outlined.Backup
import androidx.compose.material.icons.outlined.Code import androidx.compose.material.icons.outlined.Code
import androidx.compose.material.icons.outlined.DragHandle
import androidx.compose.material.icons.outlined.Search import androidx.compose.material.icons.outlined.Search
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -37,12 +40,15 @@ import androidx.compose.ui.unit.dp
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment
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 androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import kotlinx.serialization.json.Json
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
@@ -60,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
}
)
} }
} }
} }
@@ -76,14 +92,17 @@ 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
@@ -92,6 +111,19 @@ fun MainPage(
var searchQuery by rememberSaveable { mutableStateOf("") } var searchQuery by rememberSaveable { mutableStateOf("") }
var isSearchFieldShowed by rememberSaveable { mutableStateOf(false) } 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()
) { ) {
@@ -114,12 +146,13 @@ fun MainPage(
NavigationPageData(stringResource(R.string.do_add_card), Icons.Outlined.Add, EditCardActivity::class.java), NavigationPageData(stringResource(R.string.do_add_card), Icons.Outlined.Add, EditCardActivity::class.java),
NavigationPageData(stringResource(R.string.do_sync), Icons.Outlined.Sync) { NavigationPageData(stringResource(R.string.do_sync), Icons.Outlined.Sync) {
Toast.makeText(context, context.getString(R.string.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 ->
if (isSuccess) { val isSuccess = syncCardsWithWatch(context, getDbFun().getCards())
Toast.makeText(context, context.getString(R.string.sync_is_successful), Toast.LENGTH_SHORT).show()
} else { if (isSuccess) {
Toast.makeText(context, context.getString(R.string.sync_is_fail), Toast.LENGTH_LONG).show() 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.open_backup_menu), Icons.Outlined.Backup, BackupActivity::class.java),
@@ -147,14 +180,11 @@ fun MainPage(
cardNameCleaned = cardNameCleaned.replace(from, to) cardNameCleaned = cardNameCleaned.replace(from, to)
} }
if (cardNameCleaned.contains(queryCleaned)) { return@filter cardNameCleaned.contains(queryCleaned)
return@filter true
}
return@filter false
} }
LazyColumn( LazyColumn(
state = lazyListState,
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
item { item {
@@ -188,8 +218,40 @@ fun MainPage(
} }
} else { } else {
for (card in searchResults) { for (card in searchResults) {
item { item(key = card.id) {
CardButton(card, onDeleteCard) 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 (queryCleaned.isEmpty()) {
reorderIcon = {
Icon(
imageVector = Icons.Outlined.DragHandle,
contentDescription = null,
tint = MaterialTheme.colorScheme.onPrimaryContainer,
modifier = Modifier
.draggableHandle(
onDragStopped = {
onReorderCards(cards.map { it.id })
}
)
.padding(8.dp)
)
}
}
CardButton(
card,
onDeleteCard,
reorderIcon
)
}
}
} }
} }
} }
@@ -14,7 +14,9 @@ import com.journeyapps.barcodescanner.BarcodeEncoder
import org.json.JSONObject import org.json.JSONObject
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import android.util.Base64 import android.util.Base64
import android.widget.Toast
import androidx.compose.ui.graphics.asImageBitmap 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()
@@ -33,12 +35,14 @@ fun generateCode(value: String, format: BarcodeFormat): Bitmap {
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
} }
@@ -59,13 +63,19 @@ 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 syncCardsWithWatch(context: Context, cards: List<CardInfo>): Boolean {
return sendToWatch(context, "/updateCards", Json.encodeToString(cards))
} }
fun checkCardData(name: String, value: String): Int? { fun checkCardData(name: String, value: String): Int? {
+2 -1
View File
@@ -23,7 +23,7 @@ android {
minSdk = 30 minSdk = 30
targetSdk = 36 targetSdk = 36
versionCode = generateVersionCode() versionCode = generateVersionCode()
versionName = "3.2.0" versionName = "3.3.0"
} }
buildTypes { buildTypes {
@@ -72,4 +72,5 @@ dependencies {
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.compose.material:material-icons-extended")
implementation("androidx.lifecycle:lifecycle-process:2.8.7")
} }
@@ -10,5 +10,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 icon: String?,
val position: Int
) )
@@ -2,12 +2,14 @@ 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.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, 5) { class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?) : SQLiteOpenHelper(context, "omni_cards", factory, 6) {
override fun onCreate(db: SQLiteDatabase?) { override fun onCreate(db: SQLiteDatabase?) {
db!!.execSQL( db!!.execSQL(
"CREATE TABLE IF NOT EXISTS cards (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, codeType TEXT, codeValue TEXT, color TEXT, icon TEXT)" "CREATE TABLE IF NOT EXISTS cards (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, codeType TEXT, codeValue TEXT, color TEXT, icon TEXT)"
@@ -37,6 +39,23 @@ class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?)
if (oldVersion < 5) { if (oldVersion < 5) {
db!!.execSQL("ALTER TABLE cards ADD COLUMN icon TEXT") 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() {
@@ -50,37 +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("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")),
icon = cursor.getStringOrNull(cursor.getColumnIndexOrThrow("icon"))
))
} }
cursor.close() cursor.close()
@@ -101,14 +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")),
icon = cursor.getStringOrNull(cursor.getColumnIndexOrThrow("icon"))
)
cursor.close() cursor.close()
db.close() db.close()
@@ -11,6 +11,7 @@ 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.res.stringResource
@@ -22,16 +23,25 @@ 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")
@@ -8,6 +8,8 @@ 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 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
@@ -24,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, this.getString(R.string.sync_is_successful), 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)
} }
} }
} }