Compare commits
13 Commits
7032420cb5
...
44e4bd3218
| Author | SHA1 | Date | |
|---|---|---|---|
| 44e4bd3218 | |||
| c2ea7c98ef | |||
| a10602c6a5 | |||
| 62ff3594d4 | |||
| a5bed3031a | |||
| 20d9139256 | |||
| cf799eba12 | |||
| fd04f9b5d9 | |||
| 28cc1313b4 | |||
| 7c5c31c638 | |||
| 12839ea88e | |||
| fb8e7917bc | |||
| 4f0edbca22 |
@@ -37,8 +37,7 @@ Noteshop is a versatile and privacy-focused application for managing your notes,
|
||||
|
||||
1. Clone the repository:
|
||||
```sh
|
||||
git clone YOUR_GITEA_REPOSITORY_URL_HERE
|
||||
cd noteshop
|
||||
git clone https://git.ilunix.de/punix/noteshop.git && cd noteshop
|
||||
```
|
||||
2. Build the app using Gradle:
|
||||
```sh
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?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.USE_BIOMETRIC" />
|
||||
@@ -16,7 +17,8 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:theme="@style/Theme.Noteshop">
|
||||
android:theme="@style/Theme.Noteshop"
|
||||
tools:targetApi="33">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
|
||||
@@ -125,11 +125,7 @@ class BiometricAuthenticator(private val context: Context) {
|
||||
|
||||
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(
|
||||
title: String,
|
||||
|
||||
@@ -2,6 +2,7 @@ package de.lxtools.noteshop.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
@@ -41,7 +42,6 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
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.theme.ColorTheme
|
||||
import de.lxtools.noteshop.ui.webapp.WebAppIntegrationViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.FileOutputStream
|
||||
import javax.crypto.SecretKey
|
||||
import android.util.Base64
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -98,10 +97,10 @@ fun AppShell(
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
var showStartupPasswordDialog by rememberSaveable { mutableStateOf(false) }
|
||||
var startupUnlockErrorMessage by rememberSaveable { mutableStateOf<String?>(null) }
|
||||
val (showStartupPasswordDialog, setShowStartupPasswordDialog) = rememberSaveable { mutableStateOf(false) }
|
||||
val (startupUnlockErrorMessage, setStartupUnlockErrorMessage) = rememberSaveable { mutableStateOf<String?>(null) }
|
||||
|
||||
var syncFolderUriString by rememberSaveable {
|
||||
val (syncFolderUriString, setSyncFolderUriString) = rememberSaveable {
|
||||
mutableStateOf(
|
||||
sharedPrefs.getString(
|
||||
"sync_folder_uri",
|
||||
@@ -114,7 +113,7 @@ fun AppShell(
|
||||
val listener =
|
||||
android.content.SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
|
||||
if (key == "sync_folder_uri") {
|
||||
syncFolderUriString = sharedPreferences.getString(key, null)
|
||||
setSyncFolderUriString(sharedPreferences.getString(key, null))
|
||||
}
|
||||
}
|
||||
sharedPrefs.registerOnSharedPreferenceChangeListener(listener)
|
||||
@@ -128,7 +127,7 @@ fun AppShell(
|
||||
val keyManager = remember { KeyManager(context, canUseBiometrics) }
|
||||
val fileEncryptor = remember { FileEncryptor() }
|
||||
|
||||
var isEncryptionEnabled by rememberSaveable {
|
||||
val (isEncryptionEnabled, setIsEncryptionEnabled) = rememberSaveable {
|
||||
mutableStateOf(
|
||||
sharedPrefs.getBoolean(
|
||||
"encryption_enabled",
|
||||
@@ -136,9 +135,9 @@ fun AppShell(
|
||||
)
|
||||
)
|
||||
}
|
||||
var hasEncryptionPassword by rememberSaveable { mutableStateOf(keyManager.hasKey()) }
|
||||
var showPasswordDialog by rememberSaveable { mutableStateOf(false) }
|
||||
var showDeleteConfirmationDialog by remember { mutableStateOf(false) }
|
||||
val (hasEncryptionPassword, setHasEncryptionPassword) = rememberSaveable { mutableStateOf(keyManager.hasKey()) }
|
||||
val (showPasswordDialog, setShowPasswordDialog) = rememberSaveable { mutableStateOf(false) }
|
||||
val (showDeleteConfirmationDialog, setShowDeleteConfirmationDialog) = remember { mutableStateOf(false) }
|
||||
|
||||
val onEncryptionToggle: (Boolean) -> Unit = { enabled ->
|
||||
if (!enabled) { // Turning OFF
|
||||
@@ -149,8 +148,8 @@ fun AppShell(
|
||||
if (cipher == null) {
|
||||
// Fallback for safety, though this state should be unlikely if hasEncryptionPassword is true
|
||||
keyManager.deleteKey()
|
||||
hasEncryptionPassword = false
|
||||
isEncryptionEnabled = false
|
||||
setHasEncryptionPassword(false)
|
||||
setIsEncryptionEnabled(false)
|
||||
sharedPrefs.edit {
|
||||
putBoolean("encryption_enabled", false)
|
||||
remove("biometric_unlock_enabled")
|
||||
@@ -174,8 +173,8 @@ fun AppShell(
|
||||
|
||||
// Now, safely delete the key and disable the feature
|
||||
keyManager.deleteKey()
|
||||
hasEncryptionPassword = false
|
||||
isEncryptionEnabled = false
|
||||
setHasEncryptionPassword(false)
|
||||
setIsEncryptionEnabled(false)
|
||||
sharedPrefs.edit {
|
||||
putBoolean("encryption_enabled", false)
|
||||
remove("biometric_unlock_enabled")
|
||||
@@ -188,13 +187,13 @@ fun AppShell(
|
||||
)
|
||||
} else {
|
||||
// No password was ever set, just turn the switch off.
|
||||
isEncryptionEnabled = false
|
||||
setIsEncryptionEnabled(false)
|
||||
sharedPrefs.edit {
|
||||
putBoolean("encryption_enabled", false)
|
||||
}
|
||||
}
|
||||
} else { // Turning ON
|
||||
isEncryptionEnabled = true
|
||||
setIsEncryptionEnabled(true)
|
||||
sharedPrefs.edit {
|
||||
putBoolean("encryption_enabled", true)
|
||||
}
|
||||
@@ -202,50 +201,10 @@ fun AppShell(
|
||||
}
|
||||
|
||||
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 =
|
||||
remember(notesViewModel, shoppingListsViewModel, recipesViewModel, syncFolderUri) {
|
||||
@@ -515,8 +474,8 @@ fun AppShell(
|
||||
}
|
||||
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
var secretKey by rememberSaveable(stateSaver = SecretKeySaver) { mutableStateOf(null) }
|
||||
var isDecryptionAttempted by rememberSaveable { mutableStateOf(false) }
|
||||
val (secretKey, setSecretKey) = rememberSaveable(stateSaver = SecretKeySaver) { mutableStateOf(null) }
|
||||
val (isDecryptionAttempted, setIsDecryptionAttempted) = rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
DisposableEffect(lifecycleOwner, context, scope) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
@@ -541,32 +500,33 @@ fun AppShell(
|
||||
onSuccess = { result ->
|
||||
result.cryptoObject?.cipher?.let { authenticatedCipher ->
|
||||
scope.launch {
|
||||
secretKey =
|
||||
setSecretKey(
|
||||
keyManager.getSecretKeyFromAuthenticatedCipher(
|
||||
authenticatedCipher
|
||||
)
|
||||
isDecryptionAttempted = true
|
||||
)
|
||||
setIsDecryptionAttempted(true)
|
||||
}
|
||||
}
|
||||
},
|
||||
onFailed = {},
|
||||
onError = { _, _ ->
|
||||
showStartupPasswordDialog = true
|
||||
setShowStartupPasswordDialog(true)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
isDecryptionAttempted = true // No key/cipher, proceed
|
||||
setIsDecryptionAttempted(true) // No key/cipher, proceed
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("AppShell", "Error during decryption prompt: ${e.message}")
|
||||
isDecryptionAttempted = true // Proceed without key
|
||||
setIsDecryptionAttempted(true) // Proceed without key
|
||||
}
|
||||
}
|
||||
} else {
|
||||
showStartupPasswordDialog = true
|
||||
setShowStartupPasswordDialog(true)
|
||||
}
|
||||
} else {
|
||||
isDecryptionAttempted = true // No encryption, proceed
|
||||
setIsDecryptionAttempted(true) // No encryption, proceed
|
||||
}
|
||||
}
|
||||
|
||||
@@ -594,7 +554,7 @@ fun AppShell(
|
||||
}
|
||||
}
|
||||
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 initialScreen = when (defaultStartScreenRoute) {
|
||||
Screen.ShoppingLists.route -> Screen.ShoppingLists
|
||||
@@ -624,58 +584,58 @@ fun AppShell(
|
||||
LaunchedEffect(Unit) {
|
||||
val firstLaunchCompleted = sharedPrefs.getBoolean("first_launch_completed", false)
|
||||
if (!firstLaunchCompleted) {
|
||||
currentScreen = Screen.GuidedTour
|
||||
setCurrentScreen(Screen.GuidedTour)
|
||||
sharedPrefs.edit { putBoolean("first_launch_completed", true) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var selectedListId: Int? by rememberSaveable { mutableStateOf(null) }
|
||||
var selectedNoteId: Int? by rememberSaveable { mutableStateOf(null) }
|
||||
var selectedRecipeId: Int? by rememberSaveable { mutableStateOf(null) }
|
||||
val (selectedListId, setSelectedListId) = rememberSaveable { mutableStateOf<Int?>(null) }
|
||||
val (selectedNoteId, setSelectedNoteId) = rememberSaveable { mutableStateOf<Int?>(null) }
|
||||
val (selectedRecipeId, setSelectedRecipeId) = rememberSaveable { mutableStateOf<Int?>(null) }
|
||||
|
||||
var itemWasInitiallyLocked by rememberSaveable { mutableStateOf(false) }
|
||||
val (itemWasInitiallyLocked, setItemWasInitiallyLocked) = rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(selectedNoteId, selectedListId, selectedRecipeId) {
|
||||
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()
|
||||
|
||||
var showNoteDialog by rememberSaveable { mutableStateOf(false) }
|
||||
val (showNoteDialog, setShowNoteDialog) = rememberSaveable { mutableStateOf(false) }
|
||||
val noteDetails by notesViewModel.noteDetails.collectAsState()
|
||||
|
||||
var showRecipeDialog by rememberSaveable { mutableStateOf(false) }
|
||||
val (showRecipeDialog, setShowRecipeDialog) = rememberSaveable { mutableStateOf(false) }
|
||||
val recipeDetails by recipesViewModel.recipeDetails.collectAsState()
|
||||
|
||||
var showJsonDialog by rememberSaveable { mutableStateOf(false) }
|
||||
var showSetPasswordDialog by rememberSaveable { mutableStateOf(false) }
|
||||
var showSetRecipePasswordDialog by rememberSaveable { mutableStateOf(false) }
|
||||
var showSetListPasswordDialog by rememberSaveable { mutableStateOf(false) }
|
||||
val (showJsonDialog, setShowJsonDialog) = rememberSaveable { mutableStateOf(false) }
|
||||
val (showSetPasswordDialog, setShowSetPasswordDialog) = rememberSaveable { mutableStateOf(false) }
|
||||
val (showSetRecipePasswordDialog, setShowSetRecipePasswordDialog) = rememberSaveable { mutableStateOf(false) }
|
||||
val (showSetListPasswordDialog, setShowSetListPasswordDialog) = rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
|
||||
var showUnlockPasswordDialog by rememberSaveable { mutableStateOf(false) }
|
||||
var unlockErrorMessage by rememberSaveable { mutableStateOf<String?>(null) }
|
||||
var itemToUnlockId by rememberSaveable { mutableStateOf<Int?>(null) }
|
||||
var itemToUnlockType by rememberSaveable { mutableStateOf<Screen?>(null) }
|
||||
val (showUnlockPasswordDialog, setShowUnlockPasswordDialog) = rememberSaveable { mutableStateOf(false) }
|
||||
val (unlockErrorMessage, setUnlockErrorMessage) = rememberSaveable { mutableStateOf<String?>(null) }
|
||||
val (itemToUnlockId, setItemToUnlockId) = rememberSaveable { mutableStateOf<Int?>(null) }
|
||||
val (itemToUnlockType, setItemToUnlockType) = rememberSaveable { mutableStateOf<Screen?>(null) }
|
||||
|
||||
|
||||
|
||||
|
||||
var showChooseLockMethodDialog by rememberSaveable { mutableStateOf(false) }
|
||||
var itemToLockId: Int? by rememberSaveable { mutableStateOf(null) }
|
||||
var itemToLockType: LockableItemType? by rememberSaveable { mutableStateOf(null) }
|
||||
val (showChooseLockMethodDialog, setShowChooseLockMethodDialog) = rememberSaveable { mutableStateOf(false) }
|
||||
val (itemToLockId, setItemToLockId) = rememberSaveable { mutableStateOf<Int?>(null) }
|
||||
val (itemToLockType, setItemToLockType) = rememberSaveable { mutableStateOf<LockableItemType?>(null) }
|
||||
|
||||
|
||||
val onUnlockItem: (Int, Screen, Int) -> Unit = { id, type, protectionType ->
|
||||
when (protectionType) {
|
||||
1 -> { // Password protected
|
||||
itemToUnlockId = id
|
||||
itemToUnlockType = type
|
||||
showUnlockPasswordDialog = true
|
||||
setItemToUnlockId(id)
|
||||
setItemToUnlockType(type)
|
||||
setShowUnlockPasswordDialog(true)
|
||||
}
|
||||
2 -> { // Biometric protected
|
||||
biometricAuthenticator.promptBiometricAuth(
|
||||
@@ -684,25 +644,25 @@ fun AppShell(
|
||||
fragmentActivity = context.findActivity() as FragmentActivity,
|
||||
onSuccess = { _ ->
|
||||
when (type) {
|
||||
is Screen.ShoppingListDetail -> selectedListId = id
|
||||
is Screen.NoteDetail -> selectedNoteId = id
|
||||
is Screen.RecipeDetail -> selectedRecipeId = id
|
||||
is Screen.ShoppingListDetail -> setSelectedListId(id)
|
||||
is Screen.NoteDetail -> setSelectedNoteId(id)
|
||||
is Screen.RecipeDetail -> setSelectedRecipeId(id)
|
||||
else -> {}
|
||||
}
|
||||
itemWasInitiallyLocked = true
|
||||
currentScreen = type
|
||||
setItemWasInitiallyLocked(true)
|
||||
setCurrentScreen(type)
|
||||
},
|
||||
onFailed = {
|
||||
// Biometric failed, offer password as fallback if desired
|
||||
itemToUnlockId = id
|
||||
itemToUnlockType = type
|
||||
showUnlockPasswordDialog = true
|
||||
setItemToUnlockId(id)
|
||||
setItemToUnlockType(type)
|
||||
setShowUnlockPasswordDialog(true)
|
||||
},
|
||||
onError = { _, _ ->
|
||||
// Biometric error, offer password as fallback if desired
|
||||
itemToUnlockId = id
|
||||
itemToUnlockType = type
|
||||
showUnlockPasswordDialog = true
|
||||
setItemToUnlockId(id)
|
||||
setItemToUnlockType(type)
|
||||
setShowUnlockPasswordDialog(true)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -716,7 +676,7 @@ fun AppShell(
|
||||
)
|
||||
.collectAsState(initial = null)
|
||||
|
||||
var shoppingListsTitle by remember {
|
||||
val (shoppingListsTitle, setShoppingListsTitle) = remember {
|
||||
mutableStateOf(
|
||||
sharedPrefs.getString(
|
||||
"shopping_lists_title",
|
||||
@@ -724,9 +684,9 @@ fun AppShell(
|
||||
) ?: 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(
|
||||
sharedPrefs.getString(
|
||||
"recipes_title",
|
||||
@@ -734,9 +694,9 @@ fun AppShell(
|
||||
) ?: 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(
|
||||
sharedPrefs.getString(
|
||||
"default_start_screen",
|
||||
@@ -764,7 +724,7 @@ fun AppShell(
|
||||
shoppingListsViewModel.importListFromTxt(listName, content)
|
||||
}
|
||||
}
|
||||
showListDialog = false // Close dialog on success
|
||||
setShowListDialog(false) // Close dialog on success
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -942,19 +902,19 @@ fun AppShell(
|
||||
AppDrawer(
|
||||
drawerState = drawerState,
|
||||
currentScreen = currentScreen,
|
||||
onScreenChange = { currentScreen = it },
|
||||
onScreenChange = { setCurrentScreen(it) },
|
||||
shoppingListsViewModel = shoppingListsViewModel,
|
||||
notesViewModel = notesViewModel,
|
||||
shoppingListsTitle = shoppingListsTitle,
|
||||
recipesTitle = recipesTitle,
|
||||
onShowRenameDialog = { showRenameDialog = true },
|
||||
onShowRecipeRenameDialog = { showRecipeRenameDialog = true },
|
||||
onShowJsonDialog = { showJsonDialog = true },
|
||||
onShowRenameDialog = { setShowRenameDialog(true) },
|
||||
onShowRecipeRenameDialog = { setShowRecipeRenameDialog(true) },
|
||||
onShowJsonDialog = { setShowJsonDialog(true) },
|
||||
syncFolderUriString = syncFolderUriString,
|
||||
onShowDeleteConfirmationDialog = { showDeleteConfirmationDialog = true },
|
||||
onShowDeleteConfirmationDialog = { setShowDeleteConfirmationDialog(true) },
|
||||
onLaunchSyncFolderChooser = { syncFolderLauncher.launch(null) },
|
||||
onSetSelectedListId = { selectedListId = it },
|
||||
onSetSelectedNoteId = { selectedNoteId = it }
|
||||
onSetSelectedListId = { setSelectedListId(it) },
|
||||
onSetSelectedNoteId = { setSelectedNoteId(it) }
|
||||
)
|
||||
}
|
||||
) {
|
||||
@@ -965,31 +925,27 @@ fun AppShell(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
currentScreen = currentScreen,
|
||||
onScreenChange = { currentScreen = it },
|
||||
onScreenChange = { setCurrentScreen(it) },
|
||||
drawerState = drawerState,
|
||||
shoppingListsViewModel = shoppingListsViewModel,
|
||||
notesViewModel = notesViewModel,
|
||||
recipesViewModel = recipesViewModel,
|
||||
shoppingListsTitle = shoppingListsTitle,
|
||||
recipesTitle = recipesTitle,
|
||||
onShowListDialog = { showListDialog = true },
|
||||
onShowNoteDialog = { showNoteDialog = true },
|
||||
onShowRecipeDialog = { showRecipeDialog = true },
|
||||
onShowListDialog = { setShowListDialog(true) },
|
||||
onShowNoteDialog = { setShowNoteDialog(true) },
|
||||
onShowRecipeDialog = { setShowRecipeDialog(true) },
|
||||
selectedListId = selectedListId,
|
||||
onSetSelectedListId = { selectedListId = it },
|
||||
onSetSelectedNoteId = { selectedNoteId = it },
|
||||
onSetSelectedRecipeId = { selectedRecipeId = it },
|
||||
onSetSelectedListId = { setSelectedListId(it) },
|
||||
onSetSelectedNoteId = { setSelectedNoteId(it) },
|
||||
onSetSelectedRecipeId = { setSelectedRecipeId(it) },
|
||||
exportLauncher = exportLauncher,
|
||||
noteExportLauncher = noteExportLauncher,
|
||||
recipeExportLauncher = recipeExportLauncher,
|
||||
hasEncryptionPassword = hasEncryptionPassword,
|
||||
onShowSetPasswordDialog = { showSetPasswordDialog = true },
|
||||
onShowSetRecipePasswordDialog = { showSetRecipePasswordDialog = true },
|
||||
onShowSetListPasswordDialog = { showSetListPasswordDialog = true },
|
||||
onShowChooseLockMethodDialog = { type, id ->
|
||||
itemToLockType = type
|
||||
itemToLockId = id
|
||||
showChooseLockMethodDialog = true
|
||||
setItemToLockType(type)
|
||||
setItemToLockId(id)
|
||||
setShowChooseLockMethodDialog(true)
|
||||
}
|
||||
)
|
||||
},
|
||||
@@ -1073,19 +1029,19 @@ fun AppShell(
|
||||
AppContent(
|
||||
innerPadding = innerPadding,
|
||||
currentScreen = currentScreen,
|
||||
onScreenChange = { currentScreen = it },
|
||||
onScreenChange = { setCurrentScreen(it) },
|
||||
notesViewModel = notesViewModel,
|
||||
shoppingListsViewModel = shoppingListsViewModel,
|
||||
recipesViewModel = recipesViewModel,
|
||||
selectedListId = selectedListId,
|
||||
onSetSelectedListId = { selectedListId = it },
|
||||
onShowListDialog = { showListDialog = true },
|
||||
onSetSelectedListId = { setSelectedListId(it) },
|
||||
onShowListDialog = { setShowListDialog(true) },
|
||||
selectedNoteId = selectedNoteId,
|
||||
onSetSelectedNoteId = { selectedNoteId = it },
|
||||
onShowNoteDialog = { showNoteDialog = true },
|
||||
onSetSelectedNoteId = { setSelectedNoteId(it) },
|
||||
onShowNoteDialog = { setShowNoteDialog(true) },
|
||||
selectedRecipeId = selectedRecipeId,
|
||||
onSetSelectedRecipeId = { selectedRecipeId = it },
|
||||
onShowRecipeDialog = { showRecipeDialog = true },
|
||||
onSetSelectedRecipeId = { setSelectedRecipeId(it) },
|
||||
onShowRecipeDialog = { setShowRecipeDialog(true) },
|
||||
itemWasInitiallyLocked = itemWasInitiallyLocked,
|
||||
onUnlockItem = onUnlockItem,
|
||||
shoppingListsTitle = shoppingListsTitle,
|
||||
@@ -1096,22 +1052,22 @@ fun AppShell(
|
||||
isEncryptionEnabled = isEncryptionEnabled,
|
||||
onEncryptionToggle = onEncryptionToggle,
|
||||
onSetEncryptionPassword = onSetEncryptionPassword,
|
||||
onRemoveEncryption = onRemoveEncryption,
|
||||
|
||||
hasEncryptionPassword = hasEncryptionPassword,
|
||||
biometricAuthenticator = biometricAuthenticator,
|
||||
sharedPrefs = sharedPrefs,
|
||||
onResetShoppingListsTitle = {
|
||||
sharedPrefs.edit { remove("shopping_lists_title") }
|
||||
shoppingListsTitle = context.getString(R.string.menu_shopping_lists)
|
||||
setShoppingListsTitle(context.getString(R.string.menu_shopping_lists))
|
||||
},
|
||||
onResetRecipesTitle = {
|
||||
sharedPrefs.edit { remove("recipes_title") }
|
||||
recipesTitle = context.getString(R.string.menu_recipes)
|
||||
setRecipesTitle(context.getString(R.string.menu_recipes))
|
||||
},
|
||||
startScreen = startScreen,
|
||||
onStartScreenChange = {
|
||||
sharedPrefs.edit { putString("default_start_screen", it) }
|
||||
startScreen = it
|
||||
setStartScreen(it)
|
||||
},
|
||||
canUseBiometrics = canUseBiometrics,
|
||||
webAppIntegrationViewModel = webAppIntegrationViewModel,
|
||||
@@ -1125,79 +1081,67 @@ fun AppShell(
|
||||
shoppingListsViewModel = shoppingListsViewModel,
|
||||
recipesViewModel = recipesViewModel,
|
||||
showListDialog = showListDialog,
|
||||
onShowListDialogChange = { showListDialog = it },
|
||||
onShowListDialogChange = { setShowListDialog(it) },
|
||||
listDetails = listDetails,
|
||||
onListDetailsChange = { shoppingListsViewModel.updateListDetails(it) },
|
||||
onSaveList = { scope.launch { shoppingListsViewModel.saveList() } },
|
||||
onResetListDetails = { shoppingListsViewModel.resetListDetails() },
|
||||
|
||||
|
||||
onSetListProtection = { password ->
|
||||
selectedListId?.let { listId ->
|
||||
shoppingListsViewModel.setProtection(listId, password)
|
||||
currentScreen = Screen.ShoppingLists
|
||||
setCurrentScreen(Screen.ShoppingLists)
|
||||
}
|
||||
selectedListId = null
|
||||
showSetListPasswordDialog = false
|
||||
setSelectedListId(null)
|
||||
setShowSetListPasswordDialog(false)
|
||||
},
|
||||
|
||||
onSetListProtectionBiometric = { id ->
|
||||
shoppingListsViewModel.setProtectionBiometric(id)
|
||||
currentScreen = Screen.ShoppingLists
|
||||
},
|
||||
|
||||
txtImportLauncher = txtImportLauncher,
|
||||
showNoteDialog = showNoteDialog,
|
||||
onShowNoteDialogChange = { showNoteDialog = it },
|
||||
onShowNoteDialogChange = { setShowNoteDialog(it) },
|
||||
noteDetails = noteDetails,
|
||||
onNoteDetailsChange = { notesViewModel.updateNoteDetails(it) },
|
||||
onSaveNote = { scope.launch { notesViewModel.saveNote() } },
|
||||
onResetNoteDetails = { notesViewModel.resetNoteDetails() },
|
||||
onSetNoteProtectionBiometric = { id ->
|
||||
notesViewModel.setProtectionBiometric(id)
|
||||
currentScreen = Screen.Notes
|
||||
},
|
||||
|
||||
|
||||
noteImportLauncher = noteImportLauncher,
|
||||
showRecipeDialog = showRecipeDialog,
|
||||
onShowRecipeDialogChange = { showRecipeDialog = it },
|
||||
onShowRecipeDialogChange = { setShowRecipeDialog(it) },
|
||||
recipeDetails = recipeDetails,
|
||||
onRecipeDetailsChange = { recipesViewModel.updateRecipeDetails(it) },
|
||||
onSaveRecipe = { scope.launch { recipesViewModel.saveRecipe() } },
|
||||
onResetRecipeDetails = { recipesViewModel.resetRecipeDetails() },
|
||||
onSetRecipeProtectionBiometric = { id ->
|
||||
recipesViewModel.setProtectionBiometric(id)
|
||||
currentScreen = Screen.Recipes
|
||||
},
|
||||
|
||||
recipeImportLauncher = recipeImportLauncher,
|
||||
recipesTitle = recipesTitle,
|
||||
showJsonDialog = showJsonDialog,
|
||||
onShowJsonDialogChange = { showJsonDialog = it },
|
||||
onShowJsonDialogChange = { setShowJsonDialog(it) },
|
||||
shoppingListsTitle = shoppingListsTitle,
|
||||
fileEncryptor = fileEncryptor,
|
||||
keyManager = keyManager,
|
||||
biometricAuthenticator = biometricAuthenticator,
|
||||
showSetPasswordDialog = showSetPasswordDialog,
|
||||
onShowSetPasswordDialogChange = { showSetPasswordDialog = it },
|
||||
onShowSetPasswordDialogChange = { setShowSetPasswordDialog(it) },
|
||||
onSetNotePassword = { password ->
|
||||
notesViewModel.setProtectionPassword(password)
|
||||
selectedNoteId = null
|
||||
showSetPasswordDialog = false
|
||||
currentScreen = Screen.Notes
|
||||
setSelectedNoteId(null)
|
||||
setShowSetPasswordDialog(false)
|
||||
setCurrentScreen(Screen.Notes)
|
||||
},
|
||||
showSetRecipePasswordDialog = showSetRecipePasswordDialog,
|
||||
onShowSetRecipePasswordDialogChange = { showSetRecipePasswordDialog = it },
|
||||
onShowSetRecipePasswordDialogChange = { setShowSetRecipePasswordDialog(it) },
|
||||
onSetRecipePassword = { password ->
|
||||
recipesViewModel.setProtectionPassword(password)
|
||||
selectedRecipeId = null
|
||||
showSetRecipePasswordDialog = false
|
||||
currentScreen = Screen.Recipes
|
||||
setSelectedRecipeId(null)
|
||||
setShowSetRecipePasswordDialog(false)
|
||||
setCurrentScreen(Screen.Recipes)
|
||||
},
|
||||
showSetListPasswordDialog = showSetListPasswordDialog,
|
||||
onShowSetListPasswordDialogChange = { showSetListPasswordDialog = it },
|
||||
onShowSetListPasswordDialogChange = { setShowSetListPasswordDialog(it) },
|
||||
showUnlockPasswordDialog = showUnlockPasswordDialog,
|
||||
onShowUnlockPasswordDialogChange = {
|
||||
showUnlockPasswordDialog = it
|
||||
setShowUnlockPasswordDialog(it)
|
||||
if (!it) {
|
||||
unlockErrorMessage = null
|
||||
itemToUnlockId = null
|
||||
itemToUnlockType = null
|
||||
setUnlockErrorMessage(null)
|
||||
setItemToUnlockId(null)
|
||||
setItemToUnlockType(null)
|
||||
}
|
||||
},
|
||||
onUnlock = { password ->
|
||||
@@ -1209,44 +1153,44 @@ fun AppShell(
|
||||
val note = notesViewModel.uiState.value.noteList.find { it.id == itemToUnlockId }
|
||||
if (note != null && note.protectionHash == hashedPassword) {
|
||||
isPasswordCorrect = true
|
||||
selectedNoteId = itemToUnlockId
|
||||
setSelectedNoteId(itemToUnlockId)
|
||||
}
|
||||
}
|
||||
Screen.RecipeDetail -> {
|
||||
val recipe = recipesViewModel.uiState.value.recipeList.find { it.id == itemToUnlockId }
|
||||
if (recipe != null && recipe.protectionHash == hashedPassword) {
|
||||
isPasswordCorrect = true
|
||||
selectedRecipeId = itemToUnlockId
|
||||
setSelectedRecipeId(itemToUnlockId)
|
||||
}
|
||||
}
|
||||
Screen.ShoppingListDetail -> {
|
||||
val listWithItems = shoppingListsViewModel.uiState.value.shoppingLists.find { it.shoppingList.id == itemToUnlockId }
|
||||
if (listWithItems != null && listWithItems.shoppingList.protectionHash == hashedPassword) {
|
||||
isPasswordCorrect = true
|
||||
selectedListId = itemToUnlockId
|
||||
setSelectedListId(itemToUnlockId)
|
||||
}
|
||||
}
|
||||
else -> {} // Should not happen
|
||||
}
|
||||
|
||||
if (isPasswordCorrect) {
|
||||
unlockErrorMessage = null
|
||||
itemWasInitiallyLocked = true // Mark as initially locked for content decryption
|
||||
currentScreen = itemToUnlockType!!
|
||||
itemToUnlockId = null
|
||||
itemToUnlockType = null
|
||||
showUnlockPasswordDialog = false
|
||||
setUnlockErrorMessage(null)
|
||||
setItemWasInitiallyLocked(true) // Mark as initially locked for content decryption
|
||||
setCurrentScreen(itemToUnlockType!!)
|
||||
setItemToUnlockId(null)
|
||||
setItemToUnlockType(null)
|
||||
setShowUnlockPasswordDialog(false)
|
||||
} else {
|
||||
unlockErrorMessage = context.getString(R.string.incorrect_password)
|
||||
setUnlockErrorMessage(context.getString(R.string.incorrect_password))
|
||||
}
|
||||
},
|
||||
unlockErrorMessage = unlockErrorMessage,
|
||||
showPasswordDialog = showPasswordDialog,
|
||||
onShowPasswordDialogChange = { showPasswordDialog = it },
|
||||
onHasEncryptionPasswordChange = { hasEncryptionPassword = it },
|
||||
onShowPasswordDialogChange = { setShowPasswordDialog(it) },
|
||||
onHasEncryptionPasswordChange = { setHasEncryptionPassword(it) },
|
||||
sharedPrefs = sharedPrefs,
|
||||
showDeleteConfirmationDialog = showDeleteConfirmationDialog,
|
||||
onShowDeleteConfirmationDialogChange = { showDeleteConfirmationDialog = it },
|
||||
onShowDeleteConfirmationDialogChange = { setShowDeleteConfirmationDialog(it) },
|
||||
onDeleteSyncFolder = {
|
||||
val syncFolderUriString = sharedPrefs.getString("sync_folder_uri", null)
|
||||
if (syncFolderUriString != null) {
|
||||
@@ -1259,19 +1203,19 @@ fun AppShell(
|
||||
}
|
||||
},
|
||||
showRenameDialog = showRenameDialog,
|
||||
onShowRenameDialogChange = { showRenameDialog = it },
|
||||
onShowRenameDialogChange = { setShowRenameDialog(it) },
|
||||
onRenameShoppingListsTitle = { newTitle ->
|
||||
sharedPrefs.edit { putString("shopping_lists_title", newTitle) }
|
||||
shoppingListsTitle = newTitle
|
||||
setShoppingListsTitle(newTitle)
|
||||
},
|
||||
showRecipeRenameDialog = showRecipeRenameDialog,
|
||||
onShowRecipeRenameDialogChange = { showRecipeRenameDialog = it },
|
||||
onShowRecipeRenameDialogChange = { setShowRecipeRenameDialog(it) },
|
||||
onRenameRecipesTitle = { newTitle ->
|
||||
sharedPrefs.edit { putString("recipes_title", newTitle) }
|
||||
recipesTitle = newTitle
|
||||
setRecipesTitle(newTitle)
|
||||
},
|
||||
showChooseLockMethodDialog = showChooseLockMethodDialog,
|
||||
onShowChooseLockMethodDialogChange = { showChooseLockMethodDialog = it },
|
||||
onShowChooseLockMethodDialogChange = { setShowChooseLockMethodDialog(it) },
|
||||
onConfirmLockMethod = { lockMethod ->
|
||||
itemToLockType?.let { type ->
|
||||
itemToLockId?.let { id ->
|
||||
@@ -1279,63 +1223,62 @@ fun AppShell(
|
||||
LockableItemType.SHOPPING_LIST -> {
|
||||
when (lockMethod) {
|
||||
LockMethod.PASSWORD -> {
|
||||
selectedListId = id
|
||||
showSetListPasswordDialog = true
|
||||
setSelectedListId(id)
|
||||
setShowSetListPasswordDialog(true)
|
||||
}
|
||||
LockMethod.BIOMETRIC -> {
|
||||
shoppingListsViewModel.setProtectionBiometric(id)
|
||||
currentScreen = Screen.ShoppingLists
|
||||
setCurrentScreen(Screen.ShoppingLists)
|
||||
}
|
||||
}
|
||||
}
|
||||
LockableItemType.NOTE -> {
|
||||
when (lockMethod) {
|
||||
LockMethod.PASSWORD -> {
|
||||
selectedNoteId = id
|
||||
showSetPasswordDialog = true
|
||||
setSelectedNoteId(id)
|
||||
setShowSetPasswordDialog(true)
|
||||
}
|
||||
LockMethod.BIOMETRIC -> {
|
||||
notesViewModel.setProtectionBiometric(id)
|
||||
currentScreen = Screen.Notes
|
||||
setCurrentScreen(Screen.Notes)
|
||||
}
|
||||
}
|
||||
}
|
||||
LockableItemType.RECIPE -> {
|
||||
when (lockMethod) {
|
||||
LockMethod.PASSWORD -> {
|
||||
selectedRecipeId = id
|
||||
showSetRecipePasswordDialog = true
|
||||
setSelectedRecipeId(id)
|
||||
setShowSetRecipePasswordDialog(true)
|
||||
}
|
||||
LockMethod.BIOMETRIC -> {
|
||||
recipesViewModel.setProtectionBiometric(id)
|
||||
currentScreen = Screen.Recipes
|
||||
setCurrentScreen(Screen.Recipes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
showChooseLockMethodDialog = false
|
||||
itemToLockId = null
|
||||
itemToLockType = null
|
||||
setShowChooseLockMethodDialog(false)
|
||||
setItemToLockId(null)
|
||||
setItemToLockType(null)
|
||||
},
|
||||
canUseBiometrics = canUseBiometrics,
|
||||
itemToLockType = itemToLockType,
|
||||
showStartupPasswordDialog = showStartupPasswordDialog,
|
||||
onShowStartupPasswordDialogChange = { showStartupPasswordDialog = it },
|
||||
onShowStartupPasswordDialogChange = { setShowStartupPasswordDialog(it) },
|
||||
onUnlockEncryption = { password ->
|
||||
scope.launch {
|
||||
try {
|
||||
val decryptionCipher = keyManager.getDecryptionCipher()
|
||||
if (decryptionCipher == null) {
|
||||
startupUnlockErrorMessage = context.getString(R.string.unlock_failed)
|
||||
setStartupUnlockErrorMessage(context.getString(R.string.unlock_failed))
|
||||
return@launch
|
||||
}
|
||||
|
||||
val sharedPrefs = context.getSharedPreferences("encryption_prefs", Context.MODE_PRIVATE)
|
||||
val encryptedDerivedKeyString = sharedPrefs.getString("encrypted_derived_key", null)
|
||||
if (encryptedDerivedKeyString == null) {
|
||||
startupUnlockErrorMessage = context.getString(R.string.unlock_failed)
|
||||
setStartupUnlockErrorMessage(context.getString(R.string.unlock_failed))
|
||||
return@launch
|
||||
}
|
||||
val encryptedDerivedKey = Base64.decode(encryptedDerivedKeyString, Base64.DEFAULT)
|
||||
@@ -1348,19 +1291,19 @@ fun AppShell(
|
||||
Log.d("AppShell_Unlock", "Current PBE Key: ${currentPbeKey.encoded.joinToString()}")
|
||||
|
||||
if (decryptedStoredKeyBytes.contentEquals(currentPbeKey.encoded)) {
|
||||
secretKey = SecretKeySpec(decryptedStoredKeyBytes, "AES")
|
||||
isDecryptionAttempted = true
|
||||
showStartupPasswordDialog = false
|
||||
startupUnlockErrorMessage = null
|
||||
setSecretKey(SecretKeySpec(decryptedStoredKeyBytes, "AES"))
|
||||
setIsDecryptionAttempted(true)
|
||||
setShowStartupPasswordDialog(false)
|
||||
setStartupUnlockErrorMessage(null)
|
||||
} else {
|
||||
startupUnlockErrorMessage = context.getString(R.string.incorrect_password)
|
||||
setStartupUnlockErrorMessage(context.getString(R.string.incorrect_password))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("AppShell", "Failed to unlock encryption with password", e)
|
||||
startupUnlockErrorMessage = context.getString(R.string.unlock_failed)
|
||||
setStartupUnlockErrorMessage(context.getString(R.string.unlock_failed))
|
||||
}
|
||||
}
|
||||
},
|
||||
startupUnlockErrorMessage = startupUnlockErrorMessage
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.edit
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import de.lxtools.noteshop.BiometricAuthenticator
|
||||
import de.lxtools.noteshop.R
|
||||
@@ -104,7 +105,7 @@ fun EncryptionPasswordDialog(
|
||||
val encryptedDerivedKey = authorizedCipher.doFinal(pbeKey.encoded)
|
||||
val iv = authorizedCipher.iv
|
||||
keyManager.storeEncryptedDerivedKey(encryptedDerivedKey, iv, useBiometrics)
|
||||
sharedPrefs.edit().putBoolean("biometric_unlock_enabled", useBiometrics).apply()
|
||||
sharedPrefs.edit { putBoolean("biometric_unlock_enabled", useBiometrics) }
|
||||
onPasswordSet()
|
||||
Toast.makeText(context, R.string.encryption_password_set_success, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
@@ -19,14 +19,15 @@ import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
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 androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
|
||||
@Composable
|
||||
fun GuidedTourScreen(onTourFinished: () -> Unit) {
|
||||
val pagerState = rememberPagerState()
|
||||
val pagerState = rememberPagerState(pageCount = { 3 })
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
@@ -34,7 +35,7 @@ fun GuidedTourScreen(onTourFinished: () -> Unit) {
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
HorizontalPager(
|
||||
count = 3,
|
||||
|
||||
state = pagerState,
|
||||
modifier = Modifier.weight(1f)
|
||||
) { page ->
|
||||
@@ -57,12 +58,8 @@ fun GuidedTourScreen(onTourFinished: () -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalPagerIndicator(
|
||||
pagerState = pagerState,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.padding(16.dp)
|
||||
)
|
||||
// TODO: Implement a custom HorizontalPagerIndicator or use a third-party library
|
||||
|
||||
|
||||
Button(
|
||||
onClick = onTourFinished,
|
||||
@@ -70,7 +67,7 @@ fun GuidedTourScreen(onTourFinished: () -> Unit) {
|
||||
.fillMaxWidth()
|
||||
.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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package de.lxtools.noteshop.ui
|
||||
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -89,14 +90,14 @@ fun JsonImportExportDialog(
|
||||
scope.launch {
|
||||
context.contentResolver.openInputStream(it)?.use { inputStream ->
|
||||
val fileContent = inputStream.readBytes()
|
||||
var jsonString: String? = null
|
||||
var jsonString: String?
|
||||
|
||||
if (secretKey != null) {
|
||||
try {
|
||||
val decryptedBytes = fileEncryptor.decrypt(fileContent, secretKey!!)
|
||||
jsonString = decryptedBytes.toString(Charsets.UTF_8)
|
||||
} 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
|
||||
jsonString = fileContent.toString(Charsets.UTF_8)
|
||||
}
|
||||
@@ -105,20 +106,16 @@ fun JsonImportExportDialog(
|
||||
jsonString = fileContent.toString(Charsets.UTF_8)
|
||||
}
|
||||
|
||||
if (jsonString != null) {
|
||||
try {
|
||||
notesViewModel.importNotesFromJson(jsonString)
|
||||
android.widget.Toast.makeText(context, R.string.json_import_successful, android.widget.Toast.LENGTH_SHORT).show()
|
||||
} catch (e: kotlinx.serialization.SerializationException) {
|
||||
android.util.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, R.string.json_import_successful, Toast.LENGTH_SHORT).show()
|
||||
} catch (e: SerializationException) {
|
||||
Log.e("JsonImportExportDialog", "Error parsing notes JSON: ${e.message}")
|
||||
Toast.makeText(context, context.getString(R.string.json_import_failed_invalid_format, e.message), Toast.LENGTH_LONG).show()
|
||||
} catch (e: Exception) {
|
||||
android.util.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()
|
||||
Log.e("JsonImportExportDialog", "Error importing notes: ${e.message}")
|
||||
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 {
|
||||
val allNotes = notesViewModel.uiState.value.noteList
|
||||
val content = notesViewModel.exportNotesToJson(allNotes)
|
||||
context.contentResolver.openOutputStream(it)?.use { outputStream ->
|
||||
secretKey?.let { key ->
|
||||
val encryptedContent = fileEncryptor.encrypt(content.toByteArray(), key)
|
||||
context.contentResolver.openOutputStream(it, "wt")?.use { outputStream ->
|
||||
if (secretKey != null) {
|
||||
val encryptedContent = fileEncryptor.encrypt(content.toByteArray(), secretKey!!)
|
||||
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 {
|
||||
val allShoppingLists = shoppingListsViewModel.uiState.value.shoppingLists
|
||||
val content = shoppingListsViewModel.exportShoppingListsToJson(allShoppingLists)
|
||||
context.contentResolver.openOutputStream(it)?.use { outputStream ->
|
||||
secretKey?.let { key ->
|
||||
val encryptedContent = fileEncryptor.encrypt(content.toByteArray(), key)
|
||||
context.contentResolver.openOutputStream(it, "wt")?.use { outputStream ->
|
||||
if (secretKey != null) {
|
||||
val encryptedContent = fileEncryptor.encrypt(content.toByteArray(), secretKey!!)
|
||||
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 {
|
||||
context.contentResolver.openInputStream(it)?.use { inputStream ->
|
||||
val fileContent = inputStream.readBytes()
|
||||
var jsonString: String? = null
|
||||
var jsonString: String?
|
||||
|
||||
if (secretKey != null) {
|
||||
try {
|
||||
val decryptedBytes = fileEncryptor.decrypt(fileContent, secretKey!!)
|
||||
jsonString = decryptedBytes.toString(Charsets.UTF_8)
|
||||
} 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
|
||||
jsonString = fileContent.toString(Charsets.UTF_8)
|
||||
}
|
||||
@@ -189,20 +190,16 @@ fun JsonImportExportDialog(
|
||||
jsonString = fileContent.toString(Charsets.UTF_8)
|
||||
}
|
||||
|
||||
if (jsonString != null) {
|
||||
try {
|
||||
shoppingListsViewModel.importShoppingListsFromJson(jsonString)
|
||||
android.widget.Toast.makeText(context, R.string.json_import_successful, android.widget.Toast.LENGTH_SHORT).show()
|
||||
} catch (e: kotlinx.serialization.SerializationException) {
|
||||
android.util.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, R.string.json_import_successful, Toast.LENGTH_SHORT).show()
|
||||
} catch (e: SerializationException) {
|
||||
Log.e("JsonImportExportDialog", "Error parsing shopping lists JSON: ${e.message}")
|
||||
Toast.makeText(context, context.getString(R.string.json_import_failed_invalid_format, e.message), Toast.LENGTH_LONG).show()
|
||||
} catch (e: Exception) {
|
||||
android.util.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()
|
||||
Log.e("JsonImportExportDialog", "Error importing shopping lists: ${e.message}")
|
||||
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 {
|
||||
val allRecipes = recipesViewModel.uiState.value.recipeList
|
||||
val content = recipesViewModel.exportRecipesToJson(allRecipes)
|
||||
context.contentResolver.openOutputStream(it)?.use { outputStream ->
|
||||
secretKey?.let { key ->
|
||||
val encryptedContent = fileEncryptor.encrypt(content.toByteArray(), key)
|
||||
context.contentResolver.openOutputStream(it, "wt")?.use { outputStream ->
|
||||
if (secretKey != null) {
|
||||
val encryptedContent = fileEncryptor.encrypt(content.toByteArray(), secretKey!!)
|
||||
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 {
|
||||
context.contentResolver.openInputStream(it)?.use { inputStream ->
|
||||
val fileContent = inputStream.readBytes()
|
||||
var jsonString: String? = null
|
||||
var jsonString: String?
|
||||
|
||||
if (secretKey != null) {
|
||||
try {
|
||||
val decryptedBytes = fileEncryptor.decrypt(fileContent, secretKey!!)
|
||||
jsonString = decryptedBytes.toString(Charsets.UTF_8)
|
||||
} 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
|
||||
jsonString = fileContent.toString(Charsets.UTF_8)
|
||||
}
|
||||
@@ -253,20 +252,16 @@ fun JsonImportExportDialog(
|
||||
jsonString = fileContent.toString(Charsets.UTF_8)
|
||||
}
|
||||
|
||||
if (jsonString != null) {
|
||||
try {
|
||||
recipesViewModel.importRecipesFromJson(jsonString)
|
||||
android.widget.Toast.makeText(context, R.string.json_import_successful, android.widget.Toast.LENGTH_SHORT).show()
|
||||
} catch (e: kotlinx.serialization.SerializationException) {
|
||||
android.util.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, R.string.json_import_successful, Toast.LENGTH_SHORT).show()
|
||||
} catch (e: SerializationException) {
|
||||
Log.e("JsonImportExportDialog", "Error parsing recipes JSON: ${e.message}")
|
||||
Toast.makeText(context, context.getString(R.string.json_import_failed_invalid_format, e.message), Toast.LENGTH_LONG).show()
|
||||
} catch (e: Exception) {
|
||||
android.util.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()
|
||||
Log.e("JsonImportExportDialog", "Error importing recipes: ${e.message}")
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -351,4 +346,4 @@ fun JsonImportExportDialog(
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -34,7 +34,6 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import de.lxtools.noteshop.R
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import de.lxtools.noteshop.ui.theme.*
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
@@ -64,7 +63,7 @@ fun SettingsScreen(
|
||||
isEncryptionEnabled: Boolean,
|
||||
onEncryptionToggle: (Boolean) -> Unit,
|
||||
onSetEncryptionPassword: () -> Unit,
|
||||
onRemoveEncryption: () -> Unit,
|
||||
|
||||
hasEncryptionPassword: Boolean,
|
||||
biometricAuthenticator: BiometricAuthenticator,
|
||||
sharedPrefs: SharedPreferences,
|
||||
@@ -85,10 +84,7 @@ fun SettingsScreen(
|
||||
val currentStartScreenName = startScreenOptions[currentStartScreen] ?: stringResource(R.string.start_screen_last)
|
||||
var isBiometricUnlockEnabled by remember { mutableStateOf(sharedPrefs.getBoolean("biometric_unlock_enabled", false)) }
|
||||
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 authenticateAndProceed: ((() -> Unit) -> Unit) = { successAction ->
|
||||
@@ -289,9 +285,8 @@ fun SettingsScreen(
|
||||
Switch(
|
||||
checked = isBiometricUnlockEnabled,
|
||||
onCheckedChange = {
|
||||
val newValue = it
|
||||
sharedPrefs.edit { putBoolean("biometric_unlock_enabled", newValue) }
|
||||
isBiometricUnlockEnabled = newValue
|
||||
sharedPrefs.edit { putBoolean ("biometric_unlock_enabled", it) }
|
||||
isBiometricUnlockEnabled = it
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package de.lxtools.noteshop.ui.appshell
|
||||
|
||||
import android.util.Log
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
@@ -54,7 +54,7 @@ fun AppContent(
|
||||
isEncryptionEnabled: Boolean,
|
||||
onEncryptionToggle: (Boolean) -> Unit,
|
||||
onSetEncryptionPassword: () -> Unit,
|
||||
onRemoveEncryption: () -> Unit,
|
||||
|
||||
hasEncryptionPassword: Boolean,
|
||||
biometricAuthenticator: BiometricAuthenticator,
|
||||
sharedPrefs: android.content.SharedPreferences,
|
||||
@@ -117,7 +117,6 @@ fun AppContent(
|
||||
listId = selectedListId,
|
||||
viewModel = shoppingListsViewModel,
|
||||
dynamicStrings = dynamicStrings,
|
||||
wasInitiallyLocked = itemWasInitiallyLocked,
|
||||
isContentDecrypted = itemWasInitiallyLocked,
|
||||
onUnlockClick = {
|
||||
val list = shoppingListsViewModel.uiState.value.shoppingLists.find { it.shoppingList.id == selectedListId }?.shoppingList
|
||||
@@ -186,7 +185,7 @@ fun AppContent(
|
||||
isEncryptionEnabled = isEncryptionEnabled,
|
||||
onEncryptionToggle = onEncryptionToggle,
|
||||
onSetEncryptionPassword = onSetEncryptionPassword,
|
||||
onRemoveEncryption = onRemoveEncryption,
|
||||
|
||||
hasEncryptionPassword = hasEncryptionPassword,
|
||||
biometricAuthenticator = biometricAuthenticator,
|
||||
sharedPrefs = sharedPrefs,
|
||||
@@ -201,7 +200,6 @@ fun AppContent(
|
||||
is Screen.WebAppIntegration -> {
|
||||
WebAppIntegrationScreen(
|
||||
viewModel = webAppIntegrationViewModel,
|
||||
onNavigateUp = { onScreenChange(Screen.Settings) },
|
||||
padding = innerPadding,
|
||||
onAuthenticateAndTogglePasswordVisibility = onAuthenticateAndTogglePasswordVisibility
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package de.lxtools.noteshop.ui.appshell
|
||||
|
||||
import android.util.Log
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.compose.material3.AlertDialog
|
||||
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.ShoppingListInputDialog
|
||||
import de.lxtools.noteshop.ui.ChooseLockMethodDialog
|
||||
import de.lxtools.noteshop.ui.LockableItemType
|
||||
import de.lxtools.noteshop.ui.LockMethod
|
||||
import de.lxtools.noteshop.ui.shopping.ShoppingListsViewModel
|
||||
import de.lxtools.noteshop.ui.StartupPasswordDialog
|
||||
import kotlinx.coroutines.launch
|
||||
import android.util.Log
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import de.lxtools.noteshop.findActivity
|
||||
|
||||
@@ -44,26 +44,26 @@ fun AppDialogs(
|
||||
onShowListDialogChange: (Boolean) -> Unit,
|
||||
listDetails: ShoppingListDetails,
|
||||
onListDetailsChange: (ShoppingListDetails) -> Unit,
|
||||
onSaveList: () -> Unit,
|
||||
onResetListDetails: () -> Unit,
|
||||
|
||||
|
||||
onSetListProtection: (String) -> Unit,
|
||||
onSetListProtectionBiometric: (Int) -> Unit,
|
||||
|
||||
txtImportLauncher: ActivityResultLauncher<Array<String>>,
|
||||
showNoteDialog: Boolean,
|
||||
onShowNoteDialogChange: (Boolean) -> Unit,
|
||||
noteDetails: NoteDetails,
|
||||
onNoteDetailsChange: (NoteDetails) -> Unit,
|
||||
onSaveNote: () -> Unit,
|
||||
onResetNoteDetails: () -> Unit,
|
||||
onSetNoteProtectionBiometric: (Int) -> Unit,
|
||||
|
||||
|
||||
|
||||
noteImportLauncher: ActivityResultLauncher<Array<String>>,
|
||||
showRecipeDialog: Boolean,
|
||||
onShowRecipeDialogChange: (Boolean) -> Unit,
|
||||
recipeDetails: RecipeDetails,
|
||||
onRecipeDetailsChange: (RecipeDetails) -> Unit,
|
||||
onSaveRecipe: () -> Unit,
|
||||
onResetRecipeDetails: () -> Unit,
|
||||
onSetRecipeProtectionBiometric: (Int) -> Unit,
|
||||
|
||||
|
||||
|
||||
recipeImportLauncher: ActivityResultLauncher<Array<String>>,
|
||||
recipesTitle: String,
|
||||
showJsonDialog: Boolean,
|
||||
@@ -101,7 +101,6 @@ fun AppDialogs(
|
||||
onShowChooseLockMethodDialogChange: (Boolean) -> Unit,
|
||||
onConfirmLockMethod: (LockMethod) -> Unit,
|
||||
canUseBiometrics: Boolean,
|
||||
itemToLockType: de.lxtools.noteshop.ui.LockableItemType?,
|
||||
showStartupPasswordDialog: Boolean,
|
||||
onShowStartupPasswordDialogChange: (Boolean) -> Unit,
|
||||
onUnlockEncryption: (String) -> Unit,
|
||||
|
||||
@@ -68,10 +68,6 @@ fun AppTopBar(
|
||||
exportLauncher: ActivityResultLauncher<String>,
|
||||
noteExportLauncher: ActivityResultLauncher<String>,
|
||||
recipeExportLauncher: ActivityResultLauncher<String>,
|
||||
hasEncryptionPassword: Boolean,
|
||||
onShowSetPasswordDialog: () -> Unit,
|
||||
onShowSetRecipePasswordDialog: () -> Unit,
|
||||
onShowSetListPasswordDialog: () -> Unit,
|
||||
onShowChooseLockMethodDialog: (LockableItemType, Int) -> Unit
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -296,7 +292,6 @@ fun AppTopBar(
|
||||
shoppingListWithItems?.let { listWithItems ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.lock)) },
|
||||
enabled = hasEncryptionPassword,
|
||||
onClick = {
|
||||
showMenu = false
|
||||
onShowChooseLockMethodDialog(LockableItemType.SHOPPING_LIST, listWithItems.shoppingList.id)
|
||||
@@ -381,7 +376,6 @@ fun AppTopBar(
|
||||
noteDetails.let {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.lock)) },
|
||||
enabled = hasEncryptionPassword,
|
||||
onClick = {
|
||||
showMenu = false
|
||||
onShowChooseLockMethodDialog(LockableItemType.NOTE, it.id)
|
||||
@@ -468,7 +462,6 @@ fun AppTopBar(
|
||||
recipeDetails.let {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.lock)) },
|
||||
enabled = hasEncryptionPassword,
|
||||
onClick = {
|
||||
showMenu = false
|
||||
onShowChooseLockMethodDialog(LockableItemType.RECIPE, it.id)
|
||||
|
||||
@@ -30,13 +30,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
|
||||
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.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import de.lxtools.noteshop.findActivity
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
|
||||
@@ -82,7 +82,7 @@ class NotesViewModel(private val noteshopRepository: NoteshopRepository) : ViewM
|
||||
|
||||
suspend fun saveNote() {
|
||||
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.
|
||||
|
||||
@@ -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() {
|
||||
_noteDetails.value = NoteDetails()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package de.lxtools.noteshop.ui.recipes
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
|
||||
@@ -80,7 +80,7 @@ class RecipesViewModel(private val noteshopRepository: NoteshopRepository) : Vie
|
||||
|
||||
suspend fun saveRecipe() {
|
||||
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.
|
||||
|
||||
@@ -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() {
|
||||
_recipeDetails.value = RecipeDetails()
|
||||
}
|
||||
|
||||
@@ -49,19 +49,14 @@ import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import de.lxtools.noteshop.R
|
||||
import de.lxtools.noteshop.data.ShoppingListItem
|
||||
import de.lxtools.noteshop.security.FileEncryptor
|
||||
import javax.crypto.SecretKey
|
||||
|
||||
import kotlinx.coroutines.launch
|
||||
import sh.calvin.reorderable.ReorderableItem
|
||||
import sh.calvin.reorderable.rememberReorderableLazyListState
|
||||
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 androidx.compose.ui.text.withStyle
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -70,18 +65,14 @@ fun ShoppingListDetailScreen(
|
||||
viewModel: ShoppingListsViewModel,
|
||||
dynamicStrings: de.lxtools.noteshop.DynamicListStrings,
|
||||
modifier: Modifier = Modifier,
|
||||
wasInitiallyLocked: Boolean,
|
||||
isContentDecrypted: Boolean,
|
||||
onUnlockClick: (Int) -> Unit,
|
||||
onNavigateBack: () -> Unit
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
|
||||
BackHandler(enabled = true) {
|
||||
keyboardController?.hide()
|
||||
onNavigateBack()
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Collect state from ViewModel
|
||||
val newItemName by viewModel.newItemName.collectAsState()
|
||||
@@ -100,7 +91,7 @@ fun ShoppingListDetailScreen(
|
||||
onDispose {
|
||||
val activity = context.findActivity()
|
||||
if (!activity.isChangingConfigurations) {
|
||||
// TODO: Handle lock state correctly
|
||||
onNavigateBack()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,9 +23,8 @@ import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.Json
|
||||
import de.lxtools.noteshop.security.FileEncryptor
|
||||
import java.util.Base64
|
||||
import javax.crypto.SecretKey
|
||||
import de.lxtools.noteshop.security.PasswordHasher
|
||||
import android.util.Base64
|
||||
|
||||
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) {
|
||||
noteshopRepository.deleteShoppingList(list)
|
||||
}
|
||||
@@ -474,10 +444,10 @@ class ShoppingListsViewModel(private val noteshopRepository: NoteshopRepository,
|
||||
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)
|
||||
|
||||
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)
|
||||
} 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()
|
||||
|
||||
@@ -3,7 +3,7 @@ package de.lxtools.noteshop.ui.webapp
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
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.VisibilityOff
|
||||
import androidx.compose.foundation.layout.*
|
||||
@@ -25,7 +25,7 @@ import de.lxtools.noteshop.R
|
||||
@Composable
|
||||
fun WebAppIntegrationScreen(
|
||||
viewModel: WebAppIntegrationViewModel,
|
||||
onNavigateUp: () -> Unit,
|
||||
|
||||
padding: PaddingValues,
|
||||
onAuthenticateAndTogglePasswordVisibility: (Boolean) -> Unit // New parameter
|
||||
) {
|
||||
|
||||
@@ -13,7 +13,6 @@ import de.lxtools.noteshop.data.NoteshopRepository
|
||||
import de.lxtools.noteshop.security.FileEncryptor
|
||||
import de.lxtools.noteshop.security.KeyManager
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
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)
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,4 +339,6 @@
|
||||
<string name="set_pin">PIN festlegen</string>
|
||||
<string name="enter_pin">PIN eingeben</string>
|
||||
<string name="set_pattern">Muster festlegen</string>
|
||||
<string name="tour_finish">Fertig</string>
|
||||
<string name="tour_skip">Überspringen</string>
|
||||
</resources>
|
||||
@@ -339,4 +339,6 @@
|
||||
<string name="set_pin">Set PIN</string>
|
||||
<string name="enter_pin">Enter PIN</string>
|
||||
<string name="set_pattern">Set Pattern</string>
|
||||
<string name="tour_finish">Finish</string>
|
||||
<string name="tour_skip">Skip</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user