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:
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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(
|
||||
|
||||
44
app/src/main/java/de/lxtools/noteshop/ui/AboutScreen.kt
Normal file
44
app/src/main/java/de/lxtools/noteshop/ui/AboutScreen.kt
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
23
app/src/main/java/de/lxtools/noteshop/ui/SettingsScreen.kt
Normal file
23
app/src/main/java/de/lxtools/noteshop/ui/SettingsScreen.kt
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
4
app/src/main/res/xml/file_paths.xml
Normal file
4
app/src/main/res/xml/file_paths.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<cache-path name="shared_files" path="."/>
|
||||
</paths>
|
||||
Reference in New Issue
Block a user