From c0e9b528978397fc9f50e8206b752a027415a825 Mon Sep 17 00:00:00 2001 From: Hadrian Burkhardt Date: Wed, 11 Feb 2026 03:23:42 +0100 Subject: [PATCH] nicer crosshair, icons instead of text buttons, share history --- README.md | 53 +++++---- app/build.gradle.kts | 1 + .../java/com/clean/scanner/MainActivity.kt | 3 +- .../clean/scanner/ui/CleanScannerAppRoot.kt | 36 +++---- .../scanner/ui/components/CameraPreview.kt | 34 +++++- .../clean/scanner/ui/screens/HistoryScreen.kt | 57 +++++++++- .../clean/scanner/ui/screens/HomeScreen.kt | 34 ++++-- .../clean/scanner/ui/screens/ScannerScreen.kt | 101 +++++++++++++----- .../java/com/clean/scanner/ui/theme/Theme.kt | 31 ++++++ app/src/main/res/values-de/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + build.gradle.kts | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 0 14 files changed, 275 insertions(+), 83 deletions(-) create mode 100644 app/src/main/java/com/clean/scanner/ui/theme/Theme.kt mode change 100755 => 100644 gradlew diff --git a/README.md b/README.md index c991d84..4e79c02 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,50 @@ # Clean Scanner (MVP) -Offline-first, ad-free QR/barcode scanner built with Kotlin + Compose + CameraX + ML Kit. +Offline-first, ad-free QR/barcode scanner built with Kotlin, Jetpack Compose, CameraX, and on-device ML Kit. ## Architektur -- `ui/`: Compose-Screens + ViewModels (MVVM presentation layer) -- `data/`: Scanner-Analyzer, Room entities/DAO, Repository +- `ui/`: Compose screens/components + ViewModels (MVVM) +- `data/`: ML Kit analyzer, Room entities/DAO, repository - `domain/`: app models (`ScanResult`, `ScanRecord`, `UrlRiskResult`) -- `settings/`: DataStore preferences (history toggle, warnings toggle) -- `util/`: URL risk scorer, clipboard, intents +- `settings/`: DataStore preferences (history + warnings toggles) +- `util/`: URL risk scoring, clipboard, intents ## Datenschutz - Keine Werbung - Keine Tracker/Analytics/Crashlytics -- Kein Backend/keine Webrequests +- Kein Backend, keine Servercalls - Keine `INTERNET`-Permission im Manifest ## MVP Features -- Startscreen mit Scan-Button und lokalem Historie-Toggle (Default: OFF) -- Scanner mit CameraX live preview, Taschenlampe, debounce/no double scan -- Ergebnis-Bottom-Sheet mit Copy/Share/Open/Scan again -- Lokale URL-Risikoheuristik mit Warn-Dialog ab Score `>= 3` -- Historie-Liste inkl. Suche, Swipe-delete, Alles-löschen -- Einstellungen: Historie an/aus, Warnungen an/aus, About +- Home: Scan-Button, lokaler Historie-Toggle (Default: OFF), Datenschutz-Dialog +- Scanner: CameraX Live-Preview, Fadenkreuz-Overlay, Taschenlampe, Debounce gegen Doppelscans +- Ergebnis-Bottom-Sheet: Typ/Inhalt + Copy/Share/Open/Scan again +- URL-Sicherheitswarnung bei lokalem `riskScore >= 3` (kein Blocken, nur Hinweis) +- Historie: Suche, Swipe-to-delete, Alles-löschen, Detailansicht mit Volltext +- Einstellungen: Historie an/aus (mit optionalem Löschen), Warnungen an/aus, About-Infos -## Run -1. In Android Studio: Open this folder as project. -2. Let Gradle sync dependencies. -3. Run app on emulator/device (API 24+). +## Voraussetzungen +- Android Studio (aktuell stabil) +- JDK 17+ +- Android SDK für `compileSdk = 35` + +## Build & Run +1. Projekt in Android Studio öffnen. +2. Gradle Sync ausführen. +3. App auf Emulator/Device (API 24+) starten. + +CLI: + +```bash +./gradlew :app:assembleDebug +./gradlew :app:installDebug +``` ## Tests -- URL Risk Scorer Tests: `app/src/test/java/com/clean/scanner/util/UrlRiskScorerTest.kt` (11 cases) +- Unit tests: -## Hinweis -In dieser Umgebung war kein `gradle`/`gradlew` verfügbar, daher konnte ich Builds/Tests hier nicht lokal ausführen. +```bash +./gradlew testDebugUnitTest +``` + +- URL-Risk-Scorer tests: `app/src/test/java/com/clean/scanner/util/UrlRiskScorerTest.kt` diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e80df87..8afe4d7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -74,6 +74,7 @@ dependencies { implementation("androidx.compose.ui:ui") implementation("androidx.compose.ui:ui-tooling-preview") implementation("androidx.compose.material3:material3") + implementation("androidx.compose.material:material-icons-extended") implementation("com.google.android.material:material:1.12.0") implementation("androidx.navigation:navigation-compose:2.8.2") diff --git a/app/src/main/java/com/clean/scanner/MainActivity.kt b/app/src/main/java/com/clean/scanner/MainActivity.kt index d88f4da..abe73a2 100644 --- a/app/src/main/java/com/clean/scanner/MainActivity.kt +++ b/app/src/main/java/com/clean/scanner/MainActivity.kt @@ -6,13 +6,14 @@ import androidx.activity.compose.setContent import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import com.clean.scanner.ui.CleanScannerAppRoot +import com.clean.scanner.ui.theme.CleanScannerTheme class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val container = (application as CleanScannerApp).appContainer setContent { - MaterialTheme { + CleanScannerTheme { Surface(color = MaterialTheme.colorScheme.background) { CleanScannerAppRoot(container) } 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 607698e..8f5bee0 100644 --- a/app/src/main/java/com/clean/scanner/ui/CleanScannerAppRoot.kt +++ b/app/src/main/java/com/clean/scanner/ui/CleanScannerAppRoot.kt @@ -1,5 +1,7 @@ package com.clean.scanner.ui +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Scaffold @@ -9,17 +11,17 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.clean.scanner.AppContainer import com.clean.scanner.R import com.clean.scanner.ui.screens.HistoryScreen -import com.clean.scanner.ui.screens.HomeScreen import com.clean.scanner.ui.screens.ScannerScreen import com.clean.scanner.ui.screens.SettingsScreen -private enum class RootTab { Home, History, Settings } +private enum class RootTab { Scanner, History, Settings } @Composable fun CleanScannerAppRoot(container: AppContainer) { @@ -28,17 +30,16 @@ fun CleanScannerAppRoot(container: AppContainer) { val appState by appViewModel.uiState.collectAsStateWithLifecycle() val scannerState by scannerViewModel.uiState.collectAsStateWithLifecycle() - var activeTab by remember { mutableStateOf(RootTab.Home) } - var showScanner by remember { mutableStateOf(false) } + var activeTab by remember { mutableStateOf(RootTab.Scanner) } Scaffold( bottomBar = { NavigationBar { NavigationBarItem( - selected = activeTab == RootTab.Home, + selected = activeTab == RootTab.Scanner, onClick = { - activeTab = RootTab.Home - showScanner = false + activeTab = RootTab.Scanner + scannerViewModel.resumeScanning() }, label = { Text(stringResource(R.string.scan)) }, icon = {} @@ -47,7 +48,6 @@ fun CleanScannerAppRoot(container: AppContainer) { selected = activeTab == RootTab.History, onClick = { activeTab = RootTab.History - showScanner = false }, label = { Text(stringResource(R.string.history)) }, icon = {} @@ -56,7 +56,6 @@ fun CleanScannerAppRoot(container: AppContainer) { selected = activeTab == RootTab.Settings, onClick = { activeTab = RootTab.Settings - showScanner = false }, label = { Text(stringResource(R.string.settings)) }, icon = {} @@ -64,9 +63,9 @@ fun CleanScannerAppRoot(container: AppContainer) { } } ) { padding -> - androidx.compose.foundation.layout.Box(modifier = androidx.compose.ui.Modifier.padding(padding)) { - when { - showScanner -> ScannerScreen( + Box(modifier = Modifier.padding(padding)) { + when (activeTab) { + RootTab.Scanner -> ScannerScreen( analysisEnabled = scannerState.analysisEnabled, lastResult = scannerState.lastResult, warningsEnabled = appState.warningsEnabled, @@ -74,16 +73,7 @@ fun CleanScannerAppRoot(container: AppContainer) { onScanAgain = scannerViewModel::resumeScanning ) - activeTab == RootTab.Home -> HomeScreen( - historyEnabled = appState.historyEnabled, - onHistoryToggle = { appViewModel.setHistoryEnabled(it, false) }, - onScanClick = { - showScanner = true - scannerViewModel.resumeScanning() - } - ) - - activeTab == RootTab.History -> HistoryScreen( + RootTab.History -> HistoryScreen( query = appState.searchQuery, history = appState.history, onQueryChange = appViewModel::setQuery, @@ -91,7 +81,7 @@ fun CleanScannerAppRoot(container: AppContainer) { onClearAll = appViewModel::clearHistory ) - activeTab == RootTab.Settings -> SettingsScreen( + RootTab.Settings -> SettingsScreen( historyEnabled = appState.historyEnabled, warningsEnabled = appState.warningsEnabled, onHistoryToggle = appViewModel::setHistoryEnabled, diff --git a/app/src/main/java/com/clean/scanner/ui/components/CameraPreview.kt b/app/src/main/java/com/clean/scanner/ui/components/CameraPreview.kt index 116ffd5..0c627a3 100644 --- a/app/src/main/java/com/clean/scanner/ui/components/CameraPreview.kt +++ b/app/src/main/java/com/clean/scanner/ui/components/CameraPreview.kt @@ -1,6 +1,8 @@ package com.clean.scanner.ui.components import android.annotation.SuppressLint +import android.view.MotionEvent +import android.view.ScaleGestureDetector import androidx.camera.core.CameraSelector import androidx.camera.core.ImageAnalysis import androidx.camera.core.Preview @@ -13,12 +15,14 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.compose.ui.viewinterop.AndroidView import androidx.core.content.ContextCompat import com.clean.scanner.data.scanner.MlKitBarcodeAnalyzer import java.util.concurrent.ExecutorService import java.util.concurrent.Executors +import kotlin.math.max +import kotlin.math.min @SuppressLint("UnsafeOptInUsageError") @Composable @@ -34,6 +38,21 @@ fun CameraPreview( val cameraExecutor: ExecutorService = remember { Executors.newSingleThreadExecutor() } val previewView = remember { PreviewView(context) } val cameraRef = remember { mutableStateOf(null) } + val zoomRatio = remember { mutableStateOf(1f) } + + val scaleGestureDetector = remember { + ScaleGestureDetector(context, object : ScaleGestureDetector.SimpleOnScaleGestureListener() { + override fun onScale(detector: ScaleGestureDetector): Boolean { + val camera = cameraRef.value ?: return false + val zoomState = camera.cameraInfo.zoomState.value ?: return false + val nextZoom = zoomRatio.value * detector.scaleFactor + val clampedZoom = max(zoomState.minZoomRatio, min(nextZoom, zoomState.maxZoomRatio)) + zoomRatio.value = clampedZoom + camera.cameraControl.setZoomRatio(clampedZoom) + return true + } + }) + } DisposableEffect(Unit) { onDispose { @@ -70,6 +89,7 @@ fun CameraPreview( onTorchAvailabilityChanged(camera.cameraInfo.hasFlashUnit()) cameraRef.value = camera + zoomRatio.value = camera.cameraInfo.zoomState.value?.zoomRatio ?: 1f } LaunchedEffect(torchEnabled) { @@ -78,6 +98,16 @@ fun CameraPreview( AndroidView( modifier = modifier, - factory = { previewView } + factory = { + previewView.apply { + setOnTouchListener { _, event -> + if (event.actionMasked == MotionEvent.ACTION_DOWN) { + performClick() + } + scaleGestureDetector.onTouchEvent(event) + 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 3134eb1..8876dca 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 @@ -1,7 +1,9 @@ package com.clean.scanner.ui.screens +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -19,10 +21,12 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +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.domain.ScanRecord +import com.clean.scanner.util.Intents import java.text.DateFormat import java.util.Date @@ -34,7 +38,9 @@ fun HistoryScreen( onDelete: (Long) -> Unit, onClearAll: () -> Unit ) { + val context = LocalContext.current val showDeleteAll = remember { mutableStateOf(false) } + val selectedItem = remember { mutableStateOf(null) } if (showDeleteAll.value) { AlertDialog( @@ -55,6 +61,20 @@ fun HistoryScreen( ) } + val detail = selectedItem.value + if (detail != null) { + AlertDialog( + onDismissRequest = { selectedItem.value = null }, + title = { Text(text = detail.type) }, + text = { Text(text = detail.content) }, + confirmButton = { + TextButton(onClick = { selectedItem.value = null }) { + Text(text = stringResource(R.string.confirm)) + } + } + ) + } + Column( modifier = Modifier .fillMaxSize() @@ -68,13 +88,28 @@ fun HistoryScreen( label = { Text(stringResource(R.string.search)) } ) - TextButton(onClick = { showDeleteAll.value = true }) { - Text(stringResource(R.string.delete_all)) + Row(modifier = Modifier.fillMaxWidth()) { + TextButton( + onClick = { + val exportText = buildHistoryExportText(history) + Intents.shareText(context, exportText) + }, + enabled = history.isNotEmpty() + ) { + Text(stringResource(R.string.share_history)) + } + TextButton(onClick = { showDeleteAll.value = true }) { + Text(stringResource(R.string.delete_all)) + } } LazyColumn { items(history, key = { it.id }) { item -> - HistoryRow(item = item, onDelete = onDelete) + HistoryRow( + item = item, + onDelete = onDelete, + onOpenDetails = { selectedItem.value = item } + ) } } } @@ -82,7 +117,11 @@ fun HistoryScreen( @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun HistoryRow(item: ScanRecord, onDelete: (Long) -> Unit) { +private fun HistoryRow( + item: ScanRecord, + onDelete: (Long) -> Unit, + onOpenDetails: () -> Unit +) { val dismissState = rememberSwipeToDismissBoxState( confirmValueChange = { if (it == SwipeToDismissBoxValue.EndToStart || it == SwipeToDismissBoxValue.StartToEnd) { @@ -100,6 +139,7 @@ private fun HistoryRow(item: ScanRecord, onDelete: (Long) -> Unit) { content = { Column(modifier = Modifier .fillMaxWidth() + .clickable { onOpenDetails() } .padding(vertical = 12.dp)) { Text(text = item.type) Text(text = item.content, maxLines = 2) @@ -108,3 +148,12 @@ private fun HistoryRow(item: ScanRecord, onDelete: (Long) -> Unit) { } ) } + +private fun buildHistoryExportText(history: List): String { + if (history.isEmpty()) return "" + val formatter = DateFormat.getDateTimeInstance() + return history.joinToString(separator = "\n\n") { item -> + val time = formatter.format(Date(item.timestamp)) + "$time\n${item.type}\n${item.content}" + } +} diff --git a/app/src/main/java/com/clean/scanner/ui/screens/HomeScreen.kt b/app/src/main/java/com/clean/scanner/ui/screens/HomeScreen.kt index 0488def..cd81249 100644 --- a/app/src/main/java/com/clean/scanner/ui/screens/HomeScreen.kt +++ b/app/src/main/java/com/clean/scanner/ui/screens/HomeScreen.kt @@ -7,11 +7,15 @@ 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 import androidx.compose.material3.Button import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Switch import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign @@ -24,6 +28,21 @@ fun HomeScreen( onHistoryToggle: (Boolean) -> Unit, onScanClick: () -> Unit ) { + val showPrivacyDialog = remember { mutableStateOf(false) } + + if (showPrivacyDialog.value) { + AlertDialog( + onDismissRequest = { showPrivacyDialog.value = false }, + title = { Text(text = stringResource(R.string.privacy)) }, + text = { Text(text = stringResource(R.string.privacy_text)) }, + confirmButton = { + TextButton(onClick = { showPrivacyDialog.value = false }) { + Text(text = stringResource(R.string.confirm)) + } + } + ) + } + Column( modifier = Modifier .fillMaxSize() @@ -47,13 +66,12 @@ fun HomeScreen( Spacer(modifier = Modifier.height(24.dp)) - Text( - text = stringResource(R.string.privacy), - style = MaterialTheme.typography.titleMedium - ) - Text( - text = stringResource(R.string.privacy_text), - textAlign = TextAlign.Start - ) + TextButton(onClick = { showPrivacyDialog.value = true }) { + Text( + text = stringResource(R.string.privacy), + style = MaterialTheme.typography.titleMedium, + textAlign = TextAlign.Start + ) + } } } 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 61c84c3..e84c4d3 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 @@ -7,17 +7,27 @@ import android.net.Uri import android.provider.Settings import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row 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.horizontalScroll +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ContentCopy +import androidx.compose.material.icons.filled.Share import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Switch import androidx.compose.material3.Text @@ -31,8 +41,11 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat @@ -96,12 +109,42 @@ fun ScannerScreen( } ) - Box( + Canvas( modifier = Modifier .align(Alignment.Center) .fillMaxWidth(0.7f) .height(220.dp) - .background(Color.Transparent) + ) { + val guideColor = Color(0xFF7CE6C6) + val cx = size.width / 2f + val cy = size.height / 2f + drawRoundRect( + color = guideColor.copy(alpha = 0.10f), + cornerRadius = CornerRadius(26f, 26f) + ) + drawRoundRect( + color = guideColor.copy(alpha = 0.90f), + cornerRadius = CornerRadius(26f, 26f), + style = Stroke(width = 4f) + ) + drawCircle( + color = guideColor.copy(alpha = 0.90f), + radius = 8f, + center = androidx.compose.ui.geometry.Offset(cx, cy) + ) + } + Text( + text = stringResource(R.string.pinch_to_zoom_hint), + color = Color.White, + textAlign = TextAlign.Center, + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(bottom = 40.dp) + .background( + color = Color.Black.copy(alpha = 0.35f), + shape = RoundedCornerShape(18.dp) + ) + .padding(horizontal = 14.dp, vertical = 8.dp) ) if (torchAvailable) { @@ -126,7 +169,7 @@ fun ScannerScreen( } if (lastResult != null) { - ModalBottomSheet(onDismissRequest = {}) { + ModalBottomSheet(onDismissRequest = onScanAgain) { Column( modifier = Modifier .fillMaxWidth() @@ -135,28 +178,38 @@ fun ScannerScreen( ) { Text(text = "${stringResource(R.string.content_type)}: ${lastResult.type}") Text(text = "${stringResource(R.string.content_value)}: ${lastResult.content}") - Button(onClick = { ClipboardUtil.copy(context, lastResult.content) }) { - Text(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) - } - }) { - Text(stringResource(R.string.open)) + Row( + modifier = Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + 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) + } + }) { + Text(stringResource(R.string.open)) + } + } + IconButton(onClick = { Intents.shareText(context, lastResult.content) }) { + Icon( + imageVector = Icons.Default.Share, + contentDescription = stringResource(R.string.share) + ) } - } - Button(onClick = { Intents.shareText(context, lastResult.content) }) { - Text(stringResource(R.string.share)) - } - Button(onClick = onScanAgain) { - Text(stringResource(R.string.scan_again)) } } } diff --git a/app/src/main/java/com/clean/scanner/ui/theme/Theme.kt b/app/src/main/java/com/clean/scanner/ui/theme/Theme.kt new file mode 100644 index 0000000..bf5fcec --- /dev/null +++ b/app/src/main/java/com/clean/scanner/ui/theme/Theme.kt @@ -0,0 +1,31 @@ +package com.clean.scanner.ui.theme + +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +private val LightColors = lightColorScheme() +private val DarkColors = darkColorScheme() + +@Composable +fun CleanScannerTheme(content: @Composable () -> Unit) { + val darkTheme = isSystemInDarkTheme() + val context = LocalContext.current + + val colorScheme = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } else { + if (darkTheme) DarkColors else LightColors + } + + MaterialTheme( + colorScheme = colorScheme, + content = content + ) +} diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 78fb9db..ce74424 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -31,4 +31,6 @@ Typ Inhalt Kamera erlauben + Zum Zoomen bei kleinen Codes mit zwei Fingern aufziehen + Historie teilen diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 734b747..d0cbbb7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -31,4 +31,6 @@ Type Content Allow camera + Pinch to zoom for small codes + Share history diff --git a/build.gradle.kts b/build.gradle.kts index 07acc53..60fafab 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("com.android.application") version "8.7.0" apply false + id("com.android.application") version "8.13.2" apply false id("org.jetbrains.kotlin.android") version "1.9.24" apply false id("com.google.devtools.ksp") version "1.9.24-1.0.20" apply false } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2fa91c5..f407850 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew old mode 100755 new mode 100644