feat: Implement pattern lock and fix lock icon visibility
- Implement pattern lock functionality with a custom pattern input view. - Fix an issue where the lock icon was only visible for password-locked items. - Add missing German translations.
This commit is contained in:
1
.idea/vcs.xml
generated
1
.idea/vcs.xml
generated
@@ -2,6 +2,5 @@
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/sharelist" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -645,6 +645,7 @@ fun AppShell(
|
||||
var showSetPatternDialog by rememberSaveable { mutableStateOf(false) }
|
||||
var showSetPinDialog by rememberSaveable { mutableStateOf(false) }
|
||||
var showUnlockPasswordDialog by rememberSaveable { mutableStateOf(false) }
|
||||
var showUnlockPinDialog by rememberSaveable { mutableStateOf(false) }
|
||||
var unlockErrorMessage by rememberSaveable { mutableStateOf<String?>(null) }
|
||||
var itemToUnlockId by rememberSaveable { mutableStateOf<Int?>(null) }
|
||||
var itemToUnlockType by rememberSaveable { mutableStateOf<Screen?>(null) }
|
||||
@@ -695,6 +696,10 @@ fun AppShell(
|
||||
itemToUnlockType = type
|
||||
showUnlockPasswordDialog = true
|
||||
}
|
||||
} else if (protectionType == 3) { // PIN protected
|
||||
itemToUnlockId = id
|
||||
itemToUnlockType = type
|
||||
showUnlockPinDialog = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1178,6 +1183,15 @@ fun AppShell(
|
||||
itemToUnlockType = null
|
||||
}
|
||||
},
|
||||
showUnlockPinDialog = showUnlockPinDialog,
|
||||
onShowUnlockPinDialogChange = {
|
||||
showUnlockPinDialog = it
|
||||
if (!it) {
|
||||
unlockErrorMessage = null
|
||||
itemToUnlockId = null
|
||||
itemToUnlockType = null
|
||||
}
|
||||
},
|
||||
onUnlock = { password ->
|
||||
val hashedPassword = PasswordHasher.hashPassword(password)
|
||||
var isPasswordCorrect = false
|
||||
@@ -1214,6 +1228,7 @@ fun AppShell(
|
||||
itemToUnlockId = null
|
||||
itemToUnlockType = null
|
||||
showUnlockPasswordDialog = false
|
||||
showUnlockPinDialog = false
|
||||
} else {
|
||||
unlockErrorMessage = context.getString(R.string.incorrect_password)
|
||||
}
|
||||
|
||||
@@ -93,32 +93,6 @@ fun ChooseLockMethodDialog(
|
||||
)
|
||||
Text(text = stringResource(R.string.pin), modifier = Modifier.padding(start = 8.dp))
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { selectedMethod = LockMethod.PATTERN }
|
||||
.padding(vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
RadioButton(
|
||||
selected = selectedMethod == LockMethod.PATTERN,
|
||||
onClick = { selectedMethod = LockMethod.PATTERN }
|
||||
)
|
||||
Text(text = stringResource(R.string.pattern), modifier = Modifier.padding(start = 8.dp))
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { selectedMethod = LockMethod.PIN }
|
||||
.padding(vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
RadioButton(
|
||||
selected = selectedMethod == LockMethod.PIN,
|
||||
onClick = { selectedMethod = LockMethod.PIN }
|
||||
)
|
||||
Text(text = stringResource(R.string.pin), modifier = Modifier.padding(start = 8.dp))
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
|
||||
107
app/src/main/java/de/lxtools/noteshop/ui/PatternInput.kt
Normal file
107
app/src/main/java/de/lxtools/noteshop/ui/PatternInput.kt
Normal file
@@ -0,0 +1,107 @@
|
||||
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
|
||||
}
|
||||
@@ -1,18 +1,35 @@
|
||||
package de.lxtools.noteshop.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import de.lxtools.noteshop.R
|
||||
|
||||
@Composable
|
||||
fun SetPatternDialog(onDismiss: () -> Unit, onSetPattern: (String) -> Unit) {
|
||||
var pattern by remember { mutableStateOf(emptyList<Int>()) }
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(text = "Set Pattern") },
|
||||
text = { Text(text = "Pattern entry not implemented yet.") },
|
||||
text = {
|
||||
PatternInput(onPatternComplete = {
|
||||
pattern = it
|
||||
onSetPattern(it.joinToString(""))
|
||||
})
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = { onSetPattern("") }) {
|
||||
TextButton(onClick = { onSetPattern(pattern.joinToString("")) }) {
|
||||
Text(text = "OK")
|
||||
}
|
||||
},
|
||||
@@ -24,20 +41,91 @@ fun SetPatternDialog(onDismiss: () -> Unit, onSetPattern: (String) -> Unit) {
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UnlockPatternDialog(onDismiss: () -> Unit, onUnlock: (String) -> Unit, errorMessage: String?) {
|
||||
var pattern by remember { mutableStateOf(emptyList<Int>()) }
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(text = stringResource(R.string.unlock_item)) },
|
||||
text = {
|
||||
Column {
|
||||
PatternInput(onPatternComplete = {
|
||||
pattern = it
|
||||
onUnlock(it.joinToString(""))
|
||||
})
|
||||
if (errorMessage != null) {
|
||||
Text(text = errorMessage, color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = { onUnlock(pattern.joinToString("")) }) {
|
||||
Text(text = stringResource(R.string.unlock))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(text = stringResource(R.string.cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SetPinDialog(onDismiss: () -> Unit, onSetPin: (String) -> Unit) {
|
||||
var pin by remember { mutableStateOf("") }
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(text = "Set PIN") },
|
||||
text = { Text(text = "PIN entry not implemented yet.") },
|
||||
title = { Text(text = stringResource(R.string.set_pin)) },
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
value = pin,
|
||||
onValueChange = { pin = it },
|
||||
label = { Text(stringResource(R.string.enter_pin)) },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword)
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = { onSetPin("") }) {
|
||||
Text(text = "OK")
|
||||
TextButton(onClick = { onSetPin(pin) }) {
|
||||
Text(text = stringResource(R.string.save))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(text = "Cancel")
|
||||
Text(text = stringResource(R.string.cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UnlockPinDialog(onDismiss: () -> Unit, onUnlock: (String) -> Unit, errorMessage: String?) {
|
||||
var pin by remember { mutableStateOf("") }
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(text = stringResource(R.string.unlock_item)) },
|
||||
text = {
|
||||
Column {
|
||||
OutlinedTextField(
|
||||
value = pin,
|
||||
onValueChange = { pin = it },
|
||||
label = { Text(stringResource(R.string.enter_pin)) },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword),
|
||||
isError = errorMessage != null
|
||||
)
|
||||
if (errorMessage != null) {
|
||||
Text(text = errorMessage, color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = { onUnlock(pin) }) {
|
||||
Text(text = stringResource(R.string.unlock))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(text = stringResource(R.string.cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -79,6 +79,8 @@ fun AppDialogs(
|
||||
onShowSetListPasswordDialogChange: (Boolean) -> Unit,
|
||||
showUnlockPasswordDialog: Boolean,
|
||||
onShowUnlockPasswordDialogChange: (Boolean) -> Unit,
|
||||
showUnlockPinDialog: Boolean,
|
||||
onShowUnlockPinDialogChange: (Boolean) -> Unit,
|
||||
onUnlock: (String) -> Unit,
|
||||
unlockErrorMessage: String?,
|
||||
showPasswordDialog: Boolean,
|
||||
@@ -249,6 +251,16 @@ fun AppDialogs(
|
||||
)
|
||||
}
|
||||
|
||||
if (showUnlockPinDialog) {
|
||||
de.lxtools.noteshop.ui.UnlockPinDialog(
|
||||
onDismiss = {
|
||||
onShowUnlockPinDialogChange(false)
|
||||
},
|
||||
onUnlock = onUnlock,
|
||||
errorMessage = unlockErrorMessage
|
||||
)
|
||||
}
|
||||
|
||||
if (showPasswordDialog) {
|
||||
EncryptionPasswordDialog(
|
||||
onDismiss = { onShowPasswordDialogChange(false) },
|
||||
|
||||
@@ -302,6 +302,15 @@ fun AppTopBar(
|
||||
onShowChooseLockMethodDialog(LockableItemType.SHOPPING_LIST, listWithItems.shoppingList.id)
|
||||
}
|
||||
)
|
||||
if (listWithItems.shoppingList.protectionType != 0) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.remove_lock)) },
|
||||
onClick = {
|
||||
showMenu = false
|
||||
shoppingListsViewModel.setProtection(listWithItems.shoppingList.id, "")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -378,6 +387,15 @@ fun AppTopBar(
|
||||
onShowChooseLockMethodDialog(LockableItemType.NOTE, it.id)
|
||||
}
|
||||
)
|
||||
if (it.protectionType != 0) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.remove_lock)) },
|
||||
onClick = {
|
||||
showMenu = false
|
||||
notesViewModel.setProtectionPassword("")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -456,6 +474,15 @@ fun AppTopBar(
|
||||
onShowChooseLockMethodDialog(LockableItemType.RECIPE, it.id)
|
||||
}
|
||||
)
|
||||
if (it.protectionType != 0) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.remove_lock)) },
|
||||
onClick = {
|
||||
showMenu = false
|
||||
recipesViewModel.setProtectionPassword("")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ fun NoteCard(
|
||||
Icon(Icons.Rounded.DragHandle, contentDescription = stringResource(R.string.reorder))
|
||||
}
|
||||
}
|
||||
if (note.protectionHash.isNotEmpty()) {
|
||||
if (note.lockMethod != 0) {
|
||||
Icon(Icons.Default.Lock, contentDescription = null, modifier = Modifier.padding(end = 8.dp))
|
||||
}
|
||||
Text(
|
||||
|
||||
@@ -141,7 +141,28 @@ class NotesViewModel(private val noteshopRepository: NoteshopRepository) : ViewM
|
||||
}
|
||||
|
||||
fun setProtectionPin(pin: String) {
|
||||
Log.d("NotesViewModel", "setProtectionPin called with pin: $pin")
|
||||
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() {
|
||||
|
||||
@@ -127,7 +127,7 @@ fun RecipeCard(
|
||||
Icon(Icons.Rounded.DragHandle, contentDescription = stringResource(R.string.reorder))
|
||||
}
|
||||
}
|
||||
if (recipe.protectionHash.isNotEmpty()) {
|
||||
if (recipe.lockMethod != 0) {
|
||||
Icon(Icons.Default.Lock, contentDescription = null, modifier = Modifier.padding(end = 8.dp))
|
||||
}
|
||||
Text(
|
||||
|
||||
@@ -139,7 +139,28 @@ class RecipesViewModel(private val noteshopRepository: NoteshopRepository) : Vie
|
||||
}
|
||||
|
||||
fun setProtectionPin(pin: String) {
|
||||
Log.d("RecipesViewModel", "setProtectionPin called with pin: $pin")
|
||||
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() {
|
||||
|
||||
@@ -127,7 +127,7 @@ fun ShoppingListCard(
|
||||
Icon(Icons.Rounded.DragHandle, contentDescription = stringResource(R.string.reorder))
|
||||
}
|
||||
}
|
||||
if (listWithItems.shoppingList.protectionHash.isNotEmpty()) {
|
||||
if (listWithItems.shoppingList.lockMethod != 0) {
|
||||
Icon(Icons.Default.Lock, contentDescription = null, modifier = Modifier.padding(end = 8.dp))
|
||||
}
|
||||
Text(
|
||||
|
||||
@@ -193,7 +193,28 @@ class ShoppingListsViewModel(private val noteshopRepository: NoteshopRepository,
|
||||
}
|
||||
|
||||
fun setProtectionPin(pin: String) {
|
||||
Log.d("ShoppingListsViewModel", "setProtectionPin called with pin: $pin")
|
||||
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) {
|
||||
|
||||
@@ -328,4 +328,10 @@
|
||||
<string name="remove_lock">Sperre entfernen</string>
|
||||
<string name="choose_lock_method">Sperrmethode auswählen</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="biometric_lock">Biometrische Sperre</string>
|
||||
<string name="pattern">Muster</string>
|
||||
<string name="pin">PIN</string>
|
||||
<string name="set_pin">PIN festlegen</string>
|
||||
<string name="enter_pin">PIN eingeben</string>
|
||||
<string name="set_pattern">Muster festlegen</string>
|
||||
</resources>
|
||||
@@ -331,4 +331,7 @@
|
||||
<string name="ok">OK</string>
|
||||
<string name="pattern">Pattern</string>
|
||||
<string name="pin">PIN</string>
|
||||
<string name="set_pin">Set PIN</string>
|
||||
<string name="enter_pin">Enter PIN</string>
|
||||
<string name="set_pattern">Set Pattern</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user