removed batchmode from personal usecase

update ticketing and events view
This commit is contained in:
Hadrian Burkhardt
2026-03-03 14:13:25 +01:00
parent fb94b7214a
commit 3d7620954f
12 changed files with 493 additions and 128 deletions
+6
View File
@@ -1,5 +1,10 @@
# Clean Scanner Use Cases # 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 ## 1. Everyday Personal Use
- [Done] Scan restaurant menus, product QR labels, and website links quickly. - [Done] Scan restaurant menus, product QR labels, and website links quickly.
- [Done] Copy/share scanned values to chat apps or notes. - [Done] Copy/share scanned values to chat apps or notes.
@@ -7,6 +12,7 @@
## 2. Event & Ticketing ## 2. Event & Ticketing
- [Done] Scan tickets at venues and quickly validate repeated entries. - [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] Use batch mode to process multiple attendees without leaving the camera.
- [Done] Share batch captures to organizers for quick reconciliation. - [Done] Share batch captures to organizers for quick reconciliation.
@@ -2,8 +2,10 @@ package com.clean.scanner.settings
import android.content.Context import android.content.Context
import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.preferencesDataStore import androidx.datastore.preferences.preferencesDataStore
import com.clean.scanner.ui.UseCaseView
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@@ -14,6 +16,7 @@ class SettingsRepository(private val context: Context) {
val historyEnabled = booleanPreferencesKey("history_enabled") val historyEnabled = booleanPreferencesKey("history_enabled")
val warningsEnabled = booleanPreferencesKey("warnings_enabled") val warningsEnabled = booleanPreferencesKey("warnings_enabled")
val scanFeedbackEnabled = booleanPreferencesKey("scan_feedback_enabled") val scanFeedbackEnabled = booleanPreferencesKey("scan_feedback_enabled")
val useCaseView = stringPreferencesKey("use_case_view")
} }
val historyEnabled: Flow<Boolean> = context.dataStore.data.map { prefs -> val historyEnabled: Flow<Boolean> = context.dataStore.data.map { prefs ->
@@ -28,6 +31,10 @@ class SettingsRepository(private val context: Context) {
prefs[Keys.scanFeedbackEnabled] ?: true prefs[Keys.scanFeedbackEnabled] ?: true
} }
val useCaseView: Flow<UseCaseView> = context.dataStore.data.map { prefs ->
UseCaseView.fromStorageKey(prefs[Keys.useCaseView])
}
suspend fun setHistoryEnabled(enabled: Boolean) { suspend fun setHistoryEnabled(enabled: Boolean) {
context.dataStore.edit { it[Keys.historyEnabled] = enabled } context.dataStore.edit { it[Keys.historyEnabled] = enabled }
} }
@@ -39,4 +46,8 @@ class SettingsRepository(private val context: Context) {
suspend fun setScanFeedbackEnabled(enabled: Boolean) { suspend fun setScanFeedbackEnabled(enabled: Boolean) {
context.dataStore.edit { it[Keys.scanFeedbackEnabled] = enabled } context.dataStore.edit { it[Keys.scanFeedbackEnabled] = enabled }
} }
suspend fun setUseCaseView(useCaseView: UseCaseView) {
context.dataStore.edit { it[Keys.useCaseView] = useCaseView.storageKey }
}
} }
@@ -16,6 +16,7 @@ data class AppUiState(
val historyEnabled: Boolean = false, val historyEnabled: Boolean = false,
val warningsEnabled: Boolean = true, val warningsEnabled: Boolean = true,
val scanFeedbackEnabled: Boolean = true, val scanFeedbackEnabled: Boolean = true,
val useCaseView: UseCaseView = UseCaseView.default,
val history: List<ScanRecord> = emptyList(), val history: List<ScanRecord> = emptyList(),
val searchQuery: String = "" val searchQuery: String = ""
) )
@@ -24,19 +25,39 @@ class AppViewModel(
private val container: AppContainer private val container: AppContainer
) : ViewModel() { ) : ViewModel() {
private data class SettingsState(
val historyEnabled: Boolean,
val warningsEnabled: Boolean,
val scanFeedbackEnabled: Boolean,
val useCaseView: UseCaseView
)
private val query = MutableStateFlow("") private val query = MutableStateFlow("")
val uiState: StateFlow<AppUiState> = combine( private val settingsState = combine(
container.settingsRepository.historyEnabled, container.settingsRepository.historyEnabled,
container.settingsRepository.warningsEnabled, container.settingsRepository.warningsEnabled,
container.settingsRepository.scanFeedbackEnabled, container.settingsRepository.scanFeedbackEnabled,
container.scanRepository.observeHistory(), container.settingsRepository.useCaseView
query ) { historyEnabled, warningsEnabled, scanFeedbackEnabled, useCaseView ->
) { historyEnabled, warningsEnabled, scanFeedbackEnabled, history, q -> SettingsState(
AppUiState(
historyEnabled = historyEnabled, historyEnabled = historyEnabled,
warningsEnabled = warningsEnabled, warningsEnabled = warningsEnabled,
scanFeedbackEnabled = scanFeedbackEnabled, scanFeedbackEnabled = scanFeedbackEnabled,
useCaseView = useCaseView
)
}
val uiState: StateFlow<AppUiState> = 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 { history = if (q.isBlank()) history else history.filter {
it.content.contains(q, ignoreCase = true) || it.type.contains(q, ignoreCase = true) 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) { fun deleteHistoryItem(id: Long) {
viewModelScope.launch { viewModelScope.launch {
container.scanRepository.deleteById(id) container.scanRepository.deleteById(id)
@@ -74,6 +74,7 @@ fun CleanScannerAppRoot(container: AppContainer) {
scanFeedbackNonce = scannerState.scanFeedbackNonce, scanFeedbackNonce = scannerState.scanFeedbackNonce,
warningsEnabled = appState.warningsEnabled, warningsEnabled = appState.warningsEnabled,
scanFeedbackEnabled = appState.scanFeedbackEnabled, scanFeedbackEnabled = appState.scanFeedbackEnabled,
useCaseView = appState.useCaseView,
onScan = scannerViewModel::onScan, onScan = scannerViewModel::onScan,
onScanAgain = scannerViewModel::resumeScanning, onScanAgain = scannerViewModel::resumeScanning,
onBatchModeChange = scannerViewModel::setBatchMode, onBatchModeChange = scannerViewModel::setBatchMode,
@@ -84,6 +85,7 @@ fun CleanScannerAppRoot(container: AppContainer) {
RootTab.History -> HistoryScreen( RootTab.History -> HistoryScreen(
query = appState.searchQuery, query = appState.searchQuery,
history = appState.history, history = appState.history,
useCaseView = appState.useCaseView,
onQueryChange = appViewModel::setQuery, onQueryChange = appViewModel::setQuery,
onDelete = appViewModel::deleteHistoryItem, onDelete = appViewModel::deleteHistoryItem,
onClearAll = appViewModel::clearHistory onClearAll = appViewModel::clearHistory
@@ -93,9 +95,11 @@ fun CleanScannerAppRoot(container: AppContainer) {
historyEnabled = appState.historyEnabled, historyEnabled = appState.historyEnabled,
warningsEnabled = appState.warningsEnabled, warningsEnabled = appState.warningsEnabled,
scanFeedbackEnabled = appState.scanFeedbackEnabled, scanFeedbackEnabled = appState.scanFeedbackEnabled,
selectedUseCaseView = appState.useCaseView,
onHistoryToggle = appViewModel::setHistoryEnabled, onHistoryToggle = appViewModel::setHistoryEnabled,
onWarningsToggle = appViewModel::setWarningsEnabled, onWarningsToggle = appViewModel::setWarningsEnabled,
onScanFeedbackToggle = appViewModel::setScanFeedbackEnabled onScanFeedbackToggle = appViewModel::setScanFeedbackEnabled,
onUseCaseViewSelected = appViewModel::setUseCaseView
) )
} }
} }
@@ -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
)
}
}
@@ -26,6 +26,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.clean.scanner.R import com.clean.scanner.R
import com.clean.scanner.domain.ScanRecord 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.HistoryExportFormatter
import com.clean.scanner.util.Intents import com.clean.scanner.util.Intents
import java.text.DateFormat import java.text.DateFormat
@@ -35,11 +37,13 @@ import java.util.Date
fun HistoryScreen( fun HistoryScreen(
query: String, query: String,
history: List<ScanRecord>, history: List<ScanRecord>,
useCaseView: UseCaseView,
onQueryChange: (String) -> Unit, onQueryChange: (String) -> Unit,
onDelete: (Long) -> Unit, onDelete: (Long) -> Unit,
onClearAll: () -> Unit onClearAll: () -> Unit
) { ) {
val context = LocalContext.current val context = LocalContext.current
val capabilities = useCaseView.capabilities()
val showDeleteAll = remember { mutableStateOf(false) } val showDeleteAll = remember { mutableStateOf(false) }
val selectedItem = remember { mutableStateOf<ScanRecord?>(null) } val selectedItem = remember { mutableStateOf<ScanRecord?>(null) }
@@ -90,32 +94,34 @@ fun HistoryScreen(
) )
Row(modifier = Modifier.fillMaxWidth()) { Row(modifier = Modifier.fillMaxWidth()) {
TextButton( if (capabilities.allowHistoryExport) {
onClick = { TextButton(
val exportText = HistoryExportFormatter.formatText(history) onClick = {
Intents.shareContent(context, exportText, "text/plain") val exportText = HistoryExportFormatter.formatText(history)
}, Intents.shareContent(context, exportText, "text/plain")
enabled = history.isNotEmpty() },
) { enabled = history.isNotEmpty()
Text(stringResource(R.string.share_txt)) ) {
} Text(stringResource(R.string.share_txt))
TextButton( }
onClick = { TextButton(
val exportCsv = HistoryExportFormatter.formatCsv(history) onClick = {
Intents.shareContent(context, exportCsv, "text/csv") val exportCsv = HistoryExportFormatter.formatCsv(history)
}, Intents.shareContent(context, exportCsv, "text/csv")
enabled = history.isNotEmpty() },
) { enabled = history.isNotEmpty()
Text(stringResource(R.string.share_csv)) ) {
} Text(stringResource(R.string.share_csv))
TextButton( }
onClick = { TextButton(
val exportJson = HistoryExportFormatter.formatJson(history) onClick = {
Intents.shareContent(context, exportJson, "application/json") val exportJson = HistoryExportFormatter.formatJson(history)
}, Intents.shareContent(context, exportJson, "application/json")
enabled = history.isNotEmpty() },
) { enabled = history.isNotEmpty()
Text(stringResource(R.string.share_json)) ) {
Text(stringResource(R.string.share_json))
}
} }
TextButton(onClick = { showDeleteAll.value = true }) { TextButton(onClick = { showDeleteAll.value = true }) {
Text(stringResource(R.string.delete_all)) Text(stringResource(R.string.delete_all))
@@ -75,7 +75,8 @@ internal fun OverlayIconToggle(
@Composable @Composable
internal fun BatchResultsPanel( internal fun BatchResultsPanel(
results: List<BatchScanRecord>, results: List<BatchScanRecord>,
onClear: () -> Unit onClear: () -> Unit,
allowShare: Boolean
) { ) {
val context = LocalContext.current val context = LocalContext.current
val timeFormat = remember { DateFormat.getTimeInstance(DateFormat.SHORT) } val timeFormat = remember { DateFormat.getTimeInstance(DateFormat.SHORT) }
@@ -125,12 +126,14 @@ internal fun BatchResultsPanel(
tint = Color.White tint = Color.White
) )
} }
IconButton(onClick = { Intents.shareText(context, item.result.content) }) { if (allowShare) {
Icon( IconButton(onClick = { Intents.shareText(context, item.result.content) }) {
imageVector = Icons.Default.Share, Icon(
contentDescription = stringResource(R.string.share), imageVector = Icons.Default.Share,
tint = Color.White contentDescription = stringResource(R.string.share),
) tint = Color.White
)
}
} }
} }
} }
@@ -139,11 +142,13 @@ internal fun BatchResultsPanel(
TextButton(onClick = onClear, enabled = results.isNotEmpty()) { TextButton(onClick = onClear, enabled = results.isNotEmpty()) {
Text(stringResource(R.string.clear_batch)) Text(stringResource(R.string.clear_batch))
} }
TextButton( if (allowShare) {
onClick = { Intents.shareText(context, buildBatchExport(results)) }, TextButton(
enabled = results.isNotEmpty() onClick = { Intents.shareText(context, buildBatchExport(results)) },
) { enabled = results.isNotEmpty()
Text(stringResource(R.string.share_batch)) ) {
Text(stringResource(R.string.share_batch))
}
} }
} }
} }
@@ -1,5 +1,6 @@
package com.clean.scanner.ui.screens package com.clean.scanner.ui.screens
import androidx.compose.foundation.clickable
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@@ -21,6 +22,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.clean.scanner.domain.ScanResult import com.clean.scanner.domain.ScanResult
@@ -38,6 +40,7 @@ private data class ResultField(
@Composable @Composable
internal fun ResultVisualCard( internal fun ResultVisualCard(
result: ScanResult, result: ScanResult,
onOpenUrl: ((String) -> Unit)? = null,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val contact = remember(result.content) { ScanContentParsers.parseContact(result.content) } val contact = remember(result.content) { ScanContentParsers.parseContact(result.content) }
@@ -94,9 +97,19 @@ internal fun ResultVisualCard(
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
color = Color(0xFF4F6277) color = Color(0xFF4F6277)
) )
val isClickableUrl = result.type == "URL" &&
field.label == "Link" &&
onOpenUrl != null
Text( Text(
text = field.value, text = field.value,
style = MaterialTheme.typography.bodyMedium, 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, maxLines = 3,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
@@ -75,7 +75,9 @@ import com.clean.scanner.data.scanner.DetectionBox
import com.clean.scanner.data.scanner.DetectionPoint import com.clean.scanner.data.scanner.DetectionPoint
import com.clean.scanner.domain.ScanResult import com.clean.scanner.domain.ScanResult
import com.clean.scanner.ui.BatchScanRecord import com.clean.scanner.ui.BatchScanRecord
import com.clean.scanner.ui.UseCaseView
import com.clean.scanner.ui.components.CameraPreview import com.clean.scanner.ui.components.CameraPreview
import com.clean.scanner.ui.capabilities
import com.clean.scanner.util.ClipboardUtil import com.clean.scanner.util.ClipboardUtil
import com.clean.scanner.util.Intents import com.clean.scanner.util.Intents
import com.clean.scanner.util.ScanContentParsers import com.clean.scanner.util.ScanContentParsers
@@ -103,6 +105,7 @@ fun ScannerScreen(
scanFeedbackNonce: Int, scanFeedbackNonce: Int,
warningsEnabled: Boolean, warningsEnabled: Boolean,
scanFeedbackEnabled: Boolean, scanFeedbackEnabled: Boolean,
useCaseView: UseCaseView,
onScan: (ScanResult) -> Unit, onScan: (ScanResult) -> Unit,
onScanAgain: () -> Unit, onScanAgain: () -> Unit,
onBatchModeChange: (Boolean) -> Unit, onBatchModeChange: (Boolean) -> Unit,
@@ -111,9 +114,26 @@ fun ScannerScreen(
) { ) {
val context = LocalContext.current val context = LocalContext.current
val haptic = LocalHapticFeedback.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 duplicateSnackbarHostState = remember { SnackbarHostState() }
val toneGenerator = remember { ToneGenerator(AudioManager.STREAM_NOTIFICATION, 70) } val toneGenerator = remember { ToneGenerator(AudioManager.STREAM_NOTIFICATION, 70) }
LaunchedEffect(showBatchModeToggle, batchMode) {
if (!showBatchModeToggle && batchMode) {
onBatchModeChange(false)
}
}
var cameraGranted by remember { var cameraGranted by remember {
mutableStateOf( mutableStateOf(
ContextCompat.checkSelfPermission( ContextCompat.checkSelfPermission(
@@ -382,22 +402,38 @@ fun ScannerScreen(
.padding(horizontal = 14.dp, vertical = 8.dp) .padding(horizontal = 14.dp, vertical = 8.dp)
) )
IconButton( if (capabilities.allowScanFromImage) {
onClick = { imagePicker.launch("image/*") }, 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 modifier = Modifier
.align(Alignment.TopEnd) .align(Alignment.TopCenter)
.padding(top = 12.dp, end = 12.dp) .padding(top = 16.dp, start = 64.dp, end = 64.dp)
.background( .background(
color = Color.Black.copy(alpha = 0.35f), color = Color.Black.copy(alpha = 0.4f),
shape = RoundedCornerShape(14.dp) shape = RoundedCornerShape(14.dp)
) )
) { .padding(horizontal = 12.dp, vertical = 6.dp)
Icon( )
painter = painterResource(id = android.R.drawable.ic_menu_gallery),
contentDescription = stringResource(R.string.scan_from_image),
tint = Color.White
)
}
Column( Column(
modifier = Modifier modifier = Modifier
@@ -415,20 +451,23 @@ fun ScannerScreen(
showLabel = false showLabel = false
) )
} }
OverlayIconToggle( if (showBatchModeToggle) {
checked = batchMode, OverlayIconToggle(
onCheckedChange = onBatchModeChange, checked = batchMode,
label = stringResource(R.string.batch_mode), onCheckedChange = onBatchModeChange,
checkedImageVector = Icons.Default.ViewModule, label = stringResource(R.string.batch_mode),
uncheckedImageVector = Icons.AutoMirrored.Filled.ViewList checkedImageVector = Icons.Default.ViewModule,
) uncheckedImageVector = Icons.AutoMirrored.Filled.ViewList
)
}
} }
if (batchMode) { if (batchMode && showBatchModeToggle) {
Box(modifier = Modifier.align(Alignment.BottomCenter)) { Box(modifier = Modifier.align(Alignment.BottomCenter)) {
BatchResultsPanel( BatchResultsPanel(
results = batchResults, results = batchResults,
onClear = onClearBatchResults onClear = onClearBatchResults,
allowShare = capabilities.allowBatchShare
) )
} }
} }
@@ -470,91 +509,106 @@ fun ScannerScreen(
.padding(16.dp), .padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
if (parsedContact == null && lastResult.type != "WiFi") { ResultVisualCard(
Text(text = "${stringResource(R.string.content_type)}: ${lastResult.type}") result = lastResult,
} onOpenUrl = { url ->
ResultVisualCard(result = lastResult) val risk = UrlRiskScorer.score(url)
Row( val risky = warningsEnabled && risk.score >= 3
modifier = Modifier if (risky) {
.fillMaxWidth() pendingOpenUrl = url
.horizontalScroll(rememberScrollState()), showRiskWarning = true
horizontalArrangement = Arrangement.spacedBy(8.dp) } else {
) { Intents.openUrl(context, url)
if (parsedContact != null) {
IconButton(onClick = {
Intents.addContact(context, parsedContact, lastResult.content)
}) {
Icon(
imageVector = Icons.Default.PersonAdd,
contentDescription = stringResource(R.string.add_contact)
)
} }
} }
IconButton(onClick = { ClipboardUtil.copy(context, lastResult.content) }) { )
Icon( val hasQuickActions = capabilities.allowCopy ||
imageVector = Icons.Default.ContentCopy, capabilities.allowShare ||
contentDescription = stringResource(R.string.copy) (capabilities.allowAddContact && parsedContact != null)
)
} if (hasQuickActions) {
if (lastResult.type == "URL") { Row(
Button(onClick = { modifier = Modifier
val risk = UrlRiskScorer.score(lastResult.content) .fillMaxWidth()
val risky = warningsEnabled && risk.score >= 3 .horizontalScroll(rememberScrollState()),
if (risky) { horizontalArrangement = Arrangement.spacedBy(8.dp)
pendingOpenUrl = lastResult.content ) {
showRiskWarning = true if (capabilities.allowAddContact && parsedContact != null) {
} else { IconButton(onClick = {
Intents.openUrl(context, lastResult.content) 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) { when (lastResult.type) {
"Phone" -> { "Phone" -> {
Button(onClick = { if (capabilities.allowDialPhone) {
Intents.dialPhone(context, ScanContentParsers.extractPhoneNumber(lastResult.content)) Button(onClick = {
}) { Intents.dialPhone(context, ScanContentParsers.extractPhoneNumber(lastResult.content))
Text(stringResource(R.string.call_number)) }) {
Text(stringResource(R.string.call_number))
}
} }
} }
"SMS" -> { "SMS" -> {
Button(onClick = { if (capabilities.allowSendSms) {
val smsData = ScanContentParsers.parseSms(lastResult.content) Button(onClick = {
Intents.sendSms(context, smsData.first, smsData.second) val smsData = ScanContentParsers.parseSms(lastResult.content)
}) { Intents.sendSms(context, smsData.first, smsData.second)
Text(stringResource(R.string.send_sms)) }) {
Text(stringResource(R.string.send_sms))
}
} }
} }
"Email" -> { "Email" -> {
Button(onClick = { if (capabilities.allowSendEmail) {
Intents.sendEmail(context, ScanContentParsers.extractEmail(lastResult.content), null) Button(onClick = {
}) { Intents.sendEmail(context, ScanContentParsers.extractEmail(lastResult.content), null)
Text(stringResource(R.string.send_email)) }) {
Text(stringResource(R.string.send_email))
}
} }
} }
"WiFi" -> { "WiFi" -> {
Button(onClick = { Intents.openWifiSettings(context) }) { if (capabilities.allowOpenWifiSettings) {
Text(stringResource(R.string.open_wifi_settings)) Button(onClick = { Intents.openWifiSettings(context) }) {
Text(stringResource(R.string.open_wifi_settings))
}
} }
} }
"Calendar" -> { "Calendar" -> {
Button(onClick = { if (capabilities.allowAddCalendarEvent) {
Intents.addCalendarEvent(context, parsedEvent, lastResult.content) Button(onClick = {
}) { Intents.addCalendarEvent(context, parsedEvent, lastResult.content)
Text(stringResource(R.string.add_calendar_event)) }) {
Text(stringResource(R.string.add_calendar_event))
}
} }
} }
} }
@@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Arrangement
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.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.AlertDialog 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.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.clean.scanner.R import com.clean.scanner.R
import com.clean.scanner.ui.UseCaseView
import com.clean.scanner.util.Intents import com.clean.scanner.util.Intents
@Composable @Composable
@@ -27,13 +29,16 @@ fun SettingsScreen(
historyEnabled: Boolean, historyEnabled: Boolean,
warningsEnabled: Boolean, warningsEnabled: Boolean,
scanFeedbackEnabled: Boolean, scanFeedbackEnabled: Boolean,
selectedUseCaseView: UseCaseView,
onHistoryToggle: (Boolean, Boolean) -> Unit, onHistoryToggle: (Boolean, Boolean) -> Unit,
onWarningsToggle: (Boolean) -> Unit, onWarningsToggle: (Boolean) -> Unit,
onScanFeedbackToggle: (Boolean) -> Unit onScanFeedbackToggle: (Boolean) -> Unit,
onUseCaseViewSelected: (UseCaseView) -> Unit
) { ) {
val context = LocalContext.current val context = LocalContext.current
val showDeleteConfirm = remember { mutableStateOf(false) } val showDeleteConfirm = remember { mutableStateOf(false) }
val showFeatureRequestForm = remember { mutableStateOf(false) } val showFeatureRequestForm = remember { mutableStateOf(false) }
val showUseCasePicker = remember { mutableStateOf(false) }
val requesterNeed = remember { mutableStateOf("") } val requesterNeed = remember { mutableStateOf("") }
if (showDeleteConfirm.value) { 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( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -129,6 +162,14 @@ fun SettingsScreen(
Text(text = stringResource(R.string.scan_feedback)) Text(text = stringResource(R.string.scan_feedback))
Switch(checked = scanFeedbackEnabled, onCheckedChange = onScanFeedbackToggle) 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)) Spacer(modifier = Modifier.height(24.dp))
Text(text = stringResource(R.string.about)) Text(text = stringResource(R.string.about))
Text(text = stringResource(R.string.version)) Text(text = stringResource(R.string.version))
+14 -2
View File
@@ -28,7 +28,7 @@
<string name="delete_history_on_disable">Vorhandene Historie beim Deaktivieren löschen?</string> <string name="delete_history_on_disable">Vorhandene Historie beim Deaktivieren löschen?</string>
<string name="version">Version 1.0.0</string> <string name="version">Version 1.0.0</string>
<string name="licenses">Open-Source-Lizenzen</string> <string name="licenses">Open-Source-Lizenzen</string>
<string name="contact">Kontakt: support@example.com</string> <string name="contact">Kontakt: softwareapp.hb@gmail.com</string>
<string name="content_type">Typ</string> <string name="content_type">Typ</string>
<string name="content_value">Inhalt</string> <string name="content_value">Inhalt</string>
<string name="request_camera">Kamera erlauben</string> <string name="request_camera">Kamera erlauben</string>
@@ -57,7 +57,7 @@
<string name="open_wifi_settings">WLAN-Einstellungen öffnen</string> <string name="open_wifi_settings">WLAN-Einstellungen öffnen</string>
<string name="add_contact">Kontakt hinzufügen</string> <string name="add_contact">Kontakt hinzufügen</string>
<string name="add_calendar_event">Kalendereintrag hinzufügen</string> <string name="add_calendar_event">Kalendereintrag hinzufügen</string>
<string name="support_email">support@example.com</string> <string name="support_email">softwareapp.hb@gmail.com</string>
<string name="feature_request">Feature-Request-Formular</string> <string name="feature_request">Feature-Request-Formular</string>
<string name="feature_request_title">Feature-Request</string> <string name="feature_request_title">Feature-Request</string>
<string name="feature_request_name">Dein Name</string> <string name="feature_request_name">Dein Name</string>
@@ -66,4 +66,16 @@
<string name="feature_request_subject">Feature-Request von App-Nutzer</string> <string name="feature_request_subject">Feature-Request von App-Nutzer</string>
<string name="send_request">Anfrage senden</string> <string name="send_request">Anfrage senden</string>
<string name="feature_request_sent">E-Mail-App wird geöffnet...</string> <string name="feature_request_sent">E-Mail-App wird geöffnet...</string>
<string name="active_use_case_view">Aktive Use-Case-Ansicht</string>
<string name="select_use_case_view">Use-Case-Ansicht wählen</string>
<string name="use_case_everyday_personal">Alltägliche private Nutzung</string>
<string name="use_case_event_ticketing">Events &amp; Ticketing</string>
<string name="use_case_inventory_operations">Inventur &amp; Betrieb</string>
<string name="use_case_field_work">Außendienst &amp; Service-Teams</string>
<string name="use_case_office_admin">Büro- &amp; Admin-Workflows</string>
<string name="use_case_communication_shortcuts">Kommunikations-Shortcuts</string>
<string name="use_case_security_browsing">Sicherheitsbewusstes Browsen</string>
<string name="use_case_offline_low_connectivity">Offline / geringe Konnektivität</string>
<string name="use_case_accessibility_speed">Barrierefreiheit &amp; Geschwindigkeit</string>
<string name="use_case_team_handover_transfer">Team-Übergabe &amp; Datentransfer</string>
</resources> </resources>
+12
View File
@@ -66,4 +66,16 @@
<string name="feature_request_subject">Feature request from app user</string> <string name="feature_request_subject">Feature request from app user</string>
<string name="send_request">Send request</string> <string name="send_request">Send request</string>
<string name="feature_request_sent">Opening email app...</string> <string name="feature_request_sent">Opening email app...</string>
<string name="active_use_case_view">Active use-case view</string>
<string name="select_use_case_view">Select use-case view</string>
<string name="use_case_everyday_personal">Everyday personal use</string>
<string name="use_case_event_ticketing">Event &amp; ticketing</string>
<string name="use_case_inventory_operations">Inventory &amp; operations</string>
<string name="use_case_field_work">Field work &amp; service teams</string>
<string name="use_case_office_admin">Office &amp; admin workflows</string>
<string name="use_case_communication_shortcuts">Communication shortcuts</string>
<string name="use_case_security_browsing">Security-conscious browsing</string>
<string name="use_case_offline_low_connectivity">Offline / low-connectivity</string>
<string name="use_case_accessibility_speed">Accessibility &amp; speed</string>
<string name="use_case_team_handover_transfer">Team handover &amp; data transfer</string>
</resources> </resources>