Internationalization: string resources + Dutch translation; v0.3.0
All checks were successful
Build APK / build (push) Successful in 5m55s
All checks were successful
Build APK / build (push) Successful in 5m55s
Move all user-facing strings into res/values/strings.xml (English base) and add res/values-nl/strings.xml (Dutch). Composables use stringResource; coroutine-set and non-composable strings (ViewModel, PaperlessClient) resolve via Context.getString. Mark app_name translatable=false. Bump to 0.3.0. Fixes #5 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -115,9 +115,11 @@ 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 isn't set up yet (URL + token).")
|
||||
return PaperlessClient.Result.Failure(
|
||||
getApplication<Application>().getString(R.string.err_paperless_not_setup)
|
||||
)
|
||||
}
|
||||
return PaperlessClient(s.paperlessUrl, token).upload(pdf, title)
|
||||
return PaperlessClient(s.paperlessUrl, token, getApplication<Application>()).upload(pdf, title)
|
||||
}
|
||||
|
||||
// --- Settings pass-throughs (SettingsScreen) ---
|
||||
@@ -133,9 +135,11 @@ class ScanViewModel(app: Application) : AndroidViewModel(app) {
|
||||
|
||||
suspend fun testPaperless(url: String, token: String): PaperlessClient.Result {
|
||||
if (url.isBlank() || token.isBlank()) {
|
||||
return PaperlessClient.Result.Failure("Enter a URL and token first.")
|
||||
return PaperlessClient.Result.Failure(
|
||||
getApplication<Application>().getString(R.string.err_enter_url_token)
|
||||
)
|
||||
}
|
||||
return PaperlessClient(url.trim(), token.trim()).testConnection()
|
||||
return PaperlessClient(url.trim(), token.trim(), getApplication<Application>()).testConnection()
|
||||
}
|
||||
|
||||
fun clearSession() {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package eu.geyskens.pdfscan.paperless
|
||||
|
||||
import android.content.Context
|
||||
import eu.geyskens.pdfscan.R
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -22,6 +24,7 @@ import java.util.concurrent.TimeUnit
|
||||
class PaperlessClient(
|
||||
private val baseUrl: String,
|
||||
private val token: String,
|
||||
private val context: Context,
|
||||
) {
|
||||
private val client = OkHttpClient.Builder()
|
||||
.connectTimeout(15, TimeUnit.SECONDS)
|
||||
@@ -53,11 +56,12 @@ class PaperlessClient(
|
||||
client.newCall(request).execute().use { resp ->
|
||||
when {
|
||||
resp.isSuccessful -> Result.Success
|
||||
resp.code == 401 || resp.code == 403 -> Result.Failure("Invalid token (${resp.code}).")
|
||||
else -> Result.Failure("Server responded with ${resp.code}.")
|
||||
resp.code == 401 || resp.code == 403 ->
|
||||
Result.Failure(context.getString(R.string.err_invalid_token, resp.code))
|
||||
else -> Result.Failure(context.getString(R.string.err_server_responded, resp.code))
|
||||
}
|
||||
}
|
||||
}.getOrElse { Result.Failure(it.message ?: "Connection failed.") }
|
||||
}.getOrElse { Result.Failure(it.message ?: context.getString(R.string.err_connection_failed)) }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,12 +89,16 @@ class PaperlessClient(
|
||||
val posted: kotlin.Result<String?> = withContext(NonCancellable) {
|
||||
runCatching {
|
||||
client.newCall(request).execute().use { resp ->
|
||||
if (!resp.isSuccessful) error("Upload failed: ${resp.code} ${resp.message}")
|
||||
if (!resp.isSuccessful) {
|
||||
error(context.getString(R.string.err_upload_failed_code, resp.code, resp.message))
|
||||
}
|
||||
resp.body?.string()?.trim()?.trim('"')?.takeIf { it.isNotBlank() }
|
||||
}
|
||||
}
|
||||
}
|
||||
val taskId = posted.getOrElse { return@withContext Result.Failure(it.message ?: "Upload failed.") }
|
||||
val taskId = posted.getOrElse {
|
||||
return@withContext Result.Failure(it.message ?: context.getString(R.string.err_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 +127,7 @@ class PaperlessClient(
|
||||
"FAILURE" -> {
|
||||
val msg = task.result.orEmpty()
|
||||
return if (msg.contains("duplicate", ignoreCase = true)) Result.Duplicate
|
||||
else Result.Failure(msg.ifBlank { "Paperless could not process the document." })
|
||||
else Result.Failure(msg.ifBlank { context.getString(R.string.err_paperless_process) })
|
||||
}
|
||||
// PENDING / STARTED / RETRY / unknown → keep waiting.
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.concurrent.futures.await
|
||||
@@ -58,6 +59,7 @@ import androidx.core.content.ContextCompat
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import eu.geyskens.pdfscan.R
|
||||
import eu.geyskens.pdfscan.Routes
|
||||
import eu.geyskens.pdfscan.ScanViewModel
|
||||
import eu.geyskens.pdfscan.scan.DocumentAnalyzer
|
||||
@@ -84,13 +86,12 @@ private fun PermissionRequest(onRequest: () -> Unit) {
|
||||
Box(Modifier.fillMaxSize().padding(32.dp), contentAlignment = Alignment.Center) {
|
||||
androidx.compose.foundation.layout.Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(
|
||||
"PaperScan needs camera access to scan documents. " +
|
||||
"Nothing is sent to the cloud.",
|
||||
stringResource(R.string.camera_permission_rationale),
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
androidx.compose.foundation.layout.Spacer(Modifier.size(16.dp))
|
||||
Button(onClick = onRequest) { Text("Grant access") }
|
||||
Button(onClick = onRequest) { Text(stringResource(R.string.grant_access)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -198,7 +199,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 = "Settings", tint = Color.White)
|
||||
Icon(Icons.Filled.Settings, contentDescription = stringResource(R.string.settings), tint = Color.White)
|
||||
}
|
||||
|
||||
// Bottom controls.
|
||||
@@ -259,7 +260,7 @@ private fun CameraContent(vm: ScanViewModel, navController: androidx.navigation.
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.Check,
|
||||
contentDescription = "Done",
|
||||
contentDescription = stringResource(R.string.done),
|
||||
tint = if (pages.isEmpty()) Color.Gray else Color.White,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -34,9 +34,11 @@ import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import eu.geyskens.pdfscan.R
|
||||
import eu.geyskens.pdfscan.ScanViewModel
|
||||
import eu.geyskens.pdfscan.data.ScanFilter
|
||||
import androidx.compose.foundation.Canvas
|
||||
@@ -135,9 +137,9 @@ fun CropScreen(vm: ScanViewModel, navController: NavController, pageId: String)
|
||||
Modifier.fillMaxWidth().padding(12.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
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 }
|
||||
FilterOption(stringResource(R.string.filter_color), filter == ScanFilter.COLOR) { filter = ScanFilter.COLOR }
|
||||
FilterOption(stringResource(R.string.filter_grayscale), filter == ScanFilter.GRAYSCALE) { filter = ScanFilter.GRAYSCALE }
|
||||
FilterOption(stringResource(R.string.filter_bw), filter == ScanFilter.BLACK_WHITE) { filter = ScanFilter.BLACK_WHITE }
|
||||
}
|
||||
|
||||
Row(
|
||||
@@ -145,7 +147,7 @@ fun CropScreen(vm: ScanViewModel, navController: NavController, pageId: String)
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
OutlinedButton(onClick = { vm.rotate(pageId) }, modifier = Modifier.weight(1f)) {
|
||||
Text("Rotate")
|
||||
Text(stringResource(R.string.rotate))
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
@@ -153,7 +155,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("Reset") }
|
||||
) { Text(stringResource(R.string.reset)) }
|
||||
Button(
|
||||
onClick = {
|
||||
vm.updateCorners(pageId, corners.map { PointF(it.value.x, it.value.y) })
|
||||
@@ -161,7 +163,7 @@ fun CropScreen(vm: ScanViewModel, navController: NavController, pageId: String)
|
||||
navController.popBackStack()
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
) { Text("Apply") }
|
||||
) { Text(stringResource(R.string.apply)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,8 +36,10 @@ import androidx.compose.runtime.setValue
|
||||
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.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import eu.geyskens.pdfscan.R
|
||||
import eu.geyskens.pdfscan.Routes
|
||||
import eu.geyskens.pdfscan.ScanViewModel
|
||||
import eu.geyskens.pdfscan.paperless.PaperlessClient
|
||||
@@ -70,7 +72,7 @@ fun ExportScreen(vm: ScanViewModel, navController: NavController) {
|
||||
fun mode() = if (a4) PageSizeMode.A4 else PageSizeMode.ORIGINAL
|
||||
|
||||
Scaffold(
|
||||
topBar = { TopAppBar(title = { Text("Export (${pages.size} pages)") }) },
|
||||
topBar = { TopAppBar(title = { Text(stringResource(R.string.export_title, pages.size)) }) },
|
||||
) { padding ->
|
||||
Column(
|
||||
Modifier.fillMaxSize().padding(padding).padding(16.dp),
|
||||
@@ -79,7 +81,7 @@ fun ExportScreen(vm: ScanViewModel, navController: NavController) {
|
||||
OutlinedTextField(
|
||||
value = fileName,
|
||||
onValueChange = { fileName = it },
|
||||
label = { Text("File name") },
|
||||
label = { Text(stringResource(R.string.file_name)) },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
@@ -89,12 +91,12 @@ fun ExportScreen(vm: ScanViewModel, navController: NavController) {
|
||||
selected = a4,
|
||||
onClick = { a4 = true },
|
||||
shape = SegmentedButtonDefaults.itemShape(0, 2),
|
||||
) { Text("A4 page") }
|
||||
) { Text(stringResource(R.string.page_size_a4)) }
|
||||
SegmentedButton(
|
||||
selected = !a4,
|
||||
onClick = { a4 = false },
|
||||
shape = SegmentedButtonDefaults.itemShape(1, 2),
|
||||
) { Text("Original size") }
|
||||
) { Text(stringResource(R.string.page_size_original)) }
|
||||
}
|
||||
|
||||
Button(
|
||||
@@ -113,9 +115,9 @@ fun ExportScreen(vm: ScanViewModel, navController: NavController) {
|
||||
putExtra(Intent.EXTRA_STREAM, uri)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
context.startActivity(Intent.createChooser(share, "Share PDF"))
|
||||
context.startActivity(Intent.createChooser(share, context.getString(R.string.share_pdf)))
|
||||
} catch (e: Exception) {
|
||||
status = "Export failed: ${e.message}"
|
||||
status = context.getString(R.string.export_failed, e.message ?: "")
|
||||
} finally {
|
||||
busy = false
|
||||
}
|
||||
@@ -126,7 +128,7 @@ fun ExportScreen(vm: ScanViewModel, navController: NavController) {
|
||||
) {
|
||||
Icon(Icons.Filled.Share, contentDescription = null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Share / save")
|
||||
Text(stringResource(R.string.share_save))
|
||||
}
|
||||
|
||||
Button(
|
||||
@@ -140,19 +142,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 = "Added to Paperless ✓"
|
||||
status = context.getString(R.string.upload_added)
|
||||
is PaperlessClient.Result.Duplicate ->
|
||||
status = "Already in Paperless (duplicate) — not added again."
|
||||
status = context.getString(R.string.upload_duplicate)
|
||||
is PaperlessClient.Result.Pending ->
|
||||
status = "Uploaded — Paperless is still processing…"
|
||||
status = context.getString(R.string.upload_pending)
|
||||
is PaperlessClient.Result.Failure ->
|
||||
status = r.message
|
||||
}
|
||||
} catch (e: CancellationException) {
|
||||
// User tapped "Don't wait": the document was already sent.
|
||||
status = "Uploaded — Paperless will finish processing in the background."
|
||||
status = context.getString(R.string.upload_background)
|
||||
} catch (e: Exception) {
|
||||
status = "Error: ${e.message}"
|
||||
status = context.getString(R.string.error_generic, e.message ?: "")
|
||||
} finally {
|
||||
busy = false
|
||||
uploading = false
|
||||
@@ -164,29 +166,29 @@ fun ExportScreen(vm: ScanViewModel, navController: NavController) {
|
||||
) {
|
||||
Icon(Icons.Filled.CloudUpload, contentDescription = null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Upload naar Paperless")
|
||||
Text(stringResource(R.string.upload_to_paperless))
|
||||
}
|
||||
|
||||
if (!settings.paperlessUrl.isNotBlank() || !settings.hasToken) {
|
||||
Text(
|
||||
"Configure Paperless (server URL + token) in settings first to upload.",
|
||||
stringResource(R.string.paperless_not_configured),
|
||||
style = androidx.compose.material3.MaterialTheme.typography.bodySmall,
|
||||
color = Color(0xFF9A9A9A),
|
||||
)
|
||||
OutlinedButton(
|
||||
onClick = { navController.navigate(Routes.SETTINGS) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) { Text("Set up Paperless") }
|
||||
) { Text(stringResource(R.string.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) "Uploading…" else "Working…")
|
||||
Text(if (uploading) stringResource(R.string.uploading) else stringResource(R.string.working))
|
||||
if (uploading) {
|
||||
Spacer(Modifier.width(12.dp))
|
||||
OutlinedButton(onClick = { uploadJob?.cancel() }) { Text("Don't wait") }
|
||||
OutlinedButton(onClick = { uploadJob?.cancel() }) { Text(stringResource(R.string.dont_wait)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,8 +39,10 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import eu.geyskens.pdfscan.R
|
||||
import eu.geyskens.pdfscan.Routes
|
||||
import eu.geyskens.pdfscan.ScanViewModel
|
||||
import eu.geyskens.pdfscan.data.Page
|
||||
@@ -53,12 +55,12 @@ fun PagesScreen(vm: ScanViewModel, navController: NavController) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Pages (${pages.size})") },
|
||||
title = { Text(stringResource(R.string.pages_title, pages.size)) },
|
||||
actions = {
|
||||
IconButton(
|
||||
onClick = { if (pages.isNotEmpty()) navController.navigate(Routes.EXPORT) },
|
||||
) {
|
||||
Icon(Icons.Filled.PictureAsPdf, contentDescription = "Export PDF")
|
||||
Icon(Icons.Filled.PictureAsPdf, contentDescription = stringResource(R.string.export_pdf))
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -67,7 +69,7 @@ fun PagesScreen(vm: ScanViewModel, navController: NavController) {
|
||||
ExtendedFloatingActionButton(
|
||||
onClick = { navController.navigate(Routes.CAMERA) },
|
||||
icon = { Icon(Icons.Filled.Add, contentDescription = null) },
|
||||
text = { Text("Add page") },
|
||||
text = { Text(stringResource(R.string.add_page)) },
|
||||
)
|
||||
},
|
||||
) { padding ->
|
||||
@@ -122,24 +124,24 @@ private fun PageRow(
|
||||
bitmap?.let {
|
||||
Image(
|
||||
bitmap = it.asImageBitmap(),
|
||||
contentDescription = "Page ${index + 1}",
|
||||
contentDescription = stringResource(R.string.page_number, index + 1),
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Text("Page ${index + 1}", style = MaterialTheme.typography.bodyLarge, modifier = Modifier.weight(1f))
|
||||
Text(stringResource(R.string.page_number, index + 1), style = MaterialTheme.typography.bodyLarge, modifier = Modifier.weight(1f))
|
||||
Column {
|
||||
IconButton(onClick = onUp, enabled = !isFirst) {
|
||||
Icon(Icons.Filled.ArrowUpward, contentDescription = "Move up")
|
||||
Icon(Icons.Filled.ArrowUpward, contentDescription = stringResource(R.string.move_up))
|
||||
}
|
||||
IconButton(onClick = onDown, enabled = !isLast) {
|
||||
Icon(Icons.Filled.ArrowDownward, contentDescription = "Move down")
|
||||
Icon(Icons.Filled.ArrowDownward, contentDescription = stringResource(R.string.move_down))
|
||||
}
|
||||
}
|
||||
IconButton(onClick = onDelete) {
|
||||
Icon(Icons.Filled.Delete, contentDescription = "Delete", tint = Color(0xFFD32F2F))
|
||||
Icon(Icons.Filled.Delete, contentDescription = stringResource(R.string.delete), tint = Color(0xFFD32F2F))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,9 +35,12 @@ 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.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import eu.geyskens.pdfscan.R
|
||||
import eu.geyskens.pdfscan.ScanViewModel
|
||||
import eu.geyskens.pdfscan.data.ScanFilter
|
||||
import eu.geyskens.pdfscan.paperless.PaperlessClient
|
||||
@@ -47,6 +50,7 @@ import kotlinx.coroutines.launch
|
||||
@Composable
|
||||
fun SettingsScreen(vm: ScanViewModel, navController: NavController) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
val settings by vm.settings.collectAsState()
|
||||
|
||||
var url by remember { mutableStateOf(settings.paperlessUrl) }
|
||||
@@ -59,10 +63,10 @@ fun SettingsScreen(vm: ScanViewModel, navController: NavController) {
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Settings") },
|
||||
title = { Text(stringResource(R.string.settings)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back))
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -76,27 +80,26 @@ fun SettingsScreen(vm: ScanViewModel, navController: NavController) {
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
Text("Paperless-ngx", style = androidx.compose.material3.MaterialTheme.typography.titleMedium)
|
||||
Text(stringResource(R.string.paperless_header), style = androidx.compose.material3.MaterialTheme.typography.titleMedium)
|
||||
|
||||
OutlinedTextField(
|
||||
value = url,
|
||||
onValueChange = { url = it; testResult = null },
|
||||
label = { Text("Server URL (e.g. https://paperless.home.lan)") },
|
||||
label = { Text(stringResource(R.string.server_url_label)) },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = token,
|
||||
onValueChange = { token = it; testResult = null },
|
||||
label = { Text("API token") },
|
||||
label = { Text(stringResource(R.string.api_token_label)) },
|
||||
singleLine = true,
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
if (url.startsWith("http://")) {
|
||||
Text(
|
||||
"⚠ You're using http:// — your token and documents travel unencrypted over the " +
|
||||
"network. Use https:// when you can.",
|
||||
stringResource(R.string.http_warning),
|
||||
color = Color(0xFFB26A00),
|
||||
style = androidx.compose.material3.MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
@@ -109,55 +112,55 @@ fun SettingsScreen(vm: ScanViewModel, navController: NavController) {
|
||||
testResult = null
|
||||
scope.launch {
|
||||
testResult = when (val r = vm.testPaperless(url, token)) {
|
||||
is PaperlessClient.Result.Success -> "Connection OK ✓"
|
||||
is PaperlessClient.Result.Success -> context.getString(R.string.connection_ok)
|
||||
is PaperlessClient.Result.Failure -> r.message
|
||||
else -> "Unexpected response from the server."
|
||||
else -> context.getString(R.string.unexpected_response)
|
||||
}
|
||||
testing = false
|
||||
}
|
||||
},
|
||||
enabled = !testing,
|
||||
modifier = Modifier.weight(1f),
|
||||
) { Text("Test connection") }
|
||||
) { Text(stringResource(R.string.test_connection)) }
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
vm.savePaperless(url, token)
|
||||
snackbarHostState.showSnackbar("Settings saved ✓")
|
||||
snackbarHostState.showSnackbar(context.getString(R.string.settings_saved))
|
||||
}
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
) { Text("Save") }
|
||||
) { Text(stringResource(R.string.save)) }
|
||||
}
|
||||
if (testing) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
CircularProgressIndicator(Modifier.width(20.dp))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Testing…")
|
||||
Text(stringResource(R.string.testing))
|
||||
}
|
||||
}
|
||||
testResult?.let { Text(it, color = Color(0xFF1F6FEB)) }
|
||||
|
||||
Text(
|
||||
"Default filter",
|
||||
stringResource(R.string.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("Color") },
|
||||
label = { Text(stringResource(R.string.filter_color)) },
|
||||
)
|
||||
FilterChip(
|
||||
selected = settings.defaultFilter == ScanFilter.GRAYSCALE,
|
||||
onClick = { scope.launch { vm.setDefaultFilter(ScanFilter.GRAYSCALE) } },
|
||||
label = { Text("Grayscale") },
|
||||
label = { Text(stringResource(R.string.filter_grayscale)) },
|
||||
)
|
||||
FilterChip(
|
||||
selected = settings.defaultFilter == ScanFilter.BLACK_WHITE,
|
||||
onClick = { scope.launch { vm.setDefaultFilter(ScanFilter.BLACK_WHITE) } },
|
||||
label = { Text("Black & white") },
|
||||
label = { Text(stringResource(R.string.filter_bw)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user