Compare commits

..

13 Commits

Author SHA1 Message Date
44e4bd3218 refactor: Clean up UI components and improve JSON import/export
- Removed unused parameters and functions related to biometric protection and save/reset operations in AppShell.kt and AppDialogs.kt.
- Simplified logging and toast messages in JsonImportExportDialog.kt.
- Improved null safety and file output stream handling in JsonImportExportDialog.kt.
- Updated AndroidManifest.xml with target API level and tools namespace.
2025-11-06 22:18:49 +01:00
c2ea7c98ef refactor: Remove unused parameters and code in UI components - Cleaned up AppShell.kt, AppTopBar.kt, AppContent.kt, ShoppingListDetailScreen.kt, WebAppIntegrationScreen.kt, NoteDetailScreen.kt, ShoppingListsViewModel.kt, NotesViewModel.kt, RecipesViewModel.kt, and EncryptionPasswordDialog.kt by removing unused parameters, imports, and functions, and updating Base64 usage and shared preferences editing. 2025-11-06 20:51:11 +01:00
a10602c6a5 feat: Remove unused PatternInput.kt file
This commit removes the  file as the  Composable function was not used anywhere in the project.
2025-11-06 19:24:27 +01:00
62ff3594d4 chore: Commit remaining changes 2025-11-06 19:18:15 +01:00
a5bed3031a Refactor: Clean up unused code and fix warnings
This commit addresses several linter warnings and removes unused code to improve maintainability.

- Removed unused function  and property  in .
- Removed unused parameter  from , , and .
- Fixed a variable shadowing issue with  in .
- Inlined a redundant variable in .
2025-11-06 19:16:38 +01:00
20d9139256 Fix: Remove redundant explicit type argument in AppShell.kt 2025-11-06 17:28:20 +01:00
cf799eba12 Refactor: State management in AppShell.kt 2025-11-06 17:24:08 +01:00
fd04f9b5d9 fix: Restore Log import in AppDialogs
Restored the  import in  to fix an "Unresolved reference 'Log'" error that was introduced by a previous commit.
2025-11-06 15:42:10 +01:00
28cc1313b4 refactor: Remove unused Log import from AppDialogs
Removed the unused  import from  to clean up the code.
2025-11-06 15:39:33 +01:00
7c5c31c638 fix: Correct GuidedTourScreen after pager migration
Fixed the build errors in GuidedTourScreen.kt that occurred after migrating to androidx.compose.foundation.pager.
- Added pageCount to rememberPagerState.
- Removed count from HorizontalPager.
- Restored the R import.
2025-11-06 15:35:33 +01:00
12839ea88e refactor: Remove redundant 'jsonString != null' checks
Removed redundant 'if (jsonString != null)' checks in JsonImportExportDialog.kt as jsonString is always non-null at these points, addressing 'Condition is always true' warnings.
2025-11-06 14:53:29 +01:00
fb8e7917bc refactor: Migrate GuidedTourScreen to androidx.compose.foundation.pager
Migrated the GuidedTourScreen from deprecated Accompanist Pager to androidx.compose.foundation.pager.
Removed HorizontalPagerIndicator usage, as a direct replacement is not available and requires custom implementation.
2025-11-06 14:51:46 +01:00
4f0edbca22 refactor: Remove unused import from AppContent
Removed the unused  import from  to clean up the code.
2025-11-06 14:38:17 +01:00
22 changed files with 258 additions and 548 deletions

View File

@@ -37,8 +37,7 @@ Noteshop is a versatile and privacy-focused application for managing your notes,
1. Clone the repository: 1. Clone the repository:
```sh ```sh
git clone YOUR_GITEA_REPOSITORY_URL_HERE git clone https://git.ilunix.de/punix/noteshop.git && cd noteshop
cd noteshop
``` ```
2. Build the app using Gradle: 2. Build the app using Gradle:
```sh ```sh

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" /> <uses-permission android:name="android.permission.USE_BIOMETRIC" />
@@ -16,7 +17,8 @@
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:enableOnBackInvokedCallback="true" android:enableOnBackInvokedCallback="true"
android:theme="@style/Theme.Noteshop"> android:theme="@style/Theme.Noteshop"
tools:targetApi="33">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"

View File

@@ -125,11 +125,7 @@ class BiometricAuthenticator(private val context: Context) {
private lateinit var promptInfo: BiometricPrompt.PromptInfo private lateinit var promptInfo: BiometricPrompt.PromptInfo
private val biometricManager = BiometricManager.from(context)
fun isBiometricAuthAvailable(): Boolean {
return biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.DEVICE_CREDENTIAL) == BiometricManager.BIOMETRIC_SUCCESS
}
fun promptBiometricAuth( fun promptBiometricAuth(
title: String, title: String,

View File

@@ -2,6 +2,7 @@ package de.lxtools.noteshop.ui
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.util.Base64
import android.util.Log import android.util.Log
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
@@ -41,7 +42,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@@ -74,12 +74,11 @@ import de.lxtools.noteshop.ui.recipes.RecipesViewModel
import de.lxtools.noteshop.ui.shopping.ShoppingListsViewModel import de.lxtools.noteshop.ui.shopping.ShoppingListsViewModel
import de.lxtools.noteshop.ui.theme.ColorTheme import de.lxtools.noteshop.ui.theme.ColorTheme
import de.lxtools.noteshop.ui.webapp.WebAppIntegrationViewModel import de.lxtools.noteshop.ui.webapp.WebAppIntegrationViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.FileOutputStream import java.io.FileOutputStream
import javax.crypto.SecretKey import javax.crypto.SecretKey
import android.util.Base64
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -98,10 +97,10 @@ fun AppShell(
val context = LocalContext.current val context = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
var showStartupPasswordDialog by rememberSaveable { mutableStateOf(false) } val (showStartupPasswordDialog, setShowStartupPasswordDialog) = rememberSaveable { mutableStateOf(false) }
var startupUnlockErrorMessage by rememberSaveable { mutableStateOf<String?>(null) } val (startupUnlockErrorMessage, setStartupUnlockErrorMessage) = rememberSaveable { mutableStateOf<String?>(null) }
var syncFolderUriString by rememberSaveable { val (syncFolderUriString, setSyncFolderUriString) = rememberSaveable {
mutableStateOf( mutableStateOf(
sharedPrefs.getString( sharedPrefs.getString(
"sync_folder_uri", "sync_folder_uri",
@@ -114,7 +113,7 @@ fun AppShell(
val listener = val listener =
android.content.SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key -> android.content.SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
if (key == "sync_folder_uri") { if (key == "sync_folder_uri") {
syncFolderUriString = sharedPreferences.getString(key, null) setSyncFolderUriString(sharedPreferences.getString(key, null))
} }
} }
sharedPrefs.registerOnSharedPreferenceChangeListener(listener) sharedPrefs.registerOnSharedPreferenceChangeListener(listener)
@@ -128,7 +127,7 @@ fun AppShell(
val keyManager = remember { KeyManager(context, canUseBiometrics) } val keyManager = remember { KeyManager(context, canUseBiometrics) }
val fileEncryptor = remember { FileEncryptor() } val fileEncryptor = remember { FileEncryptor() }
var isEncryptionEnabled by rememberSaveable { val (isEncryptionEnabled, setIsEncryptionEnabled) = rememberSaveable {
mutableStateOf( mutableStateOf(
sharedPrefs.getBoolean( sharedPrefs.getBoolean(
"encryption_enabled", "encryption_enabled",
@@ -136,9 +135,9 @@ fun AppShell(
) )
) )
} }
var hasEncryptionPassword by rememberSaveable { mutableStateOf(keyManager.hasKey()) } val (hasEncryptionPassword, setHasEncryptionPassword) = rememberSaveable { mutableStateOf(keyManager.hasKey()) }
var showPasswordDialog by rememberSaveable { mutableStateOf(false) } val (showPasswordDialog, setShowPasswordDialog) = rememberSaveable { mutableStateOf(false) }
var showDeleteConfirmationDialog by remember { mutableStateOf(false) } val (showDeleteConfirmationDialog, setShowDeleteConfirmationDialog) = remember { mutableStateOf(false) }
val onEncryptionToggle: (Boolean) -> Unit = { enabled -> val onEncryptionToggle: (Boolean) -> Unit = { enabled ->
if (!enabled) { // Turning OFF if (!enabled) { // Turning OFF
@@ -149,8 +148,8 @@ fun AppShell(
if (cipher == null) { if (cipher == null) {
// Fallback for safety, though this state should be unlikely if hasEncryptionPassword is true // Fallback for safety, though this state should be unlikely if hasEncryptionPassword is true
keyManager.deleteKey() keyManager.deleteKey()
hasEncryptionPassword = false setHasEncryptionPassword(false)
isEncryptionEnabled = false setIsEncryptionEnabled(false)
sharedPrefs.edit { sharedPrefs.edit {
putBoolean("encryption_enabled", false) putBoolean("encryption_enabled", false)
remove("biometric_unlock_enabled") remove("biometric_unlock_enabled")
@@ -174,8 +173,8 @@ fun AppShell(
// Now, safely delete the key and disable the feature // Now, safely delete the key and disable the feature
keyManager.deleteKey() keyManager.deleteKey()
hasEncryptionPassword = false setHasEncryptionPassword(false)
isEncryptionEnabled = false setIsEncryptionEnabled(false)
sharedPrefs.edit { sharedPrefs.edit {
putBoolean("encryption_enabled", false) putBoolean("encryption_enabled", false)
remove("biometric_unlock_enabled") remove("biometric_unlock_enabled")
@@ -188,13 +187,13 @@ fun AppShell(
) )
} else { } else {
// No password was ever set, just turn the switch off. // No password was ever set, just turn the switch off.
isEncryptionEnabled = false setIsEncryptionEnabled(false)
sharedPrefs.edit { sharedPrefs.edit {
putBoolean("encryption_enabled", false) putBoolean("encryption_enabled", false)
} }
} }
} else { // Turning ON } else { // Turning ON
isEncryptionEnabled = true setIsEncryptionEnabled(true)
sharedPrefs.edit { sharedPrefs.edit {
putBoolean("encryption_enabled", true) putBoolean("encryption_enabled", true)
} }
@@ -202,50 +201,10 @@ fun AppShell(
} }
val onSetEncryptionPassword: () -> Unit = { val onSetEncryptionPassword: () -> Unit = {
showPasswordDialog = true setShowPasswordDialog(true)
} }
val onRemoveEncryption: () -> Unit = {
val activity = context.findActivity() as FragmentActivity
val cipher = keyManager.getDecryptionCipher()
if (cipher == null) {
// This can happen if no key is set. In this case, just disable the feature.
hasEncryptionPassword = false
isEncryptionEnabled = false
sharedPrefs.edit {
putBoolean("encryption_enabled", false)
remove("biometric_unlock_enabled")
}
}
val crypto = androidx.biometric.BiometricPrompt.CryptoObject(cipher!!)
biometricAuthenticator.promptBiometricAuth(
title = context.getString(R.string.remove_encryption),
subtitle = context.getString(R.string.confirm_to_remove_encryption),
fragmentActivity = activity,
crypto = crypto,
onSuccess = { result ->
result.cryptoObject?.cipher?.let { authenticatedCipher ->
scope.launch {
keyManager.getSecretKeyFromAuthenticatedCipher(authenticatedCipher)
// Decrypt all items in all ViewModels
// Decryption of individual items is no longer needed.
// Now it's safe to delete the key
keyManager.deleteKey()
hasEncryptionPassword = false
isEncryptionEnabled = false
sharedPrefs.edit {
putBoolean("encryption_enabled", false)
remove("biometric_unlock_enabled")
}
}
}
},
onFailed = {},
onError = { _, _ -> }
)
}
val performAutomaticExport: suspend (Context, SecretKey?) -> Unit = val performAutomaticExport: suspend (Context, SecretKey?) -> Unit =
remember(notesViewModel, shoppingListsViewModel, recipesViewModel, syncFolderUri) { remember(notesViewModel, shoppingListsViewModel, recipesViewModel, syncFolderUri) {
@@ -515,8 +474,8 @@ fun AppShell(
} }
val lifecycleOwner = LocalLifecycleOwner.current val lifecycleOwner = LocalLifecycleOwner.current
var secretKey by rememberSaveable(stateSaver = SecretKeySaver) { mutableStateOf(null) } val (secretKey, setSecretKey) = rememberSaveable(stateSaver = SecretKeySaver) { mutableStateOf(null) }
var isDecryptionAttempted by rememberSaveable { mutableStateOf(false) } val (isDecryptionAttempted, setIsDecryptionAttempted) = rememberSaveable { mutableStateOf(false) }
DisposableEffect(lifecycleOwner, context, scope) { DisposableEffect(lifecycleOwner, context, scope) {
val observer = LifecycleEventObserver { _, event -> val observer = LifecycleEventObserver { _, event ->
@@ -541,32 +500,33 @@ fun AppShell(
onSuccess = { result -> onSuccess = { result ->
result.cryptoObject?.cipher?.let { authenticatedCipher -> result.cryptoObject?.cipher?.let { authenticatedCipher ->
scope.launch { scope.launch {
secretKey = setSecretKey(
keyManager.getSecretKeyFromAuthenticatedCipher( keyManager.getSecretKeyFromAuthenticatedCipher(
authenticatedCipher authenticatedCipher
) )
isDecryptionAttempted = true )
setIsDecryptionAttempted(true)
} }
} }
}, },
onFailed = {}, onFailed = {},
onError = { _, _ -> onError = { _, _ ->
showStartupPasswordDialog = true setShowStartupPasswordDialog(true)
} }
) )
} else { } else {
isDecryptionAttempted = true // No key/cipher, proceed setIsDecryptionAttempted(true) // No key/cipher, proceed
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e("AppShell", "Error during decryption prompt: ${e.message}") Log.e("AppShell", "Error during decryption prompt: ${e.message}")
isDecryptionAttempted = true // Proceed without key setIsDecryptionAttempted(true) // Proceed without key
} }
} }
} else { } else {
showStartupPasswordDialog = true setShowStartupPasswordDialog(true)
} }
} else { } else {
isDecryptionAttempted = true // No encryption, proceed setIsDecryptionAttempted(true) // No encryption, proceed
} }
} }
@@ -594,7 +554,7 @@ fun AppShell(
} }
} }
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
var currentScreen: Screen by rememberSaveable { val (currentScreen, setCurrentScreen) = rememberSaveable {
val defaultStartScreenRoute = sharedPrefs.getString("default_start_screen", "last_screen") val defaultStartScreenRoute = sharedPrefs.getString("default_start_screen", "last_screen")
val initialScreen = when (defaultStartScreenRoute) { val initialScreen = when (defaultStartScreenRoute) {
Screen.ShoppingLists.route -> Screen.ShoppingLists Screen.ShoppingLists.route -> Screen.ShoppingLists
@@ -624,58 +584,58 @@ fun AppShell(
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
val firstLaunchCompleted = sharedPrefs.getBoolean("first_launch_completed", false) val firstLaunchCompleted = sharedPrefs.getBoolean("first_launch_completed", false)
if (!firstLaunchCompleted) { if (!firstLaunchCompleted) {
currentScreen = Screen.GuidedTour setCurrentScreen(Screen.GuidedTour)
sharedPrefs.edit { putBoolean("first_launch_completed", true) } sharedPrefs.edit { putBoolean("first_launch_completed", true) }
} }
} }
var selectedListId: Int? by rememberSaveable { mutableStateOf(null) } val (selectedListId, setSelectedListId) = rememberSaveable { mutableStateOf<Int?>(null) }
var selectedNoteId: Int? by rememberSaveable { mutableStateOf(null) } val (selectedNoteId, setSelectedNoteId) = rememberSaveable { mutableStateOf<Int?>(null) }
var selectedRecipeId: Int? by rememberSaveable { mutableStateOf(null) } val (selectedRecipeId, setSelectedRecipeId) = rememberSaveable { mutableStateOf<Int?>(null) }
var itemWasInitiallyLocked by rememberSaveable { mutableStateOf(false) } val (itemWasInitiallyLocked, setItemWasInitiallyLocked) = rememberSaveable { mutableStateOf(false) }
LaunchedEffect(selectedNoteId, selectedListId, selectedRecipeId) { LaunchedEffect(selectedNoteId, selectedListId, selectedRecipeId) {
if (selectedNoteId == null && selectedListId == null && selectedRecipeId == null) { if (selectedNoteId == null && selectedListId == null && selectedRecipeId == null) {
itemWasInitiallyLocked = false setItemWasInitiallyLocked(false)
} }
} }
var showListDialog by rememberSaveable { mutableStateOf(false) } val (showListDialog, setShowListDialog) = rememberSaveable { mutableStateOf(false) }
val listDetails by shoppingListsViewModel.listDetails.collectAsState() val listDetails by shoppingListsViewModel.listDetails.collectAsState()
var showNoteDialog by rememberSaveable { mutableStateOf(false) } val (showNoteDialog, setShowNoteDialog) = rememberSaveable { mutableStateOf(false) }
val noteDetails by notesViewModel.noteDetails.collectAsState() val noteDetails by notesViewModel.noteDetails.collectAsState()
var showRecipeDialog by rememberSaveable { mutableStateOf(false) } val (showRecipeDialog, setShowRecipeDialog) = rememberSaveable { mutableStateOf(false) }
val recipeDetails by recipesViewModel.recipeDetails.collectAsState() val recipeDetails by recipesViewModel.recipeDetails.collectAsState()
var showJsonDialog by rememberSaveable { mutableStateOf(false) } val (showJsonDialog, setShowJsonDialog) = rememberSaveable { mutableStateOf(false) }
var showSetPasswordDialog by rememberSaveable { mutableStateOf(false) } val (showSetPasswordDialog, setShowSetPasswordDialog) = rememberSaveable { mutableStateOf(false) }
var showSetRecipePasswordDialog by rememberSaveable { mutableStateOf(false) } val (showSetRecipePasswordDialog, setShowSetRecipePasswordDialog) = rememberSaveable { mutableStateOf(false) }
var showSetListPasswordDialog by rememberSaveable { mutableStateOf(false) } val (showSetListPasswordDialog, setShowSetListPasswordDialog) = rememberSaveable { mutableStateOf(false) }
var showUnlockPasswordDialog by rememberSaveable { mutableStateOf(false) } val (showUnlockPasswordDialog, setShowUnlockPasswordDialog) = rememberSaveable { mutableStateOf(false) }
var unlockErrorMessage by rememberSaveable { mutableStateOf<String?>(null) } val (unlockErrorMessage, setUnlockErrorMessage) = rememberSaveable { mutableStateOf<String?>(null) }
var itemToUnlockId by rememberSaveable { mutableStateOf<Int?>(null) } val (itemToUnlockId, setItemToUnlockId) = rememberSaveable { mutableStateOf<Int?>(null) }
var itemToUnlockType by rememberSaveable { mutableStateOf<Screen?>(null) } val (itemToUnlockType, setItemToUnlockType) = rememberSaveable { mutableStateOf<Screen?>(null) }
var showChooseLockMethodDialog by rememberSaveable { mutableStateOf(false) } val (showChooseLockMethodDialog, setShowChooseLockMethodDialog) = rememberSaveable { mutableStateOf(false) }
var itemToLockId: Int? by rememberSaveable { mutableStateOf(null) } val (itemToLockId, setItemToLockId) = rememberSaveable { mutableStateOf<Int?>(null) }
var itemToLockType: LockableItemType? by rememberSaveable { mutableStateOf(null) } val (itemToLockType, setItemToLockType) = rememberSaveable { mutableStateOf<LockableItemType?>(null) }
val onUnlockItem: (Int, Screen, Int) -> Unit = { id, type, protectionType -> val onUnlockItem: (Int, Screen, Int) -> Unit = { id, type, protectionType ->
when (protectionType) { when (protectionType) {
1 -> { // Password protected 1 -> { // Password protected
itemToUnlockId = id setItemToUnlockId(id)
itemToUnlockType = type setItemToUnlockType(type)
showUnlockPasswordDialog = true setShowUnlockPasswordDialog(true)
} }
2 -> { // Biometric protected 2 -> { // Biometric protected
biometricAuthenticator.promptBiometricAuth( biometricAuthenticator.promptBiometricAuth(
@@ -684,25 +644,25 @@ fun AppShell(
fragmentActivity = context.findActivity() as FragmentActivity, fragmentActivity = context.findActivity() as FragmentActivity,
onSuccess = { _ -> onSuccess = { _ ->
when (type) { when (type) {
is Screen.ShoppingListDetail -> selectedListId = id is Screen.ShoppingListDetail -> setSelectedListId(id)
is Screen.NoteDetail -> selectedNoteId = id is Screen.NoteDetail -> setSelectedNoteId(id)
is Screen.RecipeDetail -> selectedRecipeId = id is Screen.RecipeDetail -> setSelectedRecipeId(id)
else -> {} else -> {}
} }
itemWasInitiallyLocked = true setItemWasInitiallyLocked(true)
currentScreen = type setCurrentScreen(type)
}, },
onFailed = { onFailed = {
// Biometric failed, offer password as fallback if desired // Biometric failed, offer password as fallback if desired
itemToUnlockId = id setItemToUnlockId(id)
itemToUnlockType = type setItemToUnlockType(type)
showUnlockPasswordDialog = true setShowUnlockPasswordDialog(true)
}, },
onError = { _, _ -> onError = { _, _ ->
// Biometric error, offer password as fallback if desired // Biometric error, offer password as fallback if desired
itemToUnlockId = id setItemToUnlockId(id)
itemToUnlockType = type setItemToUnlockType(type)
showUnlockPasswordDialog = true setShowUnlockPasswordDialog(true)
} }
) )
} }
@@ -716,7 +676,7 @@ fun AppShell(
) )
.collectAsState(initial = null) .collectAsState(initial = null)
var shoppingListsTitle by remember { val (shoppingListsTitle, setShoppingListsTitle) = remember {
mutableStateOf( mutableStateOf(
sharedPrefs.getString( sharedPrefs.getString(
"shopping_lists_title", "shopping_lists_title",
@@ -724,9 +684,9 @@ fun AppShell(
) ?: context.getString(R.string.menu_shopping_lists) ) ?: context.getString(R.string.menu_shopping_lists)
) )
} }
var showRenameDialog by rememberSaveable { mutableStateOf(false) } val (showRenameDialog, setShowRenameDialog) = rememberSaveable { mutableStateOf(false) }
var recipesTitle by remember { val (recipesTitle, setRecipesTitle) = remember {
mutableStateOf( mutableStateOf(
sharedPrefs.getString( sharedPrefs.getString(
"recipes_title", "recipes_title",
@@ -734,9 +694,9 @@ fun AppShell(
) ?: context.getString(R.string.menu_recipes) ) ?: context.getString(R.string.menu_recipes)
) )
} }
var showRecipeRenameDialog by rememberSaveable { mutableStateOf(false) } val (showRecipeRenameDialog, setShowRecipeRenameDialog) = rememberSaveable { mutableStateOf(false) }
var startScreen by remember { val (startScreen, setStartScreen) = remember {
mutableStateOf( mutableStateOf(
sharedPrefs.getString( sharedPrefs.getString(
"default_start_screen", "default_start_screen",
@@ -764,7 +724,7 @@ fun AppShell(
shoppingListsViewModel.importListFromTxt(listName, content) shoppingListsViewModel.importListFromTxt(listName, content)
} }
} }
showListDialog = false // Close dialog on success setShowListDialog(false) // Close dialog on success
} }
} }
) )
@@ -942,19 +902,19 @@ fun AppShell(
AppDrawer( AppDrawer(
drawerState = drawerState, drawerState = drawerState,
currentScreen = currentScreen, currentScreen = currentScreen,
onScreenChange = { currentScreen = it }, onScreenChange = { setCurrentScreen(it) },
shoppingListsViewModel = shoppingListsViewModel, shoppingListsViewModel = shoppingListsViewModel,
notesViewModel = notesViewModel, notesViewModel = notesViewModel,
shoppingListsTitle = shoppingListsTitle, shoppingListsTitle = shoppingListsTitle,
recipesTitle = recipesTitle, recipesTitle = recipesTitle,
onShowRenameDialog = { showRenameDialog = true }, onShowRenameDialog = { setShowRenameDialog(true) },
onShowRecipeRenameDialog = { showRecipeRenameDialog = true }, onShowRecipeRenameDialog = { setShowRecipeRenameDialog(true) },
onShowJsonDialog = { showJsonDialog = true }, onShowJsonDialog = { setShowJsonDialog(true) },
syncFolderUriString = syncFolderUriString, syncFolderUriString = syncFolderUriString,
onShowDeleteConfirmationDialog = { showDeleteConfirmationDialog = true }, onShowDeleteConfirmationDialog = { setShowDeleteConfirmationDialog(true) },
onLaunchSyncFolderChooser = { syncFolderLauncher.launch(null) }, onLaunchSyncFolderChooser = { syncFolderLauncher.launch(null) },
onSetSelectedListId = { selectedListId = it }, onSetSelectedListId = { setSelectedListId(it) },
onSetSelectedNoteId = { selectedNoteId = it } onSetSelectedNoteId = { setSelectedNoteId(it) }
) )
} }
) { ) {
@@ -965,31 +925,27 @@ fun AppShell(
topBar = { topBar = {
AppTopBar( AppTopBar(
currentScreen = currentScreen, currentScreen = currentScreen,
onScreenChange = { currentScreen = it }, onScreenChange = { setCurrentScreen(it) },
drawerState = drawerState, drawerState = drawerState,
shoppingListsViewModel = shoppingListsViewModel, shoppingListsViewModel = shoppingListsViewModel,
notesViewModel = notesViewModel, notesViewModel = notesViewModel,
recipesViewModel = recipesViewModel, recipesViewModel = recipesViewModel,
shoppingListsTitle = shoppingListsTitle, shoppingListsTitle = shoppingListsTitle,
recipesTitle = recipesTitle, recipesTitle = recipesTitle,
onShowListDialog = { showListDialog = true }, onShowListDialog = { setShowListDialog(true) },
onShowNoteDialog = { showNoteDialog = true }, onShowNoteDialog = { setShowNoteDialog(true) },
onShowRecipeDialog = { showRecipeDialog = true }, onShowRecipeDialog = { setShowRecipeDialog(true) },
selectedListId = selectedListId, selectedListId = selectedListId,
onSetSelectedListId = { selectedListId = it }, onSetSelectedListId = { setSelectedListId(it) },
onSetSelectedNoteId = { selectedNoteId = it }, onSetSelectedNoteId = { setSelectedNoteId(it) },
onSetSelectedRecipeId = { selectedRecipeId = it }, onSetSelectedRecipeId = { setSelectedRecipeId(it) },
exportLauncher = exportLauncher, exportLauncher = exportLauncher,
noteExportLauncher = noteExportLauncher, noteExportLauncher = noteExportLauncher,
recipeExportLauncher = recipeExportLauncher, recipeExportLauncher = recipeExportLauncher,
hasEncryptionPassword = hasEncryptionPassword,
onShowSetPasswordDialog = { showSetPasswordDialog = true },
onShowSetRecipePasswordDialog = { showSetRecipePasswordDialog = true },
onShowSetListPasswordDialog = { showSetListPasswordDialog = true },
onShowChooseLockMethodDialog = { type, id -> onShowChooseLockMethodDialog = { type, id ->
itemToLockType = type setItemToLockType(type)
itemToLockId = id setItemToLockId(id)
showChooseLockMethodDialog = true setShowChooseLockMethodDialog(true)
} }
) )
}, },
@@ -1073,19 +1029,19 @@ fun AppShell(
AppContent( AppContent(
innerPadding = innerPadding, innerPadding = innerPadding,
currentScreen = currentScreen, currentScreen = currentScreen,
onScreenChange = { currentScreen = it }, onScreenChange = { setCurrentScreen(it) },
notesViewModel = notesViewModel, notesViewModel = notesViewModel,
shoppingListsViewModel = shoppingListsViewModel, shoppingListsViewModel = shoppingListsViewModel,
recipesViewModel = recipesViewModel, recipesViewModel = recipesViewModel,
selectedListId = selectedListId, selectedListId = selectedListId,
onSetSelectedListId = { selectedListId = it }, onSetSelectedListId = { setSelectedListId(it) },
onShowListDialog = { showListDialog = true }, onShowListDialog = { setShowListDialog(true) },
selectedNoteId = selectedNoteId, selectedNoteId = selectedNoteId,
onSetSelectedNoteId = { selectedNoteId = it }, onSetSelectedNoteId = { setSelectedNoteId(it) },
onShowNoteDialog = { showNoteDialog = true }, onShowNoteDialog = { setShowNoteDialog(true) },
selectedRecipeId = selectedRecipeId, selectedRecipeId = selectedRecipeId,
onSetSelectedRecipeId = { selectedRecipeId = it }, onSetSelectedRecipeId = { setSelectedRecipeId(it) },
onShowRecipeDialog = { showRecipeDialog = true }, onShowRecipeDialog = { setShowRecipeDialog(true) },
itemWasInitiallyLocked = itemWasInitiallyLocked, itemWasInitiallyLocked = itemWasInitiallyLocked,
onUnlockItem = onUnlockItem, onUnlockItem = onUnlockItem,
shoppingListsTitle = shoppingListsTitle, shoppingListsTitle = shoppingListsTitle,
@@ -1096,22 +1052,22 @@ fun AppShell(
isEncryptionEnabled = isEncryptionEnabled, isEncryptionEnabled = isEncryptionEnabled,
onEncryptionToggle = onEncryptionToggle, onEncryptionToggle = onEncryptionToggle,
onSetEncryptionPassword = onSetEncryptionPassword, onSetEncryptionPassword = onSetEncryptionPassword,
onRemoveEncryption = onRemoveEncryption,
hasEncryptionPassword = hasEncryptionPassword, hasEncryptionPassword = hasEncryptionPassword,
biometricAuthenticator = biometricAuthenticator, biometricAuthenticator = biometricAuthenticator,
sharedPrefs = sharedPrefs, sharedPrefs = sharedPrefs,
onResetShoppingListsTitle = { onResetShoppingListsTitle = {
sharedPrefs.edit { remove("shopping_lists_title") } sharedPrefs.edit { remove("shopping_lists_title") }
shoppingListsTitle = context.getString(R.string.menu_shopping_lists) setShoppingListsTitle(context.getString(R.string.menu_shopping_lists))
}, },
onResetRecipesTitle = { onResetRecipesTitle = {
sharedPrefs.edit { remove("recipes_title") } sharedPrefs.edit { remove("recipes_title") }
recipesTitle = context.getString(R.string.menu_recipes) setRecipesTitle(context.getString(R.string.menu_recipes))
}, },
startScreen = startScreen, startScreen = startScreen,
onStartScreenChange = { onStartScreenChange = {
sharedPrefs.edit { putString("default_start_screen", it) } sharedPrefs.edit { putString("default_start_screen", it) }
startScreen = it setStartScreen(it)
}, },
canUseBiometrics = canUseBiometrics, canUseBiometrics = canUseBiometrics,
webAppIntegrationViewModel = webAppIntegrationViewModel, webAppIntegrationViewModel = webAppIntegrationViewModel,
@@ -1125,79 +1081,67 @@ fun AppShell(
shoppingListsViewModel = shoppingListsViewModel, shoppingListsViewModel = shoppingListsViewModel,
recipesViewModel = recipesViewModel, recipesViewModel = recipesViewModel,
showListDialog = showListDialog, showListDialog = showListDialog,
onShowListDialogChange = { showListDialog = it }, onShowListDialogChange = { setShowListDialog(it) },
listDetails = listDetails, listDetails = listDetails,
onListDetailsChange = { shoppingListsViewModel.updateListDetails(it) }, onListDetailsChange = { shoppingListsViewModel.updateListDetails(it) },
onSaveList = { scope.launch { shoppingListsViewModel.saveList() } },
onResetListDetails = { shoppingListsViewModel.resetListDetails() },
onSetListProtection = { password -> onSetListProtection = { password ->
selectedListId?.let { listId -> selectedListId?.let { listId ->
shoppingListsViewModel.setProtection(listId, password) shoppingListsViewModel.setProtection(listId, password)
currentScreen = Screen.ShoppingLists setCurrentScreen(Screen.ShoppingLists)
} }
selectedListId = null setSelectedListId(null)
showSetListPasswordDialog = false setShowSetListPasswordDialog(false)
}, },
onSetListProtectionBiometric = { id ->
shoppingListsViewModel.setProtectionBiometric(id)
currentScreen = Screen.ShoppingLists
},
txtImportLauncher = txtImportLauncher, txtImportLauncher = txtImportLauncher,
showNoteDialog = showNoteDialog, showNoteDialog = showNoteDialog,
onShowNoteDialogChange = { showNoteDialog = it }, onShowNoteDialogChange = { setShowNoteDialog(it) },
noteDetails = noteDetails, noteDetails = noteDetails,
onNoteDetailsChange = { notesViewModel.updateNoteDetails(it) }, onNoteDetailsChange = { notesViewModel.updateNoteDetails(it) },
onSaveNote = { scope.launch { notesViewModel.saveNote() } },
onResetNoteDetails = { notesViewModel.resetNoteDetails() },
onSetNoteProtectionBiometric = { id ->
notesViewModel.setProtectionBiometric(id)
currentScreen = Screen.Notes
},
noteImportLauncher = noteImportLauncher, noteImportLauncher = noteImportLauncher,
showRecipeDialog = showRecipeDialog, showRecipeDialog = showRecipeDialog,
onShowRecipeDialogChange = { showRecipeDialog = it }, onShowRecipeDialogChange = { setShowRecipeDialog(it) },
recipeDetails = recipeDetails, recipeDetails = recipeDetails,
onRecipeDetailsChange = { recipesViewModel.updateRecipeDetails(it) }, onRecipeDetailsChange = { recipesViewModel.updateRecipeDetails(it) },
onSaveRecipe = { scope.launch { recipesViewModel.saveRecipe() } },
onResetRecipeDetails = { recipesViewModel.resetRecipeDetails() },
onSetRecipeProtectionBiometric = { id ->
recipesViewModel.setProtectionBiometric(id)
currentScreen = Screen.Recipes
},
recipeImportLauncher = recipeImportLauncher, recipeImportLauncher = recipeImportLauncher,
recipesTitle = recipesTitle, recipesTitle = recipesTitle,
showJsonDialog = showJsonDialog, showJsonDialog = showJsonDialog,
onShowJsonDialogChange = { showJsonDialog = it }, onShowJsonDialogChange = { setShowJsonDialog(it) },
shoppingListsTitle = shoppingListsTitle, shoppingListsTitle = shoppingListsTitle,
fileEncryptor = fileEncryptor, fileEncryptor = fileEncryptor,
keyManager = keyManager, keyManager = keyManager,
biometricAuthenticator = biometricAuthenticator, biometricAuthenticator = biometricAuthenticator,
showSetPasswordDialog = showSetPasswordDialog, showSetPasswordDialog = showSetPasswordDialog,
onShowSetPasswordDialogChange = { showSetPasswordDialog = it }, onShowSetPasswordDialogChange = { setShowSetPasswordDialog(it) },
onSetNotePassword = { password -> onSetNotePassword = { password ->
notesViewModel.setProtectionPassword(password) notesViewModel.setProtectionPassword(password)
selectedNoteId = null setSelectedNoteId(null)
showSetPasswordDialog = false setShowSetPasswordDialog(false)
currentScreen = Screen.Notes setCurrentScreen(Screen.Notes)
}, },
showSetRecipePasswordDialog = showSetRecipePasswordDialog, showSetRecipePasswordDialog = showSetRecipePasswordDialog,
onShowSetRecipePasswordDialogChange = { showSetRecipePasswordDialog = it }, onShowSetRecipePasswordDialogChange = { setShowSetRecipePasswordDialog(it) },
onSetRecipePassword = { password -> onSetRecipePassword = { password ->
recipesViewModel.setProtectionPassword(password) recipesViewModel.setProtectionPassword(password)
selectedRecipeId = null setSelectedRecipeId(null)
showSetRecipePasswordDialog = false setShowSetRecipePasswordDialog(false)
currentScreen = Screen.Recipes setCurrentScreen(Screen.Recipes)
}, },
showSetListPasswordDialog = showSetListPasswordDialog, showSetListPasswordDialog = showSetListPasswordDialog,
onShowSetListPasswordDialogChange = { showSetListPasswordDialog = it }, onShowSetListPasswordDialogChange = { setShowSetListPasswordDialog(it) },
showUnlockPasswordDialog = showUnlockPasswordDialog, showUnlockPasswordDialog = showUnlockPasswordDialog,
onShowUnlockPasswordDialogChange = { onShowUnlockPasswordDialogChange = {
showUnlockPasswordDialog = it setShowUnlockPasswordDialog(it)
if (!it) { if (!it) {
unlockErrorMessage = null setUnlockErrorMessage(null)
itemToUnlockId = null setItemToUnlockId(null)
itemToUnlockType = null setItemToUnlockType(null)
} }
}, },
onUnlock = { password -> onUnlock = { password ->
@@ -1209,44 +1153,44 @@ fun AppShell(
val note = notesViewModel.uiState.value.noteList.find { it.id == itemToUnlockId } val note = notesViewModel.uiState.value.noteList.find { it.id == itemToUnlockId }
if (note != null && note.protectionHash == hashedPassword) { if (note != null && note.protectionHash == hashedPassword) {
isPasswordCorrect = true isPasswordCorrect = true
selectedNoteId = itemToUnlockId setSelectedNoteId(itemToUnlockId)
} }
} }
Screen.RecipeDetail -> { Screen.RecipeDetail -> {
val recipe = recipesViewModel.uiState.value.recipeList.find { it.id == itemToUnlockId } val recipe = recipesViewModel.uiState.value.recipeList.find { it.id == itemToUnlockId }
if (recipe != null && recipe.protectionHash == hashedPassword) { if (recipe != null && recipe.protectionHash == hashedPassword) {
isPasswordCorrect = true isPasswordCorrect = true
selectedRecipeId = itemToUnlockId setSelectedRecipeId(itemToUnlockId)
} }
} }
Screen.ShoppingListDetail -> { Screen.ShoppingListDetail -> {
val listWithItems = shoppingListsViewModel.uiState.value.shoppingLists.find { it.shoppingList.id == itemToUnlockId } val listWithItems = shoppingListsViewModel.uiState.value.shoppingLists.find { it.shoppingList.id == itemToUnlockId }
if (listWithItems != null && listWithItems.shoppingList.protectionHash == hashedPassword) { if (listWithItems != null && listWithItems.shoppingList.protectionHash == hashedPassword) {
isPasswordCorrect = true isPasswordCorrect = true
selectedListId = itemToUnlockId setSelectedListId(itemToUnlockId)
} }
} }
else -> {} // Should not happen else -> {} // Should not happen
} }
if (isPasswordCorrect) { if (isPasswordCorrect) {
unlockErrorMessage = null setUnlockErrorMessage(null)
itemWasInitiallyLocked = true // Mark as initially locked for content decryption setItemWasInitiallyLocked(true) // Mark as initially locked for content decryption
currentScreen = itemToUnlockType!! setCurrentScreen(itemToUnlockType!!)
itemToUnlockId = null setItemToUnlockId(null)
itemToUnlockType = null setItemToUnlockType(null)
showUnlockPasswordDialog = false setShowUnlockPasswordDialog(false)
} else { } else {
unlockErrorMessage = context.getString(R.string.incorrect_password) setUnlockErrorMessage(context.getString(R.string.incorrect_password))
} }
}, },
unlockErrorMessage = unlockErrorMessage, unlockErrorMessage = unlockErrorMessage,
showPasswordDialog = showPasswordDialog, showPasswordDialog = showPasswordDialog,
onShowPasswordDialogChange = { showPasswordDialog = it }, onShowPasswordDialogChange = { setShowPasswordDialog(it) },
onHasEncryptionPasswordChange = { hasEncryptionPassword = it }, onHasEncryptionPasswordChange = { setHasEncryptionPassword(it) },
sharedPrefs = sharedPrefs, sharedPrefs = sharedPrefs,
showDeleteConfirmationDialog = showDeleteConfirmationDialog, showDeleteConfirmationDialog = showDeleteConfirmationDialog,
onShowDeleteConfirmationDialogChange = { showDeleteConfirmationDialog = it }, onShowDeleteConfirmationDialogChange = { setShowDeleteConfirmationDialog(it) },
onDeleteSyncFolder = { onDeleteSyncFolder = {
val syncFolderUriString = sharedPrefs.getString("sync_folder_uri", null) val syncFolderUriString = sharedPrefs.getString("sync_folder_uri", null)
if (syncFolderUriString != null) { if (syncFolderUriString != null) {
@@ -1259,19 +1203,19 @@ fun AppShell(
} }
}, },
showRenameDialog = showRenameDialog, showRenameDialog = showRenameDialog,
onShowRenameDialogChange = { showRenameDialog = it }, onShowRenameDialogChange = { setShowRenameDialog(it) },
onRenameShoppingListsTitle = { newTitle -> onRenameShoppingListsTitle = { newTitle ->
sharedPrefs.edit { putString("shopping_lists_title", newTitle) } sharedPrefs.edit { putString("shopping_lists_title", newTitle) }
shoppingListsTitle = newTitle setShoppingListsTitle(newTitle)
}, },
showRecipeRenameDialog = showRecipeRenameDialog, showRecipeRenameDialog = showRecipeRenameDialog,
onShowRecipeRenameDialogChange = { showRecipeRenameDialog = it }, onShowRecipeRenameDialogChange = { setShowRecipeRenameDialog(it) },
onRenameRecipesTitle = { newTitle -> onRenameRecipesTitle = { newTitle ->
sharedPrefs.edit { putString("recipes_title", newTitle) } sharedPrefs.edit { putString("recipes_title", newTitle) }
recipesTitle = newTitle setRecipesTitle(newTitle)
}, },
showChooseLockMethodDialog = showChooseLockMethodDialog, showChooseLockMethodDialog = showChooseLockMethodDialog,
onShowChooseLockMethodDialogChange = { showChooseLockMethodDialog = it }, onShowChooseLockMethodDialogChange = { setShowChooseLockMethodDialog(it) },
onConfirmLockMethod = { lockMethod -> onConfirmLockMethod = { lockMethod ->
itemToLockType?.let { type -> itemToLockType?.let { type ->
itemToLockId?.let { id -> itemToLockId?.let { id ->
@@ -1279,63 +1223,62 @@ fun AppShell(
LockableItemType.SHOPPING_LIST -> { LockableItemType.SHOPPING_LIST -> {
when (lockMethod) { when (lockMethod) {
LockMethod.PASSWORD -> { LockMethod.PASSWORD -> {
selectedListId = id setSelectedListId(id)
showSetListPasswordDialog = true setShowSetListPasswordDialog(true)
} }
LockMethod.BIOMETRIC -> { LockMethod.BIOMETRIC -> {
shoppingListsViewModel.setProtectionBiometric(id) shoppingListsViewModel.setProtectionBiometric(id)
currentScreen = Screen.ShoppingLists setCurrentScreen(Screen.ShoppingLists)
} }
} }
} }
LockableItemType.NOTE -> { LockableItemType.NOTE -> {
when (lockMethod) { when (lockMethod) {
LockMethod.PASSWORD -> { LockMethod.PASSWORD -> {
selectedNoteId = id setSelectedNoteId(id)
showSetPasswordDialog = true setShowSetPasswordDialog(true)
} }
LockMethod.BIOMETRIC -> { LockMethod.BIOMETRIC -> {
notesViewModel.setProtectionBiometric(id) notesViewModel.setProtectionBiometric(id)
currentScreen = Screen.Notes setCurrentScreen(Screen.Notes)
} }
} }
} }
LockableItemType.RECIPE -> { LockableItemType.RECIPE -> {
when (lockMethod) { when (lockMethod) {
LockMethod.PASSWORD -> { LockMethod.PASSWORD -> {
selectedRecipeId = id setSelectedRecipeId(id)
showSetRecipePasswordDialog = true setShowSetRecipePasswordDialog(true)
} }
LockMethod.BIOMETRIC -> { LockMethod.BIOMETRIC -> {
recipesViewModel.setProtectionBiometric(id) recipesViewModel.setProtectionBiometric(id)
currentScreen = Screen.Recipes setCurrentScreen(Screen.Recipes)
} }
} }
} }
} }
} }
} }
showChooseLockMethodDialog = false setShowChooseLockMethodDialog(false)
itemToLockId = null setItemToLockId(null)
itemToLockType = null setItemToLockType(null)
}, },
canUseBiometrics = canUseBiometrics, canUseBiometrics = canUseBiometrics,
itemToLockType = itemToLockType,
showStartupPasswordDialog = showStartupPasswordDialog, showStartupPasswordDialog = showStartupPasswordDialog,
onShowStartupPasswordDialogChange = { showStartupPasswordDialog = it }, onShowStartupPasswordDialogChange = { setShowStartupPasswordDialog(it) },
onUnlockEncryption = { password -> onUnlockEncryption = { password ->
scope.launch { scope.launch {
try { try {
val decryptionCipher = keyManager.getDecryptionCipher() val decryptionCipher = keyManager.getDecryptionCipher()
if (decryptionCipher == null) { if (decryptionCipher == null) {
startupUnlockErrorMessage = context.getString(R.string.unlock_failed) setStartupUnlockErrorMessage(context.getString(R.string.unlock_failed))
return@launch return@launch
} }
val sharedPrefs = context.getSharedPreferences("encryption_prefs", Context.MODE_PRIVATE) val sharedPrefs = context.getSharedPreferences("encryption_prefs", Context.MODE_PRIVATE)
val encryptedDerivedKeyString = sharedPrefs.getString("encrypted_derived_key", null) val encryptedDerivedKeyString = sharedPrefs.getString("encrypted_derived_key", null)
if (encryptedDerivedKeyString == null) { if (encryptedDerivedKeyString == null) {
startupUnlockErrorMessage = context.getString(R.string.unlock_failed) setStartupUnlockErrorMessage(context.getString(R.string.unlock_failed))
return@launch return@launch
} }
val encryptedDerivedKey = Base64.decode(encryptedDerivedKeyString, Base64.DEFAULT) val encryptedDerivedKey = Base64.decode(encryptedDerivedKeyString, Base64.DEFAULT)
@@ -1348,16 +1291,16 @@ fun AppShell(
Log.d("AppShell_Unlock", "Current PBE Key: ${currentPbeKey.encoded.joinToString()}") Log.d("AppShell_Unlock", "Current PBE Key: ${currentPbeKey.encoded.joinToString()}")
if (decryptedStoredKeyBytes.contentEquals(currentPbeKey.encoded)) { if (decryptedStoredKeyBytes.contentEquals(currentPbeKey.encoded)) {
secretKey = SecretKeySpec(decryptedStoredKeyBytes, "AES") setSecretKey(SecretKeySpec(decryptedStoredKeyBytes, "AES"))
isDecryptionAttempted = true setIsDecryptionAttempted(true)
showStartupPasswordDialog = false setShowStartupPasswordDialog(false)
startupUnlockErrorMessage = null setStartupUnlockErrorMessage(null)
} else { } else {
startupUnlockErrorMessage = context.getString(R.string.incorrect_password) setStartupUnlockErrorMessage(context.getString(R.string.incorrect_password))
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e("AppShell", "Failed to unlock encryption with password", e) Log.e("AppShell", "Failed to unlock encryption with password", e)
startupUnlockErrorMessage = context.getString(R.string.unlock_failed) setStartupUnlockErrorMessage(context.getString(R.string.unlock_failed))
} }
} }
}, },

View File

@@ -30,6 +30,7 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.edit
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import de.lxtools.noteshop.BiometricAuthenticator import de.lxtools.noteshop.BiometricAuthenticator
import de.lxtools.noteshop.R import de.lxtools.noteshop.R
@@ -104,7 +105,7 @@ fun EncryptionPasswordDialog(
val encryptedDerivedKey = authorizedCipher.doFinal(pbeKey.encoded) val encryptedDerivedKey = authorizedCipher.doFinal(pbeKey.encoded)
val iv = authorizedCipher.iv val iv = authorizedCipher.iv
keyManager.storeEncryptedDerivedKey(encryptedDerivedKey, iv, useBiometrics) keyManager.storeEncryptedDerivedKey(encryptedDerivedKey, iv, useBiometrics)
sharedPrefs.edit().putBoolean("biometric_unlock_enabled", useBiometrics).apply() sharedPrefs.edit { putBoolean("biometric_unlock_enabled", useBiometrics) }
onPasswordSet() onPasswordSet()
Toast.makeText(context, R.string.encryption_password_set_success, Toast.LENGTH_SHORT).show() Toast.makeText(context, R.string.encryption_password_set_success, Toast.LENGTH_SHORT).show()
} }

View File

@@ -19,14 +19,15 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.HorizontalPagerIndicator
import com.google.accompanist.pager.rememberPagerState
import de.lxtools.noteshop.R import de.lxtools.noteshop.R
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.pager.HorizontalPager
@Composable @Composable
fun GuidedTourScreen(onTourFinished: () -> Unit) { fun GuidedTourScreen(onTourFinished: () -> Unit) {
val pagerState = rememberPagerState() val pagerState = rememberPagerState(pageCount = { 3 })
Column( Column(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
@@ -34,7 +35,7 @@ fun GuidedTourScreen(onTourFinished: () -> Unit) {
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {
HorizontalPager( HorizontalPager(
count = 3,
state = pagerState, state = pagerState,
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) { page -> ) { page ->
@@ -57,12 +58,8 @@ fun GuidedTourScreen(onTourFinished: () -> Unit) {
} }
} }
HorizontalPagerIndicator( // TODO: Implement a custom HorizontalPagerIndicator or use a third-party library
pagerState = pagerState,
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(16.dp)
)
Button( Button(
onClick = onTourFinished, onClick = onTourFinished,
@@ -70,7 +67,7 @@ fun GuidedTourScreen(onTourFinished: () -> Unit) {
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp) .padding(16.dp)
) { ) {
Text(text = if (pagerState.currentPage == 2) "Fertig" else "Überspringen") Text(text = if (pagerState.currentPage == 2) stringResource(id = R.string.tour_finish) else stringResource(id = R.string.tour_skip))
} }
} }
} }

View File

@@ -1,5 +1,6 @@
package de.lxtools.noteshop.ui package de.lxtools.noteshop.ui
import android.util.Log
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -89,14 +90,14 @@ fun JsonImportExportDialog(
scope.launch { scope.launch {
context.contentResolver.openInputStream(it)?.use { inputStream -> context.contentResolver.openInputStream(it)?.use { inputStream ->
val fileContent = inputStream.readBytes() val fileContent = inputStream.readBytes()
var jsonString: String? = null var jsonString: String?
if (secretKey != null) { if (secretKey != null) {
try { try {
val decryptedBytes = fileEncryptor.decrypt(fileContent, secretKey!!) val decryptedBytes = fileEncryptor.decrypt(fileContent, secretKey!!)
jsonString = decryptedBytes.toString(Charsets.UTF_8) jsonString = decryptedBytes.toString(Charsets.UTF_8)
} catch (e: javax.crypto.AEADBadTagException) { } catch (e: javax.crypto.AEADBadTagException) {
android.util.Log.w("JsonImportExportDialog", "Decryption failed, trying as unencrypted: ${e.message}") Log.w("JsonImportExportDialog", "Decryption failed, trying as unencrypted: ${e.message}")
// Fallback to unencrypted if decryption fails // Fallback to unencrypted if decryption fails
jsonString = fileContent.toString(Charsets.UTF_8) jsonString = fileContent.toString(Charsets.UTF_8)
} }
@@ -105,20 +106,16 @@ fun JsonImportExportDialog(
jsonString = fileContent.toString(Charsets.UTF_8) jsonString = fileContent.toString(Charsets.UTF_8)
} }
if (jsonString != null) {
try { try {
notesViewModel.importNotesFromJson(jsonString) notesViewModel.importNotesFromJson(jsonString)
android.widget.Toast.makeText(context, R.string.json_import_successful, android.widget.Toast.LENGTH_SHORT).show() Toast.makeText(context, R.string.json_import_successful, Toast.LENGTH_SHORT).show()
} catch (e: kotlinx.serialization.SerializationException) { } catch (e: SerializationException) {
android.util.Log.e("JsonImportExportDialog", "Error parsing notes JSON: ${e.message}") Log.e("JsonImportExportDialog", "Error parsing notes JSON: ${e.message}")
android.widget.Toast.makeText(context, context.getString(R.string.json_import_failed_invalid_format, e.message), android.widget.Toast.LENGTH_LONG).show() Toast.makeText(context, context.getString(R.string.json_import_failed_invalid_format, e.message), Toast.LENGTH_LONG).show()
} catch (e: Exception) { } catch (e: Exception) {
android.util.Log.e("JsonImportExportDialog", "Error importing notes: ${e.message}") Log.e("JsonImportExportDialog", "Error importing notes: ${e.message}")
android.widget.Toast.makeText(context, context.getString(R.string.json_import_failed_generic, e.message), android.widget.Toast.LENGTH_LONG).show() Toast.makeText(context, context.getString(R.string.json_import_failed_generic, e.message), Toast.LENGTH_LONG).show()
} }
} else {
android.widget.Toast.makeText(context, R.string.json_import_failed_no_data, android.widget.Toast.LENGTH_SHORT).show()
}
} }
} }
} }
@@ -133,12 +130,14 @@ fun JsonImportExportDialog(
scope.launch { scope.launch {
val allNotes = notesViewModel.uiState.value.noteList val allNotes = notesViewModel.uiState.value.noteList
val content = notesViewModel.exportNotesToJson(allNotes) val content = notesViewModel.exportNotesToJson(allNotes)
context.contentResolver.openOutputStream(it)?.use { outputStream -> context.contentResolver.openOutputStream(it, "wt")?.use { outputStream ->
secretKey?.let { key -> if (secretKey != null) {
val encryptedContent = fileEncryptor.encrypt(content.toByteArray(), key) val encryptedContent = fileEncryptor.encrypt(content.toByteArray(), secretKey!!)
outputStream.write(encryptedContent) outputStream.write(encryptedContent)
} else {
outputStream.write(content.toByteArray(Charsets.UTF_8))
} }
android.widget.Toast.makeText(context, R.string.json_export_successful, android.widget.Toast.LENGTH_SHORT).show() Toast.makeText(context, R.string.json_export_successful, Toast.LENGTH_SHORT).show()
} }
} }
} }
@@ -153,12 +152,14 @@ fun JsonImportExportDialog(
scope.launch { scope.launch {
val allShoppingLists = shoppingListsViewModel.uiState.value.shoppingLists val allShoppingLists = shoppingListsViewModel.uiState.value.shoppingLists
val content = shoppingListsViewModel.exportShoppingListsToJson(allShoppingLists) val content = shoppingListsViewModel.exportShoppingListsToJson(allShoppingLists)
context.contentResolver.openOutputStream(it)?.use { outputStream -> context.contentResolver.openOutputStream(it, "wt")?.use { outputStream ->
secretKey?.let { key -> if (secretKey != null) {
val encryptedContent = fileEncryptor.encrypt(content.toByteArray(), key) val encryptedContent = fileEncryptor.encrypt(content.toByteArray(), secretKey!!)
outputStream.write(encryptedContent) outputStream.write(encryptedContent)
} else {
outputStream.write(content.toByteArray(Charsets.UTF_8))
} }
android.widget.Toast.makeText(context, R.string.json_export_successful, android.widget.Toast.LENGTH_SHORT).show() Toast.makeText(context, R.string.json_export_successful, Toast.LENGTH_SHORT).show()
} }
} }
} }
@@ -173,14 +174,14 @@ fun JsonImportExportDialog(
scope.launch { scope.launch {
context.contentResolver.openInputStream(it)?.use { inputStream -> context.contentResolver.openInputStream(it)?.use { inputStream ->
val fileContent = inputStream.readBytes() val fileContent = inputStream.readBytes()
var jsonString: String? = null var jsonString: String?
if (secretKey != null) { if (secretKey != null) {
try { try {
val decryptedBytes = fileEncryptor.decrypt(fileContent, secretKey!!) val decryptedBytes = fileEncryptor.decrypt(fileContent, secretKey!!)
jsonString = decryptedBytes.toString(Charsets.UTF_8) jsonString = decryptedBytes.toString(Charsets.UTF_8)
} catch (e: javax.crypto.AEADBadTagException) { } catch (e: javax.crypto.AEADBadTagException) {
android.util.Log.w("JsonImportExportDialog", "Decryption failed, trying as unencrypted: ${e.message}") Log.w("JsonImportExportDialog", "Decryption failed, trying as unencrypted: ${e.message}")
// Fallback to unencrypted if decryption fails // Fallback to unencrypted if decryption fails
jsonString = fileContent.toString(Charsets.UTF_8) jsonString = fileContent.toString(Charsets.UTF_8)
} }
@@ -189,20 +190,16 @@ fun JsonImportExportDialog(
jsonString = fileContent.toString(Charsets.UTF_8) jsonString = fileContent.toString(Charsets.UTF_8)
} }
if (jsonString != null) {
try { try {
shoppingListsViewModel.importShoppingListsFromJson(jsonString) shoppingListsViewModel.importShoppingListsFromJson(jsonString)
android.widget.Toast.makeText(context, R.string.json_import_successful, android.widget.Toast.LENGTH_SHORT).show() Toast.makeText(context, R.string.json_import_successful, Toast.LENGTH_SHORT).show()
} catch (e: kotlinx.serialization.SerializationException) { } catch (e: SerializationException) {
android.util.Log.e("JsonImportExportDialog", "Error parsing shopping lists JSON: ${e.message}") Log.e("JsonImportExportDialog", "Error parsing shopping lists JSON: ${e.message}")
android.widget.Toast.makeText(context, context.getString(R.string.json_import_failed_invalid_format, e.message), android.widget.Toast.LENGTH_LONG).show() Toast.makeText(context, context.getString(R.string.json_import_failed_invalid_format, e.message), Toast.LENGTH_LONG).show()
} catch (e: Exception) { } catch (e: Exception) {
android.util.Log.e("JsonImportExportDialog", "Error importing shopping lists: ${e.message}") Log.e("JsonImportExportDialog", "Error importing shopping lists: ${e.message}")
android.widget.Toast.makeText(context, context.getString(R.string.json_import_failed_generic, e.message), android.widget.Toast.LENGTH_LONG).show() Toast.makeText(context, context.getString(R.string.json_import_failed_generic, e.message), Toast.LENGTH_LONG).show()
} }
} else {
android.widget.Toast.makeText(context, R.string.json_import_failed_no_data, android.widget.Toast.LENGTH_SHORT).show()
}
} }
} }
} }
@@ -217,12 +214,14 @@ fun JsonImportExportDialog(
scope.launch { scope.launch {
val allRecipes = recipesViewModel.uiState.value.recipeList val allRecipes = recipesViewModel.uiState.value.recipeList
val content = recipesViewModel.exportRecipesToJson(allRecipes) val content = recipesViewModel.exportRecipesToJson(allRecipes)
context.contentResolver.openOutputStream(it)?.use { outputStream -> context.contentResolver.openOutputStream(it, "wt")?.use { outputStream ->
secretKey?.let { key -> if (secretKey != null) {
val encryptedContent = fileEncryptor.encrypt(content.toByteArray(), key) val encryptedContent = fileEncryptor.encrypt(content.toByteArray(), secretKey!!)
outputStream.write(encryptedContent) outputStream.write(encryptedContent)
} else {
outputStream.write(content.toByteArray(Charsets.UTF_8))
} }
android.widget.Toast.makeText(context, R.string.json_export_successful, android.widget.Toast.LENGTH_SHORT).show() Toast.makeText(context, R.string.json_export_successful, Toast.LENGTH_SHORT).show()
} }
} }
} }
@@ -237,14 +236,14 @@ fun JsonImportExportDialog(
scope.launch { scope.launch {
context.contentResolver.openInputStream(it)?.use { inputStream -> context.contentResolver.openInputStream(it)?.use { inputStream ->
val fileContent = inputStream.readBytes() val fileContent = inputStream.readBytes()
var jsonString: String? = null var jsonString: String?
if (secretKey != null) { if (secretKey != null) {
try { try {
val decryptedBytes = fileEncryptor.decrypt(fileContent, secretKey!!) val decryptedBytes = fileEncryptor.decrypt(fileContent, secretKey!!)
jsonString = decryptedBytes.toString(Charsets.UTF_8) jsonString = decryptedBytes.toString(Charsets.UTF_8)
} catch (e: javax.crypto.AEADBadTagException) { } catch (e: javax.crypto.AEADBadTagException) {
android.util.Log.w("JsonImportExportDialog", "Decryption failed, trying as unencrypted: ${e.message}") Log.w("JsonImportExportDialog", "Decryption failed, trying as unencrypted: ${e.message}")
// Fallback to unencrypted if decryption fails // Fallback to unencrypted if decryption fails
jsonString = fileContent.toString(Charsets.UTF_8) jsonString = fileContent.toString(Charsets.UTF_8)
} }
@@ -253,20 +252,16 @@ fun JsonImportExportDialog(
jsonString = fileContent.toString(Charsets.UTF_8) jsonString = fileContent.toString(Charsets.UTF_8)
} }
if (jsonString != null) {
try { try {
recipesViewModel.importRecipesFromJson(jsonString) recipesViewModel.importRecipesFromJson(jsonString)
android.widget.Toast.makeText(context, R.string.json_import_successful, android.widget.Toast.LENGTH_SHORT).show() Toast.makeText(context, R.string.json_import_successful, Toast.LENGTH_SHORT).show()
} catch (e: kotlinx.serialization.SerializationException) { } catch (e: SerializationException) {
android.util.Log.e("JsonImportExportDialog", "Error parsing recipes JSON: ${e.message}") Log.e("JsonImportExportDialog", "Error parsing recipes JSON: ${e.message}")
android.widget.Toast.makeText(context, context.getString(R.string.json_import_failed_invalid_format, e.message), android.widget.Toast.LENGTH_LONG).show() Toast.makeText(context, context.getString(R.string.json_import_failed_invalid_format, e.message), Toast.LENGTH_LONG).show()
} catch (e: Exception) { } catch (e: Exception) {
android.util.Log.e("JsonImportExportDialog", "Error importing recipes: ${e.message}") Log.e("JsonImportExportDialog", "Error importing recipes: ${e.message}")
android.widget.Toast.makeText(context, context.getString(R.string.json_import_failed_generic, e.message), android.widget.Toast.LENGTH_LONG).show() Toast.makeText(context, context.getString(R.string.json_import_failed_generic, e.message), Toast.LENGTH_LONG).show()
} }
} else {
android.widget.Toast.makeText(context, R.string.json_import_failed_no_data, android.widget.Toast.LENGTH_SHORT).show()
}
} }
} }
} }

View File

@@ -1,107 +0,0 @@
package de.lxtools.noteshop.ui
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
@Composable
fun PatternInput(
onPatternComplete: (List<Int>) -> Unit,
modifier: Modifier = Modifier
) {
var pattern by remember { mutableStateOf(emptyList<Int>()) }
var currentPosition by remember { mutableStateOf<Offset?>(null) }
val dotPositions = remember { mutableMapOf<Int, Offset>() }
val density = LocalDensity.current
Box(modifier = modifier) {
Canvas(modifier = Modifier
.size(300.dp)
.pointerInput(Unit) {
detectDragGestures(
onDragStart = { offset ->
currentPosition = offset
val dot = getDotAt(offset, dotPositions, density)
if (dot != null && !pattern.contains(dot)) {
pattern = pattern + dot
}
},
onDrag = { change, _ ->
currentPosition = change.position
val dot = getDotAt(change.position, dotPositions, density)
if (dot != null && !pattern.contains(dot)) {
pattern = pattern + dot
}
},
onDragEnd = {
onPatternComplete(pattern)
pattern = emptyList()
currentPosition = null
}
)
}) {
val dotRadius = 16.dp.toPx()
val dotColor = Color.Gray
val selectedDotColor = Color.Black
val lineColor = Color.Black
for (i in 1..9) {
val row = (i - 1) / 3
val col = (i - 1) % 3
val x = col * (size.width / 3) + (size.width / 6)
val y = row * (size.height / 3) + (size.height / 6)
val center = Offset(x, y)
dotPositions[i] = center
drawCircle(
color = if (pattern.contains(i)) selectedDotColor else dotColor,
radius = dotRadius,
center = center
)
}
if (pattern.isNotEmpty()) {
for (i in 0 until pattern.size - 1) {
val start = dotPositions[pattern[i]]
val end = dotPositions[pattern[i + 1]]
if (start != null && end != null) {
drawLine(
color = lineColor,
start = start,
end = end,
strokeWidth = 8.dp.toPx()
)
}
}
val lastDot = pattern.last()
val lastDotCenter = dotPositions[lastDot]
if (lastDotCenter != null && currentPosition != null) {
drawLine(
color = lineColor,
start = lastDotCenter,
end = currentPosition!!,
strokeWidth = 8.dp.toPx()
)
}
}
}
}
}
private fun getDotAt(offset: Offset, dotPositions: Map<Int, Offset>, density: Density): Int? {
for ((dot, position) in dotPositions) {
val distance = (offset - position).getDistance()
if (distance < with(density) { 50.dp.toPx() }) {
return dot
}
}
return null
}

View File

@@ -34,7 +34,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import de.lxtools.noteshop.R import de.lxtools.noteshop.R
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.biometric.BiometricManager
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import de.lxtools.noteshop.ui.theme.* import de.lxtools.noteshop.ui.theme.*
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
@@ -64,7 +63,7 @@ fun SettingsScreen(
isEncryptionEnabled: Boolean, isEncryptionEnabled: Boolean,
onEncryptionToggle: (Boolean) -> Unit, onEncryptionToggle: (Boolean) -> Unit,
onSetEncryptionPassword: () -> Unit, onSetEncryptionPassword: () -> Unit,
onRemoveEncryption: () -> Unit,
hasEncryptionPassword: Boolean, hasEncryptionPassword: Boolean,
biometricAuthenticator: BiometricAuthenticator, biometricAuthenticator: BiometricAuthenticator,
sharedPrefs: SharedPreferences, sharedPrefs: SharedPreferences,
@@ -85,10 +84,7 @@ fun SettingsScreen(
val currentStartScreenName = startScreenOptions[currentStartScreen] ?: stringResource(R.string.start_screen_last) val currentStartScreenName = startScreenOptions[currentStartScreen] ?: stringResource(R.string.start_screen_last)
var isBiometricUnlockEnabled by remember { mutableStateOf(sharedPrefs.getBoolean("biometric_unlock_enabled", false)) } var isBiometricUnlockEnabled by remember { mutableStateOf(sharedPrefs.getBoolean("biometric_unlock_enabled", false)) }
val context = LocalContext.current val context = LocalContext.current
val biometricManager = BiometricManager.from(context)
val canUseBiometrics = remember {
biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) == BiometricManager.BIOMETRIC_SUCCESS
}
val listState = rememberLazyListState() val listState = rememberLazyListState()
val authenticateAndProceed: ((() -> Unit) -> Unit) = { successAction -> val authenticateAndProceed: ((() -> Unit) -> Unit) = { successAction ->
@@ -289,9 +285,8 @@ fun SettingsScreen(
Switch( Switch(
checked = isBiometricUnlockEnabled, checked = isBiometricUnlockEnabled,
onCheckedChange = { onCheckedChange = {
val newValue = it sharedPrefs.edit { putBoolean ("biometric_unlock_enabled", it) }
sharedPrefs.edit { putBoolean("biometric_unlock_enabled", newValue) } isBiometricUnlockEnabled = it
isBiometricUnlockEnabled = newValue
} }
) )
} }

View File

@@ -1,6 +1,6 @@
package de.lxtools.noteshop.ui.appshell package de.lxtools.noteshop.ui.appshell
import android.util.Log
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
@@ -54,7 +54,7 @@ fun AppContent(
isEncryptionEnabled: Boolean, isEncryptionEnabled: Boolean,
onEncryptionToggle: (Boolean) -> Unit, onEncryptionToggle: (Boolean) -> Unit,
onSetEncryptionPassword: () -> Unit, onSetEncryptionPassword: () -> Unit,
onRemoveEncryption: () -> Unit,
hasEncryptionPassword: Boolean, hasEncryptionPassword: Boolean,
biometricAuthenticator: BiometricAuthenticator, biometricAuthenticator: BiometricAuthenticator,
sharedPrefs: android.content.SharedPreferences, sharedPrefs: android.content.SharedPreferences,
@@ -117,7 +117,6 @@ fun AppContent(
listId = selectedListId, listId = selectedListId,
viewModel = shoppingListsViewModel, viewModel = shoppingListsViewModel,
dynamicStrings = dynamicStrings, dynamicStrings = dynamicStrings,
wasInitiallyLocked = itemWasInitiallyLocked,
isContentDecrypted = itemWasInitiallyLocked, isContentDecrypted = itemWasInitiallyLocked,
onUnlockClick = { onUnlockClick = {
val list = shoppingListsViewModel.uiState.value.shoppingLists.find { it.shoppingList.id == selectedListId }?.shoppingList val list = shoppingListsViewModel.uiState.value.shoppingLists.find { it.shoppingList.id == selectedListId }?.shoppingList
@@ -186,7 +185,7 @@ fun AppContent(
isEncryptionEnabled = isEncryptionEnabled, isEncryptionEnabled = isEncryptionEnabled,
onEncryptionToggle = onEncryptionToggle, onEncryptionToggle = onEncryptionToggle,
onSetEncryptionPassword = onSetEncryptionPassword, onSetEncryptionPassword = onSetEncryptionPassword,
onRemoveEncryption = onRemoveEncryption,
hasEncryptionPassword = hasEncryptionPassword, hasEncryptionPassword = hasEncryptionPassword,
biometricAuthenticator = biometricAuthenticator, biometricAuthenticator = biometricAuthenticator,
sharedPrefs = sharedPrefs, sharedPrefs = sharedPrefs,
@@ -201,7 +200,6 @@ fun AppContent(
is Screen.WebAppIntegration -> { is Screen.WebAppIntegration -> {
WebAppIntegrationScreen( WebAppIntegrationScreen(
viewModel = webAppIntegrationViewModel, viewModel = webAppIntegrationViewModel,
onNavigateUp = { onScreenChange(Screen.Settings) },
padding = innerPadding, padding = innerPadding,
onAuthenticateAndTogglePasswordVisibility = onAuthenticateAndTogglePasswordVisibility onAuthenticateAndTogglePasswordVisibility = onAuthenticateAndTogglePasswordVisibility
) )

View File

@@ -1,6 +1,6 @@
package de.lxtools.noteshop.ui.appshell package de.lxtools.noteshop.ui.appshell
import android.util.Log
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
@@ -27,11 +27,11 @@ import de.lxtools.noteshop.ui.recipes.RecipesViewModel
import de.lxtools.noteshop.ui.shopping.ShoppingListDetails import de.lxtools.noteshop.ui.shopping.ShoppingListDetails
import de.lxtools.noteshop.ui.shopping.ShoppingListInputDialog import de.lxtools.noteshop.ui.shopping.ShoppingListInputDialog
import de.lxtools.noteshop.ui.ChooseLockMethodDialog import de.lxtools.noteshop.ui.ChooseLockMethodDialog
import de.lxtools.noteshop.ui.LockableItemType
import de.lxtools.noteshop.ui.LockMethod import de.lxtools.noteshop.ui.LockMethod
import de.lxtools.noteshop.ui.shopping.ShoppingListsViewModel import de.lxtools.noteshop.ui.shopping.ShoppingListsViewModel
import de.lxtools.noteshop.ui.StartupPasswordDialog import de.lxtools.noteshop.ui.StartupPasswordDialog
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import android.util.Log
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import de.lxtools.noteshop.findActivity import de.lxtools.noteshop.findActivity
@@ -44,26 +44,26 @@ fun AppDialogs(
onShowListDialogChange: (Boolean) -> Unit, onShowListDialogChange: (Boolean) -> Unit,
listDetails: ShoppingListDetails, listDetails: ShoppingListDetails,
onListDetailsChange: (ShoppingListDetails) -> Unit, onListDetailsChange: (ShoppingListDetails) -> Unit,
onSaveList: () -> Unit,
onResetListDetails: () -> Unit,
onSetListProtection: (String) -> Unit, onSetListProtection: (String) -> Unit,
onSetListProtectionBiometric: (Int) -> Unit,
txtImportLauncher: ActivityResultLauncher<Array<String>>, txtImportLauncher: ActivityResultLauncher<Array<String>>,
showNoteDialog: Boolean, showNoteDialog: Boolean,
onShowNoteDialogChange: (Boolean) -> Unit, onShowNoteDialogChange: (Boolean) -> Unit,
noteDetails: NoteDetails, noteDetails: NoteDetails,
onNoteDetailsChange: (NoteDetails) -> Unit, onNoteDetailsChange: (NoteDetails) -> Unit,
onSaveNote: () -> Unit,
onResetNoteDetails: () -> Unit,
onSetNoteProtectionBiometric: (Int) -> Unit,
noteImportLauncher: ActivityResultLauncher<Array<String>>, noteImportLauncher: ActivityResultLauncher<Array<String>>,
showRecipeDialog: Boolean, showRecipeDialog: Boolean,
onShowRecipeDialogChange: (Boolean) -> Unit, onShowRecipeDialogChange: (Boolean) -> Unit,
recipeDetails: RecipeDetails, recipeDetails: RecipeDetails,
onRecipeDetailsChange: (RecipeDetails) -> Unit, onRecipeDetailsChange: (RecipeDetails) -> Unit,
onSaveRecipe: () -> Unit,
onResetRecipeDetails: () -> Unit,
onSetRecipeProtectionBiometric: (Int) -> Unit,
recipeImportLauncher: ActivityResultLauncher<Array<String>>, recipeImportLauncher: ActivityResultLauncher<Array<String>>,
recipesTitle: String, recipesTitle: String,
showJsonDialog: Boolean, showJsonDialog: Boolean,
@@ -101,7 +101,6 @@ fun AppDialogs(
onShowChooseLockMethodDialogChange: (Boolean) -> Unit, onShowChooseLockMethodDialogChange: (Boolean) -> Unit,
onConfirmLockMethod: (LockMethod) -> Unit, onConfirmLockMethod: (LockMethod) -> Unit,
canUseBiometrics: Boolean, canUseBiometrics: Boolean,
itemToLockType: de.lxtools.noteshop.ui.LockableItemType?,
showStartupPasswordDialog: Boolean, showStartupPasswordDialog: Boolean,
onShowStartupPasswordDialogChange: (Boolean) -> Unit, onShowStartupPasswordDialogChange: (Boolean) -> Unit,
onUnlockEncryption: (String) -> Unit, onUnlockEncryption: (String) -> Unit,

View File

@@ -68,10 +68,6 @@ fun AppTopBar(
exportLauncher: ActivityResultLauncher<String>, exportLauncher: ActivityResultLauncher<String>,
noteExportLauncher: ActivityResultLauncher<String>, noteExportLauncher: ActivityResultLauncher<String>,
recipeExportLauncher: ActivityResultLauncher<String>, recipeExportLauncher: ActivityResultLauncher<String>,
hasEncryptionPassword: Boolean,
onShowSetPasswordDialog: () -> Unit,
onShowSetRecipePasswordDialog: () -> Unit,
onShowSetListPasswordDialog: () -> Unit,
onShowChooseLockMethodDialog: (LockableItemType, Int) -> Unit onShowChooseLockMethodDialog: (LockableItemType, Int) -> Unit
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@@ -296,7 +292,6 @@ fun AppTopBar(
shoppingListWithItems?.let { listWithItems -> shoppingListWithItems?.let { listWithItems ->
DropdownMenuItem( DropdownMenuItem(
text = { Text(stringResource(R.string.lock)) }, text = { Text(stringResource(R.string.lock)) },
enabled = hasEncryptionPassword,
onClick = { onClick = {
showMenu = false showMenu = false
onShowChooseLockMethodDialog(LockableItemType.SHOPPING_LIST, listWithItems.shoppingList.id) onShowChooseLockMethodDialog(LockableItemType.SHOPPING_LIST, listWithItems.shoppingList.id)
@@ -381,7 +376,6 @@ fun AppTopBar(
noteDetails.let { noteDetails.let {
DropdownMenuItem( DropdownMenuItem(
text = { Text(stringResource(R.string.lock)) }, text = { Text(stringResource(R.string.lock)) },
enabled = hasEncryptionPassword,
onClick = { onClick = {
showMenu = false showMenu = false
onShowChooseLockMethodDialog(LockableItemType.NOTE, it.id) onShowChooseLockMethodDialog(LockableItemType.NOTE, it.id)
@@ -468,7 +462,6 @@ fun AppTopBar(
recipeDetails.let { recipeDetails.let {
DropdownMenuItem( DropdownMenuItem(
text = { Text(stringResource(R.string.lock)) }, text = { Text(stringResource(R.string.lock)) },
enabled = hasEncryptionPassword,
onClick = { onClick = {
showMenu = false showMenu = false
onShowChooseLockMethodDialog(LockableItemType.RECIPE, it.id) onShowChooseLockMethodDialog(LockableItemType.RECIPE, it.id)

View File

@@ -30,13 +30,7 @@ import androidx.compose.runtime.remember
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import de.lxtools.noteshop.security.FileEncryptor
import javax.crypto.SecretKey
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import de.lxtools.noteshop.findActivity
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable

View File

@@ -82,7 +82,7 @@ class NotesViewModel(private val noteshopRepository: NoteshopRepository) : ViewM
suspend fun saveNote() { suspend fun saveNote() {
if (noteDetails.value.isValid()) { if (noteDetails.value.isValid()) {
var currentNote = noteDetails.value.toNote() val currentNote = noteDetails.value.toNote()
// Encryption on save is now handled by the new protection flow. // Encryption on save is now handled by the new protection flow.
@@ -140,35 +140,6 @@ class NotesViewModel(private val noteshopRepository: NoteshopRepository) : ViewM
} }
} }
fun setProtectionPattern(pattern: String) {
Log.d("NotesViewModel", "setProtectionPattern called with pattern: $pattern")
}
fun setProtectionPin(pin: String) {
viewModelScope.launch {
val currentNoteDetails = _noteDetails.value
if (pin.isNotBlank()) {
val hash = PasswordHasher.hashPassword(pin)
val updatedNote = currentNoteDetails.toNote().copy(
protectionHash = hash,
protectionType = 3, // 3 for PIN protection
lockMethod = 3
)
noteshopRepository.updateNote(updatedNote)
updateNoteDetails(updatedNote) // Update the UI state
} else {
// PIN is blank, so we remove protection
val updatedNote = currentNoteDetails.toNote().copy(
protectionHash = "",
protectionType = 0,
lockMethod = 0
)
noteshopRepository.updateNote(updatedNote)
updateNoteDetails(updatedNote) // Update the UI state
}
}
}
fun resetNoteDetails() { fun resetNoteDetails() {
_noteDetails.value = NoteDetails() _noteDetails.value = NoteDetails()
} }

View File

@@ -1,6 +1,5 @@
package de.lxtools.noteshop.ui.recipes package de.lxtools.noteshop.ui.recipes
import android.util.Log
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth

View File

@@ -80,7 +80,7 @@ class RecipesViewModel(private val noteshopRepository: NoteshopRepository) : Vie
suspend fun saveRecipe() { suspend fun saveRecipe() {
if (recipeDetails.value.isValid()) { if (recipeDetails.value.isValid()) {
var currentRecipe = recipeDetails.value.toRecipe() val currentRecipe = recipeDetails.value.toRecipe()
// Encryption on save is now handled by the new protection flow. // Encryption on save is now handled by the new protection flow.
@@ -138,35 +138,6 @@ class RecipesViewModel(private val noteshopRepository: NoteshopRepository) : Vie
} }
} }
fun setProtectionPattern(pattern: String) {
Log.d("RecipesViewModel", "setProtectionPattern called with pattern: $pattern")
}
fun setProtectionPin(pin: String) {
viewModelScope.launch {
val currentRecipeDetails = _recipeDetails.value
if (pin.isNotBlank()) {
val hash = PasswordHasher.hashPassword(pin)
val updatedRecipe = currentRecipeDetails.toRecipe().copy(
protectionHash = hash,
protectionType = 3, // 3 for PIN protection
lockMethod = 3
)
noteshopRepository.updateRecipe(updatedRecipe)
updateRecipeDetails(updatedRecipe) // Update the UI state
} else {
// PIN is blank, so we remove protection
val updatedRecipe = currentRecipeDetails.toRecipe().copy(
protectionHash = "",
protectionType = 0,
lockMethod = 0
)
noteshopRepository.updateRecipe(updatedRecipe)
updateRecipeDetails(updatedRecipe) // Update the UI state
}
}
}
fun resetRecipeDetails() { fun resetRecipeDetails() {
_recipeDetails.value = RecipeDetails() _recipeDetails.value = RecipeDetails()
} }

View File

@@ -49,19 +49,14 @@ import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import de.lxtools.noteshop.R import de.lxtools.noteshop.R
import de.lxtools.noteshop.data.ShoppingListItem import de.lxtools.noteshop.data.ShoppingListItem
import de.lxtools.noteshop.security.FileEncryptor
import javax.crypto.SecretKey
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import sh.calvin.reorderable.ReorderableItem import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyListState import sh.calvin.reorderable.rememberReorderableLazyListState
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.text.withStyle
import de.lxtools.noteshop.findActivity import de.lxtools.noteshop.findActivity
import androidx.compose.ui.text.withStyle
import androidx.activity.compose.BackHandler
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -70,18 +65,14 @@ fun ShoppingListDetailScreen(
viewModel: ShoppingListsViewModel, viewModel: ShoppingListsViewModel,
dynamicStrings: de.lxtools.noteshop.DynamicListStrings, dynamicStrings: de.lxtools.noteshop.DynamicListStrings,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
wasInitiallyLocked: Boolean,
isContentDecrypted: Boolean, isContentDecrypted: Boolean,
onUnlockClick: (Int) -> Unit, onUnlockClick: (Int) -> Unit,
onNavigateBack: () -> Unit onNavigateBack: () -> Unit
) { ) {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val keyboardController = LocalSoftwareKeyboardController.current
BackHandler(enabled = true) {
keyboardController?.hide()
onNavigateBack()
}
// Collect state from ViewModel // Collect state from ViewModel
val newItemName by viewModel.newItemName.collectAsState() val newItemName by viewModel.newItemName.collectAsState()
@@ -100,7 +91,7 @@ fun ShoppingListDetailScreen(
onDispose { onDispose {
val activity = context.findActivity() val activity = context.findActivity()
if (!activity.isChangingConfigurations) { if (!activity.isChangingConfigurations) {
// TODO: Handle lock state correctly onNavigateBack()
} }
} }
} }

View File

@@ -23,9 +23,8 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import de.lxtools.noteshop.security.FileEncryptor import de.lxtools.noteshop.security.FileEncryptor
import java.util.Base64
import javax.crypto.SecretKey
import de.lxtools.noteshop.security.PasswordHasher import de.lxtools.noteshop.security.PasswordHasher
import android.util.Base64
class ShoppingListsViewModel(private val noteshopRepository: NoteshopRepository, application: Application) : AndroidViewModel(application) { class ShoppingListsViewModel(private val noteshopRepository: NoteshopRepository, application: Application) : AndroidViewModel(application) {
@@ -193,35 +192,6 @@ class ShoppingListsViewModel(private val noteshopRepository: NoteshopRepository,
} }
} }
fun setProtectionPattern(pattern: String) {
Log.d("ShoppingListsViewModel", "setProtectionPattern called with pattern: $pattern")
}
fun setProtectionPin(pin: String) {
viewModelScope.launch {
val currentListDetails = _listDetails.value
if (pin.isNotBlank()) {
val hash = PasswordHasher.hashPassword(pin)
val updatedList = currentListDetails.toShoppingList().copy(
protectionHash = hash,
protectionType = 3, // 3 for PIN protection
lockMethod = 3
)
noteshopRepository.updateShoppingList(updatedList)
updateListDetails(updatedList) // Update the UI state
} else {
// PIN is blank, so we remove protection
val updatedList = currentListDetails.toShoppingList().copy(
protectionHash = "",
protectionType = 0,
lockMethod = 0
)
noteshopRepository.updateShoppingList(updatedList)
updateListDetails(updatedList) // Update the UI state
}
}
}
suspend fun deleteList(list: ShoppingList) { suspend fun deleteList(list: ShoppingList) {
noteshopRepository.deleteShoppingList(list) noteshopRepository.deleteShoppingList(list)
} }
@@ -474,10 +444,10 @@ class ShoppingListsViewModel(private val noteshopRepository: NoteshopRepository,
return@launch return@launch
} }
val decryptedUsernameBytes = fileEncryptor.decrypt(java.util.Base64.getDecoder().decode(usernameEncrypted), secretKey) val decryptedUsernameBytes = fileEncryptor.decrypt(Base64.decode(usernameEncrypted, Base64.DEFAULT), secretKey)
username = String(decryptedUsernameBytes, Charsets.UTF_8) username = String(decryptedUsernameBytes, Charsets.UTF_8)
val decryptedPasswordBytes = fileEncryptor.decrypt(java.util.Base64.getDecoder().decode(passwordEncrypted), secretKey) val decryptedPasswordBytes = fileEncryptor.decrypt(Base64.decode(passwordEncrypted, Base64.DEFAULT), secretKey)
password = String(decryptedPasswordBytes, Charsets.UTF_8) password = String(decryptedPasswordBytes, Charsets.UTF_8)
} catch (e: Exception) { } catch (e: Exception) {
Toast.makeText(getApplication(), getApplication<Application>().getString(de.lxtools.noteshop.R.string.failed_to_decrypt_credentials_generic, e.message), Toast.LENGTH_LONG).show() Toast.makeText(getApplication(), getApplication<Application>().getString(de.lxtools.noteshop.R.string.failed_to_decrypt_credentials_generic, e.message), Toast.LENGTH_LONG).show()

View File

@@ -3,7 +3,7 @@ package de.lxtools.noteshop.ui.webapp
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
@@ -25,7 +25,7 @@ import de.lxtools.noteshop.R
@Composable @Composable
fun WebAppIntegrationScreen( fun WebAppIntegrationScreen(
viewModel: WebAppIntegrationViewModel, viewModel: WebAppIntegrationViewModel,
onNavigateUp: () -> Unit,
padding: PaddingValues, padding: PaddingValues,
onAuthenticateAndTogglePasswordVisibility: (Boolean) -> Unit // New parameter onAuthenticateAndTogglePasswordVisibility: (Boolean) -> Unit // New parameter
) { ) {

View File

@@ -13,7 +13,6 @@ import de.lxtools.noteshop.data.NoteshopRepository
import de.lxtools.noteshop.security.FileEncryptor import de.lxtools.noteshop.security.FileEncryptor
import de.lxtools.noteshop.security.KeyManager import de.lxtools.noteshop.security.KeyManager
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.delay
class WebAppIntegrationViewModel(private val repository: NoteshopRepository, application: Application) : AndroidViewModel(application) { class WebAppIntegrationViewModel(private val repository: NoteshopRepository, application: Application) : AndroidViewModel(application) {
@@ -99,7 +98,7 @@ class WebAppIntegrationViewModel(private val repository: NoteshopRepository, app
val decrypted = fileEncryptor.decrypt(Base64.decode(it, Base64.DEFAULT), secretKey) val decrypted = fileEncryptor.decrypt(Base64.decode(it, Base64.DEFAULT), secretKey)
password = String(decrypted) password = String(decrypted)
} }
} catch (e: Exception) { } catch (_: Exception) {
Toast.makeText(getApplication(), getApplication<Application>().getString(de.lxtools.noteshop.R.string.failed_to_decrypt_credentials), Toast.LENGTH_SHORT).show() Toast.makeText(getApplication(), getApplication<Application>().getString(de.lxtools.noteshop.R.string.failed_to_decrypt_credentials), Toast.LENGTH_SHORT).show()
} }
} }

View File

@@ -339,4 +339,6 @@
<string name="set_pin">PIN festlegen</string> <string name="set_pin">PIN festlegen</string>
<string name="enter_pin">PIN eingeben</string> <string name="enter_pin">PIN eingeben</string>
<string name="set_pattern">Muster festlegen</string> <string name="set_pattern">Muster festlegen</string>
<string name="tour_finish">Fertig</string>
<string name="tour_skip">Überspringen</string>
</resources> </resources>

View File

@@ -339,4 +339,6 @@
<string name="set_pin">Set PIN</string> <string name="set_pin">Set PIN</string>
<string name="enter_pin">Enter PIN</string> <string name="enter_pin">Enter PIN</string>
<string name="set_pattern">Set Pattern</string> <string name="set_pattern">Set Pattern</string>
<string name="tour_finish">Finish</string>
<string name="tour_skip">Skip</string>
</resources> </resources>