Compare commits
12 Commits
nextNoteVa
...
master
Author | SHA1 | Date | |
---|---|---|---|
58d2658dc4 | |||
|
41ab187f04 | ||
|
ad8e180134 | ||
|
c3029c062e | ||
|
5be8a19321 | ||
|
f20c8f0096 | ||
|
58ce8cbd1c | ||
|
7d65f8e725 | ||
|
f259897593 | ||
|
128935102b | ||
|
dc5be58b30 | ||
|
0a33c12cde |
1
.idea/.gitignore
vendored
@ -6,3 +6,4 @@
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
/AndroidProjectSystem.xml
|
||||
|
@ -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-03T08:34:29.354537334Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=R52N50NLGRT" />
|
||||
|
@ -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>
|
||||
|
@ -61,6 +61,6 @@ dependencies {
|
||||
implementation(libs.androidx.room.ktx)
|
||||
implementation(libs.lifecycle.livedata.ktx)
|
||||
implementation(libs.compose.runtime.livedata)
|
||||
|
||||
implementation(libs.coil.compose)
|
||||
ksp(libs.androidx.room.compiler)
|
||||
}
|
BIN
app/src/main/ic_launcher-playstore.png
Normal file
After Width: | Height: | Size: 197 KiB |
@ -2,29 +2,59 @@ 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.NoteRepository
|
||||
import come.stormborntales.notevault.ui.screens.AddNoteDialog
|
||||
import come.stormborntales.notevault.ui.screens.MainScreen
|
||||
import come.stormborntales.notevault.ui.screens.NotesScreen
|
||||
import come.stormborntales.notevault.ui.screens.SettingsScreen
|
||||
import come.stormborntales.notevault.ui.theme.NoteVaultTheme
|
||||
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
|
||||
@ -34,17 +64,15 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
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
|
||||
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 +83,139 @@ 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()
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
@ -82,10 +226,28 @@ class MainActivity : ComponentActivity() {
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,9 +7,10 @@ import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
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
|
||||
|
@ -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,7 @@ interface NoteDao {
|
||||
|
||||
@Delete
|
||||
suspend fun delete(note: NoteEntity)
|
||||
|
||||
@Update
|
||||
suspend fun update(note: NoteEntity)
|
||||
}
|
@ -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
|
||||
)
|
@ -1,15 +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?
|
||||
var composer: String?,
|
||||
var year: Int?,
|
||||
var genre: String?,
|
||||
var description: String?,
|
||||
val imagePreview: String,
|
||||
val collectionId: Int?
|
||||
)
|
@ -9,4 +9,6 @@ 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);
|
||||
}
|
||||
|
@ -14,15 +14,21 @@ 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
|
||||
) {
|
||||
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 = {
|
||||
|
@ -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,8 @@ 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? {
|
||||
return try {
|
||||
@ -40,14 +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 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(
|
||||
modifier = Modifier
|
||||
@ -56,24 +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)) {
|
||||
// Linkes Vorschaubild
|
||||
imageBitmap?.let {
|
||||
Image(
|
||||
bitmap = it,
|
||||
AsyncImage(
|
||||
model = note.imagePreview,
|
||||
contentDescription = "Vorschaubild",
|
||||
modifier = Modifier
|
||||
.size(120.dp)
|
||||
.size(imageSize)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
)
|
||||
Spacer(modifier = Modifier.width(16.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(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)
|
||||
) {
|
||||
Text(
|
||||
text = note.title,
|
||||
@ -100,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 {
|
||||
@ -117,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
|
||||
),
|
||||
@ -136,6 +231,7 @@ fun NoteCard(note: NoteEntity) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -143,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
|
||||
@ -168,7 +260,9 @@ fun MainScreen(
|
||||
.padding(16.dp)
|
||||
) {
|
||||
items(notes) { note ->
|
||||
NoteCard(note = note)
|
||||
NoteCard(note = note, onDeleteNote = { noteEntity ->
|
||||
viewModel.deleteNote(noteEntity)
|
||||
}, onEditNote = onEditNote)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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() {
|
||||
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
package come.stormborntales.notevault.ui.viewmodel
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import android.webkit.MimeTypeMap
|
||||
@ -12,11 +14,52 @@ import come.stormborntales.notevault.data.repository.NoteRepository
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
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(
|
||||
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 {
|
||||
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) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
@ -41,13 +84,17 @@ class NoteViewModel(
|
||||
}
|
||||
|
||||
if (copiedUris.isNotEmpty()) {
|
||||
val preview_image_path = createPreviewImage(context, uri = selectedUris[0])
|
||||
|
||||
val note = NoteEntity(
|
||||
title = title,
|
||||
images = copiedUris, // muss als List<String> gespeichert sein
|
||||
composer = composer,
|
||||
year = year,
|
||||
genre = genre,
|
||||
description = description
|
||||
description = description,
|
||||
imagePreview = preview_image_path.toString(),
|
||||
collectionId = null
|
||||
)
|
||||
|
||||
repository.insert(note)
|
||||
@ -62,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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<background android:drawable="@mipmap/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<background android:drawable="@mipmap/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_background.webp
Normal file
After Width: | Height: | Size: 44 B |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
Normal file
After Width: | Height: | Size: 8.6 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 5.8 KiB |
Before Width: | Height: | Size: 982 B After Width: | Height: | Size: 2.2 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_background.webp
Normal file
After Width: | Height: | Size: 44 B |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
Normal file
After Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 5.5 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_background.webp
Normal file
After Width: | Height: | Size: 46 B |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
Normal file
After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 8.7 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 9.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp
Normal file
After Width: | Height: | Size: 52 B |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
Normal file
After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 15 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp
Normal file
After Width: | Height: | Size: 52 B |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
Normal file
After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 24 KiB |
@ -15,6 +15,7 @@ ksp="2.1.21-RC-2.0.0"
|
||||
compose = "1.6.0" # oder was immer deine aktuelle Compose-Version ist
|
||||
|
||||
[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" }
|
||||
lifecycle-livedata-ktx = "androidx.lifecycle:lifecycle-livedata-ktx:2.8.7"
|
||||
androidx-compose-bom-v20240100 = { module = "androidx.compose:compose-bom", version.ref = "composeBomVersion" }
|
||||
|