Compare commits

..

21 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
sebastian
41ab187f04 ADD: Datastructure for Note Collections 2025-05-10 08:21:58 +02:00
Sebastian Böckelmann
ad8e180134 ADD: Searching for Notes 2025-05-04 10:55:10 +02:00
Sebastian Böckelmann
c3029c062e ADD: Sidenav 2025-05-03 10:36:30 +02:00
Sebastian Böckelmann
5be8a19321 UPD: Make UI more responsive: Change Layout from "My Notes" 2025-05-03 09:56:44 +02:00
Sebastian Böckelmann
f20c8f0096 ADD: Edit Notes 2025-05-03 09:35:39 +02:00
Sebastian Böckelmann
58ce8cbd1c ADD: Delete Notes 2025-04-30 18:54:51 +02:00
24 changed files with 1459 additions and 179 deletions

1
.idea/.gitignore vendored
View File

@ -6,3 +6,4 @@
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
/AndroidProjectSystem.xml

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-04-29T17:33:41.575251940Z">
<DropdownSelection timestamp="2025-05-10T10:58:30.749991183Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=R52N50NLGRT" />

View File

@ -40,6 +40,9 @@
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>

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

@ -2,49 +2,82 @@ package come.stormborntales.notevault
import android.net.Uri
import android.os.Bundle
import android.webkit.MimeTypeMap
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.NavigationDrawerItem
import androidx.compose.material3.NavigationDrawerItemDefaults
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import come.stormborntales.notevault.data.local.AppDatabase
import come.stormborntales.notevault.data.model.NoteEntry
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.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.Dispatchers
import kotlinx.coroutines.launch
import java.io.File
import java.io.InputStream
class MainActivity : ComponentActivity() {
@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
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 context = LocalContext.current
// Globale Notenliste
val notes = remember { mutableStateListOf<NoteEntry>() }
// Bildauswahl + Dialog-States
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) }
val drawerState = rememberDrawerState(DrawerValue.Closed)
val scope = rememberCoroutineScope()
// Launcher innerhalb von Compose
val imagePickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenMultipleDocuments(),
onResult = { uris ->
@ -55,23 +88,145 @@ class MainActivity : ComponentActivity() {
}
)
// UI anzeigen
val openDialog: (NoteEntity?) -> Unit = { note ->
Log.d("EditNote", "NoteEntity: ${note?.title}")
noteToEdit = note
showDialog = true
}
val navItems = listOf(
"Home" to "main",
"Notes" to "notes",
"Einstellungen" to "settings"
)
ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = {
ModalDrawerSheet {
Text("Navigation", modifier = Modifier.padding(16.dp), style = MaterialTheme.typography.titleMedium)
navItems.forEach { (label, route) ->
NavigationDrawerItem(
label = { Text(label) },
selected = false,
onClick = {
navController.navigate(route) {
popUpTo(navController.graph.startDestinationId) { saveState = true }
launchSingleTop = true
restoreState = true
}
scope.launch { drawerState.close() }
},
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
)
}
}
}
) {
Scaffold(
topBar = {
var searchQuery by remember { mutableStateOf("") }
var isMenuExpanded by remember { mutableStateOf(false) }
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
TopAppBar(
title = {
if(currentRoute == "main") {
TextField(
value = searchQuery,
onValueChange = {
searchQuery = it
viewModel.searchQuery.value = it
},
placeholder = { Text("Suche...") },
singleLine = true,
modifier = Modifier
.fillMaxWidth()
.padding(end = 48.dp), // Platz für Avatar
textStyle = MaterialTheme.typography.bodyLarge,
colors = TextFieldDefaults.textFieldColors(
containerColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent
)
)
} else {
Text("NoteVault")
}
},
navigationIcon = {
IconButton(onClick = { scope.launch { drawerState.open() } }) {
Icon(Icons.Default.Menu, contentDescription = "Menü öffnen")
}
},
actions = {
Box {
IconButton(onClick = { isMenuExpanded = true }) {
Icon(
imageVector = Icons.Default.AccountCircle, // Avatar-Icon
contentDescription = "Benutzerprofil"
)
}
DropdownMenu(
expanded = isMenuExpanded,
onDismissRequest = { isMenuExpanded = false }
) {
DropdownMenuItem(
text = { Text("Profil") },
onClick = {
isMenuExpanded = false
// TODO: Navigiere ggf. zu Profilscreen
}
)
DropdownMenuItem(
text = { Text("Abmelden") },
onClick = {
isMenuExpanded = false
// TODO: Logout-Logik
}
)
}
}
}
)
}
) { innerPadding ->
NavHost(
navController = navController,
startDestination = "main",
modifier = Modifier.padding(innerPadding)
) {
composable("main") {
MainScreen(
viewModel = viewModel,
onAddNoteClicked = {
imagePickerLauncher.launch(
arrayOf("image/*")
imagePickerLauncher.launch(arrayOf("image/*"))
},
onEditNote = openDialog
)
}
)
composable("settings") {
SettingsScreen()
}
composable("notes") {
NotesScreen(collectionRepository, noteRepository, onImportNotes = {
imagePickerLauncher.launch(arrayOf("image/*"))
}, noteBrowserViewModel,
onEditNote = {
noteToEdit = it
showDialog = true
})
}
}
// Dialog bei Bedarf
if (showDialog) {
val context = LocalContext.current;
val scope = rememberCoroutineScope();
val context = LocalContext.current
AddNoteDialog(
onDismiss = { showDialog = false },
onSave = { title, composer, year, genre, description ->
if (noteToEdit == null) {
viewModel.addNote(
context = context,
title = title,
@ -80,12 +235,31 @@ class MainActivity : ComponentActivity() {
genre = genre,
description = description,
selectedUris = selectedUris,
collectionID = noteBrowserViewModel.currentParentId.value,
onDone = { showDialog = false }
)
} else {
viewModel.updateNote(
editedNote = noteToEdit!!,
updatedTitle = title,
updatedComposer = composer,
updatedYear = year,
updatedGenre = genre,
updatedDescription = description,
onDone = { showDialog = false }
)
}
},
initialTitle = noteToEdit?.title ?: "",
initialComposer = noteToEdit?.composer,
initialYear = noteToEdit?.year,
initialGenre = noteToEdit?.genre,
initialDescription = noteToEdit?.description
)
}
}
}
}
}
}
}

View File

@ -6,13 +6,16 @@ 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
@Database(entities = [NoteEntity::class], version = 1)
@Database(entities = [NoteEntity::class, NoteCollection::class], version = 1)
@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

@ -5,6 +5,7 @@ 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.NoteEntity
import kotlinx.coroutines.flow.Flow
@ -18,4 +19,9 @@ interface NoteDao {
@Delete
suspend fun delete(note: NoteEntity)
@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,11 @@
package come.stormborntales.notevault.data.local.entity
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity("note_collections")
data class NoteCollection(
@PrimaryKey(autoGenerate = true) var id: Int = 0,
var name: String,
var parentId: Int? = null
)

View File

@ -1,16 +1,24 @@
package come.stormborntales.notevault.data.local.entity
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
@Entity(tableName = "notes")
@Entity(tableName = "notes",
foreignKeys = [ForeignKey(
entity = NoteCollection::class,
parentColumns = ["id"],
childColumns = ["collectionId"],
onDelete = ForeignKey.CASCADE
)])
data class NoteEntity(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
val title: String,
var title: String,
val images: List<String>, // oder String + TypeConverter
val composer: String?,
val year: Int?,
val genre: String?,
val description: String?,
val imagePreview: String
var composer: String?,
var year: Int?,
var genre: String?,
var description: String?,
val imagePreview: String,
val collectionId: Int?
)

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

@ -9,4 +9,7 @@ class NoteRepository(private val dao: NoteDao) {
suspend fun insert(note: NoteEntity) = dao.insert(note)
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

@ -14,15 +14,22 @@ import androidx.compose.ui.unit.dp
@Composable
fun AddNoteDialog(
onDismiss: () -> Unit,
onSave: (title: String, composer: String?, year: Int?, genre: String?, description: String?) -> Unit
onSave: (title: String, composer: String?, year: Int?, genre: String?, description: String?) -> Unit,
initialTitle: String = "",
initialComposer: String? = null,
initialYear: Int? = null,
initialGenre: String? = null,
initialDescription: String? = null,
collectionId: Int? = null
) {
var title by remember { mutableStateOf("") }
var composer by remember { mutableStateOf("") }
var yearText by remember { mutableStateOf("") }
var genre by remember { mutableStateOf("") }
var description by remember { mutableStateOf("") }
var title by remember { mutableStateOf(initialTitle) }
var composer by remember { mutableStateOf(initialComposer ?: "") }
var yearText by remember { mutableStateOf(initialYear?.toString() ?: "") }
var genre by remember { mutableStateOf(initialGenre ?: "") }
var description by remember { mutableStateOf(initialDescription ?: "") }
var showTitleError by remember { mutableStateOf(false) }
AlertDialog(
onDismissRequest = onDismiss,
confirmButton = {

View File

@ -6,10 +6,13 @@ import android.graphics.BitmapFactory
import android.graphics.ImageDecoder
import android.media.Image
import android.net.Uri
import android.util.Log
import androidx.compose.foundation.Image
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
@ -21,6 +24,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import come.stormborntales.notevault.FullscreenImageViewerActivity
@ -29,6 +33,7 @@ import come.stormborntales.notevault.data.model.NoteEntry
import come.stormborntales.notevault.ui.viewmodel.NoteViewModel
import java.io.InputStream
import androidx.core.net.toUri
import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.AsyncImage
fun loadImageBitmap(context: Context, uriString: String): ImageBitmap? {
@ -41,12 +46,14 @@ fun loadImageBitmap(context: Context, uriString: String): ImageBitmap? {
e.printStackTrace()
null
}
}
@Composable
fun NoteCard(note: NoteEntity) {
}@Composable
fun NoteCard(note: NoteEntity, onDeleteNote: (NoteEntity) -> Unit, onEditNote: (NoteEntity) -> Unit) {
val context = LocalContext.current
val screenWidth = LocalConfiguration.current.screenWidthDp // Bildschirmbreite in dp
// Dynamische Bildgröße basierend auf der Bildschirmbreite
val imageSize = if (screenWidth < 400) 80.dp else 120.dp
Card(
modifier = Modifier
@ -55,22 +62,105 @@ fun NoteCard(note: NoteEntity) {
shape = RoundedCornerShape(16.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
) {
// Responsive Layout: Überprüfen, ob der Bildschirm schmaler als 360 dp ist
if (screenWidth < 400) {
// Wenn der Bildschirm schmal ist, arrangiere die Elemente vertikal
Row(modifier = Modifier.padding(16.dp)) {
AsyncImage(
model = note.imagePreview,
contentDescription = "Vorschaubild",
modifier = Modifier
.size(imageSize)
.clip(RoundedCornerShape(12.dp))
)
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = note.title,
style = MaterialTheme.typography.titleMedium
)
note.composer?.let {
Text("von $it", style = MaterialTheme.typography.labelMedium)
}
Spacer(modifier = Modifier.height(4.dp))
note.year?.let {
Text("Jahr: $it", style = MaterialTheme.typography.bodySmall)
}
note.genre?.let {
Text("Genre: $it", style = MaterialTheme.typography.bodySmall)
}
note.description?.let {
Text("Beschreibung: $it", style = MaterialTheme.typography.bodySmall, maxLines = 2)
}
Spacer(modifier = Modifier.height(16.dp)) // Abstand zwischen Text und Buttons
}
}
// Buttons unter dem Text
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth().padding(16.dp),
) {
OutlinedButton(
onClick = {
val uris = ArrayList<Uri>()
note.images.forEach { uris.add(it.toUri()) }
val intent = Intent(context, FullscreenImageViewerActivity::class.java).apply {
putParcelableArrayListExtra("imageUris", ArrayList(uris))
}
context.startActivity(intent)
},
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp)
) {
Text("Anzeigen", style = MaterialTheme.typography.labelLarge)
}
OutlinedButton(
onClick = {
onEditNote(note)
},
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp)
) {
Text("Bearbeiten", style = MaterialTheme.typography.labelLarge)
}
OutlinedButton(
onClick = {
onDeleteNote(note)
},
colors = ButtonDefaults.outlinedButtonColors(
contentColor = MaterialTheme.colorScheme.error
),
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp)
) {
Text("Löschen", style = MaterialTheme.typography.labelLarge)
}
}
} else {
// Wenn der Bildschirm breiter als 360 dp ist, arrangiere die Elemente nebeneinander
Row(modifier = Modifier.padding(16.dp)) {
// Linkes Vorschaubild
AsyncImage(
model = note.imagePreview,
contentDescription = "Vorschaubild",
modifier = Modifier
.size(120.dp)
.size(imageSize)
.clip(RoundedCornerShape(12.dp))
)
// Rechte Info-Spalte
Column(
modifier = Modifier
.weight(1f)
.weight(1f) // Damit die Spalte den verfügbaren Platz nutzt
.align(Alignment.CenterVertically)
.padding(start=16.dp)
.padding(start = 16.dp)
) {
Text(
text = note.title,
@ -97,12 +187,14 @@ fun NoteCard(note: NoteEntity) {
Spacer(modifier = Modifier.height(8.dp))
// Buttons in einer Row anordnen
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth() // Stellt sicher, dass die Row den gesamten verfügbaren Platz einnimmt
) {
OutlinedButton(
onClick = {
val uris = ArrayList<Uri>();
val uris = ArrayList<Uri>()
note.images.forEach { uris.add(it.toUri()) }
val intent = Intent(context, FullscreenImageViewerActivity::class.java).apply {
@ -114,14 +206,20 @@ fun NoteCard(note: NoteEntity) {
) {
Text("Anzeigen", style = MaterialTheme.typography.labelLarge)
}
OutlinedButton(
onClick = { /* TODO */ },
onClick = {
onEditNote(note)
},
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp)
) {
Text("Bearbeiten", style = MaterialTheme.typography.labelLarge)
}
OutlinedButton(
onClick = { /* TODO */ },
onClick = {
onDeleteNote(note)
},
colors = ButtonDefaults.outlinedButtonColors(
contentColor = MaterialTheme.colorScheme.error
),
@ -133,6 +231,7 @@ fun NoteCard(note: NoteEntity) {
}
}
}
}
}
@ -140,15 +239,11 @@ fun NoteCard(note: NoteEntity) {
@Composable
fun MainScreen(
onAddNoteClicked: () -> Unit, // Übergib hier deine Logik für den Import
viewModel: NoteViewModel
viewModel: NoteViewModel,
onEditNote: (NoteEntity) -> Unit
) {
val notes by viewModel.notes.observeAsState(emptyList())
val notes by viewModel.filteredNotes.collectAsState(initial = emptyList())
Scaffold(
topBar = {
TopAppBar(
title = { Text("Meine Noten") }
)
},
floatingActionButton = {
FloatingActionButton(
onClick = onAddNoteClicked
@ -165,7 +260,9 @@ fun MainScreen(
.padding(16.dp)
) {
items(notes) { note ->
NoteCard(note = note)
NoteCard(note = note, onDeleteNote = { noteEntity ->
viewModel.deleteNote(noteEntity)
}, onEditNote = onEditNote)
}
}
}

View File

@ -0,0 +1,79 @@
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 SettingsScreen() {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
Text(
text = "Einstellungen",
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(bottom = 16.dp)
)
Divider()
Spacer(modifier = Modifier.height(16.dp))
// Beispielhafte Einstellung
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Text(
text = "Dark Mode",
modifier = Modifier.weight(1f),
style = MaterialTheme.typography.bodyLarge
)
Switch(
checked = false,
onCheckedChange = { /* TODO: Dark Mode aktivieren */ }
)
}
Spacer(modifier = Modifier.height(16.dp))
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Text(
text = "Benachrichtigungen",
modifier = Modifier.weight(1f),
style = MaterialTheme.typography.bodyLarge
)
Switch(
checked = true,
onCheckedChange = { /* TODO: Benachrichtigungseinstellungen */ }
)
}
Spacer(modifier = Modifier.height(32.dp))
OutlinedButton(
onClick = { /* TODO: Impressum anzeigen */ },
modifier = Modifier.fillMaxWidth()
) {
Text("Impressum")
}
}
}

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

@ -14,13 +14,35 @@ import come.stormborntales.notevault.data.repository.NoteRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File
import java.io.InputStream
import androidx.core.graphics.scale
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
class NoteViewModel(
private val repository: NoteRepository,
) : ViewModel() {
val notes = repository.allNotes.asLiveData()
// Sucheingabe als StateFlow
val searchQuery = MutableStateFlow("")
// Alle Notizen als Flow aus der Datenbank (NICHT blockierend!)
private val allNotes: Flow<List<NoteEntity>> = repository.allNotes
// Gefilterte Notizen basierend auf Sucheingabe
val filteredNotes: StateFlow<List<NoteEntity>> = combine(searchQuery, allNotes) { query, notes ->
if (query.isBlank()) {
notes
} else {
notes.filter {
it.title.contains(query, ignoreCase = true) ||
it.description?.contains(query, ignoreCase = true) == true
}
}
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
fun createPreviewImage(context: Context, uri: Uri): File? {
return try {
@ -39,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 {
@ -71,7 +93,8 @@ class NoteViewModel(
year = year,
genre = genre,
description = description,
imagePreview = preview_image_path.toString()
imagePreview = preview_image_path.toString(),
collectionId = collectionID
)
repository.insert(note)
@ -86,4 +109,26 @@ class NoteViewModel(
repository.delete(note)
}
}
fun updateNote(
editedNote: NoteEntity,
updatedTitle: String,
updatedComposer: String?,
updatedYear: Int?,
updatedGenre: String?,
updatedDescription: String?,
onDone: () -> Unit
) {
viewModelScope.launch {
editedNote.title = updatedTitle
editedNote.year = updatedYear;
editedNote.composer = updatedComposer
editedNote.genre = updatedGenre
editedNote.description = updatedDescription
repository.update(editedNote)
onDone()
}
}
}

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"