feat: Implement About/Settings screens and refactor JSON import/export UI

This commit introduces new About and Settings screens, accessible from the navigation drawer.
The JSON import/export functionality has been refactored into a single dialog, improving UI clarity and reducing clutter in the drawer.

Key changes include:
- Added AboutScreen and SettingsScreen composables.
- Updated Screen sealed class with About and Settings entries.
- Added corresponding string resources for About and Settings.
- Replaced individual JSON import/export drawer items with a single "JSON Import/Export" button.
- Implemented JsonImportExportDialog to handle all JSON import/export options in a centralized dialog.
- Added HorizontalDividers to the drawer for better visual separation.
- Implemented dynamic versioning in build.gradle.kts using Git commit count for versionCode and versionName.
- Corrected BuildConfig access in AboutScreen.kt to use PackageManager for version info.
- Cleaned up unused launcher declarations in MainActivity.kt.
This commit is contained in:
2025-10-13 22:56:23 +02:00
parent c806f9ba49
commit 1a13ba263c
15 changed files with 549 additions and 6 deletions

View File

@@ -1,10 +1,20 @@
import java.io.ByteArrayOutputStream
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.google.devtools.ksp)
id("org.jetbrains.kotlin.plugin.serialization") version "1.9.22"
}
apply(plugin = "kotlin-parcelize")
fun getGitCommitCount(): Int {
val process = Runtime.getRuntime().exec("git rev-list --count HEAD", null, project.rootDir)
process.waitFor()
val output = process.inputStream.bufferedReader().use { it.readText() }
return output.trim().toInt()
}
android {
namespace = "de.lxtools.noteshop"
compileSdk = 36
@@ -13,8 +23,8 @@ android {
applicationId = "de.lxtools.noteshop"
minSdk = 24
targetSdk = 36
versionCode = 1
versionName = "1.0"
versionCode = getGitCommitCount()
versionName = "0.1." + getGitCommitCount()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -56,6 +66,7 @@ dependencies {
implementation(libs.androidx.compose.material3)
implementation(libs.reorderable)
implementation("androidx.compose.material:material-icons-extended:1.6.8") // Oder eine neuere Version
implementation("androidx.documentfile:documentfile:1.0.1")
// Room Database
@@ -63,6 +74,9 @@ dependencies {
implementation(libs.androidx.room.ktx)
ksp(libs.androidx.room.compiler)
// Kotlinx Serialization
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)

View File

@@ -23,6 +23,15 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:authorities="${applicationId}.provider"
android:name="androidx.core.content.FileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>

View File

@@ -35,6 +35,10 @@ import androidx.compose.material.icons.filled.ExposurePlus1
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.FolderOpen
import androidx.compose.material.icons.filled.Sync
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Settings
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.ui.platform.LocalContext
@@ -55,21 +59,26 @@ import androidx.compose.material3.TopAppBar
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberDrawerState
import androidx.compose.material3.HorizontalDivider
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.DisposableEffect
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.viewmodel.compose.viewModel
import de.lxtools.noteshop.ui.notes.NoteDetailScreen
import de.lxtools.noteshop.ui.notes.NotesScreen
@@ -79,8 +88,18 @@ import de.lxtools.noteshop.ui.shopping.ShoppingListsScreen
import de.lxtools.noteshop.ui.shopping.ShoppingListsViewModel
import de.lxtools.noteshop.ui.theme.NoteshopTheme
import kotlinx.coroutines.launch
import android.content.Intent
import android.content.Context
import android.content.Intent.ACTION_SEND
import android.net.Uri
import android.os.Parcelable
import android.provider.OpenableColumns
import androidx.core.content.FileProvider
import androidx.documentfile.provider.DocumentFile
import de.lxtools.noteshop.ui.notes.NoteInputDialog
import de.lxtools.noteshop.ui.JsonImportExportDialog
import de.lxtools.noteshop.ui.AboutScreen
import de.lxtools.noteshop.ui.SettingsScreen
import kotlinx.parcelize.Parcelize
// Sealed class to represent the screens in the app
@@ -90,6 +109,14 @@ sealed class Screen(val route: String, val titleRes: Int) : Parcelable {
data object Notes : Screen("notes", R.string.menu_notes)
data object ShoppingListDetail : Screen("shopping_list_detail", R.string.menu_shopping_list_detail)
data object NoteDetail : Screen("note_detail", R.string.menu_note_detail)
data object About : Screen("about", R.string.menu_about)
data object Settings : Screen("settings", R.string.menu_settings)
}
// Function to get DocumentFile for a given URI and filename
fun getDocumentFile(context: Context, uri: Uri, fileName: String, mimeType: String): DocumentFile? {
val documentFile = DocumentFile.fromTreeUri(context, uri)
return documentFile?.findFile(fileName) ?: documentFile?.createFile(mimeType, fileName)
}
class MainActivity : ComponentActivity() {
@@ -110,8 +137,73 @@ fun AppShell(
notesViewModel: NotesViewModel = viewModel(factory = AppViewModelProvider.Factory),
shoppingListsViewModel: ShoppingListsViewModel = viewModel(factory = AppViewModelProvider.Factory)
) {
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val context = LocalContext.current
val scope = rememberCoroutineScope()
val syncFolderUri = remember {
val sharedPrefs = context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
sharedPrefs.getString("sync_folder_uri", null)?.let { Uri.parse(it) }
}
val performAutomaticSync: (Context) -> Unit = remember(notesViewModel, shoppingListsViewModel, syncFolderUri, scope) {
{ context ->
syncFolderUri?.let { uri ->
scope.launch {
// Automatic Export
val notesFileName = "notes.json"
val shoppingListsFileName = "shopping_lists.json"
val notesDocumentFile = getDocumentFile(context, uri, notesFileName, "application/json")
notesDocumentFile?.uri?.let { notesUri ->
val allNotes = notesViewModel.uiState.value.noteList
val content = notesViewModel.exportNotesToJson(allNotes)
context.contentResolver.openOutputStream(notesUri)?.use { outputStream ->
outputStream.write(content.toByteArray())
}
}
val shoppingListsDocumentFile = getDocumentFile(context, uri, shoppingListsFileName, "application/json")
shoppingListsDocumentFile?.uri?.let { shoppingListsUri ->
val allShoppingLists = shoppingListsViewModel.uiState.value.shoppingLists
val content = shoppingListsViewModel.exportShoppingListsToJson(allShoppingLists)
context.contentResolver.openOutputStream(shoppingListsUri)?.use { outputStream ->
outputStream.write(content.toByteArray())
}
}
// Automatic Import (simplified for now, will need conflict resolution)
notesDocumentFile?.uri?.let { notesUri ->
context.contentResolver.openInputStream(notesUri)?.use { inputStream ->
val jsonString = inputStream.readBytes().toString(Charsets.UTF_8)
notesViewModel.importNotesFromJson(jsonString)
}
}
shoppingListsDocumentFile?.uri?.let { shoppingListsUri ->
context.contentResolver.openInputStream(shoppingListsUri)?.use { inputStream ->
val jsonString = inputStream.readBytes().toString(Charsets.UTF_8)
shoppingListsViewModel.importShoppingListsFromJson(jsonString)
}
}
}
}
}
}
val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(lifecycleOwner, context) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_START) {
scope.launch { performAutomaticSync(context) }
} else if (event == Lifecycle.Event.ON_STOP) {
scope.launch { performAutomaticSync(context) }
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
var currentScreen: Screen by rememberSaveable { mutableStateOf(Screen.ShoppingLists) }
var selectedListId: Int? by rememberSaveable { mutableStateOf(null) }
var selectedNoteId: Int? by rememberSaveable { mutableStateOf(null) }
@@ -122,6 +214,8 @@ fun AppShell(
var showNoteDialog by rememberSaveable { mutableStateOf(false) }
val noteDetails by notesViewModel.noteDetails.collectAsState()
var showJsonDialog by rememberSaveable { mutableStateOf(false) }
val navigationItems = listOf(
Screen.ShoppingLists,
Screen.Notes
@@ -138,7 +232,6 @@ fun AppShell(
.collectAsState(initial = null)
val isDetailSearchActive by shoppingListsViewModel.isDetailSearchActive.collectAsState()
val context = LocalContext.current
val exportLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument("text/plain"),
onResult = { uri ->
@@ -167,6 +260,47 @@ fun AppShell(
}
)
val noteImportLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument(),
onResult = { uri ->
uri?.let {
context.contentResolver.openInputStream(it)?.use { inputStream ->
val content = inputStream.readBytes().toString(Charsets.UTF_8)
val fileName = context.contentResolver.query(it, null, null, null, null)?.use { cursor ->
val nameIndex = cursor.getColumnIndex(android.provider.OpenableColumns.DISPLAY_NAME)
cursor.moveToFirst()
cursor.getString(nameIndex)
} ?: "Imported Note"
scope.launch {
notesViewModel.resetNoteDetails()
notesViewModel.updateNoteDetails(notesViewModel.noteDetails.value.copy(
title = fileName.substringBeforeLast("."),
content = content
))
notesViewModel.saveNote()
}
}
}
}
)
val syncFolderLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocumentTree(),
onResult = { uri ->
uri?.let {
context.contentResolver.takePersistableUriPermission(it, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
val sharedPrefs = context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
sharedPrefs.edit().putString("sync_folder_uri", it.toString()).apply()
}
}
)
ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = {
@@ -215,6 +349,49 @@ fun AppShell(
scope.launch { drawerState.close() }
}
)
HorizontalDivider()
NavigationDrawerItem(
label = { Text(stringResource(R.string.json_import_export_title)) },
selected = false,
onClick = {
showJsonDialog = true
scope.launch { drawerState.close() }
}
)
NavigationDrawerItem(
label = { Text(stringResource(R.string.select_sync_folder)) },
selected = false,
onClick = {
syncFolderLauncher.launch(null)
scope.launch { drawerState.close() }
}
)
HorizontalDivider()
NavigationDrawerItem(
label = { Text(stringResource(R.string.menu_about)) },
selected = currentScreen == Screen.About,
onClick = {
currentScreen = Screen.About
scope.launch { drawerState.close() }
},
icon = { Icon(Icons.Default.Info, contentDescription = stringResource(R.string.menu_about)) }
)
NavigationDrawerItem(
label = { Text(stringResource(R.string.menu_settings)) },
selected = currentScreen == Screen.Settings,
onClick = {
currentScreen = Screen.Settings
scope.launch { drawerState.close() }
},
icon = { Icon(Icons.Default.Settings, contentDescription = stringResource(R.string.menu_settings)) }
)
}
}
) {
@@ -286,6 +463,9 @@ fun AppShell(
}) {
Icon(Icons.Default.Add, contentDescription = stringResource(R.string.add_note))
}
IconButton(onClick = { noteImportLauncher.launch(arrayOf("text/plain")) }) {
Icon(Icons.Default.FolderOpen, contentDescription = stringResource(R.string.import_note))
}
}
if (currentScreen == Screen.ShoppingListDetail) {
IconButton(onClick = { shoppingListsViewModel.toggleDetailSearch() }) {
@@ -309,6 +489,29 @@ fun AppShell(
}
}
)
DropdownMenuItem(
text = { Text(stringResource(R.string.share)) },
onClick = {
showMenu = false
shoppingListWithItems?.let { list ->
val content = shoppingListsViewModel.formatShoppingListForExport(list)
val fileName = "${list.shoppingList.name}.txt"
val cachePath = java.io.File(context.cacheDir, "shared_files")
cachePath.mkdirs()
val newFile = java.io.File(cachePath, fileName)
newFile.writeText(content)
val contentUri: Uri = FileProvider.getUriForFile(context, "${context.packageName}.provider", newFile)
val shareIntent = Intent(ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_STREAM, contentUri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
context.startActivity(Intent.createChooser(shareIntent, context.getString(R.string.share)))
}
}
)
}
} else if (currentScreen == Screen.NoteDetail) {
var showMenu by remember { mutableStateOf(false) }
@@ -329,6 +532,29 @@ fun AppShell(
}
}
)
DropdownMenuItem(
text = { Text(stringResource(R.string.share)) },
onClick = {
showMenu = false
notesViewModel.noteDetails.value.toNote().let { note ->
val content = notesViewModel.formatNoteForExport(note)
val fileName = "${note.title}.txt"
val cachePath = java.io.File(context.cacheDir, "shared_files")
cachePath.mkdirs()
val newFile = java.io.File(cachePath, fileName)
newFile.writeText(content)
val contentUri: Uri = FileProvider.getUriForFile(context, "${context.packageName}.provider", newFile)
val shareIntent = Intent(ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_STREAM, contentUri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
context.startActivity(Intent.createChooser(shareIntent, context.getString(R.string.share)))
}
}
)
}
}
},
@@ -424,6 +650,14 @@ fun AppShell(
)
}
if (showJsonDialog) {
JsonImportExportDialog(
onDismissRequest = { showJsonDialog = false },
notesViewModel = notesViewModel,
shoppingListsViewModel = shoppingListsViewModel
)
}
Column(
modifier = Modifier
.fillMaxSize()
@@ -469,6 +703,12 @@ fun AppShell(
viewModel = notesViewModel
)
}
is Screen.About -> {
AboutScreen()
}
is Screen.Settings -> {
SettingsScreen()
}
}
}
}

View File

@@ -2,7 +2,9 @@ package de.lxtools.noteshop.data
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.serialization.Serializable
@Serializable
@Entity(tableName = "notes")
data class Note(
@PrimaryKey(autoGenerate = true)

View File

@@ -2,7 +2,9 @@ package de.lxtools.noteshop.data
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.serialization.Serializable
@Serializable
@Entity(tableName = "shopping_lists")
data class ShoppingList(
@PrimaryKey(autoGenerate = true)

View File

@@ -4,7 +4,9 @@ import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
import kotlinx.serialization.Serializable
@Serializable
@Entity(
tableName = "shopping_list_items",
foreignKeys = [

View File

@@ -2,7 +2,9 @@ package de.lxtools.noteshop.data
import androidx.room.Embedded
import androidx.room.Relation
import kotlinx.serialization.Serializable
@Serializable
data class ShoppingListWithItems(
@Embedded val shoppingList: ShoppingList,
@Relation(

View File

@@ -0,0 +1,44 @@
package de.lxtools.noteshop.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import de.lxtools.noteshop.R
import android.content.pm.PackageManager
@Composable
fun AboutScreen() {
val context = LocalContext.current
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
val versionName = packageInfo.versionName
val versionCode = packageInfo.versionCode
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
Text(
text = stringResource(R.string.app_name),
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(bottom = 8.dp)
)
Text(
text = "Version: $versionName (Build $versionCode)",
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(bottom = 16.dp)
)
Text(
text = "© 2025 LXTools", // Replace with actual copyright holder and year
style = MaterialTheme.typography.bodyMedium
)
// Add more about information here
}
}

View File

@@ -0,0 +1,145 @@
package de.lxtools.noteshop.ui
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Divider
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import de.lxtools.noteshop.R
import de.lxtools.noteshop.ui.notes.NotesViewModel
import de.lxtools.noteshop.ui.shopping.ShoppingListsViewModel
import kotlinx.coroutines.launch
@Composable
fun JsonImportExportDialog(
onDismissRequest: () -> Unit,
notesViewModel: NotesViewModel,
shoppingListsViewModel: ShoppingListsViewModel
) {
val scope = rememberCoroutineScope()
val context = LocalContext.current
val notesImportJsonLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument(),
onResult = { uri ->
uri?.let {
scope.launch {
context.contentResolver.openInputStream(it)?.use { inputStream ->
val jsonString = inputStream.readBytes().toString(Charsets.UTF_8)
notesViewModel.importNotesFromJson(jsonString)
}
}
}
onDismissRequest()
}
)
val notesExportJsonLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument("application/json"),
onResult = { uri ->
uri?.let {
scope.launch {
val allNotes = notesViewModel.uiState.value.noteList
val content = notesViewModel.exportNotesToJson(allNotes)
context.contentResolver.openOutputStream(it)?.use { outputStream ->
outputStream.write(content.toByteArray())
}
}
}
onDismissRequest()
}
)
val shoppingListsExportJsonLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument("application/json"),
onResult = { uri ->
uri?.let {
scope.launch {
val allShoppingLists = shoppingListsViewModel.uiState.value.shoppingLists
val content = shoppingListsViewModel.exportShoppingListsToJson(allShoppingLists)
context.contentResolver.openOutputStream(it)?.use { outputStream ->
outputStream.write(content.toByteArray())
}
}
}
onDismissRequest()
}
)
val shoppingListsImportJsonLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument(),
onResult = { uri ->
uri?.let {
scope.launch {
context.contentResolver.openInputStream(it)?.use { inputStream ->
val jsonString = inputStream.readBytes().toString(Charsets.UTF_8)
shoppingListsViewModel.importShoppingListsFromJson(jsonString)
}
}
}
onDismissRequest()
}
)
AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(text = stringResource(R.string.json_import_export_title)) },
text = {
Column {
Text(text = stringResource(R.string.notes_heading))
TextButton(onClick = {
scope.launch {
val fileName = "all_notes.json"
notesExportJsonLauncher.launch(fileName)
}
}) {
Text(stringResource(R.string.export_notes))
}
TextButton(onClick = {
scope.launch {
notesImportJsonLauncher.launch(arrayOf("application/json"))
}
}) {
Text(stringResource(R.string.import_notes))
}
Spacer(modifier = Modifier.height(16.dp))
HorizontalDivider()
Spacer(modifier = Modifier.height(16.dp))
Text(text = stringResource(R.string.shopping_lists_heading))
TextButton(onClick = {
scope.launch {
val fileName = "all_shopping_lists.json"
shoppingListsExportJsonLauncher.launch(fileName)
}
}) {
Text(stringResource(R.string.export_shopping_lists))
}
TextButton(onClick = {
scope.launch {
shoppingListsImportJsonLauncher.launch(arrayOf("application/json"))
}
}) {
Text(stringResource(R.string.import_shopping_lists))
}
}
},
confirmButton = {
TextButton(onClick = onDismissRequest) {
Text(stringResource(R.string.cancel))
}
}
)
}

View File

@@ -0,0 +1,23 @@
package de.lxtools.noteshop.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import de.lxtools.noteshop.R
@Composable
fun SettingsScreen() {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
Text(text = stringResource(R.string.menu_settings))
// Add settings options here
}
}

View File

@@ -11,6 +11,9 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
class NotesViewModel(private val noteshopRepository: NoteshopRepository) : ViewModel() {
@@ -107,6 +110,17 @@ class NotesViewModel(private val noteshopRepository: NoteshopRepository) : ViewM
return builder.toString()
}
fun exportNotesToJson(notes: List<Note>): String {
return Json.encodeToString(notes)
}
suspend fun importNotesFromJson(jsonString: String) {
val notes = Json.decodeFromString<List<Note>>(jsonString)
notes.forEach { note ->
noteshopRepository.insertNote(note)
}
}
companion object {
private const val TIMEOUT_MILLIS = 5_000L
}

View File

@@ -11,10 +11,13 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine // New import
import kotlinx.coroutines.flow.firstOrNull // New import
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
class ShoppingListsViewModel(private val noteshopRepository: NoteshopRepository) : ViewModel() {
@@ -327,6 +330,19 @@ class ShoppingListsViewModel(private val noteshopRepository: NoteshopRepository)
return builder.toString()
}
fun exportShoppingListsToJson(shoppingLists: List<ShoppingListWithItems>): String {
return Json.encodeToString(shoppingLists)
}
suspend fun importShoppingListsFromJson(jsonString: String) {
val shoppingLists = Json.decodeFromString<List<ShoppingListWithItems>>(jsonString)
shoppingLists.forEach { listWithItems ->
noteshopRepository.insertShoppingList(listWithItems.shoppingList)
listWithItems.items.forEach { item ->
noteshopRepository.insertShoppingListItem(item)
}
}
}
companion object {
private const val TIMEOUT_MILLIS = 5_000L

View File

@@ -64,6 +64,19 @@
<string name="reorder">Neu anordnen</string>
<string name="standard_list_name">Standard</string>
<string name="export">Exportieren</string>
<string name="share">Teilen</string>
<string name="import_note">Notiz importieren</string>
<string name="select_sync_folder">Synchronisationsordner auswählen</string>
<string name="sync_from_file">Aus Datei synchronisieren</string>
<string name="json_import_export_title">JSON Import/Export</string>
<string name="notes_heading">Notizen</string>
<string name="shopping_lists_heading">Einkaufslisten</string>
<string name="export_notes">Notizen exportieren</string>
<string name="import_notes">Notizen importieren</string>
<string name="export_shopping_lists">Einkaufslisten exportieren</string>
<string name="import_shopping_lists">Einkaufslisten importieren</string>
<string name="menu_about">Über</string>
<string name="menu_settings">Einstellungen</string>
<string-array name="standard_list_items">
<item>Brötchen</item>
<item>Red Bull</item>

View File

@@ -64,6 +64,19 @@
<string name="reorder">Reorder</string>
<string name="standard_list_name">Default</string>
<string name="export">Export</string>
<string name="share">Share</string>
<string name="import_note">Import Note</string>
<string name="select_sync_folder">Select Sync Folder</string>
<string name="sync_from_file">Sync from File</string>
<string name="json_import_export_title">JSON Import/Export</string>
<string name="notes_heading">Notes</string>
<string name="shopping_lists_heading">Shopping Lists</string>
<string name="export_notes">Export Notes</string>
<string name="import_notes">Import Notes</string>
<string name="export_shopping_lists">Export Shopping Lists</string>
<string name="import_shopping_lists">Import Shopping Lists</string>
<string name="menu_about">About</string>
<string name="menu_settings">Settings</string>
<string-array name="standard_list_items">
<item>Bread rolls</item>
<item>Red Bull</item>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<cache-path name="shared_files" path="."/>
</paths>