Compare commits

...

15 Commits

Author SHA1 Message Date
sebastian
8e8eecf367 ADD: Rename Collections 2025-05-10 17:42:48 +02:00
sebastian
71d56519b2 ADD: Delete Collections 2025-05-10 15:57:07 +02:00
sebastian
2525ef9eac UPD: Add Dropdown Icon next to text 2025-05-10 15:20:22 +02:00
sebastian
0c1bc5265e ADD: Edit Notes from Grid 2025-05-10 15:16:57 +02:00
sebastian
8989dcd21d ADD: Delete Notes from Grid 2025-05-10 15:11:14 +02:00
sebastian
6df3f756ec ADD: Adding Notes to Collections 2025-05-10 14:11:07 +02:00
sebastian
caece7bfea UPD: Make Grid for NoteBrowser responsive 2025-05-10 13:54:44 +02:00
sebastian
234a5a7a60 UPD: Display Notes and Collections as beautiful grid 2025-05-10 13:34:36 +02:00
sebastian
dd7e9dce65 FIX: Notes are not beeing displayed 2025-05-10 13:20:40 +02:00
sebastian
b9fe9cf184 FIX: Navigation using Breadcrumbs 2025-05-10 11:45:52 +02:00
sebastian
828a4e0fd3 ADD: Breadcrumbs + navigation with "Zurücktaste" 2025-05-10 11:34:16 +02:00
sebastian
a61493c272 ADD: First approach to navigate between folders 2025-05-10 11:07:09 +02:00
sebastian
4ff09a6759 ADD: Dialog Composable to add new Collections and ViewModel for NoteBrowser 2025-05-10 10:50:46 +02:00
sebastian
d6588913fd UPD: Use Scaffold as anchor 2025-05-10 09:33:51 +02:00
sebastian
4a44f99b86 ADD: Open Context Menu to allow importing new notes and creating to collections 2025-05-10 09:32:35 +02:00
19 changed files with 913 additions and 76 deletions

View File

@ -1,49 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="music_database" uuid="95a3c2ec-2c29-4336-900a-3993de90ae66">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$USER_HOME$/.cache/JetBrains/IntelliJIdea2025.1/device-explorer/samsung SM-P610/_/data/data/com.stormtales.notevault/databases/music_database</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
<libraries>
<library>
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.45.1/org/xerial/sqlite-jdbc/3.45.1.0/sqlite-jdbc-3.45.1.0.jar</url>
</library>
<library>
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.45.1/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar</url>
</library>
</libraries>
</data-source>
<data-source source="LOCAL" name="note_database" uuid="b3770d7c-0a73-40c6-aab8-010effaa19b6">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$USER_HOME$/.cache/JetBrains/IntelliJIdea2025.1/device-explorer/samsung SM-P610/_/data/data/come.stormborntales.notevault/databases/note_database</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
<libraries>
<library>
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.45.1/org/xerial/sqlite-jdbc/3.45.1.0/sqlite-jdbc-3.45.1.0.jar</url>
</library>
<library>
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.45.1/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar</url>
</library>
<library>
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.45.1/org/xerial/sqlite-jdbc/3.45.1.0/sqlite-jdbc-3.45.1.0.jar</url>
</library>
<library>
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.45.1/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar</url>
</library>
</libraries>
</data-source>
<data-source source="LOCAL" name="note_database [2]" uuid="ad94cfd9-e485-4151-8bf4-a080c51fa27c">
<data-source source="LOCAL" name="note_database" uuid="669634a7-d50e-4c04-b2fe-786d18bbd166">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
@ -65,6 +23,12 @@
<library>
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.45.1/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar</url>
</library>
<library>
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.45.1/org/xerial/sqlite-jdbc/3.45.1.0/sqlite-jdbc-3.45.1.0.jar</url>
</library>
<library>
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.45.1/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar</url>
</library>
</libraries>
</data-source>
</component>

View File

@ -4,7 +4,7 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-05-03T08:34:29.354537334Z">
<DropdownSelection timestamp="2025-05-10T10:58:30.749991183Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=R52N50NLGRT" />

View File

@ -63,4 +63,5 @@ dependencies {
implementation(libs.compose.runtime.livedata)
implementation(libs.coil.compose)
ksp(libs.androidx.room.compiler)
implementation(libs.androidx.material.icons.extended)
}

View File

@ -42,12 +42,15 @@ import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import come.stormborntales.notevault.data.local.AppDatabase
import come.stormborntales.notevault.data.local.entity.NoteEntity
import come.stormborntales.notevault.data.repository.CollectionRepository
import come.stormborntales.notevault.data.repository.NoteRepository
import come.stormborntales.notevault.ui.screens.AddNoteDialog
import come.stormborntales.notevault.ui.screens.MainScreen
import come.stormborntales.notevault.ui.screens.NotesScreen
import come.stormborntales.notevault.ui.screens.notebrowser.NotesScreen
import come.stormborntales.notevault.ui.screens.SettingsScreen
import come.stormborntales.notevault.ui.theme.NoteVaultTheme
import come.stormborntales.notevault.ui.viewmodel.NoteBrowserViewModel
import come.stormborntales.notevault.ui.viewmodel.NoteBrowserViewModelFactory
import come.stormborntales.notevault.ui.viewmodel.NoteViewModel
import come.stormborntales.notevault.ui.viewmodel.NoteViewModelFactory
import kotlinx.coroutines.launch
@ -59,14 +62,16 @@ class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
val applicationContext = applicationContext
val database = AppDatabase.getDatabase(applicationContext)
val repository = NoteRepository(database.noteDao())
val viewModelFactory = NoteViewModelFactory(repository)
val noteRepository = NoteRepository(database.noteDao())
val collectionRepository = CollectionRepository(database.collectionDao())
val viewModelFactory = NoteViewModelFactory(noteRepository)
val noteBrowserViewModelFactory = NoteBrowserViewModelFactory(noteRepository, collectionRepository)
setContent {
NoteVaultTheme {
val navController = rememberNavController()
val viewModel: NoteViewModel = viewModel(factory = viewModelFactory)
val noteBrowserViewModel: NoteBrowserViewModel = viewModel(factory = noteBrowserViewModelFactory)
var selectedUris by remember { mutableStateOf<List<Uri>>(emptyList()) }
var showDialog by remember { mutableStateOf(false) }
var noteToEdit by remember { mutableStateOf<NoteEntity?>(null) }
@ -206,7 +211,13 @@ class MainActivity : ComponentActivity() {
SettingsScreen()
}
composable("notes") {
NotesScreen()
NotesScreen(collectionRepository, noteRepository, onImportNotes = {
imagePickerLauncher.launch(arrayOf("image/*"))
}, noteBrowserViewModel,
onEditNote = {
noteToEdit = it
showDialog = true
})
}
}
@ -224,6 +235,7 @@ class MainActivity : ComponentActivity() {
genre = genre,
description = description,
selectedUris = selectedUris,
collectionID = noteBrowserViewModel.currentParentId.value,
onDone = { showDialog = false }
)
} else {

View File

@ -6,6 +6,7 @@ import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import come.stormborntales.notevault.data.local.dao.CollectionDao
import come.stormborntales.notevault.data.local.dao.NoteDao
import come.stormborntales.notevault.data.local.entity.NoteCollection
import come.stormborntales.notevault.data.local.entity.NoteEntity
@ -14,6 +15,7 @@ import come.stormborntales.notevault.data.local.entity.NoteEntity
@TypeConverters(UriListConverter::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun noteDao(): NoteDao
abstract fun collectionDao(): CollectionDao
companion object {
@Volatile

View File

@ -0,0 +1,32 @@
package come.stormborntales.notevault.data.local.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import come.stormborntales.notevault.data.local.entity.NoteCollection
import kotlinx.coroutines.flow.Flow
@Dao
interface CollectionDao {
@Query(value = "SELECT * FROM note_collections WHERE parentId IS :parentId")
fun getCollectionsByParent(parentId: Int?): Flow<List<NoteCollection>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertCollection(collection: NoteCollection)
@Update
suspend fun updateCollection(collection: NoteCollection)
@Delete
suspend fun deleteCollection(collection: NoteCollection)
@Query("SELECT * FROM note_collections WHERE id = :id LIMIT 1")
suspend fun getById(id: Int): NoteCollection?
@Query("SELECT * FROM note_collections")
fun getAll(): Flow<List<NoteCollection>>
}

View File

@ -22,4 +22,6 @@ interface NoteDao {
@Update
suspend fun update(note: NoteEntity)
@Query("SELECT * FROM notes WHERE collectionId IS :collectionId")
fun getNotesForCollection(collectionId: Int?): Flow<List<NoteEntity>>
}

View File

@ -0,0 +1,34 @@
package come.stormborntales.notevault.data.repository
import come.stormborntales.notevault.data.local.dao.CollectionDao
import come.stormborntales.notevault.data.local.dao.NoteDao
import come.stormborntales.notevault.data.local.entity.NoteCollection
import come.stormborntales.notevault.data.local.entity.NoteEntity
import kotlinx.coroutines.flow.Flow
class CollectionRepository(private val dao: CollectionDao) {
fun getCollectionsByParent(parentId: Int?): Flow<List<NoteCollection>> {
return dao.getCollectionsByParent(parentId)
}
suspend fun insertCollection(collection: NoteCollection) {
dao.insertCollection(collection)
}
suspend fun updateCollection(collection: NoteCollection) {
dao.updateCollection(collection)
}
suspend fun deleteCollection(collection: NoteCollection) {
dao.deleteCollection(collection)
}
suspend fun getCollectionById(id: Int): NoteCollection? {
return dao.getById(id)
}
fun getAllCollections(): Flow<List<NoteCollection>> {
return dao.getAll()
}
}

View File

@ -11,4 +11,5 @@ class NoteRepository(private val dao: NoteDao) {
suspend fun delete(note: NoteEntity) = dao.delete(note)
suspend fun update(note: NoteEntity) = dao.update(note);
fun getNotesForCollection(parentId: Int?) = dao.getNotesForCollection(parentId);
}

View File

@ -19,7 +19,8 @@ fun AddNoteDialog(
initialComposer: String? = null,
initialYear: Int? = null,
initialGenre: String? = null,
initialDescription: String? = null
initialDescription: String? = null,
collectionId: Int? = null
) {
var title by remember { mutableStateOf(initialTitle) }
var composer by remember { mutableStateOf(initialComposer ?: "") }

View File

@ -1,24 +0,0 @@
package come.stormborntales.notevault.ui.screens
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Divider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun NotesScreen() {
}

View File

@ -0,0 +1,82 @@
package come.stormborntales.notevault.ui.screens.notebrowser
import android.util.Log
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun DeleteCollectionDialog(
collectionName: String,
onDismiss: () -> Unit,
onConfirm: () -> Unit
) {
var sliderPosition by remember { mutableStateOf(0f) }
AlertDialog(
onDismissRequest = onDismiss,
title = {
Text("Collection löschen", style = MaterialTheme.typography.titleLarge)
},
text = {
Column {
Text(
text = "Bist du sicher, dass du die Collection \"$collectionName\" löschen möchtest?",
style = MaterialTheme.typography.bodyMedium
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "⚠️ Alle enthaltenen Noten werden ebenfalls dauerhaft gelöscht.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Zum Bestätigen den Schieberegler ganz nach rechts ziehen.",
style = MaterialTheme.typography.bodySmall
)
Spacer(modifier = Modifier.height(8.dp))
Slider(
value = sliderPosition,
onValueChange = { sliderPosition = it },
valueRange = 0f..1f,
steps = 0,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 4.dp)
)
}
},
confirmButton = {
TextButton(onClick = {
if(sliderPosition == 1.0f) {
onConfirm()
}
}, colors = ButtonDefaults.textButtonColors(
contentColor = MaterialTheme.colorScheme.error
)) {
Text("Löschen")
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("Abbrechen")
}
}
)
}

View File

@ -0,0 +1,150 @@
package come.stormborntales.notevault.ui.screens.notebrowser
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.Folder
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
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.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import come.stormborntales.notevault.data.local.entity.NoteCollection
import come.stormborntales.notevault.data.local.entity.NoteEntity
@Composable
fun CollectionItem(
collection: NoteCollection,
onDeleteCollection: (NoteCollection) -> Unit,
onEditCollection: (NoteCollection) -> Unit,
) {
var showMenu by remember { mutableStateOf(false) }
Box(
modifier = Modifier
.height(120.dp)
.clip(RoundedCornerShape(12.dp))
.background(MaterialTheme.colorScheme.surfaceVariant)
.clickable { }
.padding(12.dp),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
imageVector = Icons.Filled.Folder,
contentDescription = "Ordner",
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(64.dp)
)
Box {
val interactionSource = remember { MutableInteractionSource() }
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clickable { showMenu = true }
.padding(top = 8.dp)
) {
Text(
text = collection.name,
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.clickable(
interactionSource = interactionSource,
indication = rememberRipple(bounded = true)
) {
showMenu = true
}
)
Icon(
imageVector = Icons.Default.ArrowDropDown,
contentDescription = "Mehr Optionen",
tint = MaterialTheme.colorScheme.onSurface
)
}
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false },
modifier = Modifier
.width(240.dp)
.background(MaterialTheme.colorScheme.background)
) {
DropdownMenuItem(
onClick = {
showMenu = false
onEditCollection(collection)
},
text = {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.Edit,
contentDescription = "Bearbeiten",
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text("Bearbeiten", color = MaterialTheme.colorScheme.primary)
}
}
)
DropdownMenuItem(
onClick = {
showMenu = false
onDeleteCollection(collection)
},
text = {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "Löschen",
tint = MaterialTheme.colorScheme.error,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text("Löschen", color = MaterialTheme.colorScheme.error)
}
}
)
}
}
}
}
}

View File

@ -0,0 +1,150 @@
package come.stormborntales.notevault.ui.screens.notebrowser
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
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.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import come.stormborntales.notevault.data.local.entity.NoteEntity
@Composable
fun NoteItem(
note: NoteEntity,
onNoteClick: (NoteEntity) -> Unit,
onEditTitle: () -> Unit,
onDeleteNote: () -> Unit
) {
var showMenu by remember { mutableStateOf(false) }
Box(
modifier = Modifier
.height(120.dp)
.clip(RoundedCornerShape(12.dp))
.background(MaterialTheme.colorScheme.surfaceVariant)
.clickable { onNoteClick(note) }
.padding(12.dp),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
AsyncImage(
model = note.imagePreview,
contentDescription = "Noten-Vorschau",
modifier = Modifier
.size(64.dp)
.clip(RoundedCornerShape(8.dp)),
contentScale = ContentScale.Crop
)
Box {
val interactionSource = remember { MutableInteractionSource() }
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clickable { showMenu = true }
.padding(top = 8.dp)
) {
Text(
text = note.title,
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.clickable(
interactionSource = interactionSource,
indication = rememberRipple(bounded = true)
) {
showMenu = true
}
)
Icon(
imageVector = Icons.Default.ArrowDropDown,
contentDescription = "Mehr Optionen",
tint = MaterialTheme.colorScheme.onSurface
)
}
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false },
modifier = Modifier
.width(240.dp)
.background(MaterialTheme.colorScheme.background)
) {
DropdownMenuItem(
onClick = {
showMenu = false
onEditTitle()
},
text = {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.Edit,
contentDescription = "Bearbeiten",
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text("Bearbeiten", color = MaterialTheme.colorScheme.primary)
}
}
)
DropdownMenuItem(
onClick = {
showMenu = false
onDeleteNote()
},
text = {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "Löschen",
tint = MaterialTheme.colorScheme.error,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text("Löschen", color = MaterialTheme.colorScheme.error)
}
}
)
}
}
}
}
}

View File

@ -0,0 +1,305 @@
package come.stormborntales.notevault.ui.screens.notebrowser
import android.util.Log
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
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.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.Folder
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.FloatingActionButton
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
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.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import come.stormborntales.notevault.data.local.entity.NoteCollection
import come.stormborntales.notevault.data.local.entity.NoteEntity
import come.stormborntales.notevault.data.repository.CollectionRepository
import come.stormborntales.notevault.data.repository.NoteRepository
import come.stormborntales.notevault.ui.viewmodel.NoteBrowserViewModel
@Composable
fun CreateCollectionDialog(
editedCollection: NoteCollection?,
onDismiss: () -> Unit,
onCreate: (String) -> Unit
) {
var collectionName by remember { mutableStateOf(editedCollection?.name ?: "") }
var isError by remember { mutableStateOf(false) }
AlertDialog(
onDismissRequest = onDismiss,
title = {
Text("Neue Collection erstellen")
},
text = {
Column {
OutlinedTextField(
value = collectionName,
onValueChange = {
collectionName = it
isError = false
},
label = { Text("Collection-Name") },
isError = isError,
singleLine = true
)
if (isError) {
Text(
text = "Name darf nicht leer sein",
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall
)
}
}
},
confirmButton = {
TextButton(onClick = {
if (collectionName.isBlank()) {
isError = true
} else {
onCreate(collectionName.trim())
onDismiss()
}
}) {
Text("Erstellen")
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("Abbrechen")
}
}
)
}
@Composable
fun CollectionBreadcrumbs(
path: List<NoteCollection>,
onNavigateTo: (index: Int) -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
BreadcrumbItem(label = "Start", onClick = { onNavigateTo(-1) })
path.forEachIndexed { index, collection ->
Text(" / ", style = MaterialTheme.typography.labelLarge)
BreadcrumbItem(label = collection.name, onClick = {
Log.d("Breadcrumb", "BreadcrumbItem " + collection.name + " was clicked")
onNavigateTo(index)
})
}
}
}
@Composable
private fun BreadcrumbItem(label: String, onClick: () -> Unit) {
Box(
modifier = Modifier
.padding(horizontal = 4.dp)
.clickable(onClick = {
Log.d("Individual Breadcrumb", "A simple test " + label)
onClick()
})
) {
Text(
text = label,
style = MaterialTheme.typography.labelLarge,
)
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun NoteGrid(
modifier: Modifier = Modifier,
collections: List<NoteCollection>,
notes: List<NoteEntity>,
onCollectionClick: (NoteCollection) -> Unit,
onNoteClick: (NoteEntity) -> Unit,
onDeleteNote: (NoteEntity) -> Unit,
onEditNote: (NoteEntity) ->Unit,
onDeleteCollection: (NoteCollection) -> Unit,
onEditCollection: (NoteCollection) -> Unit,
) {
var showCollectionConfirmationDialog by remember { mutableStateOf(false) }
var deletedCollection: NoteCollection? by remember { mutableStateOf(null) }
LazyVerticalGrid(
modifier = modifier.fillMaxSize(),
columns = GridCells.Adaptive(minSize = 240.dp),
contentPadding = PaddingValues(8.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
items(collections) { collection ->
CollectionItem(collection, onDeleteCollection = {
showCollectionConfirmationDialog = true
deletedCollection = collection
}, onEditCollection = onEditCollection)
}
items(notes) { note ->
NoteItem(note, onNoteClick = {}, onEditTitle = {onEditNote(note)}, onDeleteNote = {onDeleteNote(note)})
}
}
if(showCollectionConfirmationDialog && deletedCollection != null) {
DeleteCollectionDialog(deletedCollection!!.name , {}, {
onDeleteCollection(deletedCollection!!)
showCollectionConfirmationDialog = false
})
}
}
@Composable
fun NotesScreen(
collectionRepository: CollectionRepository,
noteRepository: NoteRepository,
onImportNotes: (Int?) -> Unit,
viewModel: NoteBrowserViewModel,
onEditNote: (NoteEntity) -> Unit
) {
var menuExpanded by remember { mutableStateOf(false) }
var showDialog by remember { mutableStateOf(false) }
var collectionToEdit by remember { mutableStateOf<NoteCollection?>(null) }
val collections by viewModel.currentCollections.collectAsState()
val notes by viewModel.currentNotes.collectAsState()
Scaffold(
floatingActionButton = {
Box {
FloatingActionButton(onClick = { menuExpanded = true }) {
Icon(Icons.Default.Add, contentDescription = "Open menu")
}
DropdownMenu(
expanded = menuExpanded,
onDismissRequest = { menuExpanded = false },
modifier = Modifier
.wrapContentWidth()
) {
DropdownMenuItem(
text = { Text("Ordner erstellen") },
onClick = {
menuExpanded = false
showDialog = true
}
)
DropdownMenuItem(
text = { Text("Noten importieren") },
onClick = {
menuExpanded = false
onImportNotes(viewModel.currentParentId.value)
}
)
}
}
},
content = { innerPadding ->
val path by viewModel.pathStack.collectAsState()
BackHandler(enabled = path.isNotEmpty()) {
viewModel.navigateUp()
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
) {
// Breadcrumbs sichtbar & klickbar machen
CollectionBreadcrumbs(
path = path,
onNavigateTo = { index ->
Log.d("NoteBrowser", "Navigate to: $index")
if (index == -1) viewModel.loadContentForParent(null)
else viewModel.navigateToLevel(index)
}
)
NoteGrid(
collections = collections,
notes = notes,
onCollectionClick = { viewModel.loadContentForParent(it.id, it) },
onNoteClick = { /* TODO: Öffnen oder Bearbeiten */ },
onDeleteNote = {
viewModel.removeNote(noteEntity = it)
},
onEditNote = onEditNote,
onDeleteCollection = {
viewModel.removeCollection(noteCollection = it)
},
onEditCollection = {
collectionToEdit = it
showDialog = true
}
)
}
if (showDialog) {
CreateCollectionDialog(
onDismiss = { showDialog = false },
onCreate = { name ->
if(collectionToEdit != null) {
viewModel.updateCollection(collectionToEdit!!, name)
} else {
// Handle the new collection name
viewModel.createCollection(name)
}
},
editedCollection = collectionToEdit
)
}
}
)
}

View File

@ -0,0 +1,103 @@
package come.stormborntales.notevault.ui.viewmodel
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import come.stormborntales.notevault.data.local.entity.NoteCollection
import come.stormborntales.notevault.data.local.entity.NoteEntity
import come.stormborntales.notevault.data.repository.CollectionRepository
import come.stormborntales.notevault.data.repository.NoteRepository
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
class NoteBrowserViewModel (
private val noteRepository: NoteRepository,
private val collectionRepository: CollectionRepository
) : ViewModel() {
private val _currentCollections = MutableStateFlow<List<NoteCollection>>(emptyList())
val currentCollections: StateFlow<List<NoteCollection>> = _currentCollections.asStateFlow()
private val _currentNotes = MutableStateFlow<List<NoteEntity>>(emptyList())
val currentNotes: StateFlow<List<NoteEntity>> = _currentNotes.asStateFlow()
private val _currentParentId = MutableStateFlow<Int?>(null)
val currentParentId: StateFlow<Int?> = _currentParentId.asStateFlow()
private val _pathStack = MutableStateFlow<List<NoteCollection>>(emptyList())
val pathStack: StateFlow<List<NoteCollection>> = _pathStack.asStateFlow()
init {
loadContentForParent(null)
}
fun loadContentForParent(parentId: Int?, selectedCollection: NoteCollection? = null) {
_currentParentId.value = parentId
viewModelScope.launch {
launch {
collectionRepository.getCollectionsByParent(parentId).collect {
_currentCollections.value = it
}
}
launch {
noteRepository.getNotesForCollection(parentId).collect {
Log.d("NoteBrowser", "Noten geladen: ${it.size}")
_currentNotes.value = it
}
}
if (selectedCollection != null) {
_pathStack.value = _pathStack.value + selectedCollection
} else if (parentId == null) {
_pathStack.value = emptyList()
}
}
}
fun createCollection(name: String) {
viewModelScope.launch {
val newCollection = NoteCollection(name = name, parentId = _currentParentId.value)
collectionRepository.insertCollection(newCollection)
loadContentForParent(_currentParentId.value)
}
}
fun removeNote(noteEntity: NoteEntity) {
viewModelScope.launch {
noteRepository.delete(noteEntity)
}
}
fun removeCollection(noteCollection: NoteCollection) {
viewModelScope.launch {
collectionRepository.deleteCollection(noteCollection)
}
}
fun navigateToLevel(index: Int) {
val newStack = _pathStack.value.take(index + 1)
_pathStack.value = newStack
val newParent = newStack.lastOrNull()?.id
loadContentForParent(newParent)
}
fun navigateUp() {
val newStack = _pathStack.value.dropLast(1)
_pathStack.value = newStack
val newParentId = newStack.lastOrNull()?.id
loadContentForParent(newParentId)
}
fun updateCollection(collectionToEdit: NoteCollection, name: String) {
if(name.isNotEmpty()) {
collectionToEdit.name = name
viewModelScope.launch {
collectionRepository.updateCollection(collectionToEdit)
}
}
}
}

View File

@ -0,0 +1,20 @@
package come.stormborntales.notevault.ui.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import come.stormborntales.notevault.data.repository.CollectionRepository
import come.stormborntales.notevault.data.repository.NoteRepository
@Suppress("UNCHECKED_CAST")
class NoteBrowserViewModelFactory (
private val noteRepository: NoteRepository,
private val collectionRepository: CollectionRepository
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(NoteBrowserViewModel::class.java)) {
return NoteBrowserViewModel(noteRepository, collectionRepository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}

View File

@ -61,7 +61,7 @@ class NoteViewModel(
}
}
fun addNote(context: Context, title: String, composer: String?, year: Int?, genre: String?, description: String?, selectedUris: List<Uri>, onDone:() -> Unit) {
fun addNote(context: Context, title: String, composer: String?, year: Int?, genre: String?, description: String?, selectedUris: List<Uri>, collectionID: Int?, onDone:() -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
val copiedUris = selectedUris.mapNotNull { uri ->
try {
@ -94,7 +94,7 @@ class NoteViewModel(
genre = genre,
description = description,
imagePreview = preview_image_path.toString(),
collectionId = null
collectionId = collectionID
)
repository.insert(note)

View File

@ -9,12 +9,14 @@ espressoCore = "3.6.1"
lifecycleRuntimeKtx = "2.8.7"
activityCompose = "1.8.0"
composeBom = "2024.04.01"
materialIconsExtended = "1.7.8"
navigationCompose = "2.8.9"
room = "2.7.1"
ksp="2.1.21-RC-2.0.0"
compose = "1.6.0" # oder was immer deine aktuelle Compose-Version ist
[libraries]
androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "materialIconsExtended" }
coil-compose = { group = "io.coil-kt", name = "coil-compose", version = "2.5.0" } # oder aktueller
compose-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata", version.ref = "compose" }
lifecycle-livedata-ktx = "androidx.lifecycle:lifecycle-livedata-ktx:2.8.7"