diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 63d77b2..04ab44d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,6 +19,9 @@ activityCompose = "1.13.0" coreSplashscreen = "1.2.0" zxing = "3.5.3" zxingAndroid = "4.3.0" +lifecycleRuntimeKtx = "2.6.1" +navigationFragmentKtx = "2.6.0" +navigationUiKtx = "2.6.0" [libraries] 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" } zxing-core = { group = "com.google.zxing", name = "core", version.ref = "zxing" } 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] android-application = { id = "com.android.application", version.ref = "agp" } diff --git a/mobile/build.gradle.kts b/mobile/build.gradle.kts index 99f9ba8..a90ca7b 100644 --- a/mobile/build.gradle.kts +++ b/mobile/build.gradle.kts @@ -1,5 +1,6 @@ plugins { alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.compose) kotlin("plugin.serialization") version "2.2.20" } @@ -15,8 +16,8 @@ android { applicationId = "ru.omni_devel.cards" minSdk = 30 targetSdk = 36 - versionCode = 1 - versionName = "1.0" + versionCode = 2 + versionName = "2.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } @@ -34,18 +35,34 @@ android { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } + buildFeatures { + compose = true + viewBinding = true + } } dependencies { + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.ktx) 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.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.play.services.wearable) testImplementation(libs.junit) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.compose.ui.test.junit4) androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.junit) + debugImplementation(libs.androidx.compose.ui.test.manifest) + debugImplementation(libs.androidx.compose.ui.tooling) implementation(libs.zxing.core) implementation(libs.zxing.android) implementation("com.google.android.gms:play-services-wearable:18.1.0") diff --git a/mobile/release/baselineProfiles/0/mobile-release.dm b/mobile/release/baselineProfiles/0/mobile-release.dm index 3110a02..c3ca3b6 100644 Binary files a/mobile/release/baselineProfiles/0/mobile-release.dm and b/mobile/release/baselineProfiles/0/mobile-release.dm differ diff --git a/mobile/release/baselineProfiles/1/mobile-release.dm b/mobile/release/baselineProfiles/1/mobile-release.dm index 91c7ea9..8cdbfe8 100644 Binary files a/mobile/release/baselineProfiles/1/mobile-release.dm and b/mobile/release/baselineProfiles/1/mobile-release.dm differ diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml index 62bae62..1c583b2 100644 --- a/mobile/src/main/AndroidManifest.xml +++ b/mobile/src/main/AndroidManifest.xml @@ -1,28 +1,33 @@ - + - + android:theme="@style/Theme.OmniCards"> + android:exported="false" + android:label="@string/title_activity_edit_card" + android:theme="@style/Theme.OmniCards" /> + + + android:exported="true" + android:label="@string/app_name" + android:theme="@style/Theme.OmniCards"> diff --git a/mobile/src/main/java/ru/omni_devel/cards/CardInfo.kt b/mobile/src/main/java/ru/omni_devel/cards/CardInfo.kt index a7e1a9c..b6ad1ec 100644 --- a/mobile/src/main/java/ru/omni_devel/cards/CardInfo.kt +++ b/mobile/src/main/java/ru/omni_devel/cards/CardInfo.kt @@ -14,4 +14,4 @@ class CardInfo ( val name: String, val codeType: BarcodeFormat, val codeValue: String, -) \ No newline at end of file +) diff --git a/mobile/src/main/java/ru/omni_devel/cards/EditCardActivity.kt b/mobile/src/main/java/ru/omni_devel/cards/EditCardActivity.kt index 702bb16..982f7d6 100644 --- a/mobile/src/main/java/ru/omni_devel/cards/EditCardActivity.kt +++ b/mobile/src/main/java/ru/omni_devel/cards/EditCardActivity.kt @@ -1,124 +1,249 @@ package ru.omni_devel.cards -import android.content.Intent +import android.app.Activity 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 androidx.activity.ComponentActivity +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat +import androidx.compose.foundation.clickable +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.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.journeyapps.barcodescanner.ScanContract 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 val barcodeLauncher = registerForActivityResult(ScanContract()) { result -> - if (result.contents != null) { - setCodeType(result.formatName) - - findViewById(R.id.cardValueInput).setText(result.contents) - } - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) 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) - 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 cardId = intent.getIntExtra("cardId", -1) + val cardInfo = db.getCard(cardId) - 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) - - db.editCard(cardId, cardName, cardValue, cardCodeType) - - val intent = Intent(this, ShowCardActivity::class.java) - - intent.putExtra("cardId", cardId) - - startActivity(intent) - finish() + setContent { + OmniCardsTheme { + Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> + EditCardPage(cardInfo, innerPadding, getDbFun = { + return@EditCardPage db + }) } } } + } +} - when (action) { - EditCardAction.ADD -> { - setCodeType("QR_CODE") +@Composable +fun CardEditable( + cardName: String, + onCardNameChange: (String) -> Unit, + codeValue: String, + oncodeValueChange: (String) -> Unit, + codeType: BarcodeFormat, + onCodeTypeChange: (BarcodeFormat) -> Unit +) { + var isCodeTypeDropdownExpanded by remember { mutableStateOf(false) } - actionLabel.text = "Adding card" - } - EditCardAction.EDIT -> { - val cardId = intent.getIntExtra("cardId", -1) + val fieldModifier = Modifier.fillMaxWidth() - val cardInfo = db.getCard(cardId) + val scanLauncher = rememberLauncherForActivityResult( + contract = ScanContract() + ) { result -> + if (result.contents != null) { + oncodeValueChange(result.contents) - if (cardInfo == null) { - Toast.makeText(this, "cardInfo is null", Toast.LENGTH_LONG).show() - return - } - - actionLabel.text = "Editing card" - - cardNameInput.setText(cardInfo.name) - cardValueInput.setText(cardInfo.codeValue) - setCodeType(cardInfo.codeType.name) + result.formatName?.let { formatName -> + try { + onCodeTypeChange(BarcodeFormat.valueOf(formatName)) + } catch (e: IllegalArgumentException) {} } } } - private fun setCodeType(codeTypeString: String) { - val codeTypes = BarcodeFormat.entries.map { it.name } - val codeTypeIndex = codeTypes.indexOf(codeTypeString) - - if (codeTypeIndex >= 0) { - findViewById(R.id.cardCodeTypeSelector).setSelection(codeTypeIndex) + 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") } } -} \ No newline at end of file + + 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 + } + ) + } + } + } +} + +@Composable +fun AddCard(innerPadding: PaddingValues, getDbFun: () -> DbHelper) { + var cardName by remember { mutableStateOf("") } + var codeValue by remember { mutableStateOf("") } + var codeType by remember { mutableStateOf(BarcodeFormat.QR_CODE) } + + val context = LocalContext.current as Activity + + 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) + } +} diff --git a/mobile/src/main/java/ru/omni_devel/cards/Elements.kt b/mobile/src/main/java/ru/omni_devel/cards/Elements.kt new file mode 100644 index 0000000..1dfbf40 --- /dev/null +++ b/mobile/src/main/java/ru/omni_devel/cards/Elements.kt @@ -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() + } +} diff --git a/mobile/src/main/java/ru/omni_devel/cards/MainActivity.kt b/mobile/src/main/java/ru/omni_devel/cards/MainActivity.kt index 94fd7ca..f715259 100644 --- a/mobile/src/main/java/ru/omni_devel/cards/MainActivity.kt +++ b/mobile/src/main/java/ru/omni_devel/cards/MainActivity.kt @@ -2,55 +2,64 @@ package ru.omni_devel.cards import android.content.Intent 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 androidx.activity.ComponentActivity +import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat -import com.google.android.material.button.MaterialButton +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.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 ru.omni_devel.cards.ui.theme.OmniCardsTheme -class MainActivity : AppCompatActivity() { - private lateinit var cardsList: LinearLayout +class MainActivity : ComponentActivity() { private lateinit var db: DbHelper + private val cards = mutableStateListOf() + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) 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) - cardsList = findViewById(R.id.cardsList) - val addCardButton: Button = findViewById(R.id.addCardButton) - val syncWithWearOSButton: Button = findViewById(R.id.syncWithWearOSButton) - - addCardButton.setOnClickListener { - val intent = Intent(this, EditCardActivity::class.java) - - intent.putExtra("action", EditCardAction.ADD.name) - - startActivity(intent) - } - - 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() + setContent { + OmniCardsTheme { + Scaffold( + modifier = Modifier.fillMaxSize() + ) { innerPadding -> + MainPage(innerPadding, cards, onDeleteCard = { cardId -> + db.removeCard(cardId) + cards.removeIf { it.id == cardId } + }, getDbFun = { + return@MainPage db + }) } } } @@ -58,84 +67,102 @@ class MainActivity : AppCompatActivity() { override fun onResume() { super.onResume() - renderCards() + + cards.clear() + cards.addAll(db.getCards()) } +} - private fun renderCards() { - val cards = db.getCards() +@Composable +fun MainPage( + innerPadding: PaddingValues, + cards: List, + onDeleteCard: (Int) -> Unit, + getDbFun: () -> DbHelper +) { + val context = LocalContext.current + val uriHandler = LocalUriHandler.current - cardsList.removeAllViews() + var isActionBarShowed by remember { mutableStateOf(false) } - if (cards.isEmpty()) { - val text = TextView(this) - - 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 { - for (card in cards) { - val button = MaterialButton(this) - - 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 - } + 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()) { + Text("Add new card to get started") + } else { + for (card in cards) { + CardButton(card, onDeleteCard, getDbFun) } - - popup.show() - - true } + } + } - cardsList.addView(button) + 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 + + 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() + } + } + + 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("<") } } } -} \ No newline at end of file +} diff --git a/mobile/src/main/java/ru/omni_devel/cards/ShowCardActivity.kt b/mobile/src/main/java/ru/omni_devel/cards/ShowCardActivity.kt index 1eb2d67..75caeb3 100644 --- a/mobile/src/main/java/ru/omni_devel/cards/ShowCardActivity.kt +++ b/mobile/src/main/java/ru/omni_devel/cards/ShowCardActivity.kt @@ -2,35 +2,61 @@ package ru.omni_devel.cards import android.os.Bundle import android.view.WindowManager -import android.widget.ImageView -import android.widget.TextView import android.widget.Toast +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat +import androidx.compose.foundation.Image +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.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 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) 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) + + 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() { super.onResume() - setBrightness(1.0f) - renderCard() + setBrightness(1f) } override fun onPause() { @@ -39,31 +65,46 @@ class ShowCardActivity : AppCompatActivity() { 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) { - val params = window.attributes - - params.screenBrightness = level - - window.attributes = params + window.attributes.screenBrightness = level } -} \ No newline at end of file +} + +@Composable +fun ShowCardPage(innerPadding: PaddingValues, cardInfo: CardInfo) { + val bitmap = remember(cardInfo) { + try { + generateCode(cardInfo.codeValue, cardInfo.codeType).asImageBitmap() + } catch (_: Exception) { + null + } + } + + 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 + ) + } + } + } +} diff --git a/mobile/src/main/java/ru/omni_devel/cards/Utils.kt b/mobile/src/main/java/ru/omni_devel/cards/Utils.kt index 0cc683f..3ce1351 100644 --- a/mobile/src/main/java/ru/omni_devel/cards/Utils.kt +++ b/mobile/src/main/java/ru/omni_devel/cards/Utils.kt @@ -2,7 +2,6 @@ package ru.omni_devel.cards import android.content.Context import android.graphics.Bitmap -import android.widget.Toast import com.google.android.gms.wearable.Wearable import com.google.zxing.BarcodeFormat import com.google.zxing.MultiFormatWriter @@ -10,16 +9,14 @@ import com.journeyapps.barcodescanner.BarcodeEncoder fun generateCode(value: String, format: BarcodeFormat): Bitmap { val writer = MultiFormatWriter() - val matrix = writer.encode(value, format, 600, 300) + val matrix = writer.encode(value, format, 800, 400) 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) - var isSuccess = false - nodeClient.connectedNodes.addOnSuccessListener { nodes -> if (nodes.isEmpty()) { onResult(false) @@ -35,7 +32,7 @@ fun sendToWatch(context: Context, path: String, message: String, onResult: (Bool node.id, path, message.toByteArray(Charsets.UTF_8) - ).addOnCompleteListener { task -> + ).addOnCompleteListener(context.mainExecutor) { task -> completedCount++ if (task.isSuccessful) { @@ -45,11 +42,19 @@ fun sendToWatch(context: Context, path: String, message: String, onResult: (Bool if (completedCount == nodes.size) { onResult(isAnySuccess) } - }.addOnFailureListener { + }.addOnFailureListener(context.mainExecutor) { 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 } diff --git a/mobile/src/main/java/ru/omni_devel/cards/ui/theme/Color.kt b/mobile/src/main/java/ru/omni_devel/cards/ui/theme/Color.kt new file mode 100644 index 0000000..326e98f --- /dev/null +++ b/mobile/src/main/java/ru/omni_devel/cards/ui/theme/Color.kt @@ -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) \ No newline at end of file diff --git a/mobile/src/main/java/ru/omni_devel/cards/ui/theme/Theme.kt b/mobile/src/main/java/ru/omni_devel/cards/ui/theme/Theme.kt new file mode 100644 index 0000000..94aca9c --- /dev/null +++ b/mobile/src/main/java/ru/omni_devel/cards/ui/theme/Theme.kt @@ -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 + ) +} \ No newline at end of file diff --git a/mobile/src/main/java/ru/omni_devel/cards/ui/theme/Type.kt b/mobile/src/main/java/ru/omni_devel/cards/ui/theme/Type.kt new file mode 100644 index 0000000..dc47ec7 --- /dev/null +++ b/mobile/src/main/java/ru/omni_devel/cards/ui/theme/Type.kt @@ -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 + ) + */ +) \ No newline at end of file diff --git a/mobile/src/main/res/layout/activity_edit_card.xml b/mobile/src/main/res/layout/activity_edit_card.xml deleted file mode 100644 index bbefaf4..0000000 --- a/mobile/src/main/res/layout/activity_edit_card.xml +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - -