feat: Add displayOrder to notes and implement UI
This commit is contained in:
@@ -4,6 +4,7 @@ import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -73,7 +74,10 @@ import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import de.lxtools.noteshop.ui.notes.NoteDetailScreen
|
||||
import de.lxtools.noteshop.ui.notes.NotesScreen
|
||||
import de.lxtools.noteshop.ui.notes.NotesViewModel
|
||||
import de.lxtools.noteshop.ui.shopping.ShoppingListDetailScreen
|
||||
@@ -82,6 +86,8 @@ import de.lxtools.noteshop.ui.shopping.ShoppingListsViewModel
|
||||
import de.lxtools.noteshop.ui.theme.NoteshopTheme
|
||||
import kotlinx.coroutines.launch
|
||||
import android.os.Parcelable
|
||||
import de.lxtools.noteshop.data.Note
|
||||
import de.lxtools.noteshop.ui.notes.NoteInputDialog
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
// Sealed class to represent the screens in the app
|
||||
@@ -89,7 +95,8 @@ import kotlinx.parcelize.Parcelize
|
||||
sealed class Screen(val route: String, val titleRes: Int) : Parcelable {
|
||||
data object ShoppingLists : Screen("shopping_lists", R.string.menu_shopping_lists)
|
||||
data object Notes : Screen("notes", R.string.menu_notes)
|
||||
data object ShoppingListDetail : Screen("shopping_list_detail", R.string.menu_shopping_list_detail) // New screen
|
||||
data object ShoppingListDetail : Screen("shopping_list_detail", R.string.menu_shopping_list_detail)
|
||||
data object NoteDetail : Screen("note_detail", R.string.menu_note_detail)
|
||||
}
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
@@ -114,10 +121,14 @@ fun AppShell(
|
||||
val scope = rememberCoroutineScope()
|
||||
var currentScreen: Screen by rememberSaveable { mutableStateOf(Screen.ShoppingLists) }
|
||||
var selectedListId: Int? by rememberSaveable { mutableStateOf(null) }
|
||||
var selectedNoteId: Int? by rememberSaveable { mutableStateOf(null) }
|
||||
|
||||
var showListDialog by rememberSaveable { mutableStateOf(false) }
|
||||
val listDetails by shoppingListsViewModel.listDetails.collectAsState()
|
||||
|
||||
var showNoteDialog by rememberSaveable { mutableStateOf(false) }
|
||||
val noteDetails by notesViewModel.noteDetails.collectAsState()
|
||||
|
||||
val navigationItems = listOf(
|
||||
Screen.ShoppingLists,
|
||||
Screen.Notes
|
||||
@@ -177,8 +188,10 @@ fun AppShell(
|
||||
selected = screen == currentScreen,
|
||||
onClick = {
|
||||
shoppingListsViewModel.disableReorderMode()
|
||||
notesViewModel.disableReorderMode()
|
||||
currentScreen = screen
|
||||
selectedListId = null
|
||||
selectedNoteId = null
|
||||
scope.launch { drawerState.close() }
|
||||
}
|
||||
)
|
||||
@@ -200,6 +213,7 @@ fun AppShell(
|
||||
topBar = {
|
||||
val topBarTitle = when (currentScreen) {
|
||||
is Screen.ShoppingListDetail -> shoppingListWithItems?.shoppingList?.name ?: ""
|
||||
is Screen.NoteDetail -> notesViewModel.noteDetails.collectAsState().value.title
|
||||
else -> stringResource(id = currentScreen.titleRes)
|
||||
}
|
||||
|
||||
@@ -224,6 +238,17 @@ fun AppShell(
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back))
|
||||
}
|
||||
}
|
||||
is Screen.NoteDetail -> {
|
||||
IconButton(onClick = {
|
||||
scope.launch {
|
||||
notesViewModel.saveNote()
|
||||
currentScreen = Screen.Notes
|
||||
selectedNoteId = null
|
||||
}
|
||||
}) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back))
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
IconButton(onClick = { scope.launch { drawerState.apply { if (isClosed) open() else close() } } }) {
|
||||
Icon(imageVector = Icons.Default.Menu, contentDescription = stringResource(id = R.string.menu_open))
|
||||
@@ -241,6 +266,14 @@ fun AppShell(
|
||||
Icon(Icons.Default.Add, contentDescription = stringResource(R.string.add_shopping_list))
|
||||
}
|
||||
}
|
||||
if (currentScreen == Screen.Notes && !isReorderMode) {
|
||||
IconButton(onClick = {
|
||||
notesViewModel.resetNoteDetails()
|
||||
showNoteDialog = true
|
||||
}) {
|
||||
Icon(Icons.Default.Add, contentDescription = stringResource(R.string.add_note))
|
||||
}
|
||||
}
|
||||
if (currentScreen == Screen.ShoppingListDetail) {
|
||||
IconButton(onClick = { shoppingListsViewModel.toggleDetailSearch() }) {
|
||||
Icon(Icons.Default.Search, contentDescription = "Search")
|
||||
@@ -327,6 +360,22 @@ fun AppShell(
|
||||
)
|
||||
}
|
||||
|
||||
if (showNoteDialog) {
|
||||
NoteInputDialog(
|
||||
noteDetails = noteDetails,
|
||||
onValueChange = notesViewModel::updateNoteDetails,
|
||||
onSave = {
|
||||
scope.launch {
|
||||
notesViewModel.saveNote()
|
||||
showNoteDialog = false
|
||||
notesViewModel.resetNoteDetails()
|
||||
}
|
||||
},
|
||||
onDismiss = { showNoteDialog = false },
|
||||
isNewNote = noteDetails.id == 0
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@@ -348,7 +397,17 @@ fun AppShell(
|
||||
)
|
||||
}
|
||||
is Screen.Notes -> {
|
||||
NotesScreen(viewModel = notesViewModel)
|
||||
NotesScreen(
|
||||
viewModel = notesViewModel,
|
||||
onNoteClick = { noteId ->
|
||||
selectedNoteId = noteId
|
||||
currentScreen = Screen.NoteDetail
|
||||
},
|
||||
onEditNote = { note ->
|
||||
notesViewModel.updateNoteDetails(note)
|
||||
showNoteDialog = true
|
||||
}
|
||||
)
|
||||
}
|
||||
is Screen.ShoppingListDetail -> {
|
||||
ShoppingListDetailScreen(
|
||||
@@ -356,6 +415,12 @@ fun AppShell(
|
||||
viewModel = shoppingListsViewModel
|
||||
)
|
||||
}
|
||||
is Screen.NoteDetail -> {
|
||||
NoteDetailScreen(
|
||||
noteId = selectedNoteId,
|
||||
viewModel = notesViewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -366,6 +431,6 @@ fun AppShell(
|
||||
@Composable
|
||||
fun DefaultPreview() {
|
||||
NoteshopTheme {
|
||||
AppShell()
|
||||
// AppShell()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
@Database(
|
||||
entities = [Note::class, ShoppingList::class, ShoppingListItem::class],
|
||||
version = 4, // Incremented version
|
||||
version = 5, // Incremented version
|
||||
exportSchema = false
|
||||
)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
@@ -31,7 +31,7 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
AppDatabase::class.java,
|
||||
"noteshop_database"
|
||||
)
|
||||
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4)
|
||||
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5)
|
||||
.build()
|
||||
INSTANCE = instance
|
||||
// return instance
|
||||
@@ -56,5 +56,11 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
db.execSQL("ALTER TABLE shopping_list_items ADD COLUMN displayOrder INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
}
|
||||
|
||||
private val MIGRATION_4_5 = object : Migration(4, 5) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL("ALTER TABLE notes ADD COLUMN displayOrder INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,5 +8,6 @@ data class Note(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
val id: Int = 0,
|
||||
val title: String,
|
||||
val content: String
|
||||
val content: String,
|
||||
val displayOrder: Int = 0
|
||||
)
|
||||
@@ -23,6 +23,12 @@ interface NoteDao {
|
||||
@Query("SELECT * FROM notes WHERE id = :id")
|
||||
fun getNote(id: Int): Flow<Note?>
|
||||
|
||||
@Query("SELECT * FROM notes ORDER BY id DESC")
|
||||
@Query("SELECT * FROM notes ORDER BY displayOrder ASC")
|
||||
fun getAllNotes(): Flow<List<Note>>
|
||||
}
|
||||
|
||||
@Query("SELECT COUNT(*) FROM notes")
|
||||
suspend fun getNotesCount(): Int
|
||||
|
||||
@Update
|
||||
suspend fun updateNote(note: Note)
|
||||
}
|
||||
|
||||
@@ -31,6 +31,11 @@ interface NoteshopRepository {
|
||||
*/
|
||||
suspend fun deleteNote(note: Note)
|
||||
|
||||
/**
|
||||
* Get the number of notes.
|
||||
*/
|
||||
suspend fun getNotesCount(): Int
|
||||
|
||||
/**
|
||||
* Retrieve all the shopping lists with their items from the data source.
|
||||
*/
|
||||
@@ -100,10 +105,12 @@ class OfflineNoteshopRepository(private val noteDao: NoteDao, private val shoppi
|
||||
|
||||
override suspend fun insertNote(note: Note) = noteDao.insert(note)
|
||||
|
||||
override suspend fun updateNote(note: Note) = noteDao.update(note)
|
||||
override suspend fun updateNote(note: Note) = noteDao.updateNote(note)
|
||||
|
||||
override suspend fun deleteNote(note: Note) = noteDao.delete(note)
|
||||
|
||||
override suspend fun getNotesCount(): Int = noteDao.getNotesCount()
|
||||
|
||||
override fun getAllShoppingListsWithItemsStream(): Flow<List<ShoppingListWithItems>> = shoppingListDao.getListsWithItems()
|
||||
|
||||
override fun getShoppingListWithItemsStream(listId: Int): Flow<ShoppingListWithItems?> = shoppingListDao.getListWithItems(listId)
|
||||
@@ -130,4 +137,4 @@ class OfflineNoteshopRepository(private val noteDao: NoteDao, private val shoppi
|
||||
return context.resources.getStringArray(de.lxtools.noteshop.R.array.standard_list_items).toList()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package de.lxtools.noteshop.ui.notes
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun NoteDetailScreen(
|
||||
noteId: Int?,
|
||||
viewModel: NotesViewModel,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val noteDetails by viewModel.noteDetails.collectAsState()
|
||||
|
||||
LaunchedEffect(key1 = noteId) {
|
||||
if (noteId != null) {
|
||||
viewModel.uiState.collect { uiState ->
|
||||
uiState.noteList.find { it.id == noteId }?.let {
|
||||
viewModel.updateNoteDetails(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = noteDetails.content,
|
||||
onValueChange = { viewModel.updateNoteDetails(noteDetails.copy(content = it)) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package de.lxtools.noteshop.ui.notes
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import de.lxtools.noteshop.R
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun NoteInputDialog(
|
||||
noteDetails: NoteDetails,
|
||||
onValueChange: (NoteDetails) -> Unit,
|
||||
onSave: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
isNewNote: Boolean,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(text = if (isNewNote) stringResource(R.string.add_note) else stringResource(R.string.edit_note)) },
|
||||
text = {
|
||||
Column {
|
||||
OutlinedTextField(
|
||||
value = noteDetails.title,
|
||||
onValueChange = { onValueChange(noteDetails.copy(title = it)) },
|
||||
label = { Text(stringResource(R.string.note_title)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(onClick = onSave, enabled = noteDetails.isValid()) {
|
||||
Text(stringResource(R.string.save))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
},
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package de.lxtools.noteshop.ui.notes
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -9,27 +10,25 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.rounded.DragHandle
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
@@ -37,130 +36,113 @@ import de.lxtools.noteshop.AppViewModelProvider
|
||||
import de.lxtools.noteshop.R
|
||||
import de.lxtools.noteshop.data.Note
|
||||
import kotlinx.coroutines.launch
|
||||
import sh.calvin.reorderable.ReorderableCollectionItemScope
|
||||
import sh.calvin.reorderable.ReorderableItem
|
||||
import sh.calvin.reorderable.rememberReorderableLazyListState
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun NotesScreen(viewModel: NotesViewModel = viewModel(factory = AppViewModelProvider.Factory), modifier: Modifier = Modifier) {
|
||||
fun NotesScreen(
|
||||
viewModel: NotesViewModel = viewModel(factory = AppViewModelProvider.Factory),
|
||||
modifier: Modifier = Modifier,
|
||||
onNoteClick: (Int) -> Unit,
|
||||
onEditNote: (Note) -> Unit
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
val noteDetails by viewModel.noteDetails.collectAsState()
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
val isReorderMode by viewModel.isReorderMode.collectAsState()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
Scaffold(
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(onClick = {
|
||||
viewModel.resetNoteDetails()
|
||||
showDialog = true
|
||||
}) {
|
||||
Icon(Icons.Default.Add, contentDescription = stringResource(R.string.add_note))
|
||||
Column(
|
||||
modifier = modifier.fillMaxSize()
|
||||
) {
|
||||
if (uiState.noteList.isEmpty()) {
|
||||
Text(text = stringResource(R.string.no_notes_yet))
|
||||
} else {
|
||||
val lazyListState = rememberLazyListState()
|
||||
val reorderableLazyListState = rememberReorderableLazyListState(lazyListState) { from, to ->
|
||||
viewModel.moveNote(from.index, to.index)
|
||||
}
|
||||
}
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
if (uiState.noteList.isEmpty()) {
|
||||
Text(text = stringResource(R.string.no_notes_yet))
|
||||
} else {
|
||||
LazyColumn {
|
||||
items(uiState.noteList) { note ->
|
||||
LazyColumn(
|
||||
state = lazyListState,
|
||||
) {
|
||||
items(uiState.noteList, { it.id }) { note ->
|
||||
ReorderableItem(reorderableLazyListState, key = note.id) { isDragging ->
|
||||
NoteCard(
|
||||
note = note,
|
||||
onEdit = {
|
||||
viewModel.updateNoteDetails(it)
|
||||
showDialog = true
|
||||
},
|
||||
onDelete = {
|
||||
onEditNote = onEditNote,
|
||||
onDeleteNote = {
|
||||
coroutineScope.launch {
|
||||
viewModel.deleteNote(it)
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier.pointerInput(isReorderMode) {
|
||||
if (!isReorderMode) {
|
||||
detectTapGestures(
|
||||
onLongPress = { viewModel.enableReorderMode() },
|
||||
onTap = { onNoteClick(note.id) }
|
||||
)
|
||||
}
|
||||
},
|
||||
scope = this,
|
||||
isDragging = isDragging,
|
||||
isReorderMode = isReorderMode
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showDialog) {
|
||||
NoteInputDialog(
|
||||
noteDetails = noteDetails,
|
||||
onValueChange = viewModel::updateNoteDetails,
|
||||
onSave = {
|
||||
coroutineScope.launch {
|
||||
viewModel.saveNote()
|
||||
showDialog = false
|
||||
viewModel.resetNoteDetails()
|
||||
}
|
||||
},
|
||||
onDismiss = { showDialog = false },
|
||||
isNewNote = noteDetails.id == 0
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NoteCard(note: Note, onEdit: (Note) -> Unit, onDelete: (Note) -> Unit, modifier: Modifier = Modifier) {
|
||||
fun NoteCard(
|
||||
note: Note,
|
||||
onEditNote: (Note) -> Unit,
|
||||
onDeleteNote: (Note) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
scope: ReorderableCollectionItemScope,
|
||||
isDragging: Boolean,
|
||||
isReorderMode: Boolean
|
||||
) {
|
||||
val scale = animateFloatAsState(if (isDragging) 1.05f else 1.0f, label = "scale")
|
||||
|
||||
Card(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp)
|
||||
.clickable { onEdit(note) }
|
||||
.padding(vertical = 4.dp, horizontal = 16.dp)
|
||||
.graphicsLayer {
|
||||
scaleX = scale.value
|
||||
scaleY = scale.value
|
||||
}
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text(text = note.title, style = MaterialTheme.typography.titleMedium)
|
||||
Text(text = note.content, style = MaterialTheme.typography.bodyMedium)
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
|
||||
TextButton(onClick = { onDelete(note) }) {
|
||||
Text(stringResource(R.string.delete))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (isReorderMode) {
|
||||
IconButton(modifier = with(scope) { Modifier.draggableHandle() }, onClick = {}) {
|
||||
Icon(Icons.Rounded.DragHandle, contentDescription = "Reorder")
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = note.title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
IconButton(onClick = { onEditNote(note) }, enabled = !isReorderMode) {
|
||||
Icon(Icons.Default.Edit, contentDescription = stringResource(R.string.edit_note))
|
||||
}
|
||||
IconButton(onClick = { onDeleteNote(note) }, enabled = !isReorderMode) {
|
||||
Icon(Icons.Default.Delete, contentDescription = stringResource(R.string.delete_note))
|
||||
}
|
||||
if (isReorderMode) {
|
||||
IconButton(modifier = with(scope) { Modifier.draggableHandle() }, onClick = {}) {
|
||||
Icon(Icons.Rounded.DragHandle, contentDescription = "Reorder")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun NoteInputDialog(
|
||||
noteDetails: NoteDetails,
|
||||
onValueChange: (NoteDetails) -> Unit,
|
||||
onSave: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
isNewNote: Boolean,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(text = if (isNewNote) stringResource(R.string.add_note) else stringResource(R.string.edit_note)) },
|
||||
text = {
|
||||
Column {
|
||||
OutlinedTextField(
|
||||
value = noteDetails.title,
|
||||
onValueChange = { onValueChange(noteDetails.copy(title = it)) },
|
||||
label = { Text(stringResource(R.string.note_title)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = noteDetails.content,
|
||||
onValueChange = { onValueChange(noteDetails.copy(content = it)) },
|
||||
label = { Text(stringResource(R.string.note_content)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(onClick = onSave, enabled = noteDetails.isValid()) {
|
||||
Text(stringResource(R.string.save))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
},
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
@@ -14,12 +14,24 @@ import kotlinx.coroutines.launch
|
||||
|
||||
class NotesViewModel(private val noteshopRepository: NoteshopRepository) : ViewModel() {
|
||||
|
||||
val uiState: StateFlow<NotesUiState> = noteshopRepository.getAllNotesStream().map { NotesUiState(it) }
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(TIMEOUT_MILLIS),
|
||||
initialValue = NotesUiState()
|
||||
)
|
||||
private val _isReorderMode = MutableStateFlow(false)
|
||||
val isReorderMode: StateFlow<Boolean> = _isReorderMode.asStateFlow()
|
||||
|
||||
fun enableReorderMode() {
|
||||
_isReorderMode.value = true
|
||||
}
|
||||
|
||||
fun disableReorderMode() {
|
||||
_isReorderMode.value = false
|
||||
}
|
||||
|
||||
val uiState: StateFlow<NotesUiState> = noteshopRepository.getAllNotesStream().map { notes ->
|
||||
NotesUiState(notes.sortedBy { it.displayOrder })
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(TIMEOUT_MILLIS),
|
||||
initialValue = NotesUiState()
|
||||
)
|
||||
|
||||
private val _noteDetails = MutableStateFlow(NoteDetails())
|
||||
val noteDetails: StateFlow<NoteDetails> = _noteDetails.asStateFlow()
|
||||
@@ -28,7 +40,8 @@ class NotesViewModel(private val noteshopRepository: NoteshopRepository) : ViewM
|
||||
_noteDetails.value = NoteDetails(
|
||||
id = note.id,
|
||||
title = note.title,
|
||||
content = note.content
|
||||
content = note.content,
|
||||
displayOrder = note.displayOrder
|
||||
)
|
||||
}
|
||||
|
||||
@@ -38,7 +51,12 @@ class NotesViewModel(private val noteshopRepository: NoteshopRepository) : ViewM
|
||||
|
||||
suspend fun saveNote() {
|
||||
if (noteDetails.value.isValid()) {
|
||||
noteshopRepository.insertNote(noteDetails.value.toNote())
|
||||
var currentNote = noteDetails.value.toNote()
|
||||
if (currentNote.id == 0) {
|
||||
val notesCount = noteshopRepository.getNotesCount()
|
||||
currentNote = currentNote.copy(displayOrder = notesCount)
|
||||
}
|
||||
noteshopRepository.insertNote(currentNote)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +68,19 @@ class NotesViewModel(private val noteshopRepository: NoteshopRepository) : ViewM
|
||||
_noteDetails.value = NoteDetails()
|
||||
}
|
||||
|
||||
fun moveNote(from: Int, to: Int) {
|
||||
viewModelScope.launch {
|
||||
val list = uiState.value.noteList.toMutableList()
|
||||
list.add(to, list.removeAt(from))
|
||||
val updatedNotes = list.mapIndexed { index, item ->
|
||||
item.copy(displayOrder = index)
|
||||
}
|
||||
updatedNotes.forEach { note ->
|
||||
noteshopRepository.updateNote(note)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TIMEOUT_MILLIS = 5_000L
|
||||
}
|
||||
@@ -62,15 +93,17 @@ data class NotesUiState(
|
||||
data class NoteDetails(
|
||||
val id: Int = 0,
|
||||
val title: String = "",
|
||||
val content: String = ""
|
||||
val content: String = "",
|
||||
val displayOrder: Int = 0
|
||||
) {
|
||||
fun toNote(): Note = Note(
|
||||
id = id,
|
||||
title = title,
|
||||
content = content
|
||||
content = content,
|
||||
displayOrder = displayOrder
|
||||
)
|
||||
|
||||
fun isValid(): Boolean {
|
||||
return title.isNotBlank() && content.isNotBlank()
|
||||
return title.isNotBlank()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
<string name="add_note">Notiz hinzufügen</string>
|
||||
<string name="edit_note">Notiz bearbeiten</string>
|
||||
<string name="delete_note">Notiz löschen</string>
|
||||
<string name="delete">Löschen</string>
|
||||
<string name="no_notes_yet">Noch keine Notizen. Klicke auf + um eine hinzuzufügen!</string>
|
||||
<string name="note_title">Titel</string>
|
||||
@@ -27,6 +28,7 @@
|
||||
<string name="edit_list">Liste bearbeiten</string>
|
||||
<string name="delete_list">Liste löschen</string>
|
||||
<string name="menu_shopping_list_detail">Einkaufslisten-Details</string>
|
||||
<string name="menu_note_detail">Notizdetails</string>
|
||||
<string name="no_items_in_list">Noch keine Artikel in dieser Liste.</string>
|
||||
<string name="shopping_list_not_found">Einkaufsliste nicht gefunden.</string>
|
||||
<string name="search_list_hint">Listen suchen...</string>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
<string name="add_note">Add Note</string>
|
||||
<string name="edit_note">Edit Note</string>
|
||||
<string name="delete_note">Delete note</string>
|
||||
<string name="delete">Delete</string>
|
||||
<string name="no_notes_yet">No notes yet. Click + to add one!</string>
|
||||
<string name="note_title">Title</string>
|
||||
@@ -27,6 +28,7 @@
|
||||
<string name="edit_list">Edit list</string>
|
||||
<string name="delete_list">Delete list</string>
|
||||
<string name="menu_shopping_list_detail">Shopping List Details</string>
|
||||
<string name="menu_note_detail">Note Details</string>
|
||||
<string name="no_items_in_list">No items in this list yet.</string>
|
||||
<string name="shopping_list_not_found">Shopping list not found.</string>
|
||||
<string name="search_list_hint">Search lists...</string>
|
||||
|
||||
Reference in New Issue
Block a user