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
@@ -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<CardInfo>()
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<CardInfo>,
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("<")
}
}
}
}
}