From 4748a46156c7dff2ced1b66df59849846a30964d Mon Sep 17 00:00:00 2001 From: Omni Devel Date: Wed, 24 Jun 2026 22:05:03 +0300 Subject: [PATCH] feat: added cards reordering feature --- mobile/build.gradle.kts | 1 + .../main/java/ru/omni_devel/cards/CardInfo.kt | 3 +- .../main/java/ru/omni_devel/cards/DbHelper.kt | 92 +++++++++++++------ .../main/java/ru/omni_devel/cards/Elements.kt | 35 ++++--- .../java/ru/omni_devel/cards/MainActivity.kt | 80 ++++++++++++++-- .../omni_devel/cards/presentation/CardInfo.kt | 3 +- .../omni_devel/cards/presentation/DbHelper.kt | 66 +++++++------ 7 files changed, 197 insertions(+), 83 deletions(-) diff --git a/mobile/build.gradle.kts b/mobile/build.gradle.kts index de017bd..fdd6bed 100644 --- a/mobile/build.gradle.kts +++ b/mobile/build.gradle.kts @@ -81,4 +81,5 @@ dependencies { implementation("io.ktor:ktor-client-core:3.5.0") implementation("io.ktor:ktor-client-cio:3.5.0") implementation("androidx.compose.material:material-icons-extended") + implementation("sh.calvin.reorderable:reorderable:2.4.3") } \ No newline at end of file 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 5889def..d48a8f3 100644 --- a/mobile/src/main/java/ru/omni_devel/cards/CardInfo.kt +++ b/mobile/src/main/java/ru/omni_devel/cards/CardInfo.kt @@ -15,5 +15,6 @@ class CardInfo ( val codeType: BarcodeFormat, val codeValue: String, val color: String?, - val icon: String? + val icon: String?, + val position: Int ) {} diff --git a/mobile/src/main/java/ru/omni_devel/cards/DbHelper.kt b/mobile/src/main/java/ru/omni_devel/cards/DbHelper.kt index 657c0f0..cf6dea5 100644 --- a/mobile/src/main/java/ru/omni_devel/cards/DbHelper.kt +++ b/mobile/src/main/java/ru/omni_devel/cards/DbHelper.kt @@ -2,15 +2,17 @@ package ru.omni_devel.cards import android.content.ContentValues import android.content.Context +import android.database.Cursor import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteOpenHelper import androidx.core.database.getStringOrNull +import androidx.core.database.sqlite.transaction import com.google.zxing.BarcodeFormat -class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?) : SQLiteOpenHelper(context, "omni_cards", factory, 6) { +class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?) : SQLiteOpenHelper(context, "omni_cards", factory, 8) { override fun onCreate(db: SQLiteDatabase?) { db!!.execSQL( - "CREATE TABLE IF NOT EXISTS cards (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, codeType TEXT, codeValue TEXT, color TEXT, icon TEXT)" + "CREATE TABLE IF NOT EXISTS cards (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, codeType TEXT, codeValue TEXT, color TEXT, icon TEXT, position DEFAULT 0)" ) } @@ -42,6 +44,32 @@ class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?) if (oldVersion < 6) { db!!.execSQL("ALTER TABLE cards ADD COLUMN icon TEXT") } + + if (oldVersion < 7) { + db!!.execSQL("ALTER TABLE cards ADD COLUMN position INTEGER DEFAULT 0") + db.execSQL("UPDATE cards SET position = id") + } + } + + private fun readCard(cursor: Cursor): CardInfo { + return CardInfo( + id = cursor.getInt(cursor.getColumnIndexOrThrow("id")), + name = cursor.getString(cursor.getColumnIndexOrThrow("name")), + codeType = BarcodeFormat.valueOf(cursor.getString(cursor.getColumnIndexOrThrow("codeType"))), + codeValue = cursor.getString(cursor.getColumnIndexOrThrow("codeValue")), + color = cursor.getString(cursor.getColumnIndexOrThrow("color")), + icon = cursor.getStringOrNull(cursor.getColumnIndexOrThrow("icon")), + position = cursor.getInt(cursor.getColumnIndexOrThrow("position")) + ) + } + + private fun nextPosition(db: SQLiteDatabase): Int { + val cursor = db.rawQuery("SELECT COALESCE(MAX(position), -1) + 1 FROM cards", null) + cursor.moveToFirst() + val next = cursor.getInt(0) + cursor.close() + + return next } fun addCard(name: String, codeValue: String, codeType: BarcodeFormat, color: String?, icon: String?) { @@ -54,6 +82,7 @@ class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?) values.put("codeType", codeType.name) values.put("color", color) values.put("icon", icon) + values.put("position", nextPosition(db)) db.insert("cards", null, values) @@ -79,18 +108,11 @@ class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?) fun getCards(): List { val db = this.readableDatabase - val cursor = db.rawQuery("SELECT * FROM cards ORDER BY id ASC", null) + val cursor = db.rawQuery("SELECT * FROM cards ORDER BY position ASC", null) val cards = mutableListOf() while (cursor.moveToNext()) { - cards.add(CardInfo( - id = cursor.getInt(cursor.getColumnIndexOrThrow("id")), - name = cursor.getString(cursor.getColumnIndexOrThrow("name")), - codeType = BarcodeFormat.valueOf(cursor.getString(cursor.getColumnIndexOrThrow("codeType"))), - codeValue = cursor.getString(cursor.getColumnIndexOrThrow("codeValue")), - color = cursor.getString(cursor.getColumnIndexOrThrow("color")), - icon = cursor.getStringOrNull(cursor.getColumnIndexOrThrow("icon")) - )) + cards.add(readCard(cursor)) } cursor.close() @@ -111,14 +133,7 @@ class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?) return null } - val cardInfo = CardInfo( - id = cursor.getInt(cursor.getColumnIndexOrThrow("id")), - name = cursor.getString(cursor.getColumnIndexOrThrow("name")), - codeType = BarcodeFormat.valueOf(cursor.getString(cursor.getColumnIndexOrThrow("codeType"))), - codeValue = cursor.getString(cursor.getColumnIndexOrThrow("codeValue")), - color = cursor.getString(cursor.getColumnIndexOrThrow("color")), - icon = cursor.getStringOrNull(cursor.getColumnIndexOrThrow("icon")) - ) + val cardInfo = readCard(cursor) cursor.close() db.close() @@ -143,19 +158,38 @@ class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?) fun addCards(cards: List) { val db = this.writableDatabase - for (card in cards) { - val values = ContentValues() + db.use { db -> + db.transaction { + for (card in cards) { + val values = ContentValues() - values.put("id", card.id) - values.put("name", card.name) - values.put("codeValue", card.codeValue) - values.put("codeType", card.codeType.name) - values.put("color", card.color) - values.put("icon", card.icon) + values.put("id", card.id) + values.put("name", card.name) + values.put("codeValue", card.codeValue) + values.put("codeType", card.codeType.name) + values.put("color", card.color) + values.put("icon", card.icon) + values.put("position", nextPosition(db)) - db.insert("cards", null, values) + db.insert("cards", null, values) + } + } } + } - db.close() + fun updatePositions(orderedIds: List) { + val db = this.writableDatabase + + db.use { db -> + db.transaction { + orderedIds.forEachIndexed { index, id -> + val values = ContentValues() + + values.put("position", index) + + db.update("cards", values, "id = ?", arrayOf(id.toString())) + } + } + } } } diff --git a/mobile/src/main/java/ru/omni_devel/cards/Elements.kt b/mobile/src/main/java/ru/omni_devel/cards/Elements.kt index ce8ccbc..ece291c 100644 --- a/mobile/src/main/java/ru/omni_devel/cards/Elements.kt +++ b/mobile/src/main/java/ru/omni_devel/cards/Elements.kt @@ -40,6 +40,7 @@ import androidx.compose.material3.Icon import androidx.compose.material.icons.outlined.Add import androidx.compose.material.icons.outlined.QrCode import androidx.compose.material.icons.outlined.DensityMedium +import androidx.compose.material.icons.outlined.DragHandle import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.FloatingActionButton @@ -185,7 +186,7 @@ fun TopBar( } @Composable -fun CardButton(cardInfo: CardInfo, onDeleteCard: (Int) -> Unit) { +fun CardButton(cardInfo: CardInfo, onDeleteCard: (Int) -> Unit, reorderIcon: @Composable () -> Unit) { val context = LocalContext.current var menuExpanded by remember { mutableStateOf(false) } @@ -222,20 +223,28 @@ fun CardButton(cardInfo: CardInfo, onDeleteCard: (Int) -> Unit) { .background(Color.Transparent) .padding(horizontal = 8.dp), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) + horizontalArrangement = Arrangement.SpaceBetween ) { - CardIcon( - if (cardInfo.icon == null) null else base64ToImageBitmap(cardInfo.icon), - cardColor = cardColor - ) + Row( + modifier = Modifier.weight(1f), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + CardIcon( + if (cardInfo.icon == null) null else base64ToImageBitmap(cardInfo.icon), + cardColor = cardColor + ) - Text( - cardInfo.name, - fontSize = 16.sp, - color = MaterialTheme.colorScheme.onPrimaryContainer, - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Start - ) + Text( + cardInfo.name, + fontSize = 16.sp, + color = MaterialTheme.colorScheme.onPrimaryContainer, + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Start + ) + } + + reorderIcon() } } 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 4daacc7..e415f1e 100644 --- a/mobile/src/main/java/ru/omni_devel/cards/MainActivity.kt +++ b/mobile/src/main/java/ru/omni_devel/cards/MainActivity.kt @@ -16,16 +16,21 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Add import androidx.compose.material.icons.outlined.Sync import androidx.compose.material.icons.outlined.Backup import androidx.compose.material.icons.outlined.Code +import androidx.compose.material.icons.outlined.DragHandle import androidx.compose.material.icons.outlined.Search import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -38,11 +43,16 @@ import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.ui.Alignment +import androidx.compose.ui.draw.alpha import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource import kotlinx.serialization.json.Json import ru.omni_devel.cards.ui.theme.OmniCardsTheme +import sh.calvin.reorderable.ReorderableItem +import sh.calvin.reorderable.rememberReorderableLazyListState class MainActivity : ComponentActivity() { private lateinit var db: DbHelper @@ -60,13 +70,22 @@ class MainActivity : ComponentActivity() { Scaffold( modifier = Modifier.fillMaxSize() ) { innerPadding -> - MainPage(innerPadding, cards, onDeleteCard = { cardId -> - db.removeCard(cardId) - cards.removeIf { it.id == cardId } - syncCardsWithWatch(this, db.getCards()) - }, getDbFun = { - return@MainPage db - }) + MainPage( + innerPadding, + cards, + onDeleteCard = { cardId -> + db.removeCard(cardId) + cards.removeIf { it.id == cardId } + syncCardsWithWatch(this, db.getCards()) + }, + onReorderCards = { + db.updatePositions(it) + syncCardsWithWatch(this, db.getCards()) + }, + getDbFun = { + return@MainPage db + } + ) } } } @@ -85,8 +104,9 @@ class MainActivity : ComponentActivity() { @Composable fun MainPage( innerPadding: PaddingValues, - cards: List, + cards: SnapshotStateList, onDeleteCard: (Int) -> Unit, + onReorderCards: (List) -> Unit, getDbFun: () -> DbHelper ) { val context = LocalContext.current @@ -95,6 +115,19 @@ fun MainPage( var searchQuery by rememberSaveable { mutableStateOf("") } var isSearchFieldShowed by rememberSaveable { mutableStateOf(false) } + val lazyListState = rememberLazyListState() + val reorderableLazyListState = rememberReorderableLazyListState(lazyListState) { from, to -> + val fromId = from.key as? Int ?: return@rememberReorderableLazyListState + val toId = to.key as? Int ?: return@rememberReorderableLazyListState + + val fromIndex = cards.indexOfFirst { it.id == fromId } + val toIndex = cards.indexOfFirst { it.id == toId } + + if (fromIndex != -1 && toIndex != -1) { + cards.add(toIndex, cards.removeAt(fromIndex)) + } + } + Box( modifier = Modifier.fillMaxSize() ) { @@ -159,6 +192,7 @@ fun MainPage( } LazyColumn( + state = lazyListState, verticalArrangement = Arrangement.spacedBy(8.dp) ) { item { @@ -192,8 +226,34 @@ fun MainPage( } } else { for (card in searchResults) { - item { - CardButton(card, onDeleteCard) + item(key = card.id) { + ReorderableItem(reorderableLazyListState, key = card.id) { isDragging -> + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .alpha(if (isDragging) 0.7f else 1f) + ) { + CardButton( + card, + onDeleteCard, + reorderIcon = { + Icon( + imageVector = Icons.Outlined.DragHandle, + contentDescription = null, + tint = MaterialTheme.colorScheme.onPrimaryContainer, + modifier = Modifier + .draggableHandle( + onDragStopped = { + onReorderCards(cards.map { it.id }) + } + ) + .padding(8.dp) + ) + } + ) + } + } } } } diff --git a/wear/src/main/java/ru/omni_devel/cards/presentation/CardInfo.kt b/wear/src/main/java/ru/omni_devel/cards/presentation/CardInfo.kt index 29ea05a..189fb1b 100644 --- a/wear/src/main/java/ru/omni_devel/cards/presentation/CardInfo.kt +++ b/wear/src/main/java/ru/omni_devel/cards/presentation/CardInfo.kt @@ -10,5 +10,6 @@ class CardInfo ( val codeType: BarcodeFormat, val codeValue: String, val color: String?, - val icon: String? + val icon: String?, + val position: Int ) \ No newline at end of file diff --git a/wear/src/main/java/ru/omni_devel/cards/presentation/DbHelper.kt b/wear/src/main/java/ru/omni_devel/cards/presentation/DbHelper.kt index d68b06e..fe74e7a 100644 --- a/wear/src/main/java/ru/omni_devel/cards/presentation/DbHelper.kt +++ b/wear/src/main/java/ru/omni_devel/cards/presentation/DbHelper.kt @@ -2,12 +2,14 @@ package ru.omni_devel.cards.presentation import android.content.ContentValues import android.content.Context +import android.database.Cursor import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteOpenHelper import androidx.core.database.getStringOrNull +import androidx.core.database.sqlite.transaction import com.google.zxing.BarcodeFormat -class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?) : SQLiteOpenHelper(context, "omni_cards", factory, 5) { +class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?) : SQLiteOpenHelper(context, "omni_cards", factory, 6) { override fun onCreate(db: SQLiteDatabase?) { db!!.execSQL( "CREATE TABLE IF NOT EXISTS cards (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, codeType TEXT, codeValue TEXT, color TEXT, icon TEXT)" @@ -37,6 +39,23 @@ class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?) if (oldVersion < 5) { db!!.execSQL("ALTER TABLE cards ADD COLUMN icon TEXT") } + + if (oldVersion < 6) { + db!!.execSQL("ALTER TABLE cards ADD COLUMN position INTEGER DEFAULT 0") + db.execSQL("UPDATE cards SET position = id") + } + } + + private fun readCard(cursor: Cursor): CardInfo { + return CardInfo( + id = cursor.getInt(cursor.getColumnIndexOrThrow("id")), + name = cursor.getString(cursor.getColumnIndexOrThrow("name")), + codeType = BarcodeFormat.valueOf(cursor.getString(cursor.getColumnIndexOrThrow("codeType"))), + codeValue = cursor.getString(cursor.getColumnIndexOrThrow("codeValue")), + color = cursor.getString(cursor.getColumnIndexOrThrow("color")), + icon = cursor.getStringOrNull(cursor.getColumnIndexOrThrow("icon")), + position = cursor.getInt(cursor.getColumnIndexOrThrow("position")) + ) } fun clearDatabase() { @@ -50,37 +69,33 @@ class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?) fun addCards(cards: List) { val db = this.writableDatabase - for (card in cards) { - val values = ContentValues() + db.use { db -> + db.transaction { + for (card in cards) { + val values = ContentValues() - values.put("id", card.id) - values.put("name", card.name) - values.put("codeValue", card.codeValue) - values.put("codeType", card.codeType.name) - values.put("color", card.color) - values.put("icon", card.icon) + values.put("id", card.id) + values.put("name", card.name) + values.put("codeValue", card.codeValue) + values.put("codeType", card.codeType.name) + values.put("color", card.color) + values.put("icon", card.icon) + values.put("position", card.position) - db.insert("cards", null, values) + db.insert("cards", null, values) + } + } } - - db.close() } fun getCards(): List { val db = this.readableDatabase - val cursor = db.rawQuery("SELECT * FROM cards", null) + val cursor = db.rawQuery("SELECT * FROM cards ORDER BY position", null) val cards = mutableListOf() while (cursor.moveToNext()) { - cards.add(CardInfo( - id = cursor.getInt(cursor.getColumnIndexOrThrow("id")), - name = cursor.getString(cursor.getColumnIndexOrThrow("name")), - codeType = BarcodeFormat.valueOf(cursor.getString(cursor.getColumnIndexOrThrow("codeType"))), - codeValue = cursor.getString(cursor.getColumnIndexOrThrow("codeValue")), - color = cursor.getString(cursor.getColumnIndexOrThrow("color")), - icon = cursor.getStringOrNull(cursor.getColumnIndexOrThrow("icon")) - )) + cards.add(readCard(cursor)) } cursor.close() @@ -101,14 +116,7 @@ class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?) return null } - val cardInfo = CardInfo( - id = cursor.getInt(cursor.getColumnIndexOrThrow("id")), - name = cursor.getString(cursor.getColumnIndexOrThrow("name")), - codeType = BarcodeFormat.valueOf(cursor.getString(cursor.getColumnIndexOrThrow("codeType"))), - codeValue = cursor.getString(cursor.getColumnIndexOrThrow("codeValue")), - color = cursor.getString(cursor.getColumnIndexOrThrow("color")), - icon = cursor.getStringOrNull(cursor.getColumnIndexOrThrow("icon")) - ) + val cardInfo = readCard(cursor) cursor.close() db.close()