Initial commit: PaperScan — lokale Android documentscanner voor Paperless-ngx

Native Kotlin/Compose app met CameraX + OpenCV randdetectie/perspectiefcorrectie,
multi-page PDF-export, en Paperless-ngx upload (of Android deelmenu). Alles lokaal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-12 22:43:09 +02:00
commit 037550afa9
37 changed files with 2051 additions and 0 deletions

View File

@@ -0,0 +1,76 @@
package eu.geyskens.pdfscan.paperless
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.asRequestBody
import java.io.File
import java.util.concurrent.TimeUnit
/**
* Minimal Paperless-ngx REST client. Talks only to the user-configured, self-hosted
* instance. The token is sent as an `Authorization: Token …` header over the connection
* the user set up (HTTPS strongly recommended — see SettingsScreen guidance).
*/
class PaperlessClient(
private val baseUrl: String,
private val token: String,
) {
private val client = OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
private val root = baseUrl.trim().trimEnd('/')
sealed interface Result {
data object Success : Result
data class Failure(val message: String) : Result
}
/** Verifies the URL + token by hitting an authenticated endpoint. */
suspend fun testConnection(): Result = withContext(Dispatchers.IO) {
val request = Request.Builder()
.url("$root/api/documents/?page_size=1")
.header("Authorization", "Token $token")
.header("Accept", "application/json")
.get()
.build()
runCatching {
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}.")
}
}
}.getOrElse { Result.Failure(it.message ?: "Verbinding mislukt.") }
}
/** Uploads a PDF to the Paperless consume/inbox pipeline. */
suspend fun upload(pdf: File, title: String): Result = withContext(Dispatchers.IO) {
val body = MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("title", title)
.addFormDataPart(
"document", "$title.pdf",
pdf.asRequestBody("application/pdf".toMediaType())
)
.build()
val request = Request.Builder()
.url("$root/api/documents/post_document/")
.header("Authorization", "Token $token")
.post(body)
.build()
runCatching {
client.newCall(request).execute().use { resp ->
if (resp.isSuccessful) Result.Success
else Result.Failure("Upload mislukt: ${resp.code} ${resp.message}")
}
}.getOrElse { Result.Failure(it.message ?: "Upload mislukt.") }
}
}