7in outline
This commit is contained in:
@@ -109,9 +109,15 @@ fun CleanScannerAppRoot(container: AppContainer) {
|
||||
query = appState.searchQuery,
|
||||
history = appState.history,
|
||||
useCaseView = appState.useCaseView,
|
||||
historyEnabled = appState.historyEnabled,
|
||||
warningsEnabled = appState.warningsEnabled,
|
||||
scanFeedbackEnabled = appState.scanFeedbackEnabled,
|
||||
onQueryChange = appViewModel::setQuery,
|
||||
onDelete = appViewModel::deleteHistoryItem,
|
||||
onClearAll = appViewModel::clearHistory
|
||||
onClearAll = appViewModel::clearHistory,
|
||||
onHistoryToggle = appViewModel::setHistoryEnabled,
|
||||
onWarningsToggle = appViewModel::setWarningsEnabled,
|
||||
onScanFeedbackToggle = appViewModel::setScanFeedbackEnabled
|
||||
)
|
||||
|
||||
RootTab.Settings -> SettingsScreen(
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -31,6 +32,8 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.SwitchDefaults
|
||||
import androidx.compose.material3.SwipeToDismissBox
|
||||
import androidx.compose.material3.SwipeToDismissBoxValue
|
||||
import androidx.compose.material3.Text
|
||||
@@ -65,9 +68,15 @@ fun HistoryScreen(
|
||||
query: String,
|
||||
history: List<ScanRecord>,
|
||||
useCaseView: UseCaseView,
|
||||
historyEnabled: Boolean,
|
||||
warningsEnabled: Boolean,
|
||||
scanFeedbackEnabled: Boolean,
|
||||
onQueryChange: (String) -> Unit,
|
||||
onDelete: (Long) -> Unit,
|
||||
onClearAll: () -> Unit
|
||||
onClearAll: () -> Unit,
|
||||
onHistoryToggle: (Boolean, Boolean) -> Unit,
|
||||
onWarningsToggle: (Boolean) -> Unit,
|
||||
onScanFeedbackToggle: (Boolean) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val capabilities = useCaseView.capabilities()
|
||||
@@ -117,92 +126,216 @@ fun HistoryScreen(
|
||||
)
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(PrivateQrColors.AppBackground)
|
||||
.padding(horizontal = 20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(18.dp))
|
||||
HistoryHeader()
|
||||
Spacer(modifier = Modifier.height(18.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.history),
|
||||
color = PrivateQrColors.TextPrimary,
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
OutlinedTextField(
|
||||
value = query,
|
||||
onValueChange = onQueryChange,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Search,
|
||||
contentDescription = null,
|
||||
tint = PrivateQrColors.Teal700
|
||||
)
|
||||
},
|
||||
placeholder = { Text(stringResource(R.string.search)) },
|
||||
singleLine = true,
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = PrivateQrColors.Teal300,
|
||||
unfocusedBorderColor = PrivateQrColors.Divider,
|
||||
focusedContainerColor = PrivateQrColors.Surface,
|
||||
unfocusedContainerColor = PrivateQrColors.Surface
|
||||
)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
val wideLayout = maxWidth >= 600.dp
|
||||
|
||||
if (wideLayout) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(18.dp)
|
||||
) {
|
||||
if (capabilities.allowHistoryExport) {
|
||||
ExportButton(
|
||||
text = stringResource(R.string.share_txt),
|
||||
enabled = history.isNotEmpty(),
|
||||
onClick = {
|
||||
val exportText = HistoryExportFormatter.formatText(history)
|
||||
Intents.shareContent(context, exportText, "text/plain")
|
||||
}
|
||||
)
|
||||
ExportButton(
|
||||
text = stringResource(R.string.share_csv),
|
||||
enabled = history.isNotEmpty(),
|
||||
onClick = {
|
||||
val exportCsv = HistoryExportFormatter.formatCsv(history)
|
||||
Intents.shareContent(context, exportCsv, "text/csv")
|
||||
}
|
||||
)
|
||||
ExportButton(
|
||||
text = stringResource(R.string.share_json),
|
||||
enabled = history.isNotEmpty(),
|
||||
onClick = {
|
||||
val exportJson = HistoryExportFormatter.formatJson(history)
|
||||
Intents.shareContent(context, exportJson, "application/json")
|
||||
}
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
TextButton(
|
||||
onClick = { showDeleteAll.value = true },
|
||||
enabled = history.isNotEmpty(),
|
||||
colors = ButtonDefaults.textButtonColors(
|
||||
contentColor = Color(0xFFBE123C),
|
||||
disabledContentColor = PrivateQrColors.TextSecondary.copy(alpha = 0.45f)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(18.dp))
|
||||
HistoryHeader()
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f),
|
||||
horizontalArrangement = Arrangement.spacedBy(24.dp)
|
||||
) {
|
||||
Text(stringResource(R.string.delete_all), fontWeight = FontWeight.Bold)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1.1f)
|
||||
.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.history),
|
||||
color = PrivateQrColors.TextPrimary,
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
)
|
||||
TextButton(
|
||||
onClick = { showDeleteAll.value = true },
|
||||
enabled = history.isNotEmpty(),
|
||||
colors = ButtonDefaults.textButtonColors(
|
||||
contentColor = Color(0xFFBE123C),
|
||||
disabledContentColor = PrivateQrColors.TextSecondary.copy(alpha = 0.45f)
|
||||
)
|
||||
) {
|
||||
Text(stringResource(R.string.delete_all), fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
HistorySearchField(query = query, onQueryChange = onQueryChange)
|
||||
HistoryList(
|
||||
history = history,
|
||||
onDelete = onDelete,
|
||||
onOpenDetails = { selectedItem.value = it },
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.weight(0.95f),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.local_controls),
|
||||
color = PrivateQrColors.TextPrimary,
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
)
|
||||
LocalControlsCard(
|
||||
history = history,
|
||||
historyEnabled = historyEnabled,
|
||||
warningsEnabled = warningsEnabled,
|
||||
scanFeedbackEnabled = scanFeedbackEnabled,
|
||||
allowHistoryExport = capabilities.allowHistoryExport,
|
||||
onHistoryToggle = onHistoryToggle,
|
||||
onWarningsToggle = onWarningsToggle,
|
||||
onScanFeedbackToggle = onScanFeedbackToggle
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(18.dp))
|
||||
HistoryHeader()
|
||||
Spacer(modifier = Modifier.height(18.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.history),
|
||||
color = PrivateQrColors.TextPrimary,
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
HistorySearchField(query = query, onQueryChange = onQueryChange)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (capabilities.allowHistoryExport) {
|
||||
ExportButton(
|
||||
text = stringResource(R.string.share_txt),
|
||||
enabled = history.isNotEmpty(),
|
||||
onClick = {
|
||||
val exportText = HistoryExportFormatter.formatText(history)
|
||||
Intents.shareContent(context, exportText, "text/plain")
|
||||
}
|
||||
)
|
||||
ExportButton(
|
||||
text = stringResource(R.string.share_csv),
|
||||
enabled = history.isNotEmpty(),
|
||||
onClick = {
|
||||
val exportCsv = HistoryExportFormatter.formatCsv(history)
|
||||
Intents.shareContent(context, exportCsv, "text/csv")
|
||||
}
|
||||
)
|
||||
ExportButton(
|
||||
text = stringResource(R.string.share_json),
|
||||
enabled = history.isNotEmpty(),
|
||||
onClick = {
|
||||
val exportJson = HistoryExportFormatter.formatJson(history)
|
||||
Intents.shareContent(context, exportJson, "application/json")
|
||||
}
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
TextButton(
|
||||
onClick = { showDeleteAll.value = true },
|
||||
enabled = history.isNotEmpty(),
|
||||
colors = ButtonDefaults.textButtonColors(
|
||||
contentColor = Color(0xFFBE123C),
|
||||
disabledContentColor = PrivateQrColors.TextSecondary.copy(alpha = 0.45f)
|
||||
)
|
||||
) {
|
||||
Text(stringResource(R.string.delete_all), fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
}
|
||||
|
||||
if (history.isEmpty()) {
|
||||
item {
|
||||
EmptyHistoryCard()
|
||||
}
|
||||
} else {
|
||||
items(history, key = { it.id }) { item ->
|
||||
HistoryRow(
|
||||
item = item,
|
||||
onDelete = onDelete,
|
||||
onOpenDetails = { selectedItem.value = item }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HistorySearchField(
|
||||
query: String,
|
||||
onQueryChange: (String) -> Unit
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = query,
|
||||
onValueChange = onQueryChange,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Search,
|
||||
contentDescription = null,
|
||||
tint = PrivateQrColors.Teal700
|
||||
)
|
||||
},
|
||||
placeholder = { Text(stringResource(R.string.search)) },
|
||||
singleLine = true,
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = PrivateQrColors.Teal300,
|
||||
unfocusedBorderColor = PrivateQrColors.Divider,
|
||||
focusedContainerColor = PrivateQrColors.Surface,
|
||||
unfocusedContainerColor = PrivateQrColors.Surface
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HistoryList(
|
||||
history: List<ScanRecord>,
|
||||
onDelete: (Long) -> Unit,
|
||||
onOpenDetails: (ScanRecord) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
if (history.isEmpty()) {
|
||||
item {
|
||||
EmptyHistoryCard()
|
||||
@@ -212,15 +345,111 @@ fun HistoryScreen(
|
||||
HistoryRow(
|
||||
item = item,
|
||||
onDelete = onDelete,
|
||||
onOpenDetails = { selectedItem.value = item }
|
||||
onOpenDetails = { onOpenDetails(item) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
@Composable
|
||||
private fun LocalControlsCard(
|
||||
history: List<ScanRecord>,
|
||||
historyEnabled: Boolean,
|
||||
warningsEnabled: Boolean,
|
||||
scanFeedbackEnabled: Boolean,
|
||||
allowHistoryExport: Boolean,
|
||||
onHistoryToggle: (Boolean, Boolean) -> Unit,
|
||||
onWarningsToggle: (Boolean) -> Unit,
|
||||
onScanFeedbackToggle: (Boolean) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(containerColor = PrivateQrColors.Surface),
|
||||
shape = RoundedCornerShape(28.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
LocalControlRow(
|
||||
title = stringResource(R.string.save_history),
|
||||
checked = historyEnabled,
|
||||
onCheckedChange = { onHistoryToggle(it, false) }
|
||||
)
|
||||
LocalControlRow(
|
||||
title = stringResource(R.string.security_warnings),
|
||||
checked = warningsEnabled,
|
||||
onCheckedChange = onWarningsToggle
|
||||
)
|
||||
LocalControlRow(
|
||||
title = stringResource(R.string.scan_feedback),
|
||||
checked = scanFeedbackEnabled,
|
||||
onCheckedChange = onScanFeedbackToggle
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (allowHistoryExport) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
ExportButton(
|
||||
text = stringResource(R.string.share_txt),
|
||||
enabled = history.isNotEmpty(),
|
||||
onClick = {
|
||||
Intents.shareContent(context, HistoryExportFormatter.formatText(history), "text/plain")
|
||||
}
|
||||
)
|
||||
ExportButton(
|
||||
text = stringResource(R.string.share_csv),
|
||||
enabled = history.isNotEmpty(),
|
||||
onClick = {
|
||||
Intents.shareContent(context, HistoryExportFormatter.formatCsv(history), "text/csv")
|
||||
}
|
||||
)
|
||||
ExportButton(
|
||||
text = stringResource(R.string.share_json),
|
||||
enabled = history.isNotEmpty(),
|
||||
onClick = {
|
||||
Intents.shareContent(context, HistoryExportFormatter.formatJson(history), "application/json")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LocalControlRow(
|
||||
title: String,
|
||||
checked: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(58.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
color = PrivateQrColors.TextPrimary,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
)
|
||||
Switch(
|
||||
checked = checked,
|
||||
onCheckedChange = onCheckedChange,
|
||||
colors = SwitchDefaults.colors(
|
||||
checkedThumbColor = PrivateQrColors.Surface,
|
||||
checkedTrackColor = PrivateQrColors.Teal700,
|
||||
uncheckedThumbColor = PrivateQrColors.Surface,
|
||||
uncheckedTrackColor = PrivateQrColors.TextSecondary.copy(alpha = 0.35f)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -243,7 +472,7 @@ private fun HistoryHeader() {
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
)
|
||||
Text(
|
||||
text = "Optional history stays on your device",
|
||||
text = stringResource(R.string.history_header_subtitle),
|
||||
color = PrivateQrColors.TextSecondary,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
@@ -266,20 +495,20 @@ private fun HistoryHeader() {
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Review past scans locally",
|
||||
text = stringResource(R.string.history_hero_title),
|
||||
color = PrivateQrColors.Surface,
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
)
|
||||
Text(
|
||||
text = "Search, export, or delete saved scans whenever you choose.",
|
||||
text = stringResource(R.string.history_hero_subtitle),
|
||||
color = PrivateQrColors.Mint,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
PrivacyPill("No ads")
|
||||
PrivacyPill("No tracking")
|
||||
PrivacyPill("No account")
|
||||
PrivacyPill(stringResource(R.string.privacy_pill_no_ads))
|
||||
PrivacyPill(stringResource(R.string.privacy_pill_no_tracking))
|
||||
PrivacyPill(stringResource(R.string.privacy_pill_no_account))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -338,13 +567,13 @@ private fun EmptyHistoryCard() {
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "No saved scans yet",
|
||||
text = stringResource(R.string.no_saved_scans_yet),
|
||||
color = PrivateQrColors.TextPrimary,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
)
|
||||
Text(
|
||||
text = "Enable local history in settings to keep a private record on this device.",
|
||||
text = stringResource(R.string.empty_history_description),
|
||||
color = PrivateQrColors.TextSecondary,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
|
||||
+79
-21
@@ -13,9 +13,12 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ContentCopy
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.IconToggleButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -25,10 +28,12 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import de.softwareapp_hb.privateqrscanner.R
|
||||
import de.softwareapp_hb.privateqrscanner.ui.BatchScanRecord
|
||||
import de.softwareapp_hb.privateqrscanner.ui.theme.PrivateQrColors
|
||||
import de.softwareapp_hb.privateqrscanner.util.ClipboardUtil
|
||||
import de.softwareapp_hb.privateqrscanner.util.Intents
|
||||
import java.text.DateFormat
|
||||
@@ -91,22 +96,46 @@ internal fun BatchResultsPanel(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
color = Color.Black.copy(alpha = 0.42f),
|
||||
shape = RoundedCornerShape(14.dp)
|
||||
color = PrivateQrColors.Deep.copy(alpha = 0.88f),
|
||||
shape = RoundedCornerShape(28.dp)
|
||||
)
|
||||
.padding(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
.padding(20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.batch_captures_count, results.size),
|
||||
color = Color.White
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.batch_captures_count, results.size),
|
||||
color = Color.White,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
)
|
||||
if (allowShare) {
|
||||
TextButton(
|
||||
onClick = { Intents.shareText(context, buildBatchExport(results)) },
|
||||
enabled = results.isNotEmpty(),
|
||||
colors = ButtonDefaults.textButtonColors(
|
||||
containerColor = PrivateQrColors.Mint,
|
||||
contentColor = PrivateQrColors.Teal700,
|
||||
disabledContainerColor = PrivateQrColors.Mint.copy(alpha = 0.32f),
|
||||
disabledContentColor = PrivateQrColors.Teal700.copy(alpha = 0.45f)
|
||||
),
|
||||
shape = RoundedCornerShape(18.dp)
|
||||
) {
|
||||
Text(stringResource(R.string.share_batch), fontWeight = FontWeight.ExtraBold)
|
||||
}
|
||||
}
|
||||
}
|
||||
results.take(3).forEach { item ->
|
||||
val contentText = if (item.result.isBase64Encoded) {
|
||||
stringResource(R.string.base64_encoded_inline, item.result.content)
|
||||
} else {
|
||||
item.result.content
|
||||
}
|
||||
HorizontalDivider(color = Color.White.copy(alpha = 0.12f))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@@ -116,11 +145,15 @@ internal fun BatchResultsPanel(
|
||||
Text(
|
||||
text = "${item.result.displayType}: $contentText",
|
||||
color = Color.White.copy(alpha = 0.92f),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 1
|
||||
)
|
||||
Text(
|
||||
text = timeFormat.format(Date(item.timestamp)),
|
||||
color = Color.White.copy(alpha = 0.7f)
|
||||
color = Color.White.copy(alpha = 0.7f),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
Row {
|
||||
@@ -143,18 +176,43 @@ internal fun BatchResultsPanel(
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
TextButton(onClick = onClear, enabled = results.isNotEmpty()) {
|
||||
Text(stringResource(R.string.clear_batch))
|
||||
}
|
||||
if (allowShare) {
|
||||
TextButton(
|
||||
onClick = { Intents.shareText(context, buildBatchExport(results)) },
|
||||
enabled = results.isNotEmpty()
|
||||
) {
|
||||
Text(stringResource(R.string.share_batch))
|
||||
}
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(Color(0xFF831843).copy(alpha = 0.42f), RoundedCornerShape(18.dp))
|
||||
.padding(horizontal = 14.dp, vertical = 12.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(text = "▲", color = Color(0xFFFF7A9A), fontWeight = FontWeight.ExtraBold)
|
||||
Text(
|
||||
text = stringResource(R.string.batch_ticket_privacy_note),
|
||||
color = Color.White,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
fontWeight = FontWeight.ExtraBold,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
TextButton(
|
||||
onClick = onClear,
|
||||
enabled = results.isNotEmpty(),
|
||||
colors = ButtonDefaults.textButtonColors(contentColor = PrivateQrColors.Mint)
|
||||
) {
|
||||
Text(stringResource(R.string.clear_batch), fontWeight = FontWeight.Bold)
|
||||
}
|
||||
if (results.isEmpty()) {
|
||||
Text(
|
||||
text = stringResource(R.string.aim_center_hint),
|
||||
color = Color.White.copy(alpha = 0.68f),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
if (!allowShare && results.isNotEmpty()) {
|
||||
Text(
|
||||
text = stringResource(R.string.batch_ticket_privacy_note),
|
||||
color = Color.White.copy(alpha = 0.68f),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -96,7 +96,7 @@ internal fun ResultVisualCard(
|
||||
)
|
||||
if (!result.isBase64Encoded && result.type == "URL") {
|
||||
Text(
|
||||
text = "Local check",
|
||||
text = stringResource(R.string.result_local_check),
|
||||
color = PrivateQrColors.Teal700,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
modifier = Modifier
|
||||
@@ -166,7 +166,7 @@ internal fun ResultVisualCard(
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Text(
|
||||
text = "Checked on device before opening",
|
||||
text = stringResource(R.string.result_checked_on_device),
|
||||
color = PrivateQrColors.Teal700,
|
||||
style = MaterialTheme.typography.labelLarge
|
||||
)
|
||||
|
||||
@@ -21,9 +21,11 @@ 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.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.OpenInNew
|
||||
import androidx.compose.material.icons.automirrored.filled.ViewList
|
||||
import androidx.compose.material.icons.filled.ContentCopy
|
||||
import androidx.compose.material.icons.filled.FlashOff
|
||||
@@ -70,6 +72,8 @@ import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -187,6 +191,17 @@ fun ScannerScreen(
|
||||
}
|
||||
}
|
||||
|
||||
fun requestOpenUrl(url: String) {
|
||||
val risk = UrlRiskScorer.score(url)
|
||||
val risky = warningsEnabled && risk.score >= 3
|
||||
if (risky) {
|
||||
pendingOpenUrl = url
|
||||
showRiskWarning = true
|
||||
} else {
|
||||
Intents.openUrl(context, url)
|
||||
}
|
||||
}
|
||||
|
||||
val permissionLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.RequestPermission()
|
||||
) { granted ->
|
||||
@@ -647,20 +662,12 @@ fun ScannerScreen(
|
||||
) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
onOpenUrl = ::requestOpenUrl
|
||||
)
|
||||
val hasQuickActions = capabilities.allowCopy ||
|
||||
capabilities.allowShare ||
|
||||
(capabilities.allowAddContact && parsedContact != null)
|
||||
(capabilities.allowAddContact && parsedContact != null) ||
|
||||
(capabilities.allowOpenUrl && !lastResult.isBase64Encoded && lastResult.type == "URL")
|
||||
|
||||
if (hasQuickActions) {
|
||||
Row(
|
||||
@@ -670,49 +677,34 @@ fun ScannerScreen(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
if (capabilities.allowAddContact && parsedContact != null) {
|
||||
IconButton(
|
||||
ResultActionPill(
|
||||
text = stringResource(R.string.add_contact),
|
||||
imageVector = Icons.Default.PersonAdd,
|
||||
onClick = { Intents.addContact(context, parsedContact, lastResult.content) },
|
||||
modifier = Modifier.background(
|
||||
color = PrivateQrColors.AppBackground,
|
||||
shape = RoundedCornerShape(50)
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.PersonAdd,
|
||||
contentDescription = stringResource(R.string.add_contact),
|
||||
tint = PrivateQrColors.TextPrimary
|
||||
)
|
||||
}
|
||||
primary = true
|
||||
)
|
||||
}
|
||||
if (capabilities.allowCopy) {
|
||||
IconButton(
|
||||
onClick = { ClipboardUtil.copy(context, lastResult.content) },
|
||||
modifier = Modifier.background(
|
||||
color = PrivateQrColors.AppBackground,
|
||||
shape = RoundedCornerShape(50)
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ContentCopy,
|
||||
contentDescription = stringResource(R.string.copy),
|
||||
tint = PrivateQrColors.TextPrimary
|
||||
)
|
||||
}
|
||||
ResultActionPill(
|
||||
text = stringResource(R.string.copy),
|
||||
imageVector = Icons.Default.ContentCopy,
|
||||
onClick = { ClipboardUtil.copy(context, lastResult.content) }
|
||||
)
|
||||
}
|
||||
if (capabilities.allowShare) {
|
||||
IconButton(
|
||||
onClick = { Intents.shareText(context, lastResult.content) },
|
||||
modifier = Modifier.background(
|
||||
color = PrivateQrColors.AppBackground,
|
||||
shape = RoundedCornerShape(50)
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Share,
|
||||
contentDescription = stringResource(R.string.share),
|
||||
tint = PrivateQrColors.TextPrimary
|
||||
)
|
||||
}
|
||||
ResultActionPill(
|
||||
text = stringResource(R.string.share),
|
||||
imageVector = Icons.Default.Share,
|
||||
onClick = { Intents.shareText(context, lastResult.content) }
|
||||
)
|
||||
}
|
||||
if (capabilities.allowOpenUrl && !lastResult.isBase64Encoded && lastResult.type == "URL") {
|
||||
ResultActionPill(
|
||||
text = stringResource(R.string.open),
|
||||
imageVector = Icons.AutoMirrored.Filled.OpenInNew,
|
||||
onClick = { requestOpenUrl(lastResult.content) },
|
||||
primary = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -929,3 +921,31 @@ private fun parseWhitelistIds(raw: String): Set<String> {
|
||||
.filter { it.isNotBlank() }
|
||||
.toSet()
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ResultActionPill(
|
||||
text: String,
|
||||
imageVector: ImageVector,
|
||||
onClick: () -> Unit,
|
||||
primary: Boolean = false
|
||||
) {
|
||||
TextButton(
|
||||
onClick = onClick,
|
||||
colors = ButtonDefaults.textButtonColors(
|
||||
containerColor = if (primary) PrivateQrColors.Teal700 else PrivateQrColors.AppBackground,
|
||||
contentColor = if (primary) PrivateQrColors.Surface else PrivateQrColors.TextPrimary
|
||||
),
|
||||
shape = RoundedCornerShape(18.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = imageVector,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Text(
|
||||
text = text,
|
||||
fontWeight = FontWeight.ExtraBold,
|
||||
modifier = Modifier.padding(start = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+164
-134
@@ -6,6 +6,7 @@ import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -57,7 +58,6 @@ fun SettingsScreen(
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val showDeleteConfirm = remember { mutableStateOf(false) }
|
||||
val showUseCasePicker = remember { mutableStateOf(false) }
|
||||
val showPrivacyPolicy = remember { mutableStateOf(false) }
|
||||
|
||||
if (showDeleteConfirm.value) {
|
||||
@@ -80,157 +80,187 @@ fun SettingsScreen(
|
||||
)
|
||||
}
|
||||
|
||||
if (showUseCasePicker.value) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showUseCasePicker.value = false },
|
||||
containerColor = PrivateQrColors.Surface,
|
||||
shape = RoundedCornerShape(30.dp),
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.select_use_case_view),
|
||||
color = PrivateQrColors.TextPrimary,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
UseCaseView.entries.forEach { candidate ->
|
||||
UseCasePickerOption(
|
||||
candidate = candidate,
|
||||
selected = candidate == selectedUseCaseView,
|
||||
onClick = {
|
||||
onUseCaseViewSelected(candidate)
|
||||
showUseCasePicker.value = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showUseCasePicker.value = false }) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (showPrivacyPolicy.value) {
|
||||
PrivacyPolicyDialog(onDismiss = { showPrivacyPolicy.value = false })
|
||||
}
|
||||
|
||||
Column(
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(PrivateQrColors.AppBackground)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(18.dp)
|
||||
) {
|
||||
SettingsHeader()
|
||||
val wideLayout = maxWidth >= 600.dp
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(18.dp)
|
||||
) {
|
||||
SettingsHeader()
|
||||
|
||||
if (wideLayout) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(24.dp),
|
||||
verticalAlignment = Alignment.Top
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(18.dp)
|
||||
) {
|
||||
SettingsToggleCard(
|
||||
historyEnabled = historyEnabled,
|
||||
warningsEnabled = warningsEnabled,
|
||||
scanFeedbackEnabled = scanFeedbackEnabled,
|
||||
onHistoryToggle = onHistoryToggle,
|
||||
onWarningsToggle = onWarningsToggle,
|
||||
onScanFeedbackToggle = onScanFeedbackToggle,
|
||||
onRequestDisableHistory = { showDeleteConfirm.value = true }
|
||||
)
|
||||
AboutCard(
|
||||
onPrivacyClick = { showPrivacyPolicy.value = true },
|
||||
onReviewClick = { InAppReviewRequester.requestReview(context) }
|
||||
)
|
||||
}
|
||||
UseCaseSelectionCard(
|
||||
selectedUseCaseView = selectedUseCaseView,
|
||||
onUseCaseViewSelected = onUseCaseViewSelected,
|
||||
modifier = Modifier.weight(0.9f)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
SettingsToggleCard(
|
||||
historyEnabled = historyEnabled,
|
||||
warningsEnabled = warningsEnabled,
|
||||
scanFeedbackEnabled = scanFeedbackEnabled,
|
||||
onHistoryToggle = onHistoryToggle,
|
||||
onWarningsToggle = onWarningsToggle,
|
||||
onScanFeedbackToggle = onScanFeedbackToggle,
|
||||
onRequestDisableHistory = { showDeleteConfirm.value = true }
|
||||
)
|
||||
UseCaseSelectionCard(
|
||||
selectedUseCaseView = selectedUseCaseView,
|
||||
onUseCaseViewSelected = onUseCaseViewSelected
|
||||
)
|
||||
AboutCard(
|
||||
onPrivacyClick = { showPrivacyPolicy.value = true },
|
||||
onReviewClick = { InAppReviewRequester.requestReview(context) }
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsToggleCard(
|
||||
historyEnabled: Boolean,
|
||||
warningsEnabled: Boolean,
|
||||
scanFeedbackEnabled: Boolean,
|
||||
onHistoryToggle: (Boolean, Boolean) -> Unit,
|
||||
onWarningsToggle: (Boolean) -> Unit,
|
||||
onScanFeedbackToggle: (Boolean) -> Unit,
|
||||
onRequestDisableHistory: () -> Unit
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(containerColor = PrivateQrColors.Surface),
|
||||
shape = RoundedCornerShape(28.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(horizontal = 20.dp, vertical = 8.dp)) {
|
||||
SettingsToggleRow(
|
||||
title = stringResource(R.string.save_history),
|
||||
checked = historyEnabled,
|
||||
onCheckedChange = { enabled ->
|
||||
if (!enabled && historyEnabled) {
|
||||
onRequestDisableHistory()
|
||||
} else {
|
||||
onHistoryToggle(enabled, false)
|
||||
}
|
||||
}
|
||||
)
|
||||
SettingsDivider()
|
||||
SettingsToggleRow(
|
||||
title = stringResource(R.string.security_warnings),
|
||||
checked = warningsEnabled,
|
||||
onCheckedChange = onWarningsToggle
|
||||
)
|
||||
SettingsDivider()
|
||||
SettingsToggleRow(
|
||||
title = stringResource(R.string.scan_feedback),
|
||||
checked = scanFeedbackEnabled,
|
||||
onCheckedChange = onScanFeedbackToggle
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UseCaseSelectionCard(
|
||||
selectedUseCaseView: UseCaseView,
|
||||
onUseCaseViewSelected: (UseCaseView) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.settings),
|
||||
text = stringResource(R.string.select_use_case_view),
|
||||
color = PrivateQrColors.TextPrimary,
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
)
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(containerColor = PrivateQrColors.Surface),
|
||||
shape = RoundedCornerShape(28.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(horizontal = 20.dp, vertical = 8.dp)) {
|
||||
SettingsToggleRow(
|
||||
title = stringResource(R.string.save_history),
|
||||
checked = historyEnabled,
|
||||
onCheckedChange = { enabled ->
|
||||
if (!enabled && historyEnabled) {
|
||||
showDeleteConfirm.value = true
|
||||
} else {
|
||||
onHistoryToggle(enabled, false)
|
||||
}
|
||||
}
|
||||
)
|
||||
SettingsDivider()
|
||||
SettingsToggleRow(
|
||||
title = stringResource(R.string.security_warnings),
|
||||
checked = warningsEnabled,
|
||||
onCheckedChange = onWarningsToggle
|
||||
)
|
||||
SettingsDivider()
|
||||
SettingsToggleRow(
|
||||
title = stringResource(R.string.scan_feedback),
|
||||
checked = scanFeedbackEnabled,
|
||||
onCheckedChange = onScanFeedbackToggle
|
||||
)
|
||||
}
|
||||
UseCaseView.entries.forEach { candidate ->
|
||||
UseCasePickerOption(
|
||||
candidate = candidate,
|
||||
selected = candidate == selectedUseCaseView,
|
||||
onClick = { onUseCaseViewSelected(candidate) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(containerColor = PrivateQrColors.Surface),
|
||||
shape = RoundedCornerShape(28.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
Text(
|
||||
text = stringResource(R.string.active_use_case_view),
|
||||
color = PrivateQrColors.TextPrimary,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
)
|
||||
Text(
|
||||
text = stringResource(selectedUseCaseView.titleRes),
|
||||
color = PrivateQrColors.TextSecondary,
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
@Composable
|
||||
private fun AboutCard(
|
||||
onPrivacyClick: () -> Unit,
|
||||
onReviewClick: () -> Unit
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(containerColor = PrivateQrColors.Surface),
|
||||
shape = RoundedCornerShape(28.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
Text(
|
||||
text = stringResource(R.string.about),
|
||||
color = PrivateQrColors.TextPrimary,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
)
|
||||
InfoLine(stringResource(R.string.version))
|
||||
InfoLine(stringResource(R.string.licenses))
|
||||
InfoLine(stringResource(R.string.contact))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
TextButton(
|
||||
onClick = { showUseCasePicker.value = true },
|
||||
onClick = onPrivacyClick,
|
||||
colors = ButtonDefaults.textButtonColors(contentColor = PrivateQrColors.Teal700)
|
||||
) {
|
||||
Text(stringResource(R.string.select_use_case_view), fontWeight = FontWeight.Bold)
|
||||
Text(text = stringResource(R.string.privacy_policy), fontWeight = FontWeight.Bold)
|
||||
}
|
||||
TextButton(
|
||||
onClick = onReviewClick,
|
||||
colors = ButtonDefaults.textButtonColors(contentColor = PrivateQrColors.Teal700)
|
||||
) {
|
||||
Text(text = stringResource(R.string.review_app), fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(containerColor = PrivateQrColors.Surface),
|
||||
shape = RoundedCornerShape(28.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
Text(
|
||||
text = stringResource(R.string.about),
|
||||
color = PrivateQrColors.TextPrimary,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
)
|
||||
InfoLine(stringResource(R.string.version))
|
||||
InfoLine(stringResource(R.string.licenses))
|
||||
InfoLine(stringResource(R.string.contact))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
TextButton(
|
||||
onClick = { showPrivacyPolicy.value = true },
|
||||
colors = ButtonDefaults.textButtonColors(contentColor = PrivateQrColors.Teal700)
|
||||
) {
|
||||
Text(text = stringResource(R.string.privacy_policy), fontWeight = FontWeight.Bold)
|
||||
}
|
||||
TextButton(
|
||||
onClick = { InAppReviewRequester.requestReview(context) },
|
||||
colors = ButtonDefaults.textButtonColors(contentColor = PrivateQrColors.Teal700)
|
||||
) {
|
||||
Text(text = stringResource(R.string.review_app), fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,7 +339,7 @@ private fun SettingsHeader() {
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
)
|
||||
Text(
|
||||
text = "Local privacy controls",
|
||||
text = stringResource(R.string.settings_header_subtitle),
|
||||
color = PrivateQrColors.TextSecondary,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
@@ -332,13 +362,13 @@ private fun SettingsHeader() {
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Control what stays saved",
|
||||
text = stringResource(R.string.settings_hero_title),
|
||||
color = PrivateQrColors.Surface,
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
)
|
||||
Text(
|
||||
text = "History, warnings, and feedback are optional device-local settings.",
|
||||
text = stringResource(R.string.settings_hero_subtitle),
|
||||
color = PrivateQrColors.Mint,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
|
||||
@@ -45,6 +45,15 @@
|
||||
<string name="share_txt">TXT</string>
|
||||
<string name="share_csv">CSV</string>
|
||||
<string name="share_json">JSON</string>
|
||||
<string name="history_header_subtitle">Optionale Historie bleibt auf deinem Gerät</string>
|
||||
<string name="history_hero_title">Vergangene Scans lokal prüfen</string>
|
||||
<string name="history_hero_subtitle">Gespeicherte Scans jederzeit suchen, exportieren oder löschen.</string>
|
||||
<string name="privacy_pill_no_ads">Keine Werbung</string>
|
||||
<string name="privacy_pill_no_tracking">Kein Tracking</string>
|
||||
<string name="privacy_pill_no_account">Kein Konto</string>
|
||||
<string name="local_controls">Lokale Kontrolle</string>
|
||||
<string name="no_saved_scans_yet">Noch keine gespeicherten Scans</string>
|
||||
<string name="empty_history_description">Aktiviere lokale Historie in den Einstellungen, um private Einträge auf diesem Gerät zu speichern.</string>
|
||||
<string name="scan_from_image">Aus Bild scannen</string>
|
||||
<string name="import_whitelist">Whitelist importieren</string>
|
||||
<string name="whitelist_loaded_count">Geladene registrierte IDs: %1$d</string>
|
||||
@@ -56,7 +65,7 @@
|
||||
<string name="share_batch">Stapel teilen</string>
|
||||
<string name="no_code_found_in_image">Im gewählten Bild wurde kein QR- oder Barcode gefunden.</string>
|
||||
<string name="image_scan_pick_title">%1$d Codes im Bild gefunden</string>
|
||||
<string name="image_scan_pick_subtitle">Wähle ein Ergebnis aus:</string>
|
||||
<string name="image_scan_pick_subtitle">Wähle das Ergebnis, das du verwenden möchtest. Erkennung und Auswertung erfolgen lokal.</string>
|
||||
<string name="image_scan_use_selected">Ausgewähltes verwenden</string>
|
||||
<string name="image_scan_failed">Dieses Bild konnte nicht gelesen werden. Bitte anderes Bild versuchen.</string>
|
||||
<string name="already_scanned">Bereits gescannt</string>
|
||||
@@ -80,4 +89,11 @@
|
||||
<string name="use_case_event_ticketing">Events & Ticketing</string>
|
||||
<string name="use_case_everyday_description">Vollständiger privater Scanner mit lokalem Verlauf und üblichen Ergebnisaktionen.</string>
|
||||
<string name="use_case_event_ticketing_description">Batch-Scanning, Duplikaterkennung, Whitelist-Import und Batch-Teilen.</string>
|
||||
<string name="settings_header_subtitle">Einstellungen, die dir Kontrolle geben</string>
|
||||
<string name="settings_hero_title">Deine Privatsphäre-Einstellungen</string>
|
||||
<string name="settings_hero_subtitle">Wähle lokale Historie, Sicherheitswarnungen, Feedback und die Scanneransicht, die zu deinem Workflow passt.</string>
|
||||
<string name="result_local_check">Lokale Prüfung</string>
|
||||
<string name="result_checked_on_device">Vor dem Öffnen auf dem Gerät geprüft</string>
|
||||
<string name="result_encoded_data">Codierte Daten</string>
|
||||
<string name="batch_ticket_privacy_note">Warnungen zu doppelten und nicht registrierten Tickets bleiben auf dem Gerät.</string>
|
||||
</resources>
|
||||
|
||||
@@ -45,6 +45,15 @@
|
||||
<string name="share_txt">TXT</string>
|
||||
<string name="share_csv">CSV</string>
|
||||
<string name="share_json">JSON</string>
|
||||
<string name="history_header_subtitle">Optional history stays on your device</string>
|
||||
<string name="history_hero_title">Review past scans locally</string>
|
||||
<string name="history_hero_subtitle">Search, export, or delete saved scans whenever you choose.</string>
|
||||
<string name="privacy_pill_no_ads">No ads</string>
|
||||
<string name="privacy_pill_no_tracking">No tracking</string>
|
||||
<string name="privacy_pill_no_account">No account</string>
|
||||
<string name="local_controls">Local controls</string>
|
||||
<string name="no_saved_scans_yet">No saved scans yet</string>
|
||||
<string name="empty_history_description">Enable local history in settings to keep a private record on this device.</string>
|
||||
<string name="scan_from_image">Scan from image</string>
|
||||
<string name="import_whitelist">Import whitelist</string>
|
||||
<string name="whitelist_loaded_count">Registered IDs loaded: %1$d</string>
|
||||
@@ -56,7 +65,7 @@
|
||||
<string name="share_batch">Share batch</string>
|
||||
<string name="no_code_found_in_image">No QR or barcode found in the selected image.</string>
|
||||
<string name="image_scan_pick_title">Found %1$d codes in image</string>
|
||||
<string name="image_scan_pick_subtitle">Choose a result to use:</string>
|
||||
<string name="image_scan_pick_subtitle">Choose the result you want to use. Detection and parsing happen locally.</string>
|
||||
<string name="image_scan_use_selected">Use selected</string>
|
||||
<string name="image_scan_failed">Could not read this image. Try another one.</string>
|
||||
<string name="already_scanned">Already scanned</string>
|
||||
@@ -80,4 +89,11 @@
|
||||
<string name="use_case_event_ticketing">Event & ticketing</string>
|
||||
<string name="use_case_everyday_description">Full personal scanner with local history and common result actions.</string>
|
||||
<string name="use_case_event_ticketing_description">Batch scanning, duplicate detection, whitelist import, and batch sharing.</string>
|
||||
<string name="settings_header_subtitle">Settings that keep you in control</string>
|
||||
<string name="settings_hero_title">Privacy settings are yours</string>
|
||||
<string name="settings_hero_subtitle">Choose local history, security warnings, feedback, and the scanner view that fits your workflow.</string>
|
||||
<string name="result_local_check">Local check</string>
|
||||
<string name="result_checked_on_device">Checked on device before opening</string>
|
||||
<string name="result_encoded_data">Encoded data</string>
|
||||
<string name="batch_ticket_privacy_note">Duplicate and unregistered ticket alerts stay on device.</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user