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