Compare commits

...

12 Commits

Author SHA1 Message Date
58d2658dc4 Merge pull request 'nextNoteVault' (#23) from nextNoteVault into master
Reviewed-on: #23
2025-05-10 06:23:23 +00: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
Fawkes100
7d65f8e725 Merge remote-tracking branch 'origin/nextNoteVault' into nextNoteVault 2025-04-29 21:08:58 +02:00
Fawkes100
f259897593 ADD: Change Logo 2025-04-29 21:08:55 +02:00
Fawkes100
128935102b ADD: Change Logo 2025-04-29 21:08:47 +02:00
Fawkes100
dc5be58b30 ADD: Further Performance Improvements by storing a special preview image 2025-04-29 20:54:00 +02:00
Fawkes100
0a33c12cde UPD: Improve Performance by using coil to load image preview 2025-04-29 20:34:17 +02:00
39 changed files with 602 additions and 138 deletions

1
.idea/.gitignore vendored
View File

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

View File

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

View File

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

View File

@ -61,6 +61,6 @@ dependencies {
implementation(libs.androidx.room.ktx) implementation(libs.androidx.room.ktx)
implementation(libs.lifecycle.livedata.ktx) implementation(libs.lifecycle.livedata.ktx)
implementation(libs.compose.runtime.livedata) implementation(libs.compose.runtime.livedata)
implementation(libs.coil.compose)
ksp(libs.androidx.room.compiler) ksp(libs.androidx.room.compiler)
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

View File

@ -2,29 +2,59 @@ package come.stormborntales.notevault
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.webkit.MimeTypeMap import android.util.Log
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts 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.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel 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.local.AppDatabase
import come.stormborntales.notevault.data.model.NoteEntry import come.stormborntales.notevault.data.local.entity.NoteEntity
import come.stormborntales.notevault.data.repository.NoteRepository import come.stormborntales.notevault.data.repository.NoteRepository
import come.stormborntales.notevault.ui.screens.AddNoteDialog import come.stormborntales.notevault.ui.screens.AddNoteDialog
import come.stormborntales.notevault.ui.screens.MainScreen import come.stormborntales.notevault.ui.screens.MainScreen
import come.stormborntales.notevault.ui.screens.NotesScreen
import come.stormborntales.notevault.ui.screens.SettingsScreen
import come.stormborntales.notevault.ui.theme.NoteVaultTheme import come.stormborntales.notevault.ui.theme.NoteVaultTheme
import come.stormborntales.notevault.ui.viewmodel.NoteViewModel import come.stormborntales.notevault.ui.viewmodel.NoteViewModel
import come.stormborntales.notevault.ui.viewmodel.NoteViewModelFactory import come.stormborntales.notevault.ui.viewmodel.NoteViewModelFactory
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File
import java.io.InputStream
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val applicationContext = applicationContext val applicationContext = applicationContext
@ -34,17 +64,15 @@ class MainActivity : ComponentActivity() {
setContent { setContent {
NoteVaultTheme { NoteVaultTheme {
val navController = rememberNavController()
val viewModel: NoteViewModel = viewModel(factory = viewModelFactory) val viewModel: NoteViewModel = viewModel(factory = viewModelFactory)
val context = LocalContext.current
// Globale Notenliste
val notes = remember { mutableStateListOf<NoteEntry>() }
// Bildauswahl + Dialog-States
var selectedUris by remember { mutableStateOf<List<Uri>>(emptyList()) } var selectedUris by remember { mutableStateOf<List<Uri>>(emptyList()) }
var showDialog by remember { mutableStateOf(false) } 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( val imagePickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenMultipleDocuments(), contract = ActivityResultContracts.OpenMultipleDocuments(),
onResult = { uris -> onResult = { uris ->
@ -55,35 +83,169 @@ class MainActivity : ComponentActivity() {
} }
) )
// UI anzeigen val openDialog: (NoteEntity?) -> Unit = { note ->
MainScreen( Log.d("EditNote", "NoteEntity: ${note?.title}")
viewModel = viewModel, noteToEdit = note
onAddNoteClicked = { showDialog = true
imagePickerLauncher.launch( }
arrayOf("image/*")
) val navItems = listOf(
} "Home" to "main",
"Notes" to "notes",
"Einstellungen" to "settings"
) )
// Dialog bei Bedarf ModalNavigationDrawer(
if (showDialog) { drawerState = drawerState,
val context = LocalContext.current; drawerContent = {
val scope = rememberCoroutineScope(); ModalDrawerSheet {
AddNoteDialog( Text("Navigation", modifier = Modifier.padding(16.dp), style = MaterialTheme.typography.titleMedium)
onDismiss = { showDialog = false }, navItems.forEach { (label, route) ->
onSave = { title, composer, year, genre, description -> NavigationDrawerItem(
viewModel.addNote( label = { Text(label) },
context = context, selected = false,
title = title, onClick = {
composer = composer, navController.navigate(route) {
year = year, popUpTo(navController.graph.startDestinationId) { saveState = true }
genre = genre, launchSingleTop = true
description = description, restoreState = true
selectedUris = selectedUris, }
onDone = { showDialog = false } 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/*"))
},
onEditNote = openDialog
)
}
composable("settings") {
SettingsScreen()
}
composable("notes") {
NotesScreen()
}
}
if (showDialog) {
val context = LocalContext.current
AddNoteDialog(
onDismiss = { showDialog = false },
onSave = { title, composer, year, genre, description ->
if (noteToEdit == null) {
viewModel.addNote(
context = context,
title = title,
composer = composer,
year = year,
genre = genre,
description = description,
selectedUris = selectedUris,
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

@ -7,9 +7,10 @@ import androidx.room.Room
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.TypeConverters import androidx.room.TypeConverters
import come.stormborntales.notevault.data.local.dao.NoteDao 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 come.stormborntales.notevault.data.local.entity.NoteEntity
@Database(entities = [NoteEntity::class], version = 1) @Database(entities = [NoteEntity::class, NoteCollection::class], version = 1)
@TypeConverters(UriListConverter::class) @TypeConverters(UriListConverter::class)
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
abstract fun noteDao(): NoteDao abstract fun noteDao(): NoteDao

View File

@ -5,6 +5,7 @@ import androidx.room.Delete
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import androidx.room.Update
import come.stormborntales.notevault.data.local.entity.NoteEntity import come.stormborntales.notevault.data.local.entity.NoteEntity
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -18,4 +19,7 @@ interface NoteDao {
@Delete @Delete
suspend fun delete(note: NoteEntity) suspend fun delete(note: NoteEntity)
@Update
suspend fun update(note: 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,15 +1,24 @@
package come.stormborntales.notevault.data.local.entity package come.stormborntales.notevault.data.local.entity
import androidx.room.Entity import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey 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( data class NoteEntity(
@PrimaryKey(autoGenerate = true) val id: Int = 0, @PrimaryKey(autoGenerate = true) val id: Int = 0,
val title: String, var title: String,
val images: List<String>, // oder String + TypeConverter val images: List<String>, // oder String + TypeConverter
val composer: String?, var composer: String?,
val year: Int?, var year: Int?,
val genre: String?, var genre: String?,
val description: String? var description: String?,
) val imagePreview: String,
val collectionId: Int?
)

View File

@ -9,4 +9,6 @@ class NoteRepository(private val dao: NoteDao) {
suspend fun insert(note: NoteEntity) = dao.insert(note) suspend fun insert(note: NoteEntity) = dao.insert(note)
suspend fun delete(note: NoteEntity) = dao.delete(note) suspend fun delete(note: NoteEntity) = dao.delete(note)
suspend fun update(note: NoteEntity) = dao.update(note);
} }

View File

@ -14,15 +14,21 @@ import androidx.compose.ui.unit.dp
@Composable @Composable
fun AddNoteDialog( fun AddNoteDialog(
onDismiss: () -> Unit, 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
) { ) {
var title by remember { mutableStateOf("") } var title by remember { mutableStateOf(initialTitle) }
var composer by remember { mutableStateOf("") } var composer by remember { mutableStateOf(initialComposer ?: "") }
var yearText by remember { mutableStateOf("") } var yearText by remember { mutableStateOf(initialYear?.toString() ?: "") }
var genre by remember { mutableStateOf("") } var genre by remember { mutableStateOf(initialGenre ?: "") }
var description by remember { mutableStateOf("") } var description by remember { mutableStateOf(initialDescription ?: "") }
var showTitleError by remember { mutableStateOf(false) } var showTitleError by remember { mutableStateOf(false) }
AlertDialog( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
confirmButton = { confirmButton = {

View File

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

View File

@ -0,0 +1,24 @@
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,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

@ -1,6 +1,8 @@
package come.stormborntales.notevault.ui.viewmodel package come.stormborntales.notevault.ui.viewmodel
import android.content.Context import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import android.util.Log import android.util.Log
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
@ -12,11 +14,52 @@ import come.stormborntales.notevault.data.repository.NoteRepository
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File import java.io.File
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( class NoteViewModel(
private val repository: NoteRepository, private val repository: NoteRepository,
) : ViewModel() { ) : 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 {
val inputStream = context.contentResolver.openInputStream(uri)
val originalBitmap = BitmapFactory.decodeStream(inputStream) ?: return null
val previewBitmap = originalBitmap.scale(128, 128, false)
val previewFile = File(context.filesDir, "preview_${System.currentTimeMillis()}.jpg")
previewFile.outputStream().use { out ->
previewBitmap.compress(Bitmap.CompressFormat.JPEG, 75, out)
}
previewFile
} catch (e: Exception) {
e.printStackTrace()
null
}
}
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>, onDone:() -> Unit) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
@ -41,13 +84,17 @@ class NoteViewModel(
} }
if (copiedUris.isNotEmpty()) { if (copiedUris.isNotEmpty()) {
val preview_image_path = createPreviewImage(context, uri = selectedUris[0])
val note = NoteEntity( val note = NoteEntity(
title = title, title = title,
images = copiedUris, // muss als List<String> gespeichert sein images = copiedUris, // muss als List<String> gespeichert sein
composer = composer, composer = composer,
year = year, year = year,
genre = genre, genre = genre,
description = description description = description,
imagePreview = preview_image_path.toString(),
collectionId = null
) )
repository.insert(note) repository.insert(note)
@ -62,4 +109,26 @@ class NoteViewModel(
repository.delete(note) 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

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/> <background android:drawable="@mipmap/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/> <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon> </adaptive-icon>

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/> <background android:drawable="@mipmap/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/> <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon> </adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 B

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -15,6 +15,7 @@ ksp="2.1.21-RC-2.0.0"
compose = "1.6.0" # oder was immer deine aktuelle Compose-Version ist compose = "1.6.0" # oder was immer deine aktuelle Compose-Version ist
[libraries] [libraries]
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" } compose-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata", version.ref = "compose" }
lifecycle-livedata-ktx = "androidx.lifecycle:lifecycle-livedata-ktx:2.8.7" lifecycle-livedata-ktx = "androidx.lifecycle:lifecycle-livedata-ktx:2.8.7"
androidx-compose-bom-v20240100 = { module = "androidx.compose:compose-bom", version.ref = "composeBomVersion" } androidx-compose-bom-v20240100 = { module = "androidx.compose:compose-bom", version.ref = "composeBomVersion" }