From f93979950b861e43f09dba9563b8c8f73bff42f3 Mon Sep 17 00:00:00 2001 From: Christian Nagel Date: Thu, 12 Mar 2026 22:58:43 +0100 Subject: [PATCH 1/7] Fix layouting error in FoundScannerItem, add editing for custom scanners, add deletion confirmation for custom scanners --- app/build.gradle.kts | 4 - .../chrisimx/scanbridge/ScanBridgeTest.kt | 5 -- .../screenshot/ScanBridgeScreenshotTest.kt | 15 +--- .../chrisimx/scanbridge/ScannerBrowser.kt | 71 ++++++++++++----- .../chrisimx/scanbridge/ScanningScreen.kt | 2 + .../chrisimx/scanbridge/StartupScreen.kt | 9 ++- .../data/model/EditedCustomScanner.kt | 8 ++ .../data/ui/CustomScannerViewModel.kt | 10 +++ .../scanbridge/db/daos/CustomScannerDao.kt | 10 ++- .../uicomponents/FoundScannerItem.kt | 53 ++++++++----- .../dialog/CustomScannerDialog.kt | 76 +++++++++++++------ .../uicomponents/dialog/DeleteDialog.kt | 16 ++-- app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values-it/strings.xml | 2 +- app/src/main/res/values/strings.xml | 6 +- 15 files changed, 198 insertions(+), 91 deletions(-) create mode 100644 app/src/main/java/io/github/chrisimx/scanbridge/data/model/EditedCustomScanner.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 55c940ab..1c3d3164 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,7 +1,4 @@ -import java.security.MessageDigest import java.util.* -import okhttp3.OkHttpClient -import okhttp3.Request import org.jetbrains.kotlin.gradle.dsl.JvmTarget val testConfig = Properties() @@ -199,7 +196,6 @@ protobuf { } } - afterEvaluate { tasks.named("clean") { doLast { diff --git a/app/src/androidTest/java/io/github/chrisimx/scanbridge/ScanBridgeTest.kt b/app/src/androidTest/java/io/github/chrisimx/scanbridge/ScanBridgeTest.kt index 1f091691..e764baa4 100644 --- a/app/src/androidTest/java/io/github/chrisimx/scanbridge/ScanBridgeTest.kt +++ b/app/src/androidTest/java/io/github/chrisimx/scanbridge/ScanBridgeTest.kt @@ -14,16 +14,11 @@ import androidx.test.espresso.Espresso.pressBack import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule import io.github.chrisimx.esclmockserver.EsclMockServer -import io.github.chrisimx.esclmockserver.EsclMockServerArgs -import io.github.chrisimx.esclmockserver.esclMockServerArgs import io.github.chrisimx.scanbridge.datastore.appSettingsStore import io.github.chrisimx.scanbridge.datastore.lastRouteStore import io.github.chrisimx.scanbridge.datastore.shownMessagesStore import io.github.chrisimx.scanbridge.datastore.updateSettings import io.github.chrisimx.scanbridge.proto.copy -import java.io.BufferedReader -import java.io.File -import java.io.InputStreamReader import kotlinx.coroutines.runBlocking import org.junit.After import org.junit.Before diff --git a/app/src/androidTest/java/io/github/chrisimx/scanbridge/screenshot/ScanBridgeScreenshotTest.kt b/app/src/androidTest/java/io/github/chrisimx/scanbridge/screenshot/ScanBridgeScreenshotTest.kt index 2064ee4b..8242c789 100644 --- a/app/src/androidTest/java/io/github/chrisimx/scanbridge/screenshot/ScanBridgeScreenshotTest.kt +++ b/app/src/androidTest/java/io/github/chrisimx/scanbridge/screenshot/ScanBridgeScreenshotTest.kt @@ -1,7 +1,6 @@ package io.github.chrisimx.scanbridge.screenshot import android.content.Context -import android.util.Log import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.junit4.createAndroidComposeRule @@ -13,7 +12,6 @@ import androidx.compose.ui.test.swipeUp import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule import io.github.chrisimx.esclmockserver.EsclMockServer -import io.github.chrisimx.esclmockserver.esclMockServerArgs import io.github.chrisimx.scanbridge.BuildConfig import io.github.chrisimx.scanbridge.MainActivity import io.github.chrisimx.scanbridge.datastore.appSettingsStore @@ -21,9 +19,6 @@ import io.github.chrisimx.scanbridge.datastore.lastRouteStore import io.github.chrisimx.scanbridge.datastore.shownMessagesStore import io.github.chrisimx.scanbridge.datastore.updateSettings import io.github.chrisimx.scanbridge.proto.copy -import java.io.BufferedReader -import java.io.File -import java.io.InputStreamReader import kotlinx.coroutines.runBlocking import org.junit.Before import org.junit.Rule @@ -46,8 +41,7 @@ class ScanBridgeScreenshotTest { fun startServer(): EsclMockServer { val context = InstrumentationRegistry.getInstrumentation().context - val imageFile = copyAssetToByteArray( context, "scan-1.jpg") - + val imageFile = copyAssetToByteArray(context, "scan-1.jpg") val server = EsclMockServer { servedImage = imageFile @@ -56,10 +50,9 @@ class ScanBridgeScreenshotTest { return server } - fun copyAssetToByteArray(testContext: Context, assetName: String): ByteArray = - testContext.assets.open(assetName).use { - it.readBytes() - } + fun copyAssetToByteArray(testContext: Context, assetName: String): ByteArray = testContext.assets.open(assetName).use { + it.readBytes() + } @Before fun cleanupForTest() { diff --git a/app/src/main/java/io/github/chrisimx/scanbridge/ScannerBrowser.kt b/app/src/main/java/io/github/chrisimx/scanbridge/ScannerBrowser.kt index 0ddd8e0d..7030942a 100644 --- a/app/src/main/java/io/github/chrisimx/scanbridge/ScannerBrowser.kt +++ b/app/src/main/java/io/github/chrisimx/scanbridge/ScannerBrowser.kt @@ -34,6 +34,9 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshots.SnapshotStateMap import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -42,11 +45,13 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat.getSystemService import androidx.navigation.NavController +import io.github.chrisimx.scanbridge.data.model.EditedCustomScanner import io.github.chrisimx.scanbridge.data.ui.CustomScannerViewModel import io.github.chrisimx.scanbridge.db.entities.CustomScanner import io.github.chrisimx.scanbridge.uicomponents.FoundScannerItem import io.github.chrisimx.scanbridge.uicomponents.FullScreenError import io.github.chrisimx.scanbridge.uicomponents.dialog.CustomScannerDialog +import io.github.chrisimx.scanbridge.uicomponents.dialog.DeletionDialog import io.ktor.http.Url import java.util.* import kotlin.uuid.ExperimentalUuidApi @@ -78,7 +83,9 @@ fun ScannerList( navController: NavController, statefulScannerMap: SnapshotStateMap, statefulScannerMapSecure: SnapshotStateMap, - customScannerViewModel: CustomScannerViewModel + customScannerViewModel: CustomScannerViewModel, + setScannerToDelete: (Uuid?) -> Unit, + setScannerToEdit: (EditedCustomScanner?) -> Unit ) { val customScanners by customScannerViewModel.customScanners.collectAsState() @@ -129,7 +136,12 @@ fun ScannerList( customScanner.url.toString(), navController, { - customScannerViewModel.deleteScanner(customScanner) + setScannerToDelete(customScanner.uuid) + }, + { + setScannerToEdit( + EditedCustomScanner.EditingOld(customScanner) + ) } ) } @@ -152,20 +164,24 @@ fun ScannerList( fun ScannerBrowser( innerPadding: PaddingValues, navController: NavController, - showCustomDialog: Boolean, - setShowCustomDialog: (Boolean) -> Unit, + currentlyEditedScanner: EditedCustomScanner?, + setEditedCustomDialog: (EditedCustomScanner?) -> Unit, statefulScannerMap: SnapshotStateMap, statefulScannerMapSecure: SnapshotStateMap ) { val customScannerViewModel: CustomScannerViewModel = koinViewModel() val customScanners by customScannerViewModel.customScanners.collectAsState() + var deletionScheduledScanner: Uuid? by remember { mutableStateOf(null) } + AnimatedContent( targetState = statefulScannerMap.isNotEmpty() || customScanners.isNotEmpty(), label = "ScannerList" ) { if (it) { - ScannerList(innerPadding, navController, statefulScannerMap, statefulScannerMapSecure, customScannerViewModel) + ScannerList(innerPadding, navController, statefulScannerMap, statefulScannerMapSecure, customScannerViewModel, { + deletionScheduledScanner = it + }, setEditedCustomDialog) } else { FullScreenError( R.drawable.twotone_wifi_find_24, @@ -174,26 +190,47 @@ fun ScannerBrowser( } } - if (showCustomDialog) { + val deletionScheduledScannerImmutable = deletionScheduledScanner + if (deletionScheduledScannerImmutable != null) { + DeletionDialog( + R.string.custom_scanner_deletion, + R.string.custom_scanner_deletion_confirmation, + onDismiss = { deletionScheduledScanner = null }, + onConfirmed = { + customScannerViewModel.deleteScannerByUuid(deletionScheduledScannerImmutable) + deletionScheduledScanner = null + } + ) + } + + if (currentlyEditedScanner != null) { val context = LocalContext.current + CustomScannerDialog( - onDismiss = { setShowCustomDialog(false) }, - onConnectClicked = { name, url, save -> + onDismiss = { setEditedCustomDialog(null) }, + onConnectClicked = { name, url, save, navigate -> val name = name.ifEmpty { context.getString(R.string.custom_scanner) } val url = if (url.toString().endsWith("/")) url.toString() else "$url/" val sessionID = Uuid.random() + val uuid = when (currentlyEditedScanner) { + is EditedCustomScanner.EditingOld -> currentlyEditedScanner.scanner.uuid + EditedCustomScanner.New -> Uuid.random() + } if (save) { - customScannerViewModel.addScanner(CustomScanner(Uuid.random(), name, Url(url))) + customScannerViewModel.addScanner(CustomScanner(uuid, name, Url(url))) } - setShowCustomDialog(false) - navController.navigate( - ScannerRoute( - name, - url, - sessionID.toString() + setEditedCustomDialog(null) + if (navigate) { + navController.navigate( + ScannerRoute( + name, + url, + sessionID.toString() + ) ) - ) - } + } + }, + currentlyEditedScanner ) } } diff --git a/app/src/main/java/io/github/chrisimx/scanbridge/ScanningScreen.kt b/app/src/main/java/io/github/chrisimx/scanbridge/ScanningScreen.kt index 856d52a8..0e695d25 100644 --- a/app/src/main/java/io/github/chrisimx/scanbridge/ScanningScreen.kt +++ b/app/src/main/java/io/github/chrisimx/scanbridge/ScanningScreen.kt @@ -574,6 +574,8 @@ fun ScanningScreen( if (scanningViewModel.scanningScreenData.confirmPageDeleteDialogShown) { DeletionDialog( + R.string.delete_current_page, + R.string.page_deletion_confirmation, onDismiss = { scanningViewModel.setDeletePageDialogShown(false) }, onConfirmed = { Timber.d("Deleting page") diff --git a/app/src/main/java/io/github/chrisimx/scanbridge/StartupScreen.kt b/app/src/main/java/io/github/chrisimx/scanbridge/StartupScreen.kt index fa07d319..0323e206 100644 --- a/app/src/main/java/io/github/chrisimx/scanbridge/StartupScreen.kt +++ b/app/src/main/java/io/github/chrisimx/scanbridge/StartupScreen.kt @@ -49,6 +49,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.navigation.NavController +import io.github.chrisimx.scanbridge.data.model.EditedCustomScanner import timber.log.Timber @OptIn(ExperimentalMaterial3Api::class) @@ -68,8 +69,8 @@ data class StartupScreen( val screenComposable: @Composable ( innerPadding: PaddingValues, navController: NavController, - showCustomDialog: Boolean, - setShowCustomDialog: (Boolean) -> Unit, + showCustomDialog: EditedCustomScanner?, + setShowCustomDialog: (EditedCustomScanner?) -> Unit, statefulScannerMap: SnapshotStateMap, statefulScannerMapSecure: SnapshotStateMap ) -> Unit @@ -109,7 +110,7 @@ fun StartupScreen(navController: NavController) { } } - var showCustomDialog by remember { mutableStateOf(false) } + var showCustomDialog: EditedCustomScanner? by remember { mutableStateOf(null) } Scaffold( modifier = Modifier.fillMaxSize(), @@ -137,7 +138,7 @@ fun StartupScreen(navController: NavController) { FloatingActionButton( modifier = Modifier.testTag("custom_scanner_fab"), onClick = { - showCustomDialog = true + showCustomDialog = EditedCustomScanner.New } ) { Icon( diff --git a/app/src/main/java/io/github/chrisimx/scanbridge/data/model/EditedCustomScanner.kt b/app/src/main/java/io/github/chrisimx/scanbridge/data/model/EditedCustomScanner.kt new file mode 100644 index 00000000..5c19bc0f --- /dev/null +++ b/app/src/main/java/io/github/chrisimx/scanbridge/data/model/EditedCustomScanner.kt @@ -0,0 +1,8 @@ +package io.github.chrisimx.scanbridge.data.model + +import io.github.chrisimx.scanbridge.db.entities.CustomScanner + +sealed class EditedCustomScanner { + data object New : EditedCustomScanner() + data class EditingOld(val scanner: CustomScanner) : EditedCustomScanner() +} diff --git a/app/src/main/java/io/github/chrisimx/scanbridge/data/ui/CustomScannerViewModel.kt b/app/src/main/java/io/github/chrisimx/scanbridge/data/ui/CustomScannerViewModel.kt index 7ab4cb85..bcce836c 100644 --- a/app/src/main/java/io/github/chrisimx/scanbridge/data/ui/CustomScannerViewModel.kt +++ b/app/src/main/java/io/github/chrisimx/scanbridge/data/ui/CustomScannerViewModel.kt @@ -25,6 +25,7 @@ import androidx.lifecycle.viewModelScope import io.github.chrisimx.scanbridge.db.ScanBridgeDb import io.github.chrisimx.scanbridge.db.entities.CustomScanner import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.stateIn @@ -42,10 +43,19 @@ class CustomScannerViewModel(application: Application, appDb: ScanBridgeDb) : An } } + suspend fun loadScannerByUuid(scanner: Uuid): CustomScanner? = customScannerDao.getByScanId(scanner) + @OptIn(ExperimentalUuidApi::class) fun deleteScanner(scanner: CustomScanner) { viewModelScope.launch { customScannerDao.delete(scanner) } } + + @OptIn(ExperimentalUuidApi::class) + fun deleteScannerByUuid(scanner: Uuid) { + viewModelScope.launch { + customScannerDao.deleteById(scanner) + } + } } diff --git a/app/src/main/java/io/github/chrisimx/scanbridge/db/daos/CustomScannerDao.kt b/app/src/main/java/io/github/chrisimx/scanbridge/db/daos/CustomScannerDao.kt index b4e2841d..b3c4b264 100644 --- a/app/src/main/java/io/github/chrisimx/scanbridge/db/daos/CustomScannerDao.kt +++ b/app/src/main/java/io/github/chrisimx/scanbridge/db/daos/CustomScannerDao.kt @@ -3,10 +3,12 @@ package io.github.chrisimx.scanbridge.db.daos import androidx.room.Dao import androidx.room.Delete import androidx.room.Insert +import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.Update import io.github.chrisimx.scanbridge.db.entities.CustomScanner import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid import kotlinx.coroutines.flow.Flow @Dao @@ -18,7 +20,10 @@ interface CustomScannerDao { @Query("SELECT * FROM customscanners") suspend fun getAll(): List - @Insert + @Query("SELECT * FROM customscanners WHERE uuid = :id") + suspend fun getByScanId(id: Uuid): CustomScanner? + + @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertAll(vararg scanners: CustomScanner) @Update @@ -26,4 +31,7 @@ interface CustomScannerDao { @Delete suspend fun delete(scanner: CustomScanner) + + @Query("DELETE FROM customscanners WHERE uuid = :id") + suspend fun deleteById(id: Uuid) } diff --git a/app/src/main/java/io/github/chrisimx/scanbridge/uicomponents/FoundScannerItem.kt b/app/src/main/java/io/github/chrisimx/scanbridge/uicomponents/FoundScannerItem.kt index f8124030..bdad1e50 100644 --- a/app/src/main/java/io/github/chrisimx/scanbridge/uicomponents/FoundScannerItem.kt +++ b/app/src/main/java/io/github/chrisimx/scanbridge/uicomponents/FoundScannerItem.kt @@ -21,7 +21,6 @@ package io.github.chrisimx.scanbridge.uicomponents import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -49,7 +48,13 @@ import kotlin.uuid.Uuid import timber.log.Timber @Composable -fun FoundScannerItem(name: String, address: String, navController: NavController, deleteScanner: (() -> Unit)? = null) { +fun FoundScannerItem( + name: String, + address: String, + navController: NavController, + deleteScanner: (() -> Unit)? = null, + editScanner: (() -> Unit)? = null +) { ElevatedCard( modifier = Modifier .defaultMinSize(minHeight = 60.dp) @@ -74,7 +79,11 @@ fun FoundScannerItem(name: String, address: String, navController: NavController tint = MaterialTheme.colorScheme.surfaceTint, contentDescription = stringResource(id = R.string.print_symbol_desc) ) - Column { + Column( + modifier = Modifier + .weight(1f) + .padding(vertical = 10.dp) + ) { Row { Text( name, @@ -82,14 +91,6 @@ fun FoundScannerItem(name: String, address: String, navController: NavController fontSize = 18.sp, style = MaterialTheme.typography.labelLarge ) - if (deleteScanner != null) { - Icon( - painter = painterResource(R.drawable.outline_edit_24), - tint = MaterialTheme.colorScheme.surfaceTint, - modifier = Modifier.padding(start = 8.dp), - contentDescription = stringResource(id = R.string.custom) - ) - } } Text( address, @@ -97,16 +98,30 @@ fun FoundScannerItem(name: String, address: String, navController: NavController ) } if (deleteScanner != null) { - Spacer(modifier = Modifier.weight(1f)) - IconButton(onClick = { - deleteScanner.invoke() - Timber.i("Delete button clicked for custom scanner: $name at $address") - }) { + IconButton( + modifier = Modifier.padding(start = 8.dp, top = 8.dp, bottom = 8.dp), + onClick = { + editScanner!!.invoke() + Timber.i("Edit button clicked for custom scanner: $name at $address") + } + ) { + Icon( + painter = painterResource(R.drawable.outline_edit_24), + tint = MaterialTheme.colorScheme.surfaceTint, + contentDescription = stringResource(id = R.string.custom) + ) + } + IconButton( + modifier = Modifier.padding(end = 8.dp, top = 8.dp, bottom = 8.dp), + onClick = { + deleteScanner.invoke() + Timber.i("Delete button clicked for custom scanner: $name at $address") + } + ) { Icon( imageVector = Icons.Rounded.Delete, - tint = MaterialTheme.colorScheme.secondary, - contentDescription = stringResource(id = R.string.delete), - modifier = Modifier.padding(8.dp) + tint = MaterialTheme.colorScheme.error, + contentDescription = stringResource(id = R.string.delete) ) } } diff --git a/app/src/main/java/io/github/chrisimx/scanbridge/uicomponents/dialog/CustomScannerDialog.kt b/app/src/main/java/io/github/chrisimx/scanbridge/uicomponents/dialog/CustomScannerDialog.kt index 3cb1ed43..5d01d3f1 100644 --- a/app/src/main/java/io/github/chrisimx/scanbridge/uicomponents/dialog/CustomScannerDialog.kt +++ b/app/src/main/java/io/github/chrisimx/scanbridge/uicomponents/dialog/CustomScannerDialog.kt @@ -24,17 +24,30 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import io.github.chrisimx.scanbridge.R +import io.github.chrisimx.scanbridge.data.model.EditedCustomScanner import io.ktor.http.Url @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable -fun CustomScannerDialog(onDismiss: () -> Unit, onConnectClicked: (String, Url, Boolean) -> Unit) { +fun CustomScannerDialog( + onDismiss: () -> Unit, + onConnectClicked: (name: String, url: Url, save: Boolean, navigate: Boolean) -> Unit, + editingType: EditedCustomScanner +) { var urlErrorState: String? by remember { mutableStateOf(null) } - var urlText: String by remember { mutableStateOf("") } - var nameText: String by remember { mutableStateOf("") } + val initializeWith = when (editingType) { + is EditedCustomScanner.EditingOld -> editingType.scanner + EditedCustomScanner.New -> null + } + var urlText: String by remember { + mutableStateOf(initializeWith?.url?.toString() ?: "") + } + var nameText: String by remember { mutableStateOf(initializeWith?.name ?: "") } val context = LocalContext.current + val isNewScanner = editingType is EditedCustomScanner.New + val validateUrl = fun(): Url? { if (urlText.isEmpty()) { urlErrorState = context.getString(R.string.error_state_please_enter_an_url) @@ -63,8 +76,14 @@ fun CustomScannerDialog(onDismiss: () -> Unit, onConnectClicked: (String, Url, B verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { + val title = if (isNewScanner) { + R.string.new_custom_scanner_dialog_title + } else { + R.string.edit_custom_scanner + } + Text( - stringResource(R.string.custom_scanner_dialog_title), + stringResource(title), style = MaterialTheme.typography.titleSmall, modifier = Modifier.padding(bottom = 16.dp) ) @@ -78,7 +97,7 @@ fun CustomScannerDialog(onDismiss: () -> Unit, onConnectClicked: (String, Url, B placeholder = { Text(stringResource(R.string.scanner_name_placeholder)) } ) OutlinedTextField( - modifier = Modifier.testTag("url_input"), + modifier = Modifier.testTag("url_input").padding(top = 16.dp), value = urlText, onValueChange = { urlErrorState = null @@ -96,23 +115,36 @@ fun CustomScannerDialog(onDismiss: () -> Unit, onConnectClicked: (String, Url, B } } ) - Button( - onClick = { - val url = validateUrl() ?: return@Button - onConnectClicked(nameText, url, true) - }, - modifier = Modifier.padding(top = 16.dp) - ) { - Text(stringResource(R.string.connect_and_save)) - } - Button( - onClick = { - val url = validateUrl() ?: return@Button - onConnectClicked(nameText, url, false) - }, - modifier = Modifier.padding(top = 8.dp).testTag("justconnect") - ) { - Text(stringResource(R.string.connect)) + + if (isNewScanner) { + Button( + onClick = { + val url = validateUrl() ?: return@Button + onConnectClicked(nameText, url, true, true) + }, + modifier = Modifier.padding(top = 16.dp) + ) { + Text(stringResource(R.string.connect_and_save)) + } + Button( + onClick = { + val url = validateUrl() ?: return@Button + onConnectClicked(nameText, url, false, true) + }, + modifier = Modifier.padding(top = 8.dp).testTag("justconnect") + ) { + Text(stringResource(R.string.connect)) + } + } else { + Button( + onClick = { + val url = validateUrl() ?: return@Button + onConnectClicked(nameText, url, true, false) + }, + modifier = Modifier.padding(top = 0.dp).testTag("editcustomscanner") + ) { + Text(stringResource(R.string.save)) + } } } } diff --git a/app/src/main/java/io/github/chrisimx/scanbridge/uicomponents/dialog/DeleteDialog.kt b/app/src/main/java/io/github/chrisimx/scanbridge/uicomponents/dialog/DeleteDialog.kt index 401dee4f..90d68d12 100644 --- a/app/src/main/java/io/github/chrisimx/scanbridge/uicomponents/dialog/DeleteDialog.kt +++ b/app/src/main/java/io/github/chrisimx/scanbridge/uicomponents/dialog/DeleteDialog.kt @@ -23,7 +23,9 @@ import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Delete import androidx.compose.material3.AlertDialog +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -33,20 +35,21 @@ import androidx.compose.ui.unit.dp import io.github.chrisimx.scanbridge.R @Composable -fun DeletionDialog(onDismiss: () -> Unit, onConfirmed: () -> Unit) { +fun DeletionDialog(title: Int, confirmationQuestion: Int, onDismiss: () -> Unit, onConfirmed: () -> Unit) { AlertDialog( icon = { Icon( imageVector = Icons.Rounded.Delete, - contentDescription = stringResource(R.string.delete_current_page), - modifier = Modifier.size(48.dp) + contentDescription = stringResource(title), + modifier = Modifier.size(48.dp), + tint = MaterialTheme.colorScheme.error ) }, title = { - Text(text = stringResource(R.string.delete_current_page)) + Text(text = stringResource(title)) }, text = { - Text(text = stringResource(R.string.page_deletion_confirmation)) + Text(text = stringResource(confirmationQuestion)) }, onDismissRequest = { onDismiss() @@ -62,6 +65,9 @@ fun DeletionDialog(onDismiss: () -> Unit, onConfirmed: () -> Unit) { }, confirmButton = { TextButton( + colors = ButtonDefaults.textButtonColors( + contentColor = MaterialTheme.colorScheme.error + ), onClick = { onConfirmed() } diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 61829f2a..c18ec594 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -67,7 +67,7 @@ Auf einen benutzdefinierten Scanner zugreifen Verbinden URL (eSCL-Ressource) - Verbinden mit benutzerdefinierten Scanner + Verbinden mit benutzerdefinierten Scanner Ungültige URL Gib bitte eine URL ein Benutzerdefinierter Scanner diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 76c14f72..1c9cbe18 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -65,7 +65,7 @@ Accedi allo scanner personalizzato Connettiti URL (risorsa eSCL) - Connettiti a uno scanner personalizzato + Connettiti a uno scanner personalizzato URL non valido Per favore inserisci un URL Scanner personalizzato diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 785dfdeb..1d72524c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -67,7 +67,7 @@ Access custom scanner Just connect URL (eSCL resource) - Connect to a custom scanner + Connect to a custom scanner Invalid URL Please enter an URL Custom scanner @@ -122,4 +122,8 @@ Color ({bitdepth} bit) Automatic detection Grayscale ({bitdepth} bit) + Save + Edit custom scanner + Do you really want to delete this custom scanner? This action cannot be undone. + Delete custom scanner \ No newline at end of file From 28b23201dcc7354db8a3766e67c5e9b94bcf6b34 Mon Sep 17 00:00:00 2001 From: Christian Nagel Date: Thu, 12 Mar 2026 23:21:07 +0100 Subject: [PATCH 2/7] Add translations for new strings --- app/src/main/res/values-de/strings.xml | 4 ++++ app/src/main/res/values-it/strings.xml | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index c18ec594..a56588bb 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -120,4 +120,8 @@ Farbe ({bitdepth} bit) Automatische Erkennung Graustufen ({bitdepth} bit) + Speichern + Benutzerdefinierten Scanner bearbeiten + Willst du den benutzerdefinierten Scanner wirklich löschen? Dieser Vorgang ist unwiderruflich. + Benutzerdefinierten Scanner löschen \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 1c9cbe18..7ebb1aa4 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -119,4 +119,8 @@ Colore ({bitdepth} bit) Rilevamento automatico Scala di grigi ({bitdepth} bit) + Salva + Modifica scanner personalizzato + Vuoi davvero eliminare lo scanner personalizzato? Questa operazione è irreversibile. + Elimina scanner personalizzato \ No newline at end of file From 22b341ff9af2676a84c7623a082a590fa11bd0ac Mon Sep 17 00:00:00 2001 From: Christian Nagel Date: Thu, 12 Mar 2026 23:32:50 +0100 Subject: [PATCH 3/7] Add StringRes annotation to StringRes args in DeletionDialog --- .../uicomponents/dialog/DeleteDialog.kt | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/io/github/chrisimx/scanbridge/uicomponents/dialog/DeleteDialog.kt b/app/src/main/java/io/github/chrisimx/scanbridge/uicomponents/dialog/DeleteDialog.kt index 90d68d12..6dcd6813 100644 --- a/app/src/main/java/io/github/chrisimx/scanbridge/uicomponents/dialog/DeleteDialog.kt +++ b/app/src/main/java/io/github/chrisimx/scanbridge/uicomponents/dialog/DeleteDialog.kt @@ -19,6 +19,7 @@ package io.github.chrisimx.scanbridge.uicomponents.dialog +import androidx.annotation.StringRes import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Delete @@ -35,21 +36,27 @@ import androidx.compose.ui.unit.dp import io.github.chrisimx.scanbridge.R @Composable -fun DeletionDialog(title: Int, confirmationQuestion: Int, onDismiss: () -> Unit, onConfirmed: () -> Unit) { +fun DeletionDialog( + @StringRes + titleRes: Int, + @StringRes + confirmationQuestionRes: Int, + onDismiss: () -> Unit, + onConfirmed: () -> Unit) { AlertDialog( icon = { Icon( imageVector = Icons.Rounded.Delete, - contentDescription = stringResource(title), + contentDescription = stringResource(titleRes), modifier = Modifier.size(48.dp), tint = MaterialTheme.colorScheme.error ) }, title = { - Text(text = stringResource(title)) + Text(text = stringResource(titleRes)) }, text = { - Text(text = stringResource(confirmationQuestion)) + Text(text = stringResource(confirmationQuestionRes)) }, onDismissRequest = { onDismiss() From 34ba9c4b9d1a154b0c695356a7402a7db717a5e7 Mon Sep 17 00:00:00 2001 From: Christian Nagel Date: Thu, 12 Mar 2026 23:35:38 +0100 Subject: [PATCH 4/7] Rename CustomScannerDao function getByScanId to getById --- .../chrisimx/scanbridge/data/ui/CustomScannerViewModel.kt | 2 +- .../io/github/chrisimx/scanbridge/db/daos/CustomScannerDao.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/io/github/chrisimx/scanbridge/data/ui/CustomScannerViewModel.kt b/app/src/main/java/io/github/chrisimx/scanbridge/data/ui/CustomScannerViewModel.kt index bcce836c..5e0d4ecf 100644 --- a/app/src/main/java/io/github/chrisimx/scanbridge/data/ui/CustomScannerViewModel.kt +++ b/app/src/main/java/io/github/chrisimx/scanbridge/data/ui/CustomScannerViewModel.kt @@ -43,7 +43,7 @@ class CustomScannerViewModel(application: Application, appDb: ScanBridgeDb) : An } } - suspend fun loadScannerByUuid(scanner: Uuid): CustomScanner? = customScannerDao.getByScanId(scanner) + suspend fun loadScannerByUuid(scanner: Uuid): CustomScanner? = customScannerDao.getById(scanner) @OptIn(ExperimentalUuidApi::class) fun deleteScanner(scanner: CustomScanner) { diff --git a/app/src/main/java/io/github/chrisimx/scanbridge/db/daos/CustomScannerDao.kt b/app/src/main/java/io/github/chrisimx/scanbridge/db/daos/CustomScannerDao.kt index b3c4b264..08e92fdd 100644 --- a/app/src/main/java/io/github/chrisimx/scanbridge/db/daos/CustomScannerDao.kt +++ b/app/src/main/java/io/github/chrisimx/scanbridge/db/daos/CustomScannerDao.kt @@ -21,7 +21,7 @@ interface CustomScannerDao { suspend fun getAll(): List @Query("SELECT * FROM customscanners WHERE uuid = :id") - suspend fun getByScanId(id: Uuid): CustomScanner? + suspend fun getById(id: Uuid): CustomScanner? @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertAll(vararg scanners: CustomScanner) From d05b00283604ad0254463f0ffd7c2cd09d2d631f Mon Sep 17 00:00:00 2001 From: Christian Nagel Date: Thu, 12 Mar 2026 23:36:54 +0100 Subject: [PATCH 5/7] Improve content description for custom scanner edit button --- .../github/chrisimx/scanbridge/uicomponents/FoundScannerItem.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/io/github/chrisimx/scanbridge/uicomponents/FoundScannerItem.kt b/app/src/main/java/io/github/chrisimx/scanbridge/uicomponents/FoundScannerItem.kt index bdad1e50..74b2ab82 100644 --- a/app/src/main/java/io/github/chrisimx/scanbridge/uicomponents/FoundScannerItem.kt +++ b/app/src/main/java/io/github/chrisimx/scanbridge/uicomponents/FoundScannerItem.kt @@ -108,7 +108,7 @@ fun FoundScannerItem( Icon( painter = painterResource(R.drawable.outline_edit_24), tint = MaterialTheme.colorScheme.surfaceTint, - contentDescription = stringResource(id = R.string.custom) + contentDescription = stringResource(id = R.string.edit_custom_scanner) ) } IconButton( From 1ebf0f78b7a3f4d708d281eb215bb05e4f0d696a Mon Sep 17 00:00:00 2001 From: Christian Nagel Date: Thu, 12 Mar 2026 23:40:18 +0100 Subject: [PATCH 6/7] Remove non-null assertation in FoundScannerItem.kt --- .../chrisimx/scanbridge/uicomponents/FoundScannerItem.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/io/github/chrisimx/scanbridge/uicomponents/FoundScannerItem.kt b/app/src/main/java/io/github/chrisimx/scanbridge/uicomponents/FoundScannerItem.kt index 74b2ab82..ec00c812 100644 --- a/app/src/main/java/io/github/chrisimx/scanbridge/uicomponents/FoundScannerItem.kt +++ b/app/src/main/java/io/github/chrisimx/scanbridge/uicomponents/FoundScannerItem.kt @@ -97,11 +97,11 @@ fun FoundScannerItem( style = MaterialTheme.typography.labelLarge ) } - if (deleteScanner != null) { + if (deleteScanner != null && editScanner != null) { IconButton( modifier = Modifier.padding(start = 8.dp, top = 8.dp, bottom = 8.dp), onClick = { - editScanner!!.invoke() + editScanner.invoke() Timber.i("Edit button clicked for custom scanner: $name at $address") } ) { From bbd56d0240996086105f230740efe08089a83f32 Mon Sep 17 00:00:00 2001 From: Christian Nagel Date: Thu, 12 Mar 2026 23:43:09 +0100 Subject: [PATCH 7/7] Apply format --- .../chrisimx/scanbridge/uicomponents/dialog/DeleteDialog.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/io/github/chrisimx/scanbridge/uicomponents/dialog/DeleteDialog.kt b/app/src/main/java/io/github/chrisimx/scanbridge/uicomponents/dialog/DeleteDialog.kt index 6dcd6813..9447d729 100644 --- a/app/src/main/java/io/github/chrisimx/scanbridge/uicomponents/dialog/DeleteDialog.kt +++ b/app/src/main/java/io/github/chrisimx/scanbridge/uicomponents/dialog/DeleteDialog.kt @@ -42,7 +42,8 @@ fun DeletionDialog( @StringRes confirmationQuestionRes: Int, onDismiss: () -> Unit, - onConfirmed: () -> Unit) { + onConfirmed: () -> Unit +) { AlertDialog( icon = { Icon(