forked from omni/OmniCards
feat: added cards reordering feature
This commit is contained in:
@@ -15,5 +15,6 @@ class CardInfo (
|
||||
val codeType: BarcodeFormat,
|
||||
val codeValue: String,
|
||||
val color: String?,
|
||||
val icon: String?
|
||||
val icon: String?,
|
||||
val position: Int
|
||||
) {}
|
||||
|
||||
@@ -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<CardInfo> {
|
||||
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<CardInfo>()
|
||||
|
||||
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<CardInfo>) {
|
||||
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<Int>) {
|
||||
val db = this.writableDatabase
|
||||
|
||||
db.use { db ->
|
||||
db.transaction {
|
||||
orderedIds.forEachIndexed { index, id ->
|
||||
val values = ContentValues()
|
||||
|
||||
values.put("position", index)
|
||||
|
||||
db.update("cards", values, "id = ?", arrayOf(id.toString()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<CardInfo>,
|
||||
cards: SnapshotStateList<CardInfo>,
|
||||
onDeleteCard: (Int) -> Unit,
|
||||
onReorderCards: (List<Int>) -> 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)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user