Compare commits

..

3 Commits

Author SHA1 Message Date
Fawkes100
e70d8989f1 WIP: Persist NoteEntities 2025-04-29 20:19:21 +02:00
Fawkes100
3f661e8798 WIP: Persist NoteEntities 2025-04-29 19:40:28 +02:00
Fawkes100
ba0cbcef09 WIP: Persist NoteEntities 2025-04-29 19:21:13 +02:00
15 changed files with 303 additions and 50 deletions

3
.gitignore vendored
View File

@ -16,3 +16,6 @@ local.properties
/music_database
/music_database-shm
/music_database-wal
/note_database
/note_database-shm
/note_database-wal

71
.idea/dataSources.xml Normal file
View File

@ -0,0 +1,71 @@
<?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">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/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>
</component>
</project>

View File

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

View File

@ -2,6 +2,7 @@ plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.ksp) // NEU
}
android {
@ -56,4 +57,10 @@ dependencies {
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
implementation(libs.androidx.foundation)
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.room.ktx)
implementation(libs.lifecycle.livedata.ktx)
implementation(libs.compose.runtime.livedata)
ksp(libs.androidx.room.compiler)
}

View File

@ -10,10 +10,15 @@ import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.viewmodel.compose.viewModel
import come.stormborntales.notevault.data.local.AppDatabase
import come.stormborntales.notevault.data.model.NoteEntry
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.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
@ -22,9 +27,14 @@ import java.io.InputStream
class MainActivity : ComponentActivity() {
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)
setContent {
NoteVaultTheme {
val viewModel: NoteViewModel = viewModel(factory = viewModelFactory)
val context = LocalContext.current
// Globale Notenliste
@ -47,7 +57,7 @@ class MainActivity : ComponentActivity() {
// UI anzeigen
MainScreen(
notes = notes,
viewModel = viewModel,
onAddNoteClicked = {
imagePickerLauncher.launch(
arrayOf("image/*")
@ -62,43 +72,16 @@ class MainActivity : ComponentActivity() {
AddNoteDialog(
onDismiss = { showDialog = false },
onSave = { title, composer, year, genre, description ->
scope.launch(Dispatchers.IO) {
val copiedUris = selectedUris.mapNotNull { uri ->
try {
val inputStream: InputStream? = context.contentResolver.openInputStream(uri)
val extension = MimeTypeMap.getSingleton()
.getExtensionFromMimeType(context.contentResolver.getType(uri)) ?: "jpg" // Fallback
val outputFile = File(context.filesDir, "note_${System.currentTimeMillis()}.$extension")
inputStream?.use { input ->
outputFile.outputStream().use { output ->
input.copyTo(output)
}
}
Uri.fromFile(outputFile)
} catch (e: Exception) {
e.printStackTrace()
null
}
}
if (copiedUris.isNotEmpty()) {
notes.add(
NoteEntry(
title = title,
images = copiedUris,
composer = composer,
year = year,
genre = genre,
description = description
)
)
}
showDialog = false
}
viewModel.addNote(
context = context,
title = title,
composer = composer,
year = year,
genre = genre,
description = description,
selectedUris = selectedUris,
onDone = { showDialog = false }
)
}
)
}

View File

@ -0,0 +1,34 @@
package come.stormborntales.notevault.data.local
import android.content.Context
import android.util.Log
import androidx.room.Database
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.NoteEntity
@Database(entities = [NoteEntity::class], version = 1)
@TypeConverters(UriListConverter::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun noteDao(): NoteDao
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getDatabase(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"note_database"
).build()
INSTANCE = instance
Log.d("AppDatabase", "Datenbank erstellt: ${instance.openHelper.writableDatabase}")
instance
}
}
}
}

View File

@ -0,0 +1,11 @@
package come.stormborntales.notevault.data.local
import androidx.room.TypeConverter
class UriListConverter {
@TypeConverter
fun fromList(list: List<String>): String = list.joinToString(",")
@TypeConverter
fun toList(data: String): List<String> = if (data.isBlank()) emptyList() else data.split(",")
}

View File

@ -0,0 +1,21 @@
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 come.stormborntales.notevault.data.local.entity.NoteEntity
import kotlinx.coroutines.flow.Flow
@Dao
interface NoteDao {
@Query("SELECT * FROM notes")
fun getAll(): Flow<List<NoteEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(note: NoteEntity)
@Delete
suspend fun delete(note: NoteEntity)
}

View File

@ -0,0 +1,15 @@
package come.stormborntales.notevault.data.local.entity
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "notes")
data class NoteEntity(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
val title: String,
val images: List<String>, // oder String + TypeConverter
val composer: String?,
val year: Int?,
val genre: String?,
val description: String?
)

View File

@ -0,0 +1,12 @@
package come.stormborntales.notevault.data.repository
import come.stormborntales.notevault.data.local.dao.NoteDao
import come.stormborntales.notevault.data.local.entity.NoteEntity
import kotlinx.coroutines.flow.Flow
class NoteRepository(private val dao: NoteDao) {
val allNotes: Flow<List<NoteEntity>> = dao.getAll()
suspend fun insert(note: NoteEntity) = dao.insert(note)
suspend fun delete(note: NoteEntity) = dao.delete(note)
}

View File

@ -31,7 +31,7 @@ fun FullscreenImageViewer(
HorizontalPager(state = pagerState) { page ->
val context = LocalContext.current
val imageBitmap = remember(images[page]) {
loadImageBitmap(context, images[page])
loadImageBitmap(context, images[page].toString())
}
imageBitmap?.let {

View File

@ -15,6 +15,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -23,11 +24,15 @@ import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import come.stormborntales.notevault.FullscreenImageViewerActivity
import come.stormborntales.notevault.data.local.entity.NoteEntity
import come.stormborntales.notevault.data.model.NoteEntry
import come.stormborntales.notevault.ui.viewmodel.NoteViewModel
import java.io.InputStream
import androidx.core.net.toUri
fun loadImageBitmap(context: Context, uri: Uri): ImageBitmap? {
fun loadImageBitmap(context: Context, uriString: String): ImageBitmap? {
return try {
val uri = uriString.toUri()
val inputStream: InputStream? = context.contentResolver.openInputStream(uri)
val bitmap = BitmapFactory.decodeStream(inputStream)
bitmap?.asImageBitmap()
@ -38,7 +43,7 @@ fun loadImageBitmap(context: Context, uri: Uri): ImageBitmap? {
}
@Composable
fun NoteCard(note: NoteEntry) {
fun NoteCard(note: NoteEntity) {
val context = LocalContext.current
val imageBitmap = remember(note.images) {
note.images.firstOrNull()?.let { loadImageBitmap(context, it) }
@ -100,8 +105,11 @@ fun NoteCard(note: NoteEntry) {
) {
OutlinedButton(
onClick = {
val uris = ArrayList<Uri>();
note.images.forEach { uris.add(it.toUri()) }
val intent = Intent(context, FullscreenImageViewerActivity::class.java).apply {
putParcelableArrayListExtra("imageUris", ArrayList(note.images))
putParcelableArrayListExtra("imageUris", ArrayList(uris))
}
context.startActivity(intent)
},
@ -135,12 +143,9 @@ fun NoteCard(note: NoteEntry) {
@Composable
fun MainScreen(
onAddNoteClicked: () -> Unit, // Übergib hier deine Logik für den Import
notes: MutableList<NoteEntry>, // Liste der Notenbilder (hier als URI),
viewModel: NoteViewModel
) {
val dummyNotes = notes
var noteToShow by remember { mutableStateOf<NoteEntry?>(null) }
// Hole den aktuellen Context
val context = LocalContext.current
val notes by viewModel.notes.observeAsState(emptyList())
Scaffold(
topBar = {
TopAppBar(
@ -162,7 +167,7 @@ fun MainScreen(
.fillMaxSize()
.padding(16.dp)
) {
items(dummyNotes) { note ->
items(notes) { note ->
NoteCard(note = note)
}
}

View File

@ -0,0 +1,65 @@
package come.stormborntales.notevault.ui.viewmodel
import android.content.Context
import android.net.Uri
import android.util.Log
import android.webkit.MimeTypeMap
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import come.stormborntales.notevault.data.local.entity.NoteEntity
import come.stormborntales.notevault.data.repository.NoteRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File
class NoteViewModel(
private val repository: NoteRepository,
) : ViewModel() {
val notes = repository.allNotes.asLiveData()
fun addNote(context: Context, title: String, composer: String?, year: Int?, genre: String?, description: String?, selectedUris: List<Uri>, onDone:() -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
val copiedUris = selectedUris.mapNotNull { uri ->
try {
val inputStream = context.contentResolver.openInputStream(uri)
val extension = MimeTypeMap.getSingleton()
.getExtensionFromMimeType(context.contentResolver.getType(uri)) ?: "jpg"
val outputFile = File(context.filesDir, "note_${System.currentTimeMillis()}.$extension")
inputStream?.use { input ->
outputFile.outputStream().use { output ->
input.copyTo(output)
}
}
Log.d("NoteViewModel", "NoteEntityFile" + outputFile.absolutePath)
Uri.fromFile(outputFile).toString() // speichern als String
} catch (e: Exception) {
e.printStackTrace()
null
}
}
if (copiedUris.isNotEmpty()) {
val note = NoteEntity(
title = title,
images = copiedUris, // muss als List<String> gespeichert sein
composer = composer,
year = year,
genre = genre,
description = description
)
repository.insert(note)
}
onDone()
}
}
fun deleteNote(note: NoteEntity) {
viewModelScope.launch {
repository.delete(note)
}
}
}

View File

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

View File

@ -10,13 +10,19 @@ lifecycleRuntimeKtx = "2.8.7"
activityCompose = "1.8.0"
composeBom = "2024.04.01"
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]
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" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-foundation = { module = "androidx.compose.foundation:foundation" }
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleRuntimeKtx" }
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
@ -30,9 +36,12 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }