refactor: rewrite mobile app in Compose and add error handling for wear

This commit is contained in:
2026-06-11 17:32:02 +03:00
parent 59207d3351
commit 19a571ac06
40 changed files with 1003 additions and 608 deletions
+7
View File
@@ -19,6 +19,9 @@ activityCompose = "1.13.0"
coreSplashscreen = "1.2.0" coreSplashscreen = "1.2.0"
zxing = "3.5.3" zxing = "3.5.3"
zxingAndroid = "4.3.0" zxingAndroid = "4.3.0"
lifecycleRuntimeKtx = "2.6.1"
navigationFragmentKtx = "2.6.0"
navigationUiKtx = "2.6.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" }
@@ -45,6 +48,10 @@ androidx-activity-compose = { group = "androidx.activity", name = "activity-comp
androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "coreSplashscreen" } androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "coreSplashscreen" }
zxing-core = { group = "com.google.zxing", name = "core", version.ref = "zxing" } zxing-core = { group = "com.google.zxing", name = "core", version.ref = "zxing" }
zxing-android = { group = "com.journeyapps", name = "zxing-android-embedded", version.ref = "zxingAndroid" } zxing-android = { group = "com.journeyapps", name = "zxing-android-embedded", version.ref = "zxingAndroid" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
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" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }
+20 -3
View File
@@ -1,5 +1,6 @@
plugins { plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.compose)
kotlin("plugin.serialization") version "2.2.20" kotlin("plugin.serialization") version "2.2.20"
} }
@@ -15,8 +16,8 @@ android {
applicationId = "ru.omni_devel.cards" applicationId = "ru.omni_devel.cards"
minSdk = 30 minSdk = 30
targetSdk = 36 targetSdk = 36
versionCode = 1 versionCode = 2
versionName = "1.0" versionName = "2.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
@@ -34,18 +35,34 @@ android {
sourceCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11
} }
buildFeatures {
compose = true
viewBinding = true
}
} }
dependencies { dependencies {
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.activity.ktx) implementation(libs.androidx.activity.ktx)
implementation(libs.androidx.appcompat) implementation(libs.androidx.appcompat)
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.constraintlayout) implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.material3)
implementation(libs.androidx.navigation.fragment.ktx)
implementation(libs.androidx.navigation.ui.ktx)
implementation(libs.material) implementation(libs.material)
implementation(libs.play.services.wearable)
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)
debugImplementation(libs.androidx.compose.ui.test.manifest)
debugImplementation(libs.androidx.compose.ui.tooling)
implementation(libs.zxing.core) implementation(libs.zxing.core)
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")
Binary file not shown.
Binary file not shown.
+18 -13
View File
@@ -1,28 +1,33 @@
<?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">
<application <application
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Base.Theme.OmniCards"> android:theme="@style/Theme.OmniCards">
<activity
android:name=".ShowCardActivity"
android:screenOrientation="portrait"
android:exported="false" />
<activity <activity
android:name=".EditCardActivity" android:name=".EditCardActivity"
android:screenOrientation="portrait" android:exported="false"
android:exported="false" /> android:label="@string/title_activity_edit_card"
android:theme="@style/Theme.OmniCards" />
<activity
android:name=".AddCardActivity"
android:exported="false"
android:label="@string/title_activity_add_card"
android:theme="@style/Theme.OmniCards" />
<activity
android:name=".ShowCardActivity"
android:exported="false"
android:label="@string/title_activity_show_card"
android:theme="@style/Theme.OmniCards" />
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:screenOrientation="portrait" android:exported="true"
android:exported="true"> android:label="@string/app_name"
android:theme="@style/Theme.OmniCards">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
@@ -1,124 +1,249 @@
package ru.omni_devel.cards package ru.omni_devel.cards
import android.content.Intent import android.app.Activity
import android.os.Bundle import android.os.Bundle
import android.widget.ArrayAdapter
import android.widget.Button
import android.widget.EditText
import android.widget.Spinner
import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.clickable
import androidx.core.view.ViewCompat import androidx.compose.foundation.layout.Arrangement
import androidx.core.view.WindowInsetsCompat 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.material3.Button
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.IconButton
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.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
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
import ru.omni_devel.cards.ui.theme.OmniCardsTheme
class EditCardActivity : AppCompatActivity() { class EditCardActivity : ComponentActivity() {
private lateinit var db: DbHelper private lateinit var db: DbHelper
private val barcodeLauncher = registerForActivityResult(ScanContract()) { result ->
if (result.contents != null) {
setCodeType(result.formatName)
findViewById<EditText>(R.id.cardValueInput).setText(result.contents)
}
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() enableEdgeToEdge()
setContentView(R.layout.activity_edit_card)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
val actionName = intent.getStringExtra("action") ?: EditCardAction.ADD.name
val action = EditCardAction.valueOf(actionName)
db = DbHelper(this, null) db = DbHelper(this, null)
val actionLabel: TextView = findViewById(R.id.editCardActionLabel)
val cardNameInput: EditText = findViewById(R.id.cardNameInput)
val cardValueInput: EditText = findViewById(R.id.cardValueInput)
val scanCodeButton: Button = findViewById(R.id.scanCodeButton)
val cardCodeTypeSelector: Spinner = findViewById(R.id.cardCodeTypeSelector)
val saveCardButton: Button = findViewById(R.id.saveCardButton)
val options = BarcodeFormat.entries.map { it.name }
val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, options)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
cardCodeTypeSelector.adapter = adapter
scanCodeButton.setOnClickListener {
val options = ScanOptions()
options.setPrompt("Scan code")
options.setBeepEnabled(false)
options.setOrientationLocked(false)
barcodeLauncher.launch(options)
}
saveCardButton.setOnClickListener {
val cardName = cardNameInput.text.toString()
val cardValue = cardValueInput.text.toString()
val cardCodeType = BarcodeFormat.valueOf(cardCodeTypeSelector.selectedItem.toString())
when (action) {
EditCardAction.ADD -> {
db.addCard(cardName, cardValue, cardCodeType)
finish()
}
EditCardAction.EDIT -> {
val cardId = intent.getIntExtra("cardId", -1) val cardId = intent.getIntExtra("cardId", -1)
db.editCard(cardId, cardName, cardValue, cardCodeType)
val intent = Intent(this, ShowCardActivity::class.java)
intent.putExtra("cardId", cardId)
startActivity(intent)
finish()
}
}
}
when (action) {
EditCardAction.ADD -> {
setCodeType("QR_CODE")
actionLabel.text = "Adding card"
}
EditCardAction.EDIT -> {
val cardId = intent.getIntExtra("cardId", -1)
val cardInfo = db.getCard(cardId) val cardInfo = db.getCard(cardId)
if (cardInfo == null) { setContent {
Toast.makeText(this, "cardInfo is null", Toast.LENGTH_LONG).show() OmniCardsTheme {
return Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
} EditCardPage(cardInfo, innerPadding, getDbFun = {
return@EditCardPage db
actionLabel.text = "Editing card" })
cardNameInput.setText(cardInfo.name)
cardValueInput.setText(cardInfo.codeValue)
setCodeType(cardInfo.codeType.name)
} }
} }
} }
}
}
@Composable
fun CardEditable(
cardName: String,
onCardNameChange: (String) -> Unit,
codeValue: String,
oncodeValueChange: (String) -> Unit,
codeType: BarcodeFormat,
onCodeTypeChange: (BarcodeFormat) -> Unit
) {
var isCodeTypeDropdownExpanded by remember { mutableStateOf(false) }
val fieldModifier = Modifier.fillMaxWidth()
val scanLauncher = rememberLauncherForActivityResult(
contract = ScanContract()
) { result ->
if (result.contents != null) {
oncodeValueChange(result.contents)
result.formatName?.let { formatName ->
try {
onCodeTypeChange(BarcodeFormat.valueOf(formatName))
} catch (e: IllegalArgumentException) {}
}
}
}
OutlinedTextField(
value = cardName,
onValueChange = onCardNameChange,
modifier = fieldModifier,
label = { Text("Name") }
)
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(
modifier = fieldModifier,
) {
OutlinedTextField(
value = codeType.name,
onValueChange = {},
modifier = fieldModifier,
label = { Text("Code type") },
readOnly = true,
)
Box(
modifier = fieldModifier
.matchParentSize()
.clickable { isCodeTypeDropdownExpanded = !isCodeTypeDropdownExpanded }
)
DropdownMenu(
expanded = isCodeTypeDropdownExpanded,
onDismissRequest = {
isCodeTypeDropdownExpanded = false
},
modifier = fieldModifier
) {
BarcodeFormat.entries.forEach { format ->
DropdownMenuItem(
text = { Text(format.name) },
onClick = {
onCodeTypeChange(format)
isCodeTypeDropdownExpanded = false
}
)
}
}
}
}
private fun setCodeType(codeTypeString: String) { @Composable
val codeTypes = BarcodeFormat.entries.map { it.name } fun AddCard(innerPadding: PaddingValues, getDbFun: () -> DbHelper) {
val codeTypeIndex = codeTypes.indexOf(codeTypeString) var cardName by remember { mutableStateOf("") }
var codeValue by remember { mutableStateOf("") }
var codeType by remember { mutableStateOf(BarcodeFormat.QR_CODE) }
if (codeTypeIndex >= 0) { val context = LocalContext.current as Activity
findViewById<Spinner>(R.id.cardCodeTypeSelector).setSelection(codeTypeIndex)
Page(
"New card",
innerPadding
) {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
CardEditable(
cardName = cardName,
onCardNameChange = { cardName = it },
codeValue = codeValue,
oncodeValueChange = { codeValue = it },
codeType = codeType,
onCodeTypeChange = { codeType = it }
)
Button(
modifier = Modifier.fillMaxWidth(),
onClick = {
val err = checkCardData(cardName, codeValue)
if (err == null) {
getDbFun().addCard(cardName, codeValue, codeType)
context.finish()
} else {
Toast.makeText(context, err, Toast.LENGTH_SHORT)
}
}
) {
Text("Save")
}
}
}
}
@Composable
fun EditCard(cardInfo: CardInfo, innerPadding: PaddingValues, getDbFun: () -> DbHelper) {
var cardName by remember { mutableStateOf(cardInfo.name) }
var codeValue by remember { mutableStateOf(cardInfo.codeValue) }
var codeType by remember { mutableStateOf(cardInfo.codeType) }
val context = LocalContext.current as Activity
Page(
"Editing card id${cardInfo.id}",
innerPadding
) {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
CardEditable(
cardName = cardName,
onCardNameChange = { cardName = it },
codeValue = codeValue,
oncodeValueChange = { codeValue = it },
codeType = codeType,
onCodeTypeChange = { codeType = it }
)
Button(
modifier = Modifier.fillMaxWidth(),
onClick = {
val err = checkCardData(cardName, codeValue)
if (err == null) {
getDbFun().editCard(cardInfo.id, cardName, codeValue, codeType)
context.finish()
} else {
Toast.makeText(context, err, Toast.LENGTH_SHORT)
}
} }
) {
Text("Save")
}
}
}
}
@Composable
fun EditCardPage(cardInfo: CardInfo?, innerPadding: PaddingValues, getDbFun: () -> DbHelper) {
if (cardInfo == null) {
AddCard(innerPadding, getDbFun)
} else {
EditCard(cardInfo, innerPadding, getDbFun)
} }
} }
@@ -0,0 +1,132 @@
package ru.omni_devel.cards
import android.content.Intent
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.gestures.detectTapGestures
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.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.runtime.setValue
import androidx.compose.ui.input.pointer.pointerInput
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
@Composable
fun Page(name: String, innerPadding: PaddingValues, content: @Composable () -> Unit) {
Column(
modifier = Modifier.padding(innerPadding)
) {
TopBar(name)
Column(
modifier = Modifier.padding(horizontal = 16.dp)
) {
content()
}
}
}
@Composable
fun TopBar(text: String) {
Row(
modifier = Modifier.fillMaxWidth().background(MaterialTheme.colorScheme.primaryContainer).padding(horizontal = 12.dp, vertical = 16.dp)
) {
Text(
text,
fontSize = 24.sp,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
}
}
@Composable
fun CardButton(cardInfo: CardInfo, onDeleteCard: (Int) -> Unit, getDbFun: () -> DbHelper) {
val context = LocalContext.current
var menuExpanded by remember { mutableStateOf(false) }
Box(modifier = Modifier.fillMaxWidth()) {
Surface(
modifier = Modifier
.fillMaxWidth()
.pointerInput(Unit) {
detectTapGestures(
onTap = {
val intent = Intent(context, ShowCardActivity::class.java)
intent.putExtra("cardId", cardInfo.id)
context.startActivity(intent)
},
onLongPress = {
menuExpanded = true
}
)
},
shape = ButtonDefaults.shape,
color = MaterialTheme.colorScheme.primaryContainer,
tonalElevation = 2.dp
) {
Text(
cardInfo.name,
fontSize = 16.sp,
color = MaterialTheme.colorScheme.onPrimaryContainer,
modifier = Modifier
.padding(vertical = 16.dp, horizontal = 16.dp)
.fillMaxWidth(),
textAlign = TextAlign.Start
)
}
DropdownMenu(
expanded = menuExpanded,
onDismissRequest = { menuExpanded = false }
) {
DropdownMenuItem(
text = { Text("Edit") },
onClick = {
menuExpanded = false
val intent = Intent(context, EditCardActivity::class.java)
intent.putExtra("cardId", cardInfo.id)
context.startActivity(intent)
}
)
DropdownMenuItem(
text = { Text("Delete") },
onClick = {
menuExpanded = false
onDeleteCard(cardInfo.id)
}
)
}
}
}
@Composable
fun FullWidthButton(onClick: () -> Unit, content: @Composable () -> Unit) {
Button(
modifier = Modifier.fillMaxWidth(),
onClick = onClick
) {
content()
}
}
@@ -2,55 +2,64 @@ package ru.omni_devel.cards
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.View
import android.widget.Button
import android.widget.LinearLayout
import android.widget.PopupMenu
import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.background
import androidx.core.view.ViewCompat import androidx.compose.foundation.layout.Arrangement
import androidx.core.view.WindowInsetsCompat import androidx.compose.foundation.layout.Box
import com.google.android.material.button.MaterialButton 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.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import ru.omni_devel.cards.ui.theme.OmniCardsTheme
class MainActivity : AppCompatActivity() { class MainActivity : ComponentActivity() {
private lateinit var cardsList: LinearLayout
private lateinit var db: DbHelper private lateinit var db: DbHelper
private val cards = mutableStateListOf<CardInfo>()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() enableEdgeToEdge()
setContentView(R.layout.activity_main)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
db = DbHelper(this, null) db = DbHelper(this, null)
cardsList = findViewById(R.id.cardsList)
val addCardButton: Button = findViewById(R.id.addCardButton) setContent {
val syncWithWearOSButton: Button = findViewById(R.id.syncWithWearOSButton) OmniCardsTheme {
Scaffold(
addCardButton.setOnClickListener { modifier = Modifier.fillMaxSize()
val intent = Intent(this, EditCardActivity::class.java) ) { innerPadding ->
MainPage(innerPadding, cards, onDeleteCard = { cardId ->
intent.putExtra("action", EditCardAction.ADD.name) db.removeCard(cardId)
cards.removeIf { it.id == cardId }
startActivity(intent) }, getDbFun = {
} return@MainPage db
})
syncWithWearOSButton.setOnClickListener {
Toast.makeText(this, "Sync in progress...", Toast.LENGTH_SHORT).show()
sendToWatch(this, "/updateCards", Json.encodeToString(db.getCards())) { isSuccess ->
if (isSuccess) {
Toast.makeText(this, "Successfully synced!", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this, "Sync failed! Please, check your connection", Toast.LENGTH_LONG).show()
} }
} }
} }
@@ -58,83 +67,101 @@ class MainActivity : AppCompatActivity() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
renderCards()
cards.clear()
cards.addAll(db.getCards())
} }
}
private fun renderCards() { @Composable
val cards = db.getCards() fun MainPage(
innerPadding: PaddingValues,
cards: List<CardInfo>,
onDeleteCard: (Int) -> Unit,
getDbFun: () -> DbHelper
) {
val context = LocalContext.current
val uriHandler = LocalUriHandler.current
cardsList.removeAllViews() var isActionBarShowed by remember { mutableStateOf(false) }
Box(
modifier = Modifier.fillMaxSize()
) {
Page(
"OmniCards",
innerPadding
) {
Column(
modifier = Modifier.verticalScroll(rememberScrollState()).padding(bottom = 64.dp).background(Color.Transparent).padding(top = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
if (cards.isEmpty()) { if (cards.isEmpty()) {
val text = TextView(this) Text("Add new card to get started")
val params = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
text.text = "Add a card to get started"
text.layoutParams = params
text.textAlignment = View.TEXT_ALIGNMENT_CENTER
cardsList.addView(text)
} else { } else {
for (card in cards) { for (card in cards) {
val button = MaterialButton(this) CardButton(card, onDeleteCard, getDbFun)
val params = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
params.setMargins(16, 4, 16, 4)
button.text = card.name
button.layoutParams = params
button.cornerRadius = (12 * resources.displayMetrics.density).toInt()
button.setOnClickListener {
val intent = Intent(this, ShowCardActivity::class.java)
intent.putExtra("cardId", card.id)
startActivity(intent)
} }
button.setOnLongClickListener { view ->
val popup = PopupMenu(this, view)
popup.menu.add("Edit")
popup.menu.add("Remove")
popup.setOnMenuItemClickListener { menuItem ->
when (menuItem.title) {
"Edit" -> {
val intent = Intent(this, EditCardActivity::class.java)
intent.putExtra("action", EditCardAction.EDIT.name)
intent.putExtra("cardId", card.id)
startActivity(intent)
true
} }
"Remove" -> {
db.removeCard(card.id)
renderCards()
true
}
else -> false
} }
} }
popup.show() if (isActionBarShowed) {
Column(
modifier = Modifier.align(Alignment.BottomEnd).padding(16.dp).background(MaterialTheme.colorScheme.inversePrimary,
RoundedCornerShape(24.dp)).padding(16.dp).fillMaxWidth()
) {
FullWidthButton(
onClick = {
isActionBarShowed = false
true val intent = Intent(context, EditCardActivity::class.java)
context.startActivity(intent)
}
) {
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()
}
} }
cardsList.addView(button) 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(32.dp),
onClick = {
isActionBarShowed = true
}
) {
Text("<")
} }
} }
} }
@@ -2,35 +2,61 @@ package ru.omni_devel.cards
import android.os.Bundle import android.os.Bundle
import android.view.WindowManager import android.view.WindowManager
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.Image
import androidx.core.view.ViewCompat import androidx.compose.foundation.layout.Arrangement
import androidx.core.view.WindowInsetsCompat 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.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextAlign
import ru.omni_devel.cards.ui.theme.OmniCardsTheme
class ShowCardActivity : AppCompatActivity() { class ShowCardActivity : ComponentActivity() {
private lateinit var db: DbHelper private lateinit var db: DbHelper
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() enableEdgeToEdge()
setContentView(R.layout.activity_show_card)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
db = DbHelper(this, null) db = DbHelper(this, null)
val cardId = intent.getIntExtra("cardId", -1)
val cardInfo = db.getCard(cardId)
if (cardInfo == null) {
Toast.makeText(this, "Card is not exists", Toast.LENGTH_LONG).show()
finish()
return
}
setContent {
OmniCardsTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
ShowCardPage(innerPadding, cardInfo)
}
}
}
} }
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
setBrightness(1.0f) setBrightness(1f)
renderCard()
} }
override fun onPause() { override fun onPause() {
@@ -39,31 +65,46 @@ class ShowCardActivity : AppCompatActivity() {
setBrightness(WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE) setBrightness(WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE)
} }
private fun renderCard() {
val cardId = intent.getIntExtra("cardId", 0)
val cardInfo = db.getCard(cardId)
if (cardInfo == null) {
Toast.makeText(this, "cardInfo is null", Toast.LENGTH_LONG).show()
return
}
val cardCodeView: ImageView = findViewById(R.id.cardCodeView)
val cardNameElement: TextView = findViewById(R.id.editCardActionLabel)
cardNameElement.text = cardInfo.name
val code = generateCode(cardInfo.codeValue, cardInfo.codeType)
cardCodeView.setImageBitmap(code)
}
private fun setBrightness(level: Float) { private fun setBrightness(level: Float) {
val params = window.attributes window.attributes.screenBrightness = level
}
}
params.screenBrightness = level @Composable
fun ShowCardPage(innerPadding: PaddingValues, cardInfo: CardInfo) {
val bitmap = remember(cardInfo) {
try {
generateCode(cardInfo.codeValue, cardInfo.codeType).asImageBitmap()
} catch (_: Exception) {
null
}
}
window.attributes = params Page(cardInfo.name, innerPadding) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center
) {
if (bitmap != null) {
Image(
bitmap = bitmap,
contentDescription = cardInfo.codeValue,
modifier = Modifier.fillMaxWidth(),
contentScale = ContentScale.FillWidth
)
Text(
cardInfo.codeValue,
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center
)
} else {
Text(
"Failed to generate code. Check card data",
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.error
)
}
}
} }
} }
@@ -2,7 +2,6 @@ package ru.omni_devel.cards
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.widget.Toast
import com.google.android.gms.wearable.Wearable import com.google.android.gms.wearable.Wearable
import com.google.zxing.BarcodeFormat import com.google.zxing.BarcodeFormat
import com.google.zxing.MultiFormatWriter import com.google.zxing.MultiFormatWriter
@@ -10,16 +9,14 @@ import com.journeyapps.barcodescanner.BarcodeEncoder
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, 600, 300) val matrix = writer.encode(value, format, 800, 400)
return BarcodeEncoder().createBitmap(matrix) return BarcodeEncoder().createBitmap(matrix)
} }
fun sendToWatch(context: Context, path: String, message: String, onResult: (Boolean) -> Unit): Boolean { fun sendToWatch(context: Context, path: String, message: String, onResult: (Boolean) -> Unit) {
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) onResult(false)
@@ -35,7 +32,7 @@ fun sendToWatch(context: Context, path: String, message: String, onResult: (Bool
node.id, node.id,
path, path,
message.toByteArray(Charsets.UTF_8) message.toByteArray(Charsets.UTF_8)
).addOnCompleteListener { task -> ).addOnCompleteListener(context.mainExecutor) { task ->
completedCount++ completedCount++
if (task.isSuccessful) { if (task.isSuccessful) {
@@ -45,11 +42,19 @@ fun sendToWatch(context: Context, path: String, message: String, onResult: (Bool
if (completedCount == nodes.size) { if (completedCount == nodes.size) {
onResult(isAnySuccess) onResult(isAnySuccess)
} }
}.addOnFailureListener { }.addOnFailureListener(context.mainExecutor) {
onResult(false) onResult(false)
} }
} }
} }
}
return isSuccess fun checkCardData(name: String, value: String): String? {
if (name.trim().isEmpty()) {
return "Name should not be empty"
} else if (value.trim().isEmpty()) {
return "Code value should not be empty"
}
return null
} }
@@ -0,0 +1,11 @@
package ru.omni_devel.cards.ui.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)
@@ -0,0 +1,58 @@
package ru.omni_devel.cards.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
@Composable
fun OmniCardsTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
@@ -0,0 +1,34 @@
package ru.omni_devel.cards.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)
@@ -1,87 +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"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".EditCardActivity">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
android:minHeight="?attr/actionBarSize"
android:theme="?attr/actionBarTheme"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/editCardActionLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="Action"
android:textColor="@color/text_primary"
android:textSize="24sp"
app:layout_constraintBottom_toBottomOf="@+id/toolbar2"
app:layout_constraintStart_toStartOf="@+id/toolbar2"
app:layout_constraintTop_toTopOf="@+id/toolbar2" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar2"
app:layout_constraintVertical_bias="0.0">
<EditText
android:id="@+id/cardNameInput"
android:layout_width="match_parent"
android:layout_height="46dp"
android:ems="10"
android:hint="Card name"
android:inputType="text" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<EditText
android:id="@+id/cardValueInput"
android:layout_width="259dp"
android:layout_height="46dp"
android:ems="10"
android:hint="Card value"
android:inputType="text" />
<Button
android:id="@+id/scanCodeButton"
android:layout_width="match_parent"
android:layout_height="38dp"
android:text="Scan" />
</LinearLayout>
<Spinner
android:id="@+id/cardCodeTypeSelector"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:id="@+id/saveCardButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Save" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -1,73 +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"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
android:minHeight="?attr/actionBarSize"
android:theme="?attr/actionBarTheme"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="OmniCards"
android:textColor="@color/text_primary"
android:textSize="24sp"
app:layout_constraintBottom_toBottomOf="@+id/toolbar"
app:layout_constraintStart_toStartOf="@+id/toolbar"
app:layout_constraintTop_toTopOf="@+id/toolbar" />
<ScrollView
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar">
<LinearLayout
android:id="@+id/cardsList"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" />
</ScrollView>
<Button
android:id="@+id/addCardButton"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:text="+"
android:textSize="24sp"
app:cornerRadius="12dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<Button
android:id="@+id/syncWithWearOSButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:backgroundTint="#0E000000"
android:text="Sync with WearOS"
app:cornerRadius="12dp"
app:layout_constraintBottom_toBottomOf="@+id/toolbar"
app:layout_constraintEnd_toEndOf="@+id/toolbar"
app:layout_constraintTop_toTopOf="@+id/toolbar" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -1,43 +1,36 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main" android:id="@+id/main"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".EditCardActivity"> android:fitsSystemWindows="true"
tools:context=".ShowCardActivity">
<androidx.appcompat.widget.Toolbar <com.google.android.material.appbar.AppBarLayout
android:id="@+id/toolbar2"
android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?attr/colorPrimary" android:layout_width="match_parent"
android:minHeight="?attr/actionBarSize" android:fitsSystemWindows="true"
android:theme="?attr/actionBarTheme" >
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/editCardActionLabel" 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_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_gravity="bottom|end"
android:text="Card name" android:layout_marginEnd="@dimen/fab_margin"
android:textColor="@color/text_primary" android:layout_marginBottom="16dp"
android:textSize="24sp" app:srcCompat="@android:drawable/ic_dialog_email" />
app:layout_constraintBottom_toBottomOf="@+id/toolbar2"
app:layout_constraintStart_toStartOf="@+id/toolbar2"
app:layout_constraintTop_toTopOf="@+id/toolbar2" />
<ImageView </androidx.coordinatorlayout.widget.CoordinatorLayout>
android:id="@+id/cardCodeView"
android:layout_width="match_parent"
android:layout_height="300dp"
android:scaleType="fitCenter"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar2" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,20 @@
<?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>
@@ -0,0 +1,34 @@
<?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>
@@ -0,0 +1,34 @@
<?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>
@@ -1,18 +1,4 @@
<!-- <?xml version="1.0" encoding="utf-8"?>
~ Copyright (C) 2026 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/> <background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/> <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
@@ -1,18 +1,4 @@
<!-- <?xml version="1.0" encoding="utf-8"?>
~ Copyright (C) 2026 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/> <background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/> <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation 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/nav_graph"
app:startDestination="@id/FirstFragment">
<fragment
android:id="@+id/FirstFragment"
android:name="ru.omni_devel.cards.FirstFragment"
android:label="@string/first_fragment_label"
tools:layout="@layout/fragment_first" >
<action
android:id="@+id/action_FirstFragment_to_SecondFragment"
app:destination="@id/SecondFragment" />
</fragment>
<fragment
android:id="@+id/SecondFragment"
android:name="ru.omni_devel.cards.SecondFragment"
android:label="@string/second_fragment_label"
tools:layout="@layout/fragment_second" >
<action
android:id="@+id/action_SecondFragment_to_FirstFragment"
app:destination="@id/FirstFragment" />
</fragment>
</navigation>
@@ -0,0 +1,3 @@
<resources>
<dimen name="fab_margin">48dp</dimen>
</resources>
@@ -0,0 +1,7 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.OmniCards" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your dark theme here. -->
<!-- <item name="colorPrimary">@color/my_dark_primary</item> -->
</style>
</resources>
@@ -0,0 +1,8 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="Theme.OmniCards" parent="Base.Theme.OmniCards">
<!-- Transparent system bars for edge-to-edge. -->
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:windowLightStatusBar">?attr/isLightTheme</item>
</style>
</resources>
@@ -0,0 +1,3 @@
<resources>
<dimen name="fab_margin">200dp</dimen>
</resources>
@@ -0,0 +1,3 @@
<resources>
<dimen name="fab_margin">48dp</dimen>
</resources>
+7 -30
View File
@@ -1,33 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<!-- Backgrounds --> <color name="purple_200">#FFBB86FC</color>
<color name="bg_app">#0B0E17</color> <color name="purple_500">#FF6200EE</color>
<color name="bg_surface">#0E1118</color> <color name="purple_700">#FF3700B3</color>
<color name="bg_card">#131726</color> <color name="teal_200">#FF03DAC5</color>
<color name="bg_elevated">#1A2235</color> <color name="teal_700">#FF018786</color>
<color name="bg_hover">#212B42</color> <color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<!-- Accent (indigo) -->
<color name="accent_subtle">#152045</color>
<color name="accent_primary">#2952C8</color>
<color name="accent_hover">#3D6AE8</color>
<color name="accent_light">#6B90FF</color>
<!-- Text -->
<color name="text_primary">#DCE3F5</color>
<color name="text_secondary">#8A9BBC</color>
<color name="text_hint">#4E5E80</color>
<!-- Borders -->
<color name="border_subtle">#1C2030</color>
<color name="border_default">#1C2A42</color>
<color name="border_active">#2A3D60</color>
<!-- Semantic -->
<color name="success_bg">#0F3320</color>
<color name="success">#2ECC71</color>
<color name="error_bg">#3D1515</color>
<color name="error">#E05555</color>
<color name="warning_bg">#3D2E00</color>
<color name="warning">#F0A500</color>
</resources> </resources>
+3
View File
@@ -0,0 +1,3 @@
<resources>
<dimen name="fab_margin">16dp</dimen>
</resources>
+45
View File
@@ -1,3 +1,48 @@
<resources> <resources>
<string name="app_name">OmniCards</string> <string name="app_name">OmniCards</string>
<!-- Strings used for fragments for navigation -->
<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">
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
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
diam, vel rutrum tellus vulputate quis. Aliquam eget libero aliquet, imperdiet nisl a,
ornare ex. Sed rhoncus est ut libero porta lobortis. Fusce in dictum tellus.\n\n
Suspendisse interdum ornare ante. Aliquam nec cursus lorem. Morbi id magna felis. Vivamus
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
fames ac ante ipsum primis in faucibus. Curabitur et eros porttitor, ultricies urna vitae,
molestie nibh. Phasellus at commodo eros, non aliquet metus. Sed maximus nisl nec dolor
bibendum, vel congue leo egestas.\n\n
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,
molestie quam. Fusce blandit tincidunt nulla, quis sollicitudin libero facilisis et. Integer
interdum nunc ligula, et fermentum metus hendrerit id. Vestibulum lectus felis, dictum at
lacinia sit amet, tristique id quam. Cras eu consequat dui. Suspendisse sodales nunc ligula,
in lobortis sem porta sed. Integer id ultrices magna, in luctus elit. Sed a pellentesque
est.\n\n
Aenean nunc velit, lacinia sed dolor sed, ultrices viverra nulla. Etiam a venenatis nibh.
Morbi laoreet, tortor sed facilisis varius, nibh orci rhoncus nulla, id elementum leo dui
non lorem. Nam mollis ipsum quis auctor varius. Quisque elementum eu libero sed commodo. In
eros nisl, imperdiet vel imperdiet et, scelerisque a mauris. Pellentesque varius ex nunc,
quis imperdiet eros placerat ac. Duis finibus orci et est auctor tincidunt. Sed non viverra
ipsum. Nunc quis augue egestas, cursus lorem at, molestie sem. Morbi a consectetur ipsum, a
placerat diam. Etiam vulputate dignissim convallis. Integer faucibus mauris sit amet finibus
convallis.\n\n
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
gravida. Pellentesque id tellus orci. Integer dictum, lorem sed efficitur ullamcorper,
libero justo consectetur ipsum, in mollis nisl ex sed nisl. Donec maximus ullamcorper
sodales. Praesent bibendum rhoncus tellus nec feugiat. In a ornare nulla. Donec rhoncus
libero vel nunc consequat, quis tincidunt nisl eleifend. Cras bibendum enim a justo luctus
vestibulum. Fusce dictum libero quis erat maximus, vitae volutpat diam dignissim.
</string>
<string name="title_activity_show_card">ShowCardActivity</string>
<string name="title_activity_add_card">AddCardActivity</string>
<string name="title_activity_edit_card">EditCardActivity</string>
</resources> </resources>
-10
View File
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="CardButton" parent="Widget.Material3.Button">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">64dp</item>
<item name="android:textAllCaps">false</item>
<item name="android:textSize">16sp</item>
<item name="cornerRadius">8dp</item>
</style>
</resources>
+7 -26
View File
@@ -1,29 +1,10 @@
<resources xmlns:tools="http://schemas.android.com/tools"> <?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.OmniCards" parent="android:Theme.Material.Light.NoActionBar" />
<!-- Base application theme. -->
<style name="Base.Theme.OmniCards" parent="Theme.Material3.DayNight.NoActionBar"> <style name="Base.Theme.OmniCards" parent="Theme.Material3.DayNight.NoActionBar">
<item name="colorPrimary">@color/accent_primary</item> <!-- Customize your light theme here. -->
<item name="colorOnPrimary">@color/text_primary</item> <!-- <item name="colorPrimary">@color/my_light_primary</item> -->
<item name="colorPrimaryContainer">@color/accent_subtle</item>
<item name="colorOnPrimaryContainer">@color/accent_light</item>
<item name="colorSurface">@color/bg_card</item>
<item name="colorOnSurface">@color/text_primary</item>
<item name="colorSurfaceVariant">@color/bg_elevated</item>
<item name="colorOnSurfaceVariant">@color/text_secondary</item>
<item name="colorOutline">@color/border_default</item>
<item name="colorOutlineVariant">@color/border_subtle</item>
<item name="colorError">@color/error</item>
<item name="colorOnError">@color/text_primary</item>
<item name="colorErrorContainer">@color/error_bg</item>
<item name="android:windowBackground">@color/bg_app</item>
<item name="android:textColorPrimary">@color/text_primary</item>
<item name="android:textColorSecondary">@color/text_secondary</item>
<item name="android:textColorHint">@color/text_hint</item>
<item name="android:colorBackground">@color/bg_app</item>
<item name="android:statusBarColor">@color/bg_app</item>
<item name="android:navigationBarColor">@color/bg_app</item>
</style> </style>
</resources> </resources>
-13
View File
@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older than API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>
@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>
+1 -2
View File
@@ -23,6 +23,5 @@ dependencyResolutionManagement {
} }
rootProject.name = "OmniCards" rootProject.name = "OmniCards"
include(":mobile")
include(":wear") include(":wear")
include(":mobile")
+2 -2
View File
@@ -16,8 +16,8 @@ android {
applicationId = "ru.omni_devel.cards" applicationId = "ru.omni_devel.cards"
minSdk = 30 minSdk = 30
targetSdk = 36 targetSdk = 36
versionCode = 1 versionCode = 2
versionName = "1.0" versionName = "1.1"
} }
Binary file not shown.
Binary file not shown.
@@ -12,11 +12,21 @@ 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.runtime.Composable import androidx.compose.runtime.Composable
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.text.style.TextAlign
import androidx.wear.compose.material3.MaterialTheme
import androidx.wear.compose.material3.Text
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
@@ -43,7 +53,7 @@ class ShowCardActivity : ComponentActivity() {
} }
setContent { setContent {
CardView(cardInfo) ShowCardPage(cardInfo)
} }
} }
@@ -69,25 +79,40 @@ class ShowCardActivity : ComponentActivity() {
} }
@Composable @Composable
fun CardView(cardInfo: CardInfo) { fun ShowCardPage(cardInfo: CardInfo) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
BoxWithConstraints( BoxWithConstraints(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center
) { ) {
val density = LocalDensity.current val screenSize = minOf(maxWidth, maxHeight)
val safeSize = (screenSize * 0.65f)
val sizePx = with(LocalDensity.current) { safeSize.roundToPx() }
val widthPx = with(density) { maxWidth.roundToPx() } val bitmap = remember(cardInfo, sizePx) {
val heightPx = with(density) { maxHeight.roundToPx() } try {
generateCode(cardInfo.codeValue, cardInfo.codeType, sizePx, sizePx).asImageBitmap()
} catch (_: Exception) {
null
}
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
if (bitmap != null) {
Image( Image(
bitmap = generateCode(cardInfo.codeValue, cardInfo.codeType, widthPx, heightPx).asImageBitmap(), bitmap = bitmap,
contentDescription = "Card code" contentDescription = cardInfo.codeValue,
modifier = Modifier.size(safeSize)
)
} else {
Text(
"Failed to generate code",
color = MaterialTheme.colorScheme.error,
textAlign = TextAlign.Center
) )
} }
} }
}
} }