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:
|
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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,19 +1291,19 @@ 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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
startupUnlockErrorMessage = startupUnlockErrorMessage
|
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.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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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 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
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user