Compare commits

..
17 Commits
19 changed files with 419 additions and 187 deletions
+3 -1
View File
@@ -23,7 +23,7 @@ android {
minSdk = 30
targetSdk = 36
versionCode = generateVersionCode()
versionName = "4.0.0"
versionName = "4.3.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -47,6 +47,7 @@ android {
buildFeatures {
compose = true
viewBinding = true
buildConfig = true
}
}
@@ -80,4 +81,5 @@ dependencies {
implementation("io.ktor:ktor-client-core:3.5.0")
implementation("io.ktor:ktor-client-cio:3.5.0")
implementation("androidx.compose.material:material-icons-extended")
implementation("sh.calvin.reorderable:reorderable:2.4.3")
}
+10 -1
View File
@@ -1,5 +1,6 @@
<?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" />
@@ -10,6 +11,14 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
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
android:name=".SelectIconActivity"
android:exported="false"
@@ -15,5 +15,6 @@ class CardInfo (
val codeType: BarcodeFormat,
val codeValue: String,
val color: String?,
val icon: String?
val icon: String?,
val position: Int
) {}
@@ -2,7 +2,10 @@ package ru.omni_devel.cards
const val REPO_BASE_URL = "https://gitea.omni-devel.ru/omni/OmniCardsRepo/raw/branch/main"
const val TEMPLATES_REPO_FILE = "templates.json"
val TEMPLATES_REPO_FILE = if (BuildConfig.DEBUG)
"templates.debug.json"
else
"templates.json"
const val ICONS_REPO_FILE = "icons.json"
const val ICONS_REPO_FOLDER = "icons"
@@ -2,15 +2,17 @@ package ru.omni_devel.cards
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import androidx.core.database.getStringOrNull
import androidx.core.database.sqlite.transaction
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?) {
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) {
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?) {
@@ -54,6 +82,7 @@ class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?)
values.put("codeType", codeType.name)
values.put("color", color)
values.put("icon", icon)
values.put("position", nextPosition(db))
db.insert("cards", null, values)
@@ -79,18 +108,11 @@ class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?)
fun getCards(): List<CardInfo> {
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>()
while (cursor.moveToNext()) {
cards.add(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"))
))
cards.add(readCard(cursor))
}
cursor.close()
@@ -111,14 +133,7 @@ class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?)
return null
}
val cardInfo = 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"))
)
val cardInfo = readCard(cursor)
cursor.close()
db.close()
@@ -143,6 +158,8 @@ class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?)
fun addCards(cards: List<CardInfo>) {
val db = this.writableDatabase
db.use { db ->
db.transaction {
for (card in cards) {
val values = ContentValues()
@@ -152,10 +169,27 @@ class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?)
values.put("codeType", card.codeType.name)
values.put("color", card.color)
values.put("icon", card.icon)
values.put("position", nextPosition(db))
db.insert("cards", null, values)
}
}
}
}
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.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.graphics.toColorInt
import com.google.zxing.BarcodeFormat
import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanOptions
@@ -96,6 +97,11 @@ fun CardEditable(
val fieldModifier = Modifier.fillMaxWidth()
val cardColor = if (color == null)
MaterialTheme.colorScheme.primary
else
Color(color.toColorInt())
val scanLauncher = rememberLauncherForActivityResult(
contract = ScanContract()
) { result ->
@@ -125,9 +131,11 @@ fun CardEditable(
contract = ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val codeValue = result.data?.getStringExtra("codeValue") ?: ""
val codeValue = result.data?.getStringExtra("codeValue")!!
val codeType = BarcodeFormat.valueOf(result.data?.getStringExtra("codeType")!!)
onCodeValueChange(codeValue)
onCodeTypeChange(codeType)
}
}
@@ -265,7 +273,7 @@ fun CardEditable(
verticalAlignment = Alignment.CenterVertically,
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))
}
@@ -40,6 +40,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material.icons.outlined.Add
import androidx.compose.material.icons.outlined.QrCode
import androidx.compose.material.icons.outlined.DensityMedium
import androidx.compose.material.icons.outlined.DragHandle
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.FloatingActionButton
@@ -59,6 +60,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.graphicsLayer
@@ -184,7 +186,7 @@ fun TopBar(
}
@Composable
fun CardButton(cardInfo: CardInfo, onDeleteCard: (Int) -> Unit) {
fun CardButton(cardInfo: CardInfo, onDeleteCard: (Int) -> Unit, reorderIcon: (@Composable () -> Unit)? = null) {
val context = LocalContext.current
var menuExpanded by remember { mutableStateOf(false) }
@@ -221,6 +223,11 @@ fun CardButton(cardInfo: CardInfo, onDeleteCard: (Int) -> Unit) {
.background(Color.Transparent)
.padding(horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Row(
modifier = Modifier.weight(1f),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
CardIcon(
@@ -236,6 +243,11 @@ fun CardButton(cardInfo: CardInfo, onDeleteCard: (Int) -> Unit) {
textAlign = TextAlign.Start
)
}
if (reorderIcon != null) {
reorderIcon()
}
}
}
DropdownMenu(
@@ -274,19 +286,18 @@ fun CardIcon(icon: ImageBitmap?, cardColor: Color, isLoading: Boolean = false) {
cardColor.copy(0.525f),
RoundedCornerShape(12.dp)
)
.padding(8.dp)
.size(32.dp),
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
modifier = Modifier.size(32.dp),
color = MaterialTheme.colorScheme.primary
)
} else if (icon == null) {
Icon(
modifier = Modifier.size(24.dp),
modifier = Modifier.size(32.dp),
imageVector = Icons.Outlined.QrCode,
tint = MaterialTheme.colorScheme.onPrimaryContainer,
contentDescription = null
@@ -295,7 +306,7 @@ fun CardIcon(icon: ImageBitmap?, cardColor: Color, isLoading: Boolean = false) {
Image(
bitmap = icon,
contentDescription = null,
modifier = Modifier.size(32.dp)
modifier = Modifier.size(36.dp).clip(RoundedCornerShape(12.dp))
)
}
}
@@ -87,15 +87,18 @@ fun FillTemplatePage(template: Template, innerPadding: PaddingValues) {
return@AdaptiveButton
}
var result = template.template
val result = try {
processQrScript(template.template, filledFields)
} catch (e: Exception) {
Toast.makeText(context, e.toString(), Toast.LENGTH_LONG).show()
for ((id, value) in filledFields) {
result = result.replace("$$id$", value)
return@AdaptiveButton
}
val resultIntent = Intent()
resultIntent.putExtra("codeValue", result)
resultIntent.putExtra("codeType", template.codeType.name)
activityContext!!.setResult(Activity.RESULT_OK, resultIntent)
activityContext.finish()
@@ -9,23 +9,26 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Add
import androidx.compose.material.icons.outlined.Sync
import androidx.compose.material.icons.outlined.Backup
import androidx.compose.material.icons.outlined.Code
import androidx.compose.material.icons.outlined.DragHandle
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
@@ -37,12 +40,15 @@ import androidx.compose.ui.unit.dp
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.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.LocalUriHandler
import androidx.compose.ui.res.stringResource
import kotlinx.serialization.json.Json
import ru.omni_devel.cards.ui.theme.OmniCardsTheme
import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyListState
class MainActivity : ComponentActivity() {
private lateinit var db: DbHelper
@@ -60,12 +66,22 @@ class MainActivity : ComponentActivity() {
Scaffold(
modifier = Modifier.fillMaxSize()
) { innerPadding ->
MainPage(innerPadding, cards, onDeleteCard = { cardId ->
MainPage(
innerPadding,
cards,
onDeleteCard = { cardId ->
db.removeCard(cardId)
cards.removeIf { it.id == cardId }
}, getDbFun = {
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.addAll(db.getCards())
syncCardsWithWatch(this, db.getCards())
}
}
@Composable
fun MainPage(
innerPadding: PaddingValues,
cards: List<CardInfo>,
cards: SnapshotStateList<CardInfo>,
onDeleteCard: (Int) -> Unit,
onReorderCards: (List<Int>) -> Unit,
getDbFun: () -> DbHelper
) {
val context = LocalContext.current
@@ -92,6 +111,19 @@ fun MainPage(
var searchQuery by rememberSaveable { mutableStateOf("") }
var isSearchFieldShowed by rememberSaveable { mutableStateOf(false) }
val lazyListState = rememberLazyListState()
val reorderableLazyListState = rememberReorderableLazyListState(lazyListState) { from, to ->
val fromId = from.key as? Int ?: return@rememberReorderableLazyListState
val toId = to.key as? Int ?: return@rememberReorderableLazyListState
val fromIndex = cards.indexOfFirst { it.id == fromId }
val toIndex = cards.indexOfFirst { it.id == toId }
if (fromIndex != -1 && toIndex != -1) {
cards.add(toIndex, cards.removeAt(fromIndex))
}
}
Box(
modifier = Modifier.fillMaxSize()
) {
@@ -114,13 +146,14 @@ fun MainPage(
NavigationPageData(stringResource(R.string.do_add_card), Icons.Outlined.Add, EditCardActivity::class.java),
NavigationPageData(stringResource(R.string.do_sync), Icons.Outlined.Sync) {
Toast.makeText(context, context.getString(R.string.sync_in_progress), Toast.LENGTH_SHORT).show()
sendToWatch(context, "/updateCards", Json.encodeToString(getDbFun().getCards())) { isSuccess ->
val isSuccess = syncCardsWithWatch(context, getDbFun().getCards())
if (isSuccess) {
Toast.makeText(context, context.getString(R.string.sync_is_successful), Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, context.getString(R.string.sync_is_fail), Toast.LENGTH_LONG).show()
}
}
},
NavigationPageData(stringResource(R.string.open_backup_menu), Icons.Outlined.Backup, BackupActivity::class.java),
NavigationPageData(stringResource(R.string.source_code), Icons.Outlined.Code) {
@@ -147,14 +180,11 @@ fun MainPage(
cardNameCleaned = cardNameCleaned.replace(from, to)
}
if (cardNameCleaned.contains(queryCleaned)) {
return@filter true
}
return@filter false
return@filter cardNameCleaned.contains(queryCleaned)
}
LazyColumn(
state = lazyListState,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
item {
@@ -188,8 +218,40 @@ fun MainPage(
}
} else {
for (card in searchResults) {
item {
CardButton(card, onDeleteCard)
item(key = card.id) {
ReorderableItem(reorderableLazyListState, key = card.id) { isDragging ->
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.alpha(if (isDragging) 0.7f else 1f)
) {
var reorderIcon: (@Composable () -> Unit)? = null
if (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
)
}
}
}
}
}
@@ -54,6 +54,7 @@ import io.ktor.client.engine.cio.CIO
import io.ktor.client.request.get
import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.bodyAsText
import kotlinx.coroutines.launch
import ru.omni_devel.cards.ui.theme.OmniCardsTheme
class SelectIconActivity : ComponentActivity() {
@@ -94,6 +95,7 @@ fun SelectIconPage(innerPadding: PaddingValues) {
for (iconData in loadedIconsData) {
if (iconData.iconId != null) {
launch {
val response = client.get("$REPO_BASE_URL/icons/${iconData.iconId}.png")
val bytes: ByteArray = response.body()
@@ -102,6 +104,7 @@ fun SelectIconPage(innerPadding: PaddingValues) {
}
}
}
}
} catch (e: Exception) {
loadingError = e.toString()
}
@@ -113,7 +116,7 @@ fun SelectIconPage(innerPadding: PaddingValues) {
queryCleaned = queryCleaned.replace(from, to)
}
val searchResults = iconsData!!.filter { iconData ->
val searchResults = iconsData?.filter { iconData ->
if (queryCleaned.isEmpty()) {
return@filter true
}
@@ -131,7 +134,7 @@ fun SelectIconPage(innerPadding: PaddingValues) {
}
return@filter false
}
} ?: emptyList()
Page(context.getString(R.string.choosing_icon), innerPadding) {
OutlinedTextField(
@@ -169,16 +172,14 @@ fun SelectIconPage(innerPadding: PaddingValues) {
Text(stringResource(R.string.icons_not_found))
}
} else {
for (chunk in searchResults.chunked(2)) {
item {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
for (iconData in chunk) {
for (iconData in searchResults) {
val loadedIcon = loadedIcons[iconData.iconId]
item {
AdaptiveButton(
modifier = Modifier.weight(1f).height(64.dp),
modifier = Modifier
.fillMaxWidth()
.height(64.dp),
contentPadding = PaddingValues(0.dp),
onClick = {
if (loadedIcon == null && iconData.iconId != null) {
@@ -234,5 +235,3 @@ fun SelectIconPage(innerPadding: PaddingValues) {
}
}
}
}
}
@@ -84,10 +84,12 @@ fun SelectTemplatePage(innerPadding: PaddingValues) {
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val codeValue = result.data?.getStringExtra("codeValue")
val codeTypeString = result.data?.getStringExtra("codeType")
val resultIntent = Intent()
resultIntent.putExtra("codeValue", codeValue)
resultIntent.putExtra("codeType", codeTypeString)
activityContext!!.setResult(Activity.RESULT_OK, resultIntent)
activityContext.finish()
@@ -1,5 +1,6 @@
package ru.omni_devel.cards
import com.google.zxing.BarcodeFormat
import kotlinx.serialization.Serializable
import org.json.JSONArray
import org.json.JSONObject
@@ -16,7 +17,8 @@ class Field(
class Template(
val names: LinkedHashMap<String, String>,
val fields: List<Field>,
val template: String
val template: String,
val codeType: BarcodeFormat = BarcodeFormat.QR_CODE
) {
companion object {
fun parseTemplateFromJson(json: String): Template {
@@ -39,7 +41,8 @@ class Template(
return Template(
names = jsonObjectToMap(obj.getJSONObject("names")),
fields = fields,
template = obj.getString("template")
template = obj.getString("template"),
codeType = parseCodeType(obj)
)
}
@@ -67,11 +70,21 @@ class Template(
templates.add(Template(
names = jsonObjectToMap(obj.getJSONObject("names")),
fields = fields,
template = obj.getString("template")
template = obj.getString("template"),
codeType = parseCodeType(obj)
))
}
return templates
}
private fun parseCodeType(obj: JSONObject): BarcodeFormat {
val raw = obj.optString("codeType", "")
return if (raw.isBlank()) {
BarcodeFormat.QR_CODE
} else {
BarcodeFormat.valueOf(raw)
}
}
}
}
@@ -14,7 +14,9 @@ import com.journeyapps.barcodescanner.BarcodeEncoder
import org.json.JSONObject
import java.io.ByteArrayOutputStream
import android.util.Base64
import android.widget.Toast
import androidx.compose.ui.graphics.asImageBitmap
import kotlinx.serialization.json.Json
fun generateCode(value: String, format: BarcodeFormat): Bitmap {
val writer = MultiFormatWriter()
@@ -33,12 +35,14 @@ fun generateCode(value: String, format: BarcodeFormat): Bitmap {
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)
var isSuccess = false
nodeClient.connectedNodes.addOnSuccessListener { nodes ->
if (nodes.isEmpty()) {
onResult(false)
isSuccess = false
return@addOnSuccessListener
}
@@ -59,13 +63,19 @@ fun sendToWatch(context: Context, path: String, message: String, onResult: (Bool
}
if (completedCount == nodes.size) {
onResult(isAnySuccess)
isSuccess = isAnySuccess
}
}.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? {
@@ -121,3 +131,41 @@ fun base64ToImageBitmap(base64String: String): ImageBitmap {
return bitmap.asImageBitmap()
}
fun processQrScript(input: String, fills: Map<String, String>): String {
val outerRegex = Regex("""\$(\w+)((?:\([^)]*\))*)\$""")
val innerRegex = Regex("""\(([^)]*)\)""")
var result = input
outerRegex.findAll(input).forEach { match ->
val fullBlock = match.value
val name = match.groupValues[1]
val contents = innerRegex.findAll(match.groupValues[2]).map { it.groupValues[1] }.toList()
var blockResult = fills[name] ?: return@forEach
contents.forEach { content ->
val args = content.split(" ")
val action = args[0]
when (action) {
"first" -> {
val count = args[1].toInt()
blockResult = blockResult.take(count)
}
"last" -> {
val count = args[1].toInt()
blockResult = blockResult.takeLast(count)
}
}
}
result = result.replace(fullBlock, blockResult)
}
return result
}
+2 -1
View File
@@ -23,7 +23,7 @@ android {
minSdk = 30
targetSdk = 36
versionCode = generateVersionCode()
versionName = "3.0.0"
versionName = "3.3.0"
}
buildTypes {
@@ -72,4 +72,5 @@ dependencies {
implementation(libs.material)
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
implementation("androidx.compose.material:material-icons-extended")
implementation("androidx.lifecycle:lifecycle-process:2.8.7")
}
@@ -10,5 +10,6 @@ class CardInfo (
val codeType: BarcodeFormat,
val codeValue: 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.Context
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import androidx.core.database.getStringOrNull
import androidx.core.database.sqlite.transaction
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?) {
db!!.execSQL(
"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) {
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() {
@@ -50,6 +69,8 @@ class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?)
fun addCards(cards: List<CardInfo>) {
val db = this.writableDatabase
db.use { db ->
db.transaction {
for (card in cards) {
val values = ContentValues()
@@ -59,28 +80,22 @@ class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?)
values.put("codeType", card.codeType.name)
values.put("color", card.color)
values.put("icon", card.icon)
values.put("position", card.position)
db.insert("cards", null, values)
}
db.close()
}
}
}
fun getCards(): List<CardInfo> {
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>()
while (cursor.moveToNext()) {
cards.add(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"))
))
cards.add(readCard(cursor))
}
cursor.close()
@@ -101,14 +116,7 @@ class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?)
return null
}
val cardInfo = 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"))
)
val cardInfo = readCard(cursor)
cursor.close()
db.close()
@@ -18,6 +18,7 @@ import androidx.compose.material.icons.outlined.QrCode
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.platform.LocalContext
@@ -89,14 +90,13 @@ fun CardIcon(icon: ImageBitmap?, cardColor: Color) {
cardColor.copy(0.525f),
RoundedCornerShape(12.dp)
)
.padding(8.dp)
.size(32.dp),
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
if (icon == null) {
Icon(
modifier = Modifier.size(24.dp),
modifier = Modifier.size(32.dp),
imageVector = Icons.Outlined.QrCode,
tint = MaterialTheme.colorScheme.onPrimaryContainer,
contentDescription = null
@@ -105,7 +105,7 @@ fun CardIcon(icon: ImageBitmap?, cardColor: Color) {
Image(
bitmap = icon,
contentDescription = null,
modifier = Modifier.size(32.dp)
modifier = Modifier.size(36.dp).clip(RoundedCornerShape(12.dp))
)
}
}
@@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
@@ -22,16 +23,25 @@ import ru.omni_devel.cards.R
class MainActivity : ComponentActivity() {
private lateinit var db: DbHelper
private val cardsState = mutableStateOf<List<CardInfo>>(emptyList())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
db = DbHelper(this, null)
cardsState.value = db.getCards()
setContent {
App(db.getCards())
App(cardsState.value)
}
}
override fun onResume() {
super.onResume()
cardsState.value = db.getCards()
}
}
@SuppressLint("UnusedBoxWithConstraintsScope")
@@ -8,6 +8,8 @@ import com.google.android.gms.wearable.MessageEvent
import com.google.android.gms.wearable.WearableListenerService
import kotlinx.serialization.json.Json
import ru.omni_devel.cards.R
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.Lifecycle
class MessagesReceiver : WearableListenerService() {
private lateinit var db: DbHelper
@@ -24,12 +26,26 @@ class MessagesReceiver : WearableListenerService() {
val json = String(event.data, Charsets.UTF_8)
val cards = Json.decodeFromString<List<CardInfo>>(json)
val oldCards = db.getCards()
if (cards == oldCards) {
return
}
db.clearDatabase()
db.addCards(cards)
val isAppInForeground = ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(
Lifecycle.State.STARTED
)
if (isAppInForeground) {
Handler(Looper.getMainLooper()).post {
Toast.makeText(this, this.getString(R.string.sync_is_successful), 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 {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
@@ -40,3 +56,4 @@ class MessagesReceiver : WearableListenerService() {
}
}
}
}