diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index d3ec1d0..ef5237f 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -1,6 +1,6 @@ name: Build APK -# Bouwt een getekende release-APK op elke versietag (v0.1.0, …) en op handmatige trigger. +# Builds a signed release APK on every version tag (v0.1.0, …) and on manual trigger. on: push: tags: @@ -9,26 +9,26 @@ on: jobs: build: - # Dedicated x86_64 runner (LXC op Proxmox) — de globale ARM64-runners hebben deze - # label niet, dus zij pikken deze job niet op. + # Dedicated x86_64 runner (LXC on Proxmox) — the global ARM64 runners don't have + # this label, so they won't pick up this job. runs-on: paperscan - # Laat het automatische Actions-token een release aanmaken + assets uploaden. + # Let the automatic Actions token create a release + upload assets. permissions: contents: write - # Image met Android SDK + JDK 17 + Gradle voorgeïnstalleerd. + # Image with Android SDK + JDK 17 + Gradle preinstalled. container: image: mingc/android-build-box:latest steps: - name: Checkout uses: actions/checkout@v4 - - name: Herstel signing keystore uit secret + - name: Restore signing keystore from secret run: | echo "$SIGNING_KEYSTORE_BASE64" | base64 -d > "$GITHUB_WORKSPACE/release.jks" env: SIGNING_KEYSTORE_BASE64: ${{ secrets.SIGNING_KEYSTORE_BASE64 }} - - name: Genereer Gradle wrapper indien nodig + - name: Generate Gradle wrapper if needed run: | if [ ! -f gradle/wrapper/gradle-wrapper.jar ]; then gradle wrapper --gradle-version 8.11.1 @@ -44,7 +44,7 @@ jobs: SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }} SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }} - - name: Publiceer release met APK als download + - name: Publish release with APK as a download env: TOKEN: ${{ github.token }} TAG: ${{ github.ref_name }} @@ -54,22 +54,22 @@ jobs: APK=$(ls app/build/outputs/apk/release/*.apk | head -1) AUTH="Authorization: token $TOKEN" - # Hergebruik een bestaande release voor deze tag, of maak 'm aan. + # Reuse an existing release for this tag, or create it. REL_ID=$(curl -sS -H "$AUTH" "$API/releases/tags/$TAG" | grep -oP '"id":\s*\K[0-9]+' | head -1 || true) if [ -z "$REL_ID" ]; then REL_ID=$(curl -sS -X POST "$API/releases" -H "$AUTH" -H "Content-Type: application/json" \ - -d "{\"tag_name\":\"$TAG\",\"name\":\"PaperScan $TAG\",\"body\":\"Automatische build van $TAG.\"}" \ + -d "{\"tag_name\":\"$TAG\",\"name\":\"PaperScan $TAG\",\"body\":\"Automated build of $TAG.\"}" \ | grep -oP '"id":\s*\K[0-9]+' | head -1) fi echo "Release id: $REL_ID" - # Verwijder een eventueel bestaande asset met dezelfde naam (bij re-run/hertag). + # Remove any existing asset with the same name (on re-run/re-tag). ASSET_NAME="paperscan-$TAG.apk" OLD=$(curl -sS -H "$AUTH" "$API/releases/$REL_ID/assets" \ | grep -oP "\"id\":\s*\K[0-9]+(?=[^}]*\"name\":\s*\"$ASSET_NAME\")" | head -1 || true) if [ -n "$OLD" ]; then curl -sS -X DELETE -H "$AUTH" "$API/releases/$REL_ID/assets/$OLD"; fi - # Upload de getekende APK als download-asset. + # Upload the signed APK as a downloadable asset. curl -sS -X POST "$API/releases/$REL_ID/assets?name=$ASSET_NAME" \ -H "$AUTH" -H "Content-Type: application/octet-stream" \ --data-binary @"$APK" | grep -oP '"browser_download_url":\s*"\K[^"]+' || true diff --git a/README.md b/README.md index 6a4881e..f4e80ed 100644 --- a/README.md +++ b/README.md @@ -1,110 +1,117 @@ # PaperScan -Een lokale, privacy-bewuste documentscanner voor Android. Scant papieren documenten -met de camera, detecteert automatisch de randen, corrigeert het perspectief en bundelt -meerdere pagina's in één PDF. Die PDF kun je delen via Android of rechtstreeks uploaden -naar je zelf-gehoste **Paperless-ngx**. +A local, privacy-conscious document scanner for Android. Scan paper documents with your +camera, automatically detect the edges, correct the perspective, and bundle multiple pages +into a single PDF. That PDF can be shared through Android or uploaded directly to your +self-hosted **Paperless-ngx**. -**Alles gebeurt op het toestel.** Camerabeelden, randdetectie (OpenCV) en PDF-creatie -verlaten je telefoon niet. De enige netwerkverbinding is de optionele upload naar de -Paperless-server die jij zelf configureert. Er is geen AI of cloud-verwerking. +**Everything happens on the device.** Camera frames, edge detection (OpenCV) and PDF +creation never leave your phone. The only network connection is the optional upload to the +Paperless server you configure yourself. There is no AI or cloud processing. -## Functies +## Features -- 📷 Camerapreview met multi-page vastleggen (pagina na pagina toevoegen) -- ✂️ Automatische randdetectie + perspectiefcorrectie (OpenCV), met handmatige hoekcorrectie -- 🎨 Filters per pagina: kleur, grijstinten, zwart-wit (documentmodus) -- 🔄 Pagina's draaien, herordenen en verwijderen -- 📄 Export naar multi-page PDF (A4 of originele grootte) -- ☁️ Directe upload naar Paperless-ngx **of** delen via het Android-deelmenu -- 🔐 API-token versleuteld opgeslagen (Android Keystore / EncryptedSharedPreferences) +- 📷 Camera preview with multi-page capture (add pages one by one) +- ✂️ Automatic edge detection + perspective correction (OpenCV), with manual corner adjustment +- 🎨 Per-page filters: color, grayscale, black & white (document mode) +- 🔄 Rotate, reorder and delete pages +- 📄 Export to a multi-page PDF (A4 or original size) +- ☁️ Direct upload to Paperless-ngx **or** share via the Android share sheet +- 🔊 Capture feedback: shutter sound (only when the ringer isn't silenced) + a brief flash +- 🔐 API token stored encrypted (Android Keystore / EncryptedSharedPreferences) ## Tech Kotlin · Jetpack Compose · CameraX · OpenCV (`org.opencv:opencv` via Maven) · `PdfDocument` · OkHttp · DataStore. minSdk 26 (Android 8), target/compile SDK 35. -## Lokaal bouwen +## Building locally -1. Open het project in **Android Studio** (Ladybug of nieuwer). Dit genereert automatisch - de Gradle wrapper (`gradlew` + `gradle-wrapper.jar`). -2. Sluit een toestel aan of start een emulator en druk op **Run**. +1. Open the project in **Android Studio** (Ladybug or newer). This automatically generates + the Gradle wrapper (`gradlew` + `gradle-wrapper.jar`). +2. Connect a device or start an emulator and press **Run**. -Of vanaf de command line (na `gradle wrapper` één keer): +Or from the command line (after `gradle wrapper` once): ```bash -./gradlew assembleDebug # debug-APK, geen signing nodig -./gradlew assembleRelease # release-APK, vereist signing (zie onder) +./gradlew assembleDebug # debug APK, no signing required +./gradlew assembleRelease # release APK, requires signing (see below) ``` -De release-APK verschijnt in `app/build/outputs/apk/release/`. +The release APK ends up in `app/build/outputs/apk/release/`. -## Signing key aanmaken +## Creating a signing key -Nodig voor release-builds (en dus voor updates die elkaar kunnen overschrijven). +Required for release builds (and therefore for updates that can overwrite each other). ```bash keytool -genkey -v -keystore release.jks -keyalg RSA -keysize 2048 \ -validity 10000 -alias paperscan ``` -Bewaar `release.jks` **veilig en buiten git** (staat al in `.gitignore`). +Keep `release.jks` **safe and out of git** (already in `.gitignore`). -Voor lokale release-builds maak je een `keystore.properties` in de projectroot -(zie `keystore.properties.example`): +For local release builds, create a `keystore.properties` in the project root +(see `keystore.properties.example`): ```properties -storeFile=/pad/naar/release.jks +storeFile=/path/to/release.jks storePassword=... keyAlias=paperscan keyPassword=... ``` -## Bouwen via Gitea Actions +## Building via Gitea Actions -De workflow `.gitea/workflows/build.yml` bouwt een getekende release-APK bij elke -versietag (`git tag v0.1.0 && git push --tags`) of via een handmatige run. +The workflow `.gitea/workflows/build.yml` builds a signed release APK on every version tag +(`git tag v0.1.0 && git push --tags`) or via a manual run, and publishes it as a **Release** +with the APK attached as a downloadable asset. -Zet in je Gitea-repo onder **Settings → Actions → Secrets** deze secrets klaar: +Add these secrets under **Settings → Actions → Secrets** in your Gitea repo: -| Secret | Inhoud | +| Secret | Contents | | --- | --- | -| `SIGNING_KEYSTORE_BASE64` | `base64 -w0 release.jks` (de keystore als base64) | -| `SIGNING_STORE_PASSWORD` | wachtwoord van de keystore | -| `SIGNING_KEY_ALIAS` | bv. `paperscan` | -| `SIGNING_KEY_PASSWORD` | wachtwoord van de sleutel | +| `SIGNING_KEYSTORE_BASE64` | `base64 -w0 release.jks` (the keystore as base64) | +| `SIGNING_STORE_PASSWORD` | keystore password | +| `SIGNING_KEY_ALIAS` | e.g. `paperscan` | +| `SIGNING_KEY_PASSWORD` | key password | -De build gebruikt env-vars, dus er komt niets gevoeligs in de repo terecht. De APK -komt als artifact `paperscan-release` beschikbaar bij de workflow-run. +The build uses env vars, so nothing sensitive ends up in the repo. -> Zorg dat je Gitea Actions-runner Docker-images kan trekken (`mingc/android-build-box`). +> The build runs in a container (`mingc/android-build-box`), so the Actions runner must be +> **x86_64** — the Android build tools (aapt2) are x86_64-Linux only and won't run natively +> on an ARM64 runner. -## Paperless-ngx instellen (in de app) +## Setting up Paperless-ngx (in the app) -1. Maak in Paperless een API-token aan: **Instellingen → profiel → API-token** - (of via `/api/token/`). -2. Open in PaperScan het tandwiel → vul **server-URL** (bv. `https://paperless.thuis.lan`) - en het **token** in → **Verbinding testen** → **Opslaan**. +1. Create an API token in Paperless: **Settings → profile → API token** (or via `/api/token/`). +2. In PaperScan, open the gear icon → enter the **server URL** (e.g. `https://paperless.home.lan`) + and the **token** → **Test connection** → **Save**. -Gebruik bij voorkeur **https**; bij `http://` waarschuwt de app dat token en documenten -onversleuteld over het netwerk gaan. +Prefer **https**; with `http://` the app warns that the token and documents travel over the +network unencrypted. -## Projectstructuur +## Project layout ``` app/src/main/java/eu/geyskens/pdfscan/ -├── PaperScanApp.kt # Application; laadt OpenCV native libs -├── MainActivity.kt # Compose host + navigatie -├── ScanViewModel.kt # scan-sessie, rendering, export, upload -├── data/ # Page-model, ScanFilter, SettingsRepository +├── PaperScanApp.kt # Application; loads OpenCV native libs +├── MainActivity.kt # Compose host + navigation +├── ScanViewModel.kt # scan session, rendering, export, upload +├── data/ # Page model, ScanFilter, SettingsRepository ├── scan/ # EdgeDetector, ImageProcessor, BitmapIo (OpenCV) ├── pdf/ # PdfBuilder (multi-page PDF) -├── paperless/ # PaperlessClient (OkHttp REST) +├── paperless/ # PaperlessClient (OkHttp REST + task polling) └── ui/screens/ # Camera, Pages, Crop, Export, Settings ``` -## Roadmap / ideeën +## Roadmap -- Live randdetectie-overlay tijdens het richten (nu bij vastleggen) -- OCR-tekstlaag (bv. Tesseract) volledig on-device -- Batch-import van bestaande foto's uit de galerij +Tracked as issues in the repository: + +- Live edge-detection overlay while aiming (like Microsoft Lens) +- Internationalization: extract UI strings to resources + add translations +- Batch import scans from the gallery + +> OCR is intentionally **not** done on-device: Paperless-ngx already runs OCR (via +> OCRmyPDF/Tesseract) when it consumes the uploaded document. diff --git a/app/src/main/java/eu/geyskens/pdfscan/ScanViewModel.kt b/app/src/main/java/eu/geyskens/pdfscan/ScanViewModel.kt index 4106456..6abf092 100644 --- a/app/src/main/java/eu/geyskens/pdfscan/ScanViewModel.kt +++ b/app/src/main/java/eu/geyskens/pdfscan/ScanViewModel.kt @@ -115,7 +115,7 @@ class ScanViewModel(app: Application) : AndroidViewModel(app) { val s = settings.value val token = settingsRepo.getToken() if (s.paperlessUrl.isBlank() || token.isNullOrBlank()) { - return PaperlessClient.Result.Failure("Paperless is nog niet ingesteld (URL + token).") + return PaperlessClient.Result.Failure("Paperless isn't set up yet (URL + token).") } return PaperlessClient(s.paperlessUrl, token).upload(pdf, title) } @@ -133,7 +133,7 @@ class ScanViewModel(app: Application) : AndroidViewModel(app) { suspend fun testPaperless(url: String, token: String): PaperlessClient.Result { if (url.isBlank() || token.isBlank()) { - return PaperlessClient.Result.Failure("Vul eerst URL en token in.") + return PaperlessClient.Result.Failure("Enter a URL and token first.") } return PaperlessClient(url.trim(), token.trim()).testConnection() } diff --git a/app/src/main/java/eu/geyskens/pdfscan/paperless/PaperlessClient.kt b/app/src/main/java/eu/geyskens/pdfscan/paperless/PaperlessClient.kt index 5b25f8f..270a5b9 100644 --- a/app/src/main/java/eu/geyskens/pdfscan/paperless/PaperlessClient.kt +++ b/app/src/main/java/eu/geyskens/pdfscan/paperless/PaperlessClient.kt @@ -53,11 +53,11 @@ class PaperlessClient( client.newCall(request).execute().use { resp -> when { resp.isSuccessful -> Result.Success - resp.code == 401 || resp.code == 403 -> Result.Failure("Ongeldig token (${resp.code}).") - else -> Result.Failure("Server antwoordde met ${resp.code}.") + resp.code == 401 || resp.code == 403 -> Result.Failure("Invalid token (${resp.code}).") + else -> Result.Failure("Server responded with ${resp.code}.") } } - }.getOrElse { Result.Failure(it.message ?: "Verbinding mislukt.") } + }.getOrElse { Result.Failure(it.message ?: "Connection failed.") } } /** @@ -85,12 +85,12 @@ class PaperlessClient( val posted: kotlin.Result = withContext(NonCancellable) { runCatching { client.newCall(request).execute().use { resp -> - if (!resp.isSuccessful) error("Upload mislukt: ${resp.code} ${resp.message}") + if (!resp.isSuccessful) error("Upload failed: ${resp.code} ${resp.message}") resp.body?.string()?.trim()?.trim('"')?.takeIf { it.isNotBlank() } } } } - val taskId = posted.getOrElse { return@withContext Result.Failure(it.message ?: "Upload mislukt.") } + val taskId = posted.getOrElse { return@withContext Result.Failure(it.message ?: "Upload failed.") } // Older servers may not return a usable id; then we can only confirm acceptance. if (taskId == null || taskId.equals("OK", ignoreCase = true)) return@withContext Result.Pending @@ -119,7 +119,7 @@ class PaperlessClient( "FAILURE" -> { val msg = task.result.orEmpty() return if (msg.contains("duplicate", ignoreCase = true)) Result.Duplicate - else Result.Failure(msg.ifBlank { "Paperless kon het document niet verwerken." }) + else Result.Failure(msg.ifBlank { "Paperless could not process the document." }) } // PENDING / STARTED / RETRY / unknown → keep waiting. } diff --git a/app/src/main/java/eu/geyskens/pdfscan/scan/BitmapIo.kt b/app/src/main/java/eu/geyskens/pdfscan/scan/BitmapIo.kt index 25c4c5f..1feb94b 100644 --- a/app/src/main/java/eu/geyskens/pdfscan/scan/BitmapIo.kt +++ b/app/src/main/java/eu/geyskens/pdfscan/scan/BitmapIo.kt @@ -21,7 +21,7 @@ object BitmapIo { val opts = BitmapFactory.Options().apply { inSampleSize = sample } val bitmap = BitmapFactory.decodeFile(file.absolutePath, opts) - ?: error("Kon afbeelding niet decoderen: ${file.name}") + ?: error("Could not decode image: ${file.name}") val orientation = ExifInterface(file.absolutePath) .getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL) diff --git a/app/src/main/java/eu/geyskens/pdfscan/ui/screens/CameraScreen.kt b/app/src/main/java/eu/geyskens/pdfscan/ui/screens/CameraScreen.kt index 0e5f46b..55aac25 100644 --- a/app/src/main/java/eu/geyskens/pdfscan/ui/screens/CameraScreen.kt +++ b/app/src/main/java/eu/geyskens/pdfscan/ui/screens/CameraScreen.kt @@ -77,13 +77,13 @@ private fun PermissionRequest(onRequest: () -> Unit) { Box(Modifier.fillMaxSize().padding(32.dp), contentAlignment = Alignment.Center) { androidx.compose.foundation.layout.Column(horizontalAlignment = Alignment.CenterHorizontally) { Text( - "PaperScan heeft cameratoegang nodig om documenten te scannen. " + - "Er wordt niets naar de cloud gestuurd.", + "PaperScan needs camera access to scan documents. " + + "Nothing is sent to the cloud.", color = Color.White, textAlign = TextAlign.Center, ) androidx.compose.foundation.layout.Spacer(Modifier.size(16.dp)) - Button(onClick = onRequest) { Text("Toegang geven") } + Button(onClick = onRequest) { Text("Grant access") } } } } @@ -143,7 +143,7 @@ private fun CameraContent(vm: ScanViewModel, navController: androidx.navigation. onClick = { navController.navigate(Routes.SETTINGS) }, modifier = Modifier.align(Alignment.TopEnd).statusBarsPadding().padding(8.dp), ) { - Icon(Icons.Filled.Settings, contentDescription = "Instellingen", tint = Color.White) + Icon(Icons.Filled.Settings, contentDescription = "Settings", tint = Color.White) } // Bottom controls. @@ -204,7 +204,7 @@ private fun CameraContent(vm: ScanViewModel, navController: androidx.navigation. ) { Icon( Icons.Filled.Check, - contentDescription = "Klaar", + contentDescription = "Done", tint = if (pages.isEmpty()) Color.Gray else Color.White, ) } diff --git a/app/src/main/java/eu/geyskens/pdfscan/ui/screens/CropScreen.kt b/app/src/main/java/eu/geyskens/pdfscan/ui/screens/CropScreen.kt index 4394a19..41b6c1e 100644 --- a/app/src/main/java/eu/geyskens/pdfscan/ui/screens/CropScreen.kt +++ b/app/src/main/java/eu/geyskens/pdfscan/ui/screens/CropScreen.kt @@ -135,9 +135,9 @@ fun CropScreen(vm: ScanViewModel, navController: NavController, pageId: String) Modifier.fillMaxWidth().padding(12.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - FilterOption("Kleur", filter == ScanFilter.COLOR) { filter = ScanFilter.COLOR } - FilterOption("Grijs", filter == ScanFilter.GRAYSCALE) { filter = ScanFilter.GRAYSCALE } - FilterOption("Zwart-wit", filter == ScanFilter.BLACK_WHITE) { filter = ScanFilter.BLACK_WHITE } + FilterOption("Color", filter == ScanFilter.COLOR) { filter = ScanFilter.COLOR } + FilterOption("Grayscale", filter == ScanFilter.GRAYSCALE) { filter = ScanFilter.GRAYSCALE } + FilterOption("Black & white", filter == ScanFilter.BLACK_WHITE) { filter = ScanFilter.BLACK_WHITE } } Row( @@ -145,7 +145,7 @@ fun CropScreen(vm: ScanViewModel, navController: NavController, pageId: String) horizontalArrangement = Arrangement.spacedBy(8.dp), ) { OutlinedButton(onClick = { vm.rotate(pageId) }, modifier = Modifier.weight(1f)) { - Text("Draaien") + Text("Rotate") } OutlinedButton( onClick = { @@ -153,7 +153,7 @@ fun CropScreen(vm: ScanViewModel, navController: NavController, pageId: String) full.forEachIndexed { i, p -> corners[i].value = PointF(p.x, p.y) } }, modifier = Modifier.weight(1f), - ) { Text("Herstel") } + ) { Text("Reset") } Button( onClick = { vm.updateCorners(pageId, corners.map { PointF(it.value.x, it.value.y) }) @@ -161,7 +161,7 @@ fun CropScreen(vm: ScanViewModel, navController: NavController, pageId: String) navController.popBackStack() }, modifier = Modifier.weight(1f), - ) { Text("Toepassen") } + ) { Text("Apply") } } } } diff --git a/app/src/main/java/eu/geyskens/pdfscan/ui/screens/ExportScreen.kt b/app/src/main/java/eu/geyskens/pdfscan/ui/screens/ExportScreen.kt index 5bf907c..c9a7dba 100644 --- a/app/src/main/java/eu/geyskens/pdfscan/ui/screens/ExportScreen.kt +++ b/app/src/main/java/eu/geyskens/pdfscan/ui/screens/ExportScreen.kt @@ -70,7 +70,7 @@ fun ExportScreen(vm: ScanViewModel, navController: NavController) { fun mode() = if (a4) PageSizeMode.A4 else PageSizeMode.ORIGINAL Scaffold( - topBar = { TopAppBar(title = { Text("Exporteren (${pages.size} pagina's)") }) }, + topBar = { TopAppBar(title = { Text("Export (${pages.size} pages)") }) }, ) { padding -> Column( Modifier.fillMaxSize().padding(padding).padding(16.dp), @@ -79,7 +79,7 @@ fun ExportScreen(vm: ScanViewModel, navController: NavController) { OutlinedTextField( value = fileName, onValueChange = { fileName = it }, - label = { Text("Bestandsnaam") }, + label = { Text("File name") }, singleLine = true, modifier = Modifier.fillMaxWidth(), ) @@ -89,12 +89,12 @@ fun ExportScreen(vm: ScanViewModel, navController: NavController) { selected = a4, onClick = { a4 = true }, shape = SegmentedButtonDefaults.itemShape(0, 2), - ) { Text("A4-pagina") } + ) { Text("A4 page") } SegmentedButton( selected = !a4, onClick = { a4 = false }, shape = SegmentedButtonDefaults.itemShape(1, 2), - ) { Text("Originele grootte") } + ) { Text("Original size") } } Button( @@ -113,9 +113,9 @@ fun ExportScreen(vm: ScanViewModel, navController: NavController) { putExtra(Intent.EXTRA_STREAM, uri) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } - context.startActivity(Intent.createChooser(share, "PDF delen")) + context.startActivity(Intent.createChooser(share, "Share PDF")) } catch (e: Exception) { - status = "Fout bij exporteren: ${e.message}" + status = "Export failed: ${e.message}" } finally { busy = false } @@ -126,7 +126,7 @@ fun ExportScreen(vm: ScanViewModel, navController: NavController) { ) { Icon(Icons.Filled.Share, contentDescription = null) Spacer(Modifier.width(8.dp)) - Text("Delen / opslaan") + Text("Share / save") } Button( @@ -140,19 +140,19 @@ fun ExportScreen(vm: ScanViewModel, navController: NavController) { val pdf = vm.exportPdf(fileName, mode()) when (val r = vm.uploadToPaperless(pdf, fileName)) { is PaperlessClient.Result.Success -> - status = "Toegevoegd aan Paperless ✓" + status = "Added to Paperless ✓" is PaperlessClient.Result.Duplicate -> - status = "Al aanwezig in Paperless (duplicaat) — niet opnieuw toegevoegd." + status = "Already in Paperless (duplicate) — not added again." is PaperlessClient.Result.Pending -> - status = "Geüpload — Paperless is het nog aan het verwerken…" + status = "Uploaded — Paperless is still processing…" is PaperlessClient.Result.Failure -> status = r.message } } catch (e: CancellationException) { - // User tapped "Niet wachten": the document was already sent. - status = "Geüpload — Paperless verwerkt het verder op de achtergrond." + // User tapped "Don't wait": the document was already sent. + status = "Uploaded — Paperless will finish processing in the background." } catch (e: Exception) { - status = "Fout: ${e.message}" + status = "Error: ${e.message}" } finally { busy = false uploading = false @@ -169,24 +169,24 @@ fun ExportScreen(vm: ScanViewModel, navController: NavController) { if (!settings.paperlessUrl.isNotBlank() || !settings.hasToken) { Text( - "Configureer eerst Paperless (server-URL + token) in de instellingen om te kunnen uploaden.", + "Configure Paperless (server URL + token) in settings first to upload.", style = androidx.compose.material3.MaterialTheme.typography.bodySmall, color = Color(0xFF9A9A9A), ) OutlinedButton( onClick = { navController.navigate(Routes.SETTINGS) }, modifier = Modifier.fillMaxWidth(), - ) { Text("Paperless instellen") } + ) { Text("Set up Paperless") } } if (busy) { Row(verticalAlignment = androidx.compose.ui.Alignment.CenterVertically) { CircularProgressIndicator(Modifier.height(24.dp)) Spacer(Modifier.width(12.dp)) - Text(if (uploading) "Uploaden…" else "Bezig…") + Text(if (uploading) "Uploading…" else "Working…") if (uploading) { Spacer(Modifier.width(12.dp)) - OutlinedButton(onClick = { uploadJob?.cancel() }) { Text("Niet wachten") } + OutlinedButton(onClick = { uploadJob?.cancel() }) { Text("Don't wait") } } } } diff --git a/app/src/main/java/eu/geyskens/pdfscan/ui/screens/PagesScreen.kt b/app/src/main/java/eu/geyskens/pdfscan/ui/screens/PagesScreen.kt index c472d5b..b9c1660 100644 --- a/app/src/main/java/eu/geyskens/pdfscan/ui/screens/PagesScreen.kt +++ b/app/src/main/java/eu/geyskens/pdfscan/ui/screens/PagesScreen.kt @@ -53,12 +53,12 @@ fun PagesScreen(vm: ScanViewModel, navController: NavController) { Scaffold( topBar = { TopAppBar( - title = { Text("Pagina's (${pages.size})") }, + title = { Text("Pages (${pages.size})") }, actions = { IconButton( onClick = { if (pages.isNotEmpty()) navController.navigate(Routes.EXPORT) }, ) { - Icon(Icons.Filled.PictureAsPdf, contentDescription = "Exporteer PDF") + Icon(Icons.Filled.PictureAsPdf, contentDescription = "Export PDF") } }, ) @@ -67,7 +67,7 @@ fun PagesScreen(vm: ScanViewModel, navController: NavController) { ExtendedFloatingActionButton( onClick = { navController.navigate(Routes.CAMERA) }, icon = { Icon(Icons.Filled.Add, contentDescription = null) }, - text = { Text("Pagina toevoegen") }, + text = { Text("Add page") }, ) }, ) { padding -> @@ -122,24 +122,24 @@ private fun PageRow( bitmap?.let { Image( bitmap = it.asImageBitmap(), - contentDescription = "Pagina ${index + 1}", + contentDescription = "Page ${index + 1}", contentScale = ContentScale.Crop, modifier = Modifier.fillMaxSize(), ) } } Spacer(Modifier.width(12.dp)) - Text("Pagina ${index + 1}", style = MaterialTheme.typography.bodyLarge, modifier = Modifier.weight(1f)) + Text("Page ${index + 1}", style = MaterialTheme.typography.bodyLarge, modifier = Modifier.weight(1f)) Column { IconButton(onClick = onUp, enabled = !isFirst) { - Icon(Icons.Filled.ArrowUpward, contentDescription = "Omhoog") + Icon(Icons.Filled.ArrowUpward, contentDescription = "Move up") } IconButton(onClick = onDown, enabled = !isLast) { - Icon(Icons.Filled.ArrowDownward, contentDescription = "Omlaag") + Icon(Icons.Filled.ArrowDownward, contentDescription = "Move down") } } IconButton(onClick = onDelete) { - Icon(Icons.Filled.Delete, contentDescription = "Verwijderen", tint = Color(0xFFD32F2F)) + Icon(Icons.Filled.Delete, contentDescription = "Delete", tint = Color(0xFFD32F2F)) } } } diff --git a/app/src/main/java/eu/geyskens/pdfscan/ui/screens/SettingsScreen.kt b/app/src/main/java/eu/geyskens/pdfscan/ui/screens/SettingsScreen.kt index 74b7c57..e0a963d 100644 --- a/app/src/main/java/eu/geyskens/pdfscan/ui/screens/SettingsScreen.kt +++ b/app/src/main/java/eu/geyskens/pdfscan/ui/screens/SettingsScreen.kt @@ -59,10 +59,10 @@ fun SettingsScreen(vm: ScanViewModel, navController: NavController) { snackbarHost = { SnackbarHost(snackbarHostState) }, topBar = { TopAppBar( - title = { Text("Instellingen") }, + title = { Text("Settings") }, navigationIcon = { IconButton(onClick = { navController.popBackStack() }) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Terug") + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") } }, ) @@ -81,22 +81,22 @@ fun SettingsScreen(vm: ScanViewModel, navController: NavController) { OutlinedTextField( value = url, onValueChange = { url = it; testResult = null }, - label = { Text("Server-URL (bv. https://paperless.thuis.lan)") }, + label = { Text("Server URL (e.g. https://paperless.home.lan)") }, singleLine = true, modifier = Modifier.fillMaxWidth(), ) OutlinedTextField( value = token, onValueChange = { token = it; testResult = null }, - label = { Text("API-token") }, + label = { Text("API token") }, singleLine = true, visualTransformation = PasswordVisualTransformation(), modifier = Modifier.fillMaxWidth(), ) if (url.startsWith("http://")) { Text( - "⚠ Je gebruikt http:// — je token en documenten gaan onversleuteld over het " + - "netwerk. Gebruik https:// als het even kan.", + "⚠ You're using http:// — your token and documents travel unencrypted over the " + + "network. Use https:// when you can.", color = Color(0xFFB26A00), style = androidx.compose.material3.MaterialTheme.typography.bodySmall, ) @@ -109,55 +109,55 @@ fun SettingsScreen(vm: ScanViewModel, navController: NavController) { testResult = null scope.launch { testResult = when (val r = vm.testPaperless(url, token)) { - is PaperlessClient.Result.Success -> "Verbinding OK ✓" + is PaperlessClient.Result.Success -> "Connection OK ✓" is PaperlessClient.Result.Failure -> r.message - else -> "Onbekend antwoord van de server." + else -> "Unexpected response from the server." } testing = false } }, enabled = !testing, modifier = Modifier.weight(1f), - ) { Text("Verbinding testen") } + ) { Text("Test connection") } Button( onClick = { scope.launch { vm.savePaperless(url, token) - snackbarHostState.showSnackbar("Instellingen opgeslagen ✓") + snackbarHostState.showSnackbar("Settings saved ✓") } }, modifier = Modifier.weight(1f), - ) { Text("Opslaan") } + ) { Text("Save") } } if (testing) { Row(verticalAlignment = Alignment.CenterVertically) { CircularProgressIndicator(Modifier.width(20.dp)) Spacer(Modifier.width(8.dp)) - Text("Testen…") + Text("Testing…") } } testResult?.let { Text(it, color = Color(0xFF1F6FEB)) } Text( - "Standaardfilter", + "Default filter", style = androidx.compose.material3.MaterialTheme.typography.titleMedium, ) Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { FilterChip( selected = settings.defaultFilter == ScanFilter.COLOR, onClick = { scope.launch { vm.setDefaultFilter(ScanFilter.COLOR) } }, - label = { Text("Kleur") }, + label = { Text("Color") }, ) FilterChip( selected = settings.defaultFilter == ScanFilter.GRAYSCALE, onClick = { scope.launch { vm.setDefaultFilter(ScanFilter.GRAYSCALE) } }, - label = { Text("Grijs") }, + label = { Text("Grayscale") }, ) FilterChip( selected = settings.defaultFilter == ScanFilter.BLACK_WHITE, onClick = { scope.launch { vm.setDefaultFilter(ScanFilter.BLACK_WHITE) } }, - label = { Text("Zwart-wit") }, + label = { Text("Black & white") }, ) } } diff --git a/keystore.properties.example b/keystore.properties.example index 1e01144..2cbd8c7 100644 --- a/keystore.properties.example +++ b/keystore.properties.example @@ -1,5 +1,5 @@ -# Kopieer naar keystore.properties (staat in .gitignore) voor lokale release-builds. -storeFile=/absoluut/pad/naar/release.jks +# Copy to keystore.properties (gitignored) for local release builds. +storeFile=/absolute/path/to/release.jks storePassword=CHANGEME keyAlias=paperscan keyPassword=CHANGEME