diff --git a/USE_CASES.md b/USE_CASES.md index 387bd84..368839a 100644 --- a/USE_CASES.md +++ b/USE_CASES.md @@ -1,5 +1,10 @@ # Clean Scanner Use Cases +## Use-Case Views +- [Done] Each use case has an individual view profile that shows only relevant functions. +- [Done] Default profile is **Everyday Personal Use**. +- [Done] Other profiles can be selected in **Settings**. + ## 1. Everyday Personal Use - [Done] Scan restaurant menus, product QR labels, and website links quickly. - [Done] Copy/share scanned values to chat apps or notes. @@ -7,6 +12,7 @@ ## 2. Event & Ticketing - [Done] Scan tickets at venues and quickly validate repeated entries. +- [Done] Enable **Stapelmodus (Batch Mode)** by default in this view for fast check-in flow. - [Done] Use batch mode to process multiple attendees without leaving the camera. - [Done] Share batch captures to organizers for quick reconciliation. diff --git a/app/src/main/java/com/clean/scanner/settings/SettingsRepository.kt b/app/src/main/java/com/clean/scanner/settings/SettingsRepository.kt index 2d2dcd5..e7f5a10 100644 --- a/app/src/main/java/com/clean/scanner/settings/SettingsRepository.kt +++ b/app/src/main/java/com/clean/scanner/settings/SettingsRepository.kt @@ -2,8 +2,10 @@ package com.clean.scanner.settings import android.content.Context import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.preferencesDataStore +import com.clean.scanner.ui.UseCaseView import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -14,6 +16,7 @@ class SettingsRepository(private val context: Context) { val historyEnabled = booleanPreferencesKey("history_enabled") val warningsEnabled = booleanPreferencesKey("warnings_enabled") val scanFeedbackEnabled = booleanPreferencesKey("scan_feedback_enabled") + val useCaseView = stringPreferencesKey("use_case_view") } val historyEnabled: Flow = context.dataStore.data.map { prefs -> @@ -28,6 +31,10 @@ class SettingsRepository(private val context: Context) { prefs[Keys.scanFeedbackEnabled] ?: true } + val useCaseView: Flow = context.dataStore.data.map { prefs -> + UseCaseView.fromStorageKey(prefs[Keys.useCaseView]) + } + suspend fun setHistoryEnabled(enabled: Boolean) { context.dataStore.edit { it[Keys.historyEnabled] = enabled } } @@ -39,4 +46,8 @@ class SettingsRepository(private val context: Context) { suspend fun setScanFeedbackEnabled(enabled: Boolean) { context.dataStore.edit { it[Keys.scanFeedbackEnabled] = enabled } } + + suspend fun setUseCaseView(useCaseView: UseCaseView) { + context.dataStore.edit { it[Keys.useCaseView] = useCaseView.storageKey } + } } diff --git a/app/src/main/java/com/clean/scanner/ui/AppViewModel.kt b/app/src/main/java/com/clean/scanner/ui/AppViewModel.kt index 5827a32..ffb5dec 100644 --- a/app/src/main/java/com/clean/scanner/ui/AppViewModel.kt +++ b/app/src/main/java/com/clean/scanner/ui/AppViewModel.kt @@ -16,6 +16,7 @@ data class AppUiState( val historyEnabled: Boolean = false, val warningsEnabled: Boolean = true, val scanFeedbackEnabled: Boolean = true, + val useCaseView: UseCaseView = UseCaseView.default, val history: List = emptyList(), val searchQuery: String = "" ) @@ -24,19 +25,39 @@ class AppViewModel( private val container: AppContainer ) : ViewModel() { + private data class SettingsState( + val historyEnabled: Boolean, + val warningsEnabled: Boolean, + val scanFeedbackEnabled: Boolean, + val useCaseView: UseCaseView + ) + private val query = MutableStateFlow("") - val uiState: StateFlow = combine( + private val settingsState = combine( container.settingsRepository.historyEnabled, container.settingsRepository.warningsEnabled, container.settingsRepository.scanFeedbackEnabled, - container.scanRepository.observeHistory(), - query - ) { historyEnabled, warningsEnabled, scanFeedbackEnabled, history, q -> - AppUiState( + container.settingsRepository.useCaseView + ) { historyEnabled, warningsEnabled, scanFeedbackEnabled, useCaseView -> + SettingsState( historyEnabled = historyEnabled, warningsEnabled = warningsEnabled, scanFeedbackEnabled = scanFeedbackEnabled, + useCaseView = useCaseView + ) + } + + val uiState: StateFlow = combine( + settingsState, + container.scanRepository.observeHistory(), + query + ) { settings, history, q -> + AppUiState( + historyEnabled = settings.historyEnabled, + warningsEnabled = settings.warningsEnabled, + scanFeedbackEnabled = settings.scanFeedbackEnabled, + useCaseView = settings.useCaseView, history = if (q.isBlank()) history else history.filter { it.content.contains(q, ignoreCase = true) || it.type.contains(q, ignoreCase = true) }, @@ -69,6 +90,12 @@ class AppViewModel( } } + fun setUseCaseView(useCaseView: UseCaseView) { + viewModelScope.launch { + container.settingsRepository.setUseCaseView(useCaseView) + } + } + fun deleteHistoryItem(id: Long) { viewModelScope.launch { container.scanRepository.deleteById(id) diff --git a/app/src/main/java/com/clean/scanner/ui/CleanScannerAppRoot.kt b/app/src/main/java/com/clean/scanner/ui/CleanScannerAppRoot.kt index 155763d..268945c 100644 --- a/app/src/main/java/com/clean/scanner/ui/CleanScannerAppRoot.kt +++ b/app/src/main/java/com/clean/scanner/ui/CleanScannerAppRoot.kt @@ -74,6 +74,7 @@ fun CleanScannerAppRoot(container: AppContainer) { scanFeedbackNonce = scannerState.scanFeedbackNonce, warningsEnabled = appState.warningsEnabled, scanFeedbackEnabled = appState.scanFeedbackEnabled, + useCaseView = appState.useCaseView, onScan = scannerViewModel::onScan, onScanAgain = scannerViewModel::resumeScanning, onBatchModeChange = scannerViewModel::setBatchMode, @@ -84,6 +85,7 @@ fun CleanScannerAppRoot(container: AppContainer) { RootTab.History -> HistoryScreen( query = appState.searchQuery, history = appState.history, + useCaseView = appState.useCaseView, onQueryChange = appViewModel::setQuery, onDelete = appViewModel::deleteHistoryItem, onClearAll = appViewModel::clearHistory @@ -93,9 +95,11 @@ fun CleanScannerAppRoot(container: AppContainer) { historyEnabled = appState.historyEnabled, warningsEnabled = appState.warningsEnabled, scanFeedbackEnabled = appState.scanFeedbackEnabled, + selectedUseCaseView = appState.useCaseView, onHistoryToggle = appViewModel::setHistoryEnabled, onWarningsToggle = appViewModel::setWarningsEnabled, - onScanFeedbackToggle = appViewModel::setScanFeedbackEnabled + onScanFeedbackToggle = appViewModel::setScanFeedbackEnabled, + onUseCaseViewSelected = appViewModel::setUseCaseView ) } } diff --git a/app/src/main/java/com/clean/scanner/ui/UseCaseView.kt b/app/src/main/java/com/clean/scanner/ui/UseCaseView.kt new file mode 100644 index 0000000..9e28431 --- /dev/null +++ b/app/src/main/java/com/clean/scanner/ui/UseCaseView.kt @@ -0,0 +1,174 @@ +package com.clean.scanner.ui + +import com.clean.scanner.R + +enum class UseCaseView( + val storageKey: String, + val titleRes: Int +) { + EverydayPersonal( + storageKey = "everyday_personal", + titleRes = R.string.use_case_everyday_personal + ), + EventTicketing( + storageKey = "event_ticketing", + titleRes = R.string.use_case_event_ticketing + ), + InventoryOperations( + storageKey = "inventory_operations", + titleRes = R.string.use_case_inventory_operations + ), + FieldWorkServiceTeams( + storageKey = "field_work_service_teams", + titleRes = R.string.use_case_field_work + ), + OfficeAdmin( + storageKey = "office_admin", + titleRes = R.string.use_case_office_admin + ), + CommunicationShortcuts( + storageKey = "communication_shortcuts", + titleRes = R.string.use_case_communication_shortcuts + ), + SecurityBrowsing( + storageKey = "security_browsing", + titleRes = R.string.use_case_security_browsing + ), + OfflineLowConnectivity( + storageKey = "offline_low_connectivity", + titleRes = R.string.use_case_offline_low_connectivity + ), + AccessibilitySpeed( + storageKey = "accessibility_speed", + titleRes = R.string.use_case_accessibility_speed + ), + TeamHandoverTransfer( + storageKey = "team_handover_transfer", + titleRes = R.string.use_case_team_handover_transfer + ); + + companion object { + val default = EverydayPersonal + + fun fromStorageKey(value: String?): UseCaseView { + return entries.firstOrNull { it.storageKey == value } ?: default + } + } +} + +data class UseCaseCapabilities( + val allowScanFromImage: Boolean = true, + val allowBatchMode: Boolean = false, + val allowCopy: Boolean = true, + val allowShare: Boolean = true, + val allowOpenUrl: Boolean = true, + val allowAddContact: Boolean = false, + val allowDialPhone: Boolean = false, + val allowSendSms: Boolean = false, + val allowSendEmail: Boolean = false, + val allowOpenWifiSettings: Boolean = false, + val allowAddCalendarEvent: Boolean = false, + val allowHistoryExport: Boolean = false, + val allowBatchShare: Boolean = true +) + +fun UseCaseView.capabilities(): UseCaseCapabilities { + return when (this) { + UseCaseView.EverydayPersonal -> UseCaseCapabilities( + allowScanFromImage = true, + allowBatchMode = false, + allowCopy = true, + allowShare = true, + allowOpenUrl = true + ) + + UseCaseView.EventTicketing -> UseCaseCapabilities( + allowScanFromImage = false, + allowBatchMode = true, + allowCopy = true, + allowShare = true, + allowOpenUrl = false, + allowBatchShare = true + ) + + UseCaseView.InventoryOperations -> UseCaseCapabilities( + allowScanFromImage = true, + allowBatchMode = true, + allowCopy = true, + allowShare = true, + allowOpenUrl = false, + allowHistoryExport = true, + allowBatchShare = true + ) + + UseCaseView.FieldWorkServiceTeams -> UseCaseCapabilities( + allowScanFromImage = true, + allowBatchMode = true, + allowCopy = true, + allowShare = true, + allowOpenUrl = true, + allowHistoryExport = true, + allowBatchShare = true + ) + + UseCaseView.OfficeAdmin -> UseCaseCapabilities( + allowScanFromImage = true, + allowBatchMode = false, + allowCopy = true, + allowShare = true, + allowOpenUrl = true, + allowAddContact = true, + allowDialPhone = true, + allowSendSms = true, + allowSendEmail = true, + allowOpenWifiSettings = true, + allowAddCalendarEvent = true + ) + + UseCaseView.CommunicationShortcuts -> UseCaseCapabilities( + allowScanFromImage = true, + allowBatchMode = false, + allowCopy = true, + allowShare = true, + allowOpenUrl = false, + allowDialPhone = true, + allowSendSms = true, + allowSendEmail = true + ) + + UseCaseView.SecurityBrowsing -> UseCaseCapabilities( + allowScanFromImage = true, + allowBatchMode = false, + allowCopy = true, + allowShare = true, + allowOpenUrl = true + ) + + UseCaseView.OfflineLowConnectivity -> UseCaseCapabilities( + allowScanFromImage = true, + allowBatchMode = true, + allowCopy = true, + allowShare = true, + allowOpenUrl = false, + allowBatchShare = true + ) + + UseCaseView.AccessibilitySpeed -> UseCaseCapabilities( + allowScanFromImage = true, + allowBatchMode = false, + allowCopy = true, + allowShare = true, + allowOpenUrl = true + ) + + UseCaseView.TeamHandoverTransfer -> UseCaseCapabilities( + allowScanFromImage = true, + allowBatchMode = true, + allowCopy = true, + allowShare = true, + allowOpenUrl = false, + allowHistoryExport = true, + allowBatchShare = true + ) + } +} diff --git a/app/src/main/java/com/clean/scanner/ui/screens/HistoryScreen.kt b/app/src/main/java/com/clean/scanner/ui/screens/HistoryScreen.kt index 4cfe656..1560114 100644 --- a/app/src/main/java/com/clean/scanner/ui/screens/HistoryScreen.kt +++ b/app/src/main/java/com/clean/scanner/ui/screens/HistoryScreen.kt @@ -26,6 +26,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.clean.scanner.R import com.clean.scanner.domain.ScanRecord +import com.clean.scanner.ui.UseCaseView +import com.clean.scanner.ui.capabilities import com.clean.scanner.util.HistoryExportFormatter import com.clean.scanner.util.Intents import java.text.DateFormat @@ -35,11 +37,13 @@ import java.util.Date fun HistoryScreen( query: String, history: List, + useCaseView: UseCaseView, onQueryChange: (String) -> Unit, onDelete: (Long) -> Unit, onClearAll: () -> Unit ) { val context = LocalContext.current + val capabilities = useCaseView.capabilities() val showDeleteAll = remember { mutableStateOf(false) } val selectedItem = remember { mutableStateOf(null) } @@ -90,32 +94,34 @@ fun HistoryScreen( ) Row(modifier = Modifier.fillMaxWidth()) { - TextButton( - onClick = { - val exportText = HistoryExportFormatter.formatText(history) - Intents.shareContent(context, exportText, "text/plain") - }, - enabled = history.isNotEmpty() - ) { - Text(stringResource(R.string.share_txt)) - } - TextButton( - onClick = { - val exportCsv = HistoryExportFormatter.formatCsv(history) - Intents.shareContent(context, exportCsv, "text/csv") - }, - enabled = history.isNotEmpty() - ) { - Text(stringResource(R.string.share_csv)) - } - TextButton( - onClick = { - val exportJson = HistoryExportFormatter.formatJson(history) - Intents.shareContent(context, exportJson, "application/json") - }, - enabled = history.isNotEmpty() - ) { - Text(stringResource(R.string.share_json)) + if (capabilities.allowHistoryExport) { + TextButton( + onClick = { + val exportText = HistoryExportFormatter.formatText(history) + Intents.shareContent(context, exportText, "text/plain") + }, + enabled = history.isNotEmpty() + ) { + Text(stringResource(R.string.share_txt)) + } + TextButton( + onClick = { + val exportCsv = HistoryExportFormatter.formatCsv(history) + Intents.shareContent(context, exportCsv, "text/csv") + }, + enabled = history.isNotEmpty() + ) { + Text(stringResource(R.string.share_csv)) + } + TextButton( + onClick = { + val exportJson = HistoryExportFormatter.formatJson(history) + Intents.shareContent(context, exportJson, "application/json") + }, + enabled = history.isNotEmpty() + ) { + Text(stringResource(R.string.share_json)) + } } TextButton(onClick = { showDeleteAll.value = true }) { Text(stringResource(R.string.delete_all)) diff --git a/app/src/main/java/com/clean/scanner/ui/screens/ScannerOverlayComponents.kt b/app/src/main/java/com/clean/scanner/ui/screens/ScannerOverlayComponents.kt index 2fac0ee..3e1e8f0 100644 --- a/app/src/main/java/com/clean/scanner/ui/screens/ScannerOverlayComponents.kt +++ b/app/src/main/java/com/clean/scanner/ui/screens/ScannerOverlayComponents.kt @@ -75,7 +75,8 @@ internal fun OverlayIconToggle( @Composable internal fun BatchResultsPanel( results: List, - onClear: () -> Unit + onClear: () -> Unit, + allowShare: Boolean ) { val context = LocalContext.current val timeFormat = remember { DateFormat.getTimeInstance(DateFormat.SHORT) } @@ -125,12 +126,14 @@ internal fun BatchResultsPanel( tint = Color.White ) } - IconButton(onClick = { Intents.shareText(context, item.result.content) }) { - Icon( - imageVector = Icons.Default.Share, - contentDescription = stringResource(R.string.share), - tint = Color.White - ) + if (allowShare) { + IconButton(onClick = { Intents.shareText(context, item.result.content) }) { + Icon( + imageVector = Icons.Default.Share, + contentDescription = stringResource(R.string.share), + tint = Color.White + ) + } } } } @@ -139,11 +142,13 @@ internal fun BatchResultsPanel( TextButton(onClick = onClear, enabled = results.isNotEmpty()) { Text(stringResource(R.string.clear_batch)) } - TextButton( - onClick = { Intents.shareText(context, buildBatchExport(results)) }, - enabled = results.isNotEmpty() - ) { - Text(stringResource(R.string.share_batch)) + if (allowShare) { + TextButton( + onClick = { Intents.shareText(context, buildBatchExport(results)) }, + enabled = results.isNotEmpty() + ) { + Text(stringResource(R.string.share_batch)) + } } } } diff --git a/app/src/main/java/com/clean/scanner/ui/screens/ScannerResultCards.kt b/app/src/main/java/com/clean/scanner/ui/screens/ScannerResultCards.kt index c590045..58a2047 100644 --- a/app/src/main/java/com/clean/scanner/ui/screens/ScannerResultCards.kt +++ b/app/src/main/java/com/clean/scanner/ui/screens/ScannerResultCards.kt @@ -1,5 +1,6 @@ package com.clean.scanner.ui.screens +import androidx.compose.foundation.clickable import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -21,6 +22,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.clean.scanner.domain.ScanResult @@ -38,6 +40,7 @@ private data class ResultField( @Composable internal fun ResultVisualCard( result: ScanResult, + onOpenUrl: ((String) -> Unit)? = null, modifier: Modifier = Modifier ) { val contact = remember(result.content) { ScanContentParsers.parseContact(result.content) } @@ -94,9 +97,19 @@ internal fun ResultVisualCard( style = MaterialTheme.typography.labelMedium, color = Color(0xFF4F6277) ) + val isClickableUrl = result.type == "URL" && + field.label == "Link" && + onOpenUrl != null Text( text = field.value, style = MaterialTheme.typography.bodyMedium, + color = if (isClickableUrl) Color(0xFF1D4ED8) else Color.Unspecified, + textDecoration = if (isClickableUrl) TextDecoration.Underline else null, + modifier = if (isClickableUrl) { + Modifier.clickable { onOpenUrl(field.value) } + } else { + Modifier + }, maxLines = 3, overflow = TextOverflow.Ellipsis ) diff --git a/app/src/main/java/com/clean/scanner/ui/screens/ScannerScreen.kt b/app/src/main/java/com/clean/scanner/ui/screens/ScannerScreen.kt index 4596607..b0b4687 100644 --- a/app/src/main/java/com/clean/scanner/ui/screens/ScannerScreen.kt +++ b/app/src/main/java/com/clean/scanner/ui/screens/ScannerScreen.kt @@ -75,7 +75,9 @@ import com.clean.scanner.data.scanner.DetectionBox import com.clean.scanner.data.scanner.DetectionPoint import com.clean.scanner.domain.ScanResult import com.clean.scanner.ui.BatchScanRecord +import com.clean.scanner.ui.UseCaseView import com.clean.scanner.ui.components.CameraPreview +import com.clean.scanner.ui.capabilities import com.clean.scanner.util.ClipboardUtil import com.clean.scanner.util.Intents import com.clean.scanner.util.ScanContentParsers @@ -103,6 +105,7 @@ fun ScannerScreen( scanFeedbackNonce: Int, warningsEnabled: Boolean, scanFeedbackEnabled: Boolean, + useCaseView: UseCaseView, onScan: (ScanResult) -> Unit, onScanAgain: () -> Unit, onBatchModeChange: (Boolean) -> Unit, @@ -111,9 +114,26 @@ fun ScannerScreen( ) { val context = LocalContext.current val haptic = LocalHapticFeedback.current + val capabilities = remember(useCaseView) { useCaseView.capabilities() } + val showBatchModeToggle = remember(useCaseView) { + when (useCaseView) { + UseCaseView.EventTicketing, + UseCaseView.InventoryOperations, + UseCaseView.FieldWorkServiceTeams, + UseCaseView.OfflineLowConnectivity, + UseCaseView.TeamHandoverTransfer -> true + else -> false + } + } val duplicateSnackbarHostState = remember { SnackbarHostState() } val toneGenerator = remember { ToneGenerator(AudioManager.STREAM_NOTIFICATION, 70) } + LaunchedEffect(showBatchModeToggle, batchMode) { + if (!showBatchModeToggle && batchMode) { + onBatchModeChange(false) + } + } + var cameraGranted by remember { mutableStateOf( ContextCompat.checkSelfPermission( @@ -382,22 +402,38 @@ fun ScannerScreen( .padding(horizontal = 14.dp, vertical = 8.dp) ) - IconButton( - onClick = { imagePicker.launch("image/*") }, + if (capabilities.allowScanFromImage) { + IconButton( + onClick = { imagePicker.launch("image/*") }, + modifier = Modifier + .align(Alignment.TopEnd) + .padding(top = 12.dp, end = 12.dp) + .background( + color = Color.Black.copy(alpha = 0.35f), + shape = RoundedCornerShape(14.dp) + ) + ) { + Icon( + painter = painterResource(id = android.R.drawable.ic_menu_gallery), + contentDescription = stringResource(R.string.scan_from_image), + tint = Color.White + ) + } + } + + Text( + text = stringResource(useCaseView.titleRes), + color = Color.White, + textAlign = TextAlign.Center, modifier = Modifier - .align(Alignment.TopEnd) - .padding(top = 12.dp, end = 12.dp) + .align(Alignment.TopCenter) + .padding(top = 16.dp, start = 64.dp, end = 64.dp) .background( - color = Color.Black.copy(alpha = 0.35f), + color = Color.Black.copy(alpha = 0.4f), shape = RoundedCornerShape(14.dp) ) - ) { - Icon( - painter = painterResource(id = android.R.drawable.ic_menu_gallery), - contentDescription = stringResource(R.string.scan_from_image), - tint = Color.White - ) - } + .padding(horizontal = 12.dp, vertical = 6.dp) + ) Column( modifier = Modifier @@ -415,20 +451,23 @@ fun ScannerScreen( showLabel = false ) } - OverlayIconToggle( - checked = batchMode, - onCheckedChange = onBatchModeChange, - label = stringResource(R.string.batch_mode), - checkedImageVector = Icons.Default.ViewModule, - uncheckedImageVector = Icons.AutoMirrored.Filled.ViewList - ) + if (showBatchModeToggle) { + OverlayIconToggle( + checked = batchMode, + onCheckedChange = onBatchModeChange, + label = stringResource(R.string.batch_mode), + checkedImageVector = Icons.Default.ViewModule, + uncheckedImageVector = Icons.AutoMirrored.Filled.ViewList + ) + } } - if (batchMode) { + if (batchMode && showBatchModeToggle) { Box(modifier = Modifier.align(Alignment.BottomCenter)) { BatchResultsPanel( results = batchResults, - onClear = onClearBatchResults + onClear = onClearBatchResults, + allowShare = capabilities.allowBatchShare ) } } @@ -470,91 +509,106 @@ fun ScannerScreen( .padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { - if (parsedContact == null && lastResult.type != "WiFi") { - Text(text = "${stringResource(R.string.content_type)}: ${lastResult.type}") - } - ResultVisualCard(result = lastResult) - Row( - modifier = Modifier - .fillMaxWidth() - .horizontalScroll(rememberScrollState()), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - if (parsedContact != null) { - IconButton(onClick = { - Intents.addContact(context, parsedContact, lastResult.content) - }) { - Icon( - imageVector = Icons.Default.PersonAdd, - contentDescription = stringResource(R.string.add_contact) - ) + ResultVisualCard( + result = lastResult, + onOpenUrl = { url -> + val risk = UrlRiskScorer.score(url) + val risky = warningsEnabled && risk.score >= 3 + if (risky) { + pendingOpenUrl = url + showRiskWarning = true + } else { + Intents.openUrl(context, url) } } - IconButton(onClick = { ClipboardUtil.copy(context, lastResult.content) }) { - Icon( - imageVector = Icons.Default.ContentCopy, - contentDescription = stringResource(R.string.copy) - ) - } - if (lastResult.type == "URL") { - Button(onClick = { - val risk = UrlRiskScorer.score(lastResult.content) - val risky = warningsEnabled && risk.score >= 3 - if (risky) { - pendingOpenUrl = lastResult.content - showRiskWarning = true - } else { - Intents.openUrl(context, lastResult.content) + ) + val hasQuickActions = capabilities.allowCopy || + capabilities.allowShare || + (capabilities.allowAddContact && parsedContact != null) + + if (hasQuickActions) { + Row( + modifier = Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + if (capabilities.allowAddContact && parsedContact != null) { + IconButton(onClick = { + Intents.addContact(context, parsedContact, lastResult.content) + }) { + Icon( + imageVector = Icons.Default.PersonAdd, + contentDescription = stringResource(R.string.add_contact) + ) + } + } + if (capabilities.allowCopy) { + IconButton(onClick = { ClipboardUtil.copy(context, lastResult.content) }) { + Icon( + imageVector = Icons.Default.ContentCopy, + contentDescription = stringResource(R.string.copy) + ) + } + } + if (capabilities.allowShare) { + IconButton(onClick = { Intents.shareText(context, lastResult.content) }) { + Icon( + imageVector = Icons.Default.Share, + contentDescription = stringResource(R.string.share) + ) } - }) { - Text(stringResource(R.string.open)) } - } - IconButton(onClick = { Intents.shareText(context, lastResult.content) }) { - Icon( - imageVector = Icons.Default.Share, - contentDescription = stringResource(R.string.share) - ) } } when (lastResult.type) { "Phone" -> { - Button(onClick = { - Intents.dialPhone(context, ScanContentParsers.extractPhoneNumber(lastResult.content)) - }) { - Text(stringResource(R.string.call_number)) + if (capabilities.allowDialPhone) { + Button(onClick = { + Intents.dialPhone(context, ScanContentParsers.extractPhoneNumber(lastResult.content)) + }) { + Text(stringResource(R.string.call_number)) + } } } "SMS" -> { - Button(onClick = { - val smsData = ScanContentParsers.parseSms(lastResult.content) - Intents.sendSms(context, smsData.first, smsData.second) - }) { - Text(stringResource(R.string.send_sms)) + if (capabilities.allowSendSms) { + Button(onClick = { + val smsData = ScanContentParsers.parseSms(lastResult.content) + Intents.sendSms(context, smsData.first, smsData.second) + }) { + Text(stringResource(R.string.send_sms)) + } } } "Email" -> { - Button(onClick = { - Intents.sendEmail(context, ScanContentParsers.extractEmail(lastResult.content), null) - }) { - Text(stringResource(R.string.send_email)) + if (capabilities.allowSendEmail) { + Button(onClick = { + Intents.sendEmail(context, ScanContentParsers.extractEmail(lastResult.content), null) + }) { + Text(stringResource(R.string.send_email)) + } } } "WiFi" -> { - Button(onClick = { Intents.openWifiSettings(context) }) { - Text(stringResource(R.string.open_wifi_settings)) + if (capabilities.allowOpenWifiSettings) { + Button(onClick = { Intents.openWifiSettings(context) }) { + Text(stringResource(R.string.open_wifi_settings)) + } } } "Calendar" -> { - Button(onClick = { - Intents.addCalendarEvent(context, parsedEvent, lastResult.content) - }) { - Text(stringResource(R.string.add_calendar_event)) + if (capabilities.allowAddCalendarEvent) { + Button(onClick = { + Intents.addCalendarEvent(context, parsedEvent, lastResult.content) + }) { + Text(stringResource(R.string.add_calendar_event)) + } } } } diff --git a/app/src/main/java/com/clean/scanner/ui/screens/SettingsScreen.kt b/app/src/main/java/com/clean/scanner/ui/screens/SettingsScreen.kt index 479c792..d8fabed 100644 --- a/app/src/main/java/com/clean/scanner/ui/screens/SettingsScreen.kt +++ b/app/src/main/java/com/clean/scanner/ui/screens/SettingsScreen.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material3.AlertDialog @@ -20,6 +21,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.clean.scanner.R +import com.clean.scanner.ui.UseCaseView import com.clean.scanner.util.Intents @Composable @@ -27,13 +29,16 @@ fun SettingsScreen( historyEnabled: Boolean, warningsEnabled: Boolean, scanFeedbackEnabled: Boolean, + selectedUseCaseView: UseCaseView, onHistoryToggle: (Boolean, Boolean) -> Unit, onWarningsToggle: (Boolean) -> Unit, - onScanFeedbackToggle: (Boolean) -> Unit + onScanFeedbackToggle: (Boolean) -> Unit, + onUseCaseViewSelected: (UseCaseView) -> Unit ) { val context = LocalContext.current val showDeleteConfirm = remember { mutableStateOf(false) } val showFeatureRequestForm = remember { mutableStateOf(false) } + val showUseCasePicker = remember { mutableStateOf(false) } val requesterNeed = remember { mutableStateOf("") } if (showDeleteConfirm.value) { @@ -101,6 +106,34 @@ fun SettingsScreen( ) } + if (showUseCasePicker.value) { + AlertDialog( + onDismissRequest = { showUseCasePicker.value = false }, + title = { Text(stringResource(R.string.select_use_case_view)) }, + text = { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + UseCaseView.entries.forEach { candidate -> + TextButton( + onClick = { + onUseCaseViewSelected(candidate) + showUseCasePicker.value = false + }, + modifier = Modifier.fillMaxWidth() + ) { + Text(text = stringResource(candidate.titleRes)) + } + } + } + }, + confirmButton = {}, + dismissButton = { + TextButton(onClick = { showUseCasePicker.value = false }) { + Text(stringResource(R.string.cancel)) + } + } + ) + } + Column( modifier = Modifier .fillMaxSize() @@ -129,6 +162,14 @@ fun SettingsScreen( Text(text = stringResource(R.string.scan_feedback)) Switch(checked = scanFeedbackEnabled, onCheckedChange = onScanFeedbackToggle) + Spacer(modifier = Modifier.height(16.dp)) + + Text(text = stringResource(R.string.active_use_case_view)) + Text(text = stringResource(selectedUseCaseView.titleRes)) + TextButton(onClick = { showUseCasePicker.value = true }) { + Text(stringResource(R.string.select_use_case_view)) + } + Spacer(modifier = Modifier.height(24.dp)) Text(text = stringResource(R.string.about)) Text(text = stringResource(R.string.version)) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index aafa5f7..15715ce 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -28,7 +28,7 @@ Vorhandene Historie beim Deaktivieren löschen? Version 1.0.0 Open-Source-Lizenzen - Kontakt: support@example.com + Kontakt: softwareapp.hb@gmail.com Typ Inhalt Kamera erlauben @@ -57,7 +57,7 @@ WLAN-Einstellungen öffnen Kontakt hinzufügen Kalendereintrag hinzufügen - support@example.com + softwareapp.hb@gmail.com Feature-Request-Formular Feature-Request Dein Name @@ -66,4 +66,16 @@ Feature-Request von App-Nutzer Anfrage senden E-Mail-App wird geöffnet... + Aktive Use-Case-Ansicht + Use-Case-Ansicht wählen + Alltägliche private Nutzung + Events & Ticketing + Inventur & Betrieb + Außendienst & Service-Teams + Büro- & Admin-Workflows + Kommunikations-Shortcuts + Sicherheitsbewusstes Browsen + Offline / geringe Konnektivität + Barrierefreiheit & Geschwindigkeit + Team-Übergabe & Datentransfer diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 708e628..2763dc0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -66,4 +66,16 @@ Feature request from app user Send request Opening email app... + Active use-case view + Select use-case view + Everyday personal use + Event & ticketing + Inventory & operations + Field work & service teams + Office & admin workflows + Communication shortcuts + Security-conscious browsing + Offline / low-connectivity + Accessibility & speed + Team handover & data transfer