From 516f5d3b7d65f1ab5bba320c7d505f31258c0535 Mon Sep 17 00:00:00 2001 From: ml Date: Tue, 29 Sep 2026 12:58:58 +0200 Subject: [PATCH] Add HTTP API server, Android app, and CI build/publish pipeline - docker-compose.yml + tooling-container/src/server.py: serve preprocess/OCR/extract over an async job API (POST /jobs, GET /jobs/{id}) instead of manual host-script runs per receipt - android/: Kotlin/Compose app - capture a receipt photo, poll job progress, review/edit extracted fields, save locally (Room), export to CSV/ODS - .gitea/workflows/build.yml: build the debug APK and publish it to dl.ladkau.de on push - build-image.sh: also pull ollama/ollama so all pipeline images are ready after one script - add project logo (also used for the app's launcher icon/splash) --- .gitea/workflows/build.yml | 95 +++++++ .gitignore | 15 ++ 0_run_ollama.sh | 0 1_preprocess_image.sh | 0 2_ocr_image.sh | 0 3_extract_data.sh | 0 android/app/build.gradle.kts | 79 ++++++ android/app/proguard-rules.pro | 9 + android/app/src/main/AndroidManifest.xml | 39 +++ .../de/ladkau/fuelish/FuelishApplication.kt | 5 + .../java/de/ladkau/fuelish/MainActivity.kt | 22 ++ .../java/de/ladkau/fuelish/data/ApiClient.kt | 47 ++++ .../java/de/ladkau/fuelish/data/AppConfig.kt | 10 + .../ladkau/fuelish/data/ConfigRepository.kt | 29 ++ .../de/ladkau/fuelish/data/FuelishDatabase.kt | 25 ++ .../java/de/ladkau/fuelish/data/JobDto.kt | 37 +++ .../java/de/ladkau/fuelish/data/Receipt.kt | 27 ++ .../java/de/ladkau/fuelish/data/ReceiptDao.kt | 26 ++ .../ladkau/fuelish/data/ReceiptRepository.kt | 25 ++ .../de/ladkau/fuelish/export/CsvExporter.kt | 28 ++ .../de/ladkau/fuelish/export/OdsExporter.kt | 92 +++++++ .../de/ladkau/fuelish/export/ReceiptRow.kt | 29 ++ .../java/de/ladkau/fuelish/ui/FuelishApp.kt | 105 ++++++++ .../fuelish/ui/capture/CaptureScreen.kt | 170 ++++++++++++ .../fuelish/ui/capture/CaptureUiState.kt | 14 + .../fuelish/ui/capture/CaptureViewModel.kt | 96 +++++++ .../fuelish/ui/overview/OverviewScreen.kt | 93 +++++++ .../fuelish/ui/overview/OverviewViewModel.kt | 46 ++++ .../fuelish/ui/settings/ConfigScreen.kt | 42 +++ .../fuelish/ui/settings/ConfigViewModel.kt | 51 ++++ .../java/de/ladkau/fuelish/ui/theme/Theme.kt | 30 +++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + android/app/src/main/res/values/colors.xml | 8 + android/app/src/main/res/values/strings.xml | 33 +++ android/app/src/main/res/values/themes.xml | 10 + android/app/src/main/res/xml/file_paths.xml | 12 + .../main/res/xml/network_security_config.xml | 15 ++ android/build.gradle.kts | 6 + android/gradle.properties | 11 + android/gradle/libs.versions.toml | 56 ++++ android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 48462 bytes .../gradle/wrapper/gradle-wrapper.properties | 9 + android/gradlew | 248 ++++++++++++++++++ android/gradlew.bat | 82 ++++++ android/scripts/gen_app_icons.py | 115 ++++++++ android/settings.gradle.kts | 26 ++ build-image.sh | 9 +- docker-compose.yml | 35 +++ logo.png | Bin 0 -> 77472 bytes test_batch.sh | 0 tooling-container/Dockerfile | 5 +- tooling-container/src/server.py | 128 +++++++++ upload-image.sh | 0 54 files changed, 2101 insertions(+), 3 deletions(-) create mode 100644 .gitea/workflows/build.yml mode change 100644 => 100755 0_run_ollama.sh mode change 100644 => 100755 1_preprocess_image.sh mode change 100644 => 100755 2_ocr_image.sh mode change 100644 => 100755 3_extract_data.sh create mode 100644 android/app/build.gradle.kts create mode 100644 android/app/proguard-rules.pro create mode 100644 android/app/src/main/AndroidManifest.xml create mode 100644 android/app/src/main/java/de/ladkau/fuelish/FuelishApplication.kt create mode 100644 android/app/src/main/java/de/ladkau/fuelish/MainActivity.kt create mode 100644 android/app/src/main/java/de/ladkau/fuelish/data/ApiClient.kt create mode 100644 android/app/src/main/java/de/ladkau/fuelish/data/AppConfig.kt create mode 100644 android/app/src/main/java/de/ladkau/fuelish/data/ConfigRepository.kt create mode 100644 android/app/src/main/java/de/ladkau/fuelish/data/FuelishDatabase.kt create mode 100644 android/app/src/main/java/de/ladkau/fuelish/data/JobDto.kt create mode 100644 android/app/src/main/java/de/ladkau/fuelish/data/Receipt.kt create mode 100644 android/app/src/main/java/de/ladkau/fuelish/data/ReceiptDao.kt create mode 100644 android/app/src/main/java/de/ladkau/fuelish/data/ReceiptRepository.kt create mode 100644 android/app/src/main/java/de/ladkau/fuelish/export/CsvExporter.kt create mode 100644 android/app/src/main/java/de/ladkau/fuelish/export/OdsExporter.kt create mode 100644 android/app/src/main/java/de/ladkau/fuelish/export/ReceiptRow.kt create mode 100644 android/app/src/main/java/de/ladkau/fuelish/ui/FuelishApp.kt create mode 100644 android/app/src/main/java/de/ladkau/fuelish/ui/capture/CaptureScreen.kt create mode 100644 android/app/src/main/java/de/ladkau/fuelish/ui/capture/CaptureUiState.kt create mode 100644 android/app/src/main/java/de/ladkau/fuelish/ui/capture/CaptureViewModel.kt create mode 100644 android/app/src/main/java/de/ladkau/fuelish/ui/overview/OverviewScreen.kt create mode 100644 android/app/src/main/java/de/ladkau/fuelish/ui/overview/OverviewViewModel.kt create mode 100644 android/app/src/main/java/de/ladkau/fuelish/ui/settings/ConfigScreen.kt create mode 100644 android/app/src/main/java/de/ladkau/fuelish/ui/settings/ConfigViewModel.kt create mode 100644 android/app/src/main/java/de/ladkau/fuelish/ui/theme/Theme.kt create mode 100644 android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 android/app/src/main/res/values/colors.xml create mode 100644 android/app/src/main/res/values/strings.xml create mode 100644 android/app/src/main/res/values/themes.xml create mode 100644 android/app/src/main/res/xml/file_paths.xml create mode 100644 android/app/src/main/res/xml/network_security_config.xml create mode 100644 android/build.gradle.kts create mode 100644 android/gradle.properties create mode 100644 android/gradle/libs.versions.toml create mode 100644 android/gradle/wrapper/gradle-wrapper.jar create mode 100644 android/gradle/wrapper/gradle-wrapper.properties create mode 100755 android/gradlew create mode 100644 android/gradlew.bat create mode 100644 android/scripts/gen_app_icons.py create mode 100644 android/settings.gradle.kts mode change 100644 => 100755 build-image.sh create mode 100644 docker-compose.yml create mode 100644 logo.png mode change 100644 => 100755 test_batch.sh create mode 100644 tooling-container/src/server.py mode change 100644 => 100755 upload-image.sh diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..d18a25e --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,95 @@ +name: build + +# Builds the Android app (android/) debug APK on every push/PR, plus +# on-demand via the Gitea "Run workflow" button. Runs inside a public, +# prebuilt Android CI image (JDK + Android SDK/cmdline-tools bundled - +# fuelish's app has no native/NDK code, so it doesn't need a custom +# builder image the way a project with native components would). +on: + push: + pull_request: + workflow_dispatch: + +jobs: + build: + # Runner defaults `run:` steps to `sh`, which doesn't understand + # `set -o pipefail` used below - force bash explicitly. + defaults: + run: + shell: bash + # Must match a label your act_runner is registered with. + runs-on: ubuntu-latest + + container: + # JDK + Android SDK cmdline-tools, no platform/build-tools installed - + # the "Install SDK components" step below fetches exactly what + # compileSdk in android/app/build.gradle.kts needs. Much smaller than + # kitchen-sink alternatives (~0.7GB vs. several GB), which matters + # since every job pulls it fresh. + image: thyrlian/android-sdk:10.0 + + steps: + - uses: actions/checkout@v4 + with: + # APK naming below uses `git rev-parse --short HEAD` - harmless + # with the default shallow clone, but fetch-depth: 0 keeps this + # consistent if that ever switches to `git describe --tags`. + fetch-depth: 0 + + - name: Preflight + run: | + set -euo pipefail + command -v java >/dev/null 2>&1 || { echo "PREFLIGHT FAIL: java not in PATH" >&2; exit 1; } + [ -n "${ANDROID_HOME:-}" ] || { echo "PREFLIGHT FAIL: ANDROID_HOME not set" >&2; exit 1; } + [ -x android/gradlew ] || { echo "PREFLIGHT FAIL: android/gradlew missing or not executable" >&2; exit 1; } + + - name: Install SDK components + run: | + set -euo pipefail + # python3-pil: android/scripts/gen_app_icons.py's Pillow dependency + # (see android/app/build.gradle.kts's generateAppIcons preBuild task) + # - not bundled in this image. + apt-get update -qq + apt-get install -y --no-install-recommends python3-pil + yes | sdkmanager --licenses >/dev/null 2>&1 || true + # Keep in sync with android/app/build.gradle.kts's compileSdk. + sdkmanager "platforms;android-36" "build-tools;36.0.0" + + - name: Build + working-directory: android + run: ./gradlew :app:assembleDebug --console=plain + + - name: Package + # Renames the debug APK with the commit it was built from, so + # multiple builds in dist/ (and on dl.ladkau.de) don't collide. + run: | + set -euo pipefail + mkdir -p dist + sha="$(git rev-parse --short HEAD)" + cp android/app/build/outputs/apk/debug/app-debug.apk "dist/fuelish-$sha.apk" + + - name: Upload build artifact + # v4 uses the newer @actions/artifact backend, which this Gitea + # instance's artifact storage doesn't support (GHESNotSupportedError) — v3 works. + uses: actions/upload-artifact@v3 + with: + name: fuelish-android + path: dist/fuelish-*.apk + + - name: Publish to dl.ladkau.de + # Uploads the APK over SFTP instead of using actions/upload-artifact + # (whose zip wrapping can't be disabled). Only runs on push so PR + # builds don't publish. + if: gitea.event_name == 'push' + run: | + set -euo pipefail + APK="$(ls dist/fuelish-*.apk)" + mkdir -p ~/.ssh + echo "${{ secrets.DL_SFTP_KEY }}" > ~/.ssh/dl_sftp_key + chmod 600 ~/.ssh/dl_sftp_key + sftp -i ~/.ssh/dl_sftp_key -P 2223 \ + -o StrictHostKeyChecking=accept-new \ + uploader@dl.ladkau.de <("generateAppIcons") { + workingDir = rootDir + commandLine("python3", "scripts/gen_app_icons.py") +} + +tasks.named("preBuild") { + dependsOn(generateAppIcons) +} + +dependencies { + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.lifecycle.viewmodel.compose) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.ui.tooling.preview) + implementation(libs.androidx.material3) + implementation(libs.androidx.material.icons.core) + implementation(libs.androidx.navigation.compose) + implementation(libs.androidx.datastore.preferences) + implementation(libs.androidx.core.splashscreen) + implementation(libs.androidx.room.runtime) + implementation(libs.androidx.room.ktx) + ksp(libs.androidx.room.compiler) + implementation(libs.kotlinx.serialization.json) + implementation(libs.kotlinx.coroutines.android) + implementation(libs.okhttp) + debugImplementation(libs.androidx.ui.tooling) +} diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 0000000..4039837 --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1,9 @@ +# kotlinx.serialization needs its generated serializer classes kept. +-keepattributes *Annotation*, InnerClasses +-dontnote kotlinx.serialization.AnnotationsKt +-keepclasseswithmembers class de.ladkau.fuelish.data.** { + *** Companion; +} +-keepclasseswithmembers class de.ladkau.fuelish.data.** { + kotlinx.serialization.KSerializer serializer(...); +} diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a40d298 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/java/de/ladkau/fuelish/FuelishApplication.kt b/android/app/src/main/java/de/ladkau/fuelish/FuelishApplication.kt new file mode 100644 index 0000000..b3d866a --- /dev/null +++ b/android/app/src/main/java/de/ladkau/fuelish/FuelishApplication.kt @@ -0,0 +1,5 @@ +package de.ladkau.fuelish + +import android.app.Application + +class FuelishApplication : Application() diff --git a/android/app/src/main/java/de/ladkau/fuelish/MainActivity.kt b/android/app/src/main/java/de/ladkau/fuelish/MainActivity.kt new file mode 100644 index 0000000..a322e9e --- /dev/null +++ b/android/app/src/main/java/de/ladkau/fuelish/MainActivity.kt @@ -0,0 +1,22 @@ +package de.ladkau.fuelish + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import de.ladkau.fuelish.ui.FuelishApp +import de.ladkau.fuelish.ui.theme.FuelishTheme + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + installSplashScreen() + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + FuelishTheme { + FuelishApp() + } + } + } +} diff --git a/android/app/src/main/java/de/ladkau/fuelish/data/ApiClient.kt b/android/app/src/main/java/de/ladkau/fuelish/data/ApiClient.kt new file mode 100644 index 0000000..bfa388e --- /dev/null +++ b/android/app/src/main/java/de/ladkau/fuelish/data/ApiClient.kt @@ -0,0 +1,47 @@ +package de.ladkau.fuelish.data + +import java.io.File +import java.io.IOException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonNamingStrategy +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MultipartBody +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.asRequestBody + +/** Talks to tooling-container/src/server.py's two endpoints. Every call + * takes the server's base URL explicitly (from [ConfigRepository]) rather + * than storing it, since the user can change it from Settings mid-session. */ +class ApiClient(private val client: OkHttpClient = OkHttpClient()) { + @OptIn(ExperimentalSerializationApi::class) + private val json = Json { + ignoreUnknownKeys = true + namingStrategy = JsonNamingStrategy.SnakeCase + } + + suspend fun submitImage(baseUrl: String, imageFile: File): String = withContext(Dispatchers.IO) { + val body = MultipartBody.Builder() + .setType(MultipartBody.FORM) + .addFormDataPart("image", imageFile.name, imageFile.asRequestBody("image/jpeg".toMediaType())) + .build() + val request = Request.Builder().url("${baseUrl.trimEnd('/')}/jobs").post(body).build() + client.newCall(request).execute().use { response -> + if (!response.isSuccessful) throw IOException("upload failed: HTTP ${response.code}") + val text = response.body?.string().orEmpty() + json.decodeFromString(CreateJobResponse.serializer(), text).jobId + } + } + + suspend fun getJobStatus(baseUrl: String, jobId: String): JobStatusResponse = withContext(Dispatchers.IO) { + val request = Request.Builder().url("${baseUrl.trimEnd('/')}/jobs/$jobId").get().build() + client.newCall(request).execute().use { response -> + if (!response.isSuccessful) throw IOException("status check failed: HTTP ${response.code}") + val text = response.body?.string().orEmpty() + json.decodeFromString(JobStatusResponse.serializer(), text) + } + } +} diff --git a/android/app/src/main/java/de/ladkau/fuelish/data/AppConfig.kt b/android/app/src/main/java/de/ladkau/fuelish/data/AppConfig.kt new file mode 100644 index 0000000..b002845 --- /dev/null +++ b/android/app/src/main/java/de/ladkau/fuelish/data/AppConfig.kt @@ -0,0 +1,10 @@ +package de.ladkau.fuelish.data + +/** + * Everything the app needs that isn't a saved receipt: just the fuelish + * server's base URL (e.g. "http://192.168.1.10:8080") right now. + * Persisted via Preferences DataStore, see [ConfigRepository]. + */ +data class AppConfig( + val serverUrl: String = "", +) diff --git a/android/app/src/main/java/de/ladkau/fuelish/data/ConfigRepository.kt b/android/app/src/main/java/de/ladkau/fuelish/data/ConfigRepository.kt new file mode 100644 index 0000000..35e10f4 --- /dev/null +++ b/android/app/src/main/java/de/ladkau/fuelish/data/ConfigRepository.kt @@ -0,0 +1,29 @@ +package de.ladkau.fuelish.data + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +private val Context.configDataStore: DataStore by preferencesDataStore(name = "fuelish_config") + +/** Preferences DataStore-backed persistence for [AppConfig]. */ +class ConfigRepository(private val context: Context) { + private object Keys { + val SERVER_URL = stringPreferencesKey("server_url") + } + + val config: Flow = context.configDataStore.data.map { prefs -> + AppConfig(serverUrl = prefs[Keys.SERVER_URL] ?: "") + } + + suspend fun save(config: AppConfig) { + context.configDataStore.edit { prefs -> + prefs[Keys.SERVER_URL] = config.serverUrl + } + } +} diff --git a/android/app/src/main/java/de/ladkau/fuelish/data/FuelishDatabase.kt b/android/app/src/main/java/de/ladkau/fuelish/data/FuelishDatabase.kt new file mode 100644 index 0000000..6281853 --- /dev/null +++ b/android/app/src/main/java/de/ladkau/fuelish/data/FuelishDatabase.kt @@ -0,0 +1,25 @@ +package de.ladkau.fuelish.data + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase + +@Database(entities = [Receipt::class], version = 1, exportSchema = false) +abstract class FuelishDatabase : RoomDatabase() { + abstract fun receiptDao(): ReceiptDao + + companion object { + @Volatile + private var instance: FuelishDatabase? = null + + fun get(context: Context): FuelishDatabase = + instance ?: synchronized(this) { + instance ?: Room.databaseBuilder( + context.applicationContext, + FuelishDatabase::class.java, + "fuelish.db", + ).build().also { instance = it } + } + } +} diff --git a/android/app/src/main/java/de/ladkau/fuelish/data/JobDto.kt b/android/app/src/main/java/de/ladkau/fuelish/data/JobDto.kt new file mode 100644 index 0000000..185dc8d --- /dev/null +++ b/android/app/src/main/java/de/ladkau/fuelish/data/JobDto.kt @@ -0,0 +1,37 @@ +package de.ladkau.fuelish.data + +import kotlinx.serialization.Serializable + +/** Mirrors tooling-container/src/server.py's POST /jobs response. */ +@Serializable +data class CreateJobResponse(val jobId: String) + +/** Mirrors tooling-container/src/server.py's GET /jobs/{id} response. */ +@Serializable +data class JobStatusResponse( + val status: String, + val result: ExtractedReceiptDto? = null, + val error: String? = null, +) { + companion object { + const val STATUS_QUEUED = "queued" + const val STATUS_PREPROCESSING = "preprocessing" + const val STATUS_OCR = "ocr" + const val STATUS_EXTRACTING = "extracting" + const val STATUS_DONE = "done" + const val STATUS_ERROR = "error" + } +} + +/** Mirrors tooling-container/src/extract.py's SCHEMA. */ +@Serializable +data class ExtractedReceiptDto( + val petrolStation: String? = null, + val transactionNumber: String? = null, + val date: String? = null, + val time: String? = null, + val pricePerLitre: Double? = null, + val litres: Double? = null, + val discount: Double? = null, + val totalPrice: Double? = null, +) diff --git a/android/app/src/main/java/de/ladkau/fuelish/data/Receipt.kt b/android/app/src/main/java/de/ladkau/fuelish/data/Receipt.kt new file mode 100644 index 0000000..1c185ac --- /dev/null +++ b/android/app/src/main/java/de/ladkau/fuelish/data/Receipt.kt @@ -0,0 +1,27 @@ +package de.ladkau.fuelish.data + +import androidx.room.Entity +import androidx.room.PrimaryKey + +/** + * A confirmed receipt, as the user saved it after reviewing the server's + * extraction. This app is the system of record - the server is + * stateless processing only (see docker-compose.yml / server.py). + */ +@Entity(tableName = "receipts") +data class Receipt( + @PrimaryKey(autoGenerate = true) val id: Long = 0, + val petrolStation: String?, + val transactionNumber: String?, + /** DD.MM.YYYY, matching testdata/.../benzin_kosten.ods's ground-truth format. */ + val date: String?, + /** HH:MM:SS. */ + val time: String?, + val pricePerLitre: Double?, + val litres: Double?, + val discount: Double?, + val totalPrice: Double?, + /** content:// or file:// URI of the captured photo, kept for reference - null if unavailable. */ + val imageUri: String?, + val createdAtEpochMillis: Long, +) diff --git a/android/app/src/main/java/de/ladkau/fuelish/data/ReceiptDao.kt b/android/app/src/main/java/de/ladkau/fuelish/data/ReceiptDao.kt new file mode 100644 index 0000000..732007e --- /dev/null +++ b/android/app/src/main/java/de/ladkau/fuelish/data/ReceiptDao.kt @@ -0,0 +1,26 @@ +package de.ladkau.fuelish.data + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Update +import kotlinx.coroutines.flow.Flow + +@Dao +interface ReceiptDao { + @Query("SELECT * FROM receipts ORDER BY date DESC, createdAtEpochMillis DESC") + fun observeAll(): Flow> + + @Query("SELECT * FROM receipts WHERE id = :id") + suspend fun getById(id: Long): Receipt? + + @Insert + suspend fun insert(receipt: Receipt): Long + + @Update + suspend fun update(receipt: Receipt) + + @Delete + suspend fun delete(receipt: Receipt) +} diff --git a/android/app/src/main/java/de/ladkau/fuelish/data/ReceiptRepository.kt b/android/app/src/main/java/de/ladkau/fuelish/data/ReceiptRepository.kt new file mode 100644 index 0000000..180e90a --- /dev/null +++ b/android/app/src/main/java/de/ladkau/fuelish/data/ReceiptRepository.kt @@ -0,0 +1,25 @@ +package de.ladkau.fuelish.data + +import android.content.Context +import java.io.File +import kotlinx.coroutines.flow.Flow + +/** Wraps the local Room store (the system of record for confirmed + * receipts) and the [ApiClient] (stateless per-receipt processing) behind + * one entry point for the ui layer. */ +class ReceiptRepository(context: Context) { + private val dao = FuelishDatabase.get(context).receiptDao() + private val api = ApiClient() + + val receipts: Flow> = dao.observeAll() + + suspend fun submitImage(baseUrl: String, imageFile: File): String = api.submitImage(baseUrl, imageFile) + + suspend fun pollJob(baseUrl: String, jobId: String): JobStatusResponse = api.getJobStatus(baseUrl, jobId) + + suspend fun save(receipt: Receipt): Long = dao.insert(receipt) + + suspend fun update(receipt: Receipt) = dao.update(receipt) + + suspend fun delete(receipt: Receipt) = dao.delete(receipt) +} diff --git a/android/app/src/main/java/de/ladkau/fuelish/export/CsvExporter.kt b/android/app/src/main/java/de/ladkau/fuelish/export/CsvExporter.kt new file mode 100644 index 0000000..22d79fb --- /dev/null +++ b/android/app/src/main/java/de/ladkau/fuelish/export/CsvExporter.kt @@ -0,0 +1,28 @@ +package de.ladkau.fuelish.export + +import de.ladkau.fuelish.data.Receipt +import java.io.OutputStream + +/** ";"-delimited, not ",": our cells use a German decimal comma (see + * [toExportRow]), which would otherwise collide with a comma delimiter - + * ";" is also what German-locale Excel/LibreOffice expect by default. */ +private const val DELIMITER = ";" +private val UTF8_BOM = byteArrayOf(0xEF.toByte(), 0xBB.toByte(), 0xBF.toByte()) + +fun exportCsv(receipts: List, out: OutputStream) { + out.write(UTF8_BOM) + out.write(csvLine(EXPORT_HEADERS).toByteArray(Charsets.UTF_8)) + for (receipt in receipts) { + out.write(csvLine(receipt.toExportRow()).toByteArray(Charsets.UTF_8)) + } +} + +private fun csvLine(fields: List): String = + fields.joinToString(DELIMITER) { escapeCsvField(it) } + "\r\n" + +private fun escapeCsvField(field: String): String = + if (field.contains(DELIMITER) || field.contains('"') || field.contains('\n')) { + "\"" + field.replace("\"", "\"\"") + "\"" + } else { + field + } diff --git a/android/app/src/main/java/de/ladkau/fuelish/export/OdsExporter.kt b/android/app/src/main/java/de/ladkau/fuelish/export/OdsExporter.kt new file mode 100644 index 0000000..a77665c --- /dev/null +++ b/android/app/src/main/java/de/ladkau/fuelish/export/OdsExporter.kt @@ -0,0 +1,92 @@ +package de.ladkau.fuelish.export + +import de.ladkau.fuelish.data.Receipt +import java.io.OutputStream +import java.util.zip.CRC32 +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream + +private const val MIMETYPE = "application/vnd.oasis.opendocument.spreadsheet" + +/** + * A minimal hand-rolled ODF spreadsheet writer - no dependency needed, + * since an .ods is just a zip of a few fixed XML files. Writes only what + * testdata/.../benzin_kosten.ods's own reader (compare_ground_truth.py, + * via odfpy) needs: one table, string-typed cells (matching how that + * ground-truth sheet itself stores formatted values - see + * [toExportRow]'s doc comment), no styles/formulas. + */ +fun exportOds(receipts: List, out: OutputStream) { + ZipOutputStream(out).use { zip -> + // Must be the first entry, stored uncompressed with no extra + // fields, so ODF-aware tools can sniff the file type without + // inflating - the format spec requires this. + val mimetypeBytes = MIMETYPE.toByteArray(Charsets.US_ASCII) + val mimetypeEntry = ZipEntry("mimetype").apply { + method = ZipEntry.STORED + size = mimetypeBytes.size.toLong() + compressedSize = mimetypeBytes.size.toLong() + crc = CRC32().apply { update(mimetypeBytes) }.value + } + zip.putNextEntry(mimetypeEntry) + zip.write(mimetypeBytes) + zip.closeEntry() + + zip.putNextEntry(ZipEntry("META-INF/manifest.xml")) + zip.write(manifestXml().toByteArray(Charsets.UTF_8)) + zip.closeEntry() + + zip.putNextEntry(ZipEntry("content.xml")) + zip.write(contentXml(receipts).toByteArray(Charsets.UTF_8)) + zip.closeEntry() + } +} + +private fun manifestXml(): String = """ + + + + + +""".trimIndent() + +private fun contentXml(receipts: List): String { + val rows = buildString { + append(tableRow(EXPORT_HEADERS)) + for (receipt in receipts) { + append(tableRow(receipt.toExportRow())) + } + } + return """ + + + + + + $rows + + + + + """.trimIndent() +} + +private fun tableRow(cells: List): String = buildString { + append("") + for (cell in cells) { + append("") + append(escapeXml(cell)) + append("") + } + append("") +} + +private fun escapeXml(text: String): String = text + .replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) diff --git a/android/app/src/main/java/de/ladkau/fuelish/export/ReceiptRow.kt b/android/app/src/main/java/de/ladkau/fuelish/export/ReceiptRow.kt new file mode 100644 index 0000000..5e7c37c --- /dev/null +++ b/android/app/src/main/java/de/ladkau/fuelish/export/ReceiptRow.kt @@ -0,0 +1,29 @@ +package de.ladkau.fuelish.export + +import de.ladkau.fuelish.data.Receipt +import java.util.Locale + +/** + * The first 6 columns mirror testdata/.../benzin_kosten.ods's layout + * exactly (date, time, price_per_litre, litres, discount, total_price - + * see tooling-container/src/compare_ground_truth.py's column order and + * its German-locale decimal-comma / "€"-suffix parsing, which this + * reproduces on the way out). petrol_station/transaction_number are + * appended as extra trailing columns - present in this app's data but + * not in the original ground-truth sheet. + */ +val EXPORT_HEADERS = listOf( + "Datum", "Uhrzeit", "Preis/Liter", "Liter", "Rabatt", "Gesamtpreis", + "Tankstelle", "Transaktionsnummer", +) + +fun Receipt.toExportRow(): List = listOf( + date.orEmpty(), + time.orEmpty(), + pricePerLitre?.let { String.format(Locale.GERMANY, "%.3f", it) }.orEmpty(), + litres?.let { String.format(Locale.GERMANY, "%.2f", it) }.orEmpty(), + discount?.let { String.format(Locale.GERMANY, "%.2f €", it) }.orEmpty(), + totalPrice?.let { String.format(Locale.GERMANY, "%.2f €", it) }.orEmpty(), + petrolStation.orEmpty(), + transactionNumber.orEmpty(), +) diff --git a/android/app/src/main/java/de/ladkau/fuelish/ui/FuelishApp.kt b/android/app/src/main/java/de/ladkau/fuelish/ui/FuelishApp.kt new file mode 100644 index 0000000..a0e6080 --- /dev/null +++ b/android/app/src/main/java/de/ladkau/fuelish/ui/FuelishApp.kt @@ -0,0 +1,105 @@ +package de.ladkau.fuelish.ui + +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Menu +import androidx.compose.material3.DrawerValue +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.ModalDrawerSheet +import androidx.compose.material3.ModalNavigationDrawer +import androidx.compose.material3.NavigationDrawerItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.rememberDrawerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import de.ladkau.fuelish.R +import de.ladkau.fuelish.ui.capture.CaptureScreen +import de.ladkau.fuelish.ui.overview.OverviewScreen +import de.ladkau.fuelish.ui.settings.ConfigScreen +import kotlinx.coroutines.launch + +private object Destinations { + const val CAPTURE = "capture" + const val OVERVIEW = "overview" + const val SETTINGS = "settings" +} + +/** Burger menu = ModalNavigationDrawer, leading to Scan/Receipts/Settings - + * the app's single navigation host. */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun FuelishApp() { + val navController = rememberNavController() + val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) + val scope = rememberCoroutineScope() + val currentRoute = navController.currentBackStackEntryAsState().value?.destination?.route + + ModalNavigationDrawer( + drawerState = drawerState, + drawerContent = { + ModalDrawerSheet { + NavigationDrawerItem( + label = { Text(stringResource(R.string.nav_capture)) }, + selected = currentRoute == Destinations.CAPTURE, + onClick = { navigateTo(navController, Destinations.CAPTURE); scope.launch { drawerState.close() } }, + modifier = Modifier.padding(horizontal = 12.dp), + ) + NavigationDrawerItem( + label = { Text(stringResource(R.string.nav_overview)) }, + selected = currentRoute == Destinations.OVERVIEW, + onClick = { navigateTo(navController, Destinations.OVERVIEW); scope.launch { drawerState.close() } }, + modifier = Modifier.padding(horizontal = 12.dp), + ) + NavigationDrawerItem( + label = { Text(stringResource(R.string.nav_settings)) }, + selected = currentRoute == Destinations.SETTINGS, + onClick = { navigateTo(navController, Destinations.SETTINGS); scope.launch { drawerState.close() } }, + modifier = Modifier.padding(horizontal = 12.dp), + ) + } + }, + ) { + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.app_name)) }, + navigationIcon = { + IconButton(onClick = { scope.launch { drawerState.open() } }) { + Icon(Icons.Filled.Menu, contentDescription = stringResource(R.string.nav_settings)) + } + }, + ) + }, + ) { innerPadding -> + NavHost( + navController = navController, + startDestination = Destinations.CAPTURE, + modifier = Modifier.padding(innerPadding), + ) { + composable(Destinations.CAPTURE) { CaptureScreen() } + composable(Destinations.OVERVIEW) { OverviewScreen() } + composable(Destinations.SETTINGS) { ConfigScreen() } + } + } + } +} + +private fun navigateTo(navController: NavHostController, route: String) { + navController.navigate(route) { + popUpTo(navController.graph.startDestinationId) { saveState = true } + launchSingleTop = true + restoreState = true + } +} diff --git a/android/app/src/main/java/de/ladkau/fuelish/ui/capture/CaptureScreen.kt b/android/app/src/main/java/de/ladkau/fuelish/ui/capture/CaptureScreen.kt new file mode 100644 index 0000000..75f36df --- /dev/null +++ b/android/app/src/main/java/de/ladkau/fuelish/ui/capture/CaptureScreen.kt @@ -0,0 +1,170 @@ +package de.ladkau.fuelish.ui.capture + +import android.Manifest +import android.content.pm.PackageManager +import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat +import androidx.core.content.FileProvider +import androidx.lifecycle.viewmodel.compose.viewModel +import de.ladkau.fuelish.R +import de.ladkau.fuelish.data.ExtractedReceiptDto +import de.ladkau.fuelish.data.JobStatusResponse +import java.io.File + +@Composable +fun CaptureScreen(viewModel: CaptureViewModel = viewModel()) { + val uiState by viewModel.uiState.collectAsState() + val context = LocalContext.current + + var pendingFile by remember { mutableStateOf(null) } + var pendingUri by remember { mutableStateOf(null) } + + val takePictureLauncher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicture()) { success -> + val file = pendingFile + val uri = pendingUri + if (success && file != null && uri != null) { + viewModel.submit(file, uri) + } + } + + fun launchCamera() { + val dir = File(context.cacheDir, "captures").apply { mkdirs() } + val file = File(dir, "capture-${System.currentTimeMillis()}.jpg") + val uri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file) + pendingFile = file + pendingUri = uri + takePictureLauncher.launch(uri) + } + + val cameraPermissionLauncher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted -> + if (granted) launchCamera() + } + + Box(modifier = Modifier.fillMaxSize().padding(16.dp)) { + when (val state = uiState) { + is CaptureUiState.Idle -> Button( + onClick = { + val hasPermission = ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == + PackageManager.PERMISSION_GRANTED + if (hasPermission) launchCamera() else cameraPermissionLauncher.launch(Manifest.permission.CAMERA) + }, + ) { + Text(stringResource(R.string.capture_take_photo)) + } + + is CaptureUiState.Uploading -> ProgressStage(stringResource(R.string.capture_stage_uploading), 0.2f) + + is CaptureUiState.Processing -> { + val (labelRes, progress) = when (state.stage) { + JobStatusResponse.STATUS_PREPROCESSING -> R.string.capture_stage_preprocessing to 0.45f + JobStatusResponse.STATUS_OCR -> R.string.capture_stage_ocr to 0.7f + JobStatusResponse.STATUS_EXTRACTING -> R.string.capture_stage_extracting to 0.9f + else -> R.string.capture_stage_uploading to 0.2f + } + ProgressStage(stringResource(labelRes), progress) + } + + is CaptureUiState.Editing -> EditForm(draft = state.draft, onSave = { viewModel.save(it) }) + + is CaptureUiState.Error -> Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + Text(stringResource(R.string.capture_stage_error, state.message)) + Button(onClick = { viewModel.reset() }) { Text(stringResource(R.string.capture_retry)) } + } + } + } +} + +@Composable +private fun ProgressStage(label: String, progress: Float) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Text(label) + LinearProgressIndicator(progress = { progress }, modifier = Modifier.fillMaxWidth()) + } +} + +@Composable +private fun EditForm(draft: ExtractedReceiptDto, onSave: (ExtractedReceiptDto) -> Unit) { + var petrolStation by remember { mutableStateOf(draft.petrolStation.orEmpty()) } + var transactionNumber by remember { mutableStateOf(draft.transactionNumber.orEmpty()) } + var date by remember { mutableStateOf(draft.date.orEmpty()) } + var time by remember { mutableStateOf(draft.time.orEmpty()) } + var pricePerLitre by remember { mutableStateOf(draft.pricePerLitre?.toString().orEmpty()) } + var litres by remember { mutableStateOf(draft.litres?.toString().orEmpty()) } + var discount by remember { mutableStateOf(draft.discount?.toString().orEmpty()) } + var totalPrice by remember { mutableStateOf(draft.totalPrice?.toString().orEmpty()) } + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + LabeledField(R.string.capture_field_petrol_station, petrolStation) { petrolStation = it } + LabeledField(R.string.capture_field_transaction_number, transactionNumber) { transactionNumber = it } + LabeledField(R.string.capture_field_date, date) { date = it } + LabeledField(R.string.capture_field_time, time) { time = it } + LabeledField(R.string.capture_field_price_per_litre, pricePerLitre) { pricePerLitre = it } + LabeledField(R.string.capture_field_litres, litres) { litres = it } + LabeledField(R.string.capture_field_discount, discount) { discount = it } + LabeledField(R.string.capture_field_total_price, totalPrice) { totalPrice = it } + + Button( + onClick = { + onSave( + ExtractedReceiptDto( + petrolStation = petrolStation.ifBlank { null }, + transactionNumber = transactionNumber.ifBlank { null }, + date = date.ifBlank { null }, + time = time.ifBlank { null }, + pricePerLitre = pricePerLitre.toDoubleOrNull(), + litres = litres.toDoubleOrNull(), + discount = discount.toDoubleOrNull(), + totalPrice = totalPrice.toDoubleOrNull(), + ), + ) + }, + modifier = Modifier.align(Alignment.End), + ) { + Text(stringResource(R.string.capture_save)) + } + } +} + +@Composable +private fun LabeledField(labelRes: Int, value: String, onValueChange: (String) -> Unit) { + OutlinedTextField( + value = value, + onValueChange = onValueChange, + label = { Text(stringResource(labelRes)) }, + singleLine = true, + modifier = Modifier.fillMaxWidth(), + ) +} diff --git a/android/app/src/main/java/de/ladkau/fuelish/ui/capture/CaptureUiState.kt b/android/app/src/main/java/de/ladkau/fuelish/ui/capture/CaptureUiState.kt new file mode 100644 index 0000000..02e42f2 --- /dev/null +++ b/android/app/src/main/java/de/ladkau/fuelish/ui/capture/CaptureUiState.kt @@ -0,0 +1,14 @@ +package de.ladkau.fuelish.ui.capture + +import de.ladkau.fuelish.data.ExtractedReceiptDto + +sealed interface CaptureUiState { + data object Idle : CaptureUiState + data object Uploading : CaptureUiState + + /** [stage] is one of JobStatusResponse's STATUS_* constants (preprocessing/ocr/extracting). */ + data class Processing(val stage: String) : CaptureUiState + + data class Editing(val draft: ExtractedReceiptDto) : CaptureUiState + data class Error(val message: String) : CaptureUiState +} diff --git a/android/app/src/main/java/de/ladkau/fuelish/ui/capture/CaptureViewModel.kt b/android/app/src/main/java/de/ladkau/fuelish/ui/capture/CaptureViewModel.kt new file mode 100644 index 0000000..0d32a9f --- /dev/null +++ b/android/app/src/main/java/de/ladkau/fuelish/ui/capture/CaptureViewModel.kt @@ -0,0 +1,96 @@ +package de.ladkau.fuelish.ui.capture + +import android.app.Application +import android.net.Uri +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import de.ladkau.fuelish.data.ConfigRepository +import de.ladkau.fuelish.data.ExtractedReceiptDto +import de.ladkau.fuelish.data.JobStatusResponse +import de.ladkau.fuelish.data.Receipt +import de.ladkau.fuelish.data.ReceiptRepository +import java.io.File +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch + +private const val POLL_INTERVAL_MILLIS = 1_500L + +class CaptureViewModel(application: Application) : AndroidViewModel(application) { + private val repository = ReceiptRepository(application) + private val configRepository = ConfigRepository(application) + + private val _uiState = MutableStateFlow(CaptureUiState.Idle) + val uiState: StateFlow = _uiState.asStateFlow() + + // The captured photo's local URI, kept alongside the receipt once saved + // (see Receipt.imageUri) - set right before submit(), cleared on save()/reset(). + private var pendingImageUri: Uri? = null + + fun submit(imageFile: File, imageUri: Uri) { + pendingImageUri = imageUri + viewModelScope.launch { + _uiState.value = CaptureUiState.Uploading + try { + val baseUrl = configRepository.config.first().serverUrl + if (baseUrl.isBlank()) { + _uiState.value = CaptureUiState.Error("Set the server URL in Settings first") + return@launch + } + val jobId = repository.submitImage(baseUrl, imageFile) + pollUntilDone(baseUrl, jobId) + } catch (e: Exception) { + _uiState.value = CaptureUiState.Error(e.message ?: "unknown error") + } + } + } + + private suspend fun pollUntilDone(baseUrl: String, jobId: String) { + while (true) { + val status = repository.pollJob(baseUrl, jobId) + when (status.status) { + JobStatusResponse.STATUS_DONE -> { + _uiState.value = CaptureUiState.Editing(status.result ?: ExtractedReceiptDto()) + return + } + JobStatusResponse.STATUS_ERROR -> { + _uiState.value = CaptureUiState.Error(status.error ?: "processing failed") + return + } + else -> { + _uiState.value = CaptureUiState.Processing(status.status) + delay(POLL_INTERVAL_MILLIS) + } + } + } + } + + fun save(draft: ExtractedReceiptDto) { + viewModelScope.launch { + repository.save( + Receipt( + petrolStation = draft.petrolStation, + transactionNumber = draft.transactionNumber, + date = draft.date, + time = draft.time, + pricePerLitre = draft.pricePerLitre, + litres = draft.litres, + discount = draft.discount, + totalPrice = draft.totalPrice, + imageUri = pendingImageUri?.toString(), + createdAtEpochMillis = System.currentTimeMillis(), + ), + ) + pendingImageUri = null + _uiState.value = CaptureUiState.Idle + } + } + + fun reset() { + pendingImageUri = null + _uiState.value = CaptureUiState.Idle + } +} diff --git a/android/app/src/main/java/de/ladkau/fuelish/ui/overview/OverviewScreen.kt b/android/app/src/main/java/de/ladkau/fuelish/ui/overview/OverviewScreen.kt new file mode 100644 index 0000000..b54ee98 --- /dev/null +++ b/android/app/src/main/java/de/ladkau/fuelish/ui/overview/OverviewScreen.kt @@ -0,0 +1,93 @@ +package de.ladkau.fuelish.ui.overview + +import android.content.Intent +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 +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material3.Button +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import de.ladkau.fuelish.R +import de.ladkau.fuelish.data.Receipt + +@Composable +fun OverviewScreen(viewModel: OverviewViewModel = viewModel()) { + val receipts by viewModel.receipts.collectAsState() + val context = LocalContext.current + + Column(modifier = Modifier.fillMaxSize()) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + Button(onClick = { viewModel.exportCsv { uri -> shareFile(context, uri, "text/csv") } }) { + Text(stringResource(R.string.overview_export_csv)) + } + Button(onClick = { viewModel.exportOds { uri -> shareFile(context, uri, "application/vnd.oasis.opendocument.spreadsheet") } }) { + Text(stringResource(R.string.overview_export_ods)) + } + } + + if (receipts.isEmpty()) { + Text( + text = stringResource(R.string.overview_empty), + modifier = Modifier.padding(16.dp), + ) + } else { + LazyColumn(modifier = Modifier.fillMaxSize()) { + items(receipts, key = { it.id }) { receipt -> + ReceiptRow(receipt = receipt, onDelete = { viewModel.delete(receipt) }) + HorizontalDivider() + } + } + } + } +} + +@Composable +private fun ReceiptRow(receipt: Receipt, onDelete: () -> Unit) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Column { + Text(text = receipt.date.orEmpty()) + Text(text = listOfNotNull(receipt.petrolStation, receipt.totalPrice?.let { "%.2f €".format(it) }).joinToString(" · ")) + } + IconButton(onClick = onDelete) { + Icon(Icons.Filled.Delete, contentDescription = null) + } + } +} + +private fun shareFile(context: android.content.Context, uri: android.net.Uri, mimeType: String) { + val intent = Intent(Intent.ACTION_SEND).apply { + type = mimeType + putExtra(Intent.EXTRA_STREAM, uri) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + context.startActivity(Intent.createChooser(intent, null)) +} diff --git a/android/app/src/main/java/de/ladkau/fuelish/ui/overview/OverviewViewModel.kt b/android/app/src/main/java/de/ladkau/fuelish/ui/overview/OverviewViewModel.kt new file mode 100644 index 0000000..aacfb32 --- /dev/null +++ b/android/app/src/main/java/de/ladkau/fuelish/ui/overview/OverviewViewModel.kt @@ -0,0 +1,46 @@ +package de.ladkau.fuelish.ui.overview + +import android.app.Application +import android.net.Uri +import androidx.core.content.FileProvider +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import de.ladkau.fuelish.data.Receipt +import de.ladkau.fuelish.data.ReceiptRepository +import de.ladkau.fuelish.export.exportCsv as writeCsvRows +import de.ladkau.fuelish.export.exportOds as writeOdsRows +import java.io.File +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class OverviewViewModel(application: Application) : AndroidViewModel(application) { + private val repository = ReceiptRepository(application) + + val receipts: StateFlow> = repository.receipts + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList()) + + fun delete(receipt: Receipt) { + viewModelScope.launch { repository.delete(receipt) } + } + + fun exportCsv(onReady: (Uri) -> Unit) = export("fuelish-receipts.csv", ::writeCsvRows, onReady) + + fun exportOds(onReady: (Uri) -> Unit) = export("fuelish-receipts.ods", ::writeOdsRows, onReady) + + private fun export(fileName: String, write: (List, java.io.OutputStream) -> Unit, onReady: (Uri) -> Unit) { + val app = getApplication() + viewModelScope.launch { + val uri = withContext(Dispatchers.IO) { + val dir = File(app.cacheDir, "exports").apply { mkdirs() } + val file = File(dir, fileName) + file.outputStream().use { write(receipts.value, it) } + FileProvider.getUriForFile(app, "${app.packageName}.fileprovider", file) + } + onReady(uri) + } + } +} diff --git a/android/app/src/main/java/de/ladkau/fuelish/ui/settings/ConfigScreen.kt b/android/app/src/main/java/de/ladkau/fuelish/ui/settings/ConfigScreen.kt new file mode 100644 index 0000000..18a6439 --- /dev/null +++ b/android/app/src/main/java/de/ladkau/fuelish/ui/settings/ConfigScreen.kt @@ -0,0 +1,42 @@ +package de.ladkau.fuelish.ui.settings + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import de.ladkau.fuelish.R + +@Composable +fun ConfigScreen(viewModel: ConfigViewModel = viewModel()) { + val config by viewModel.config.collectAsState() + val isDirty by viewModel.isDirty.collectAsState() + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + OutlinedTextField( + value = config.serverUrl, + onValueChange = { viewModel.update { c -> c.copy(serverUrl = it) } }, + label = { Text(stringResource(R.string.settings_server_url)) }, + placeholder = { Text(stringResource(R.string.settings_server_url_hint)) }, + singleLine = true, + modifier = Modifier.fillMaxWidth(), + ) + Button(onClick = { viewModel.save() }, enabled = isDirty) { + Text(stringResource(R.string.capture_save)) + } + } +} diff --git a/android/app/src/main/java/de/ladkau/fuelish/ui/settings/ConfigViewModel.kt b/android/app/src/main/java/de/ladkau/fuelish/ui/settings/ConfigViewModel.kt new file mode 100644 index 0000000..63ae510 --- /dev/null +++ b/android/app/src/main/java/de/ladkau/fuelish/ui/settings/ConfigViewModel.kt @@ -0,0 +1,51 @@ +package de.ladkau.fuelish.ui.settings + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import de.ladkau.fuelish.data.AppConfig +import de.ladkau.fuelish.data.ConfigRepository +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +class ConfigViewModel(application: Application) : AndroidViewModel(application) { + private val configRepository = ConfigRepository(application) + + // A local editable draft, seeded once from the persisted config below - + // deliberately not a live collection of configRepository.config, so + // in-progress edits aren't clobbered by DataStore's own emission of the + // write this same screen is about to make on save(). + private val _config = MutableStateFlow(AppConfig()) + val config: StateFlow = _config.asStateFlow() + + private val _savedConfig = MutableStateFlow(AppConfig()) + + val isDirty: StateFlow = combine(_config, _savedConfig) { draft, saved -> draft != saved } + .stateIn(viewModelScope, SharingStarted.Eagerly, false) + + init { + viewModelScope.launch { + val loaded = configRepository.config.first() + _config.value = loaded + _savedConfig.value = loaded + } + } + + fun update(transform: (AppConfig) -> AppConfig) { + _config.value = transform(_config.value) + } + + fun save() { + viewModelScope.launch { + val toSave = _config.value + configRepository.save(toSave) + _savedConfig.value = toSave + } + } +} diff --git a/android/app/src/main/java/de/ladkau/fuelish/ui/theme/Theme.kt b/android/app/src/main/java/de/ladkau/fuelish/ui/theme/Theme.kt new file mode 100644 index 0000000..88c1e7b --- /dev/null +++ b/android/app/src/main/java/de/ladkau/fuelish/ui/theme/Theme.kt @@ -0,0 +1,30 @@ +package de.ladkau.fuelish.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 FuelishTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + dynamicColor: Boolean = true, + content: @Composable () -> Unit, +) { + val context = LocalContext.current + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + darkTheme -> DarkColors + else -> LightColors + } + MaterialTheme(colorScheme = colorScheme, content = content) +} diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..be43858 --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..be43858 --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..405b96c --- /dev/null +++ b/android/app/src/main/res/values/colors.xml @@ -0,0 +1,8 @@ + + + + #FFFFFF + #FFFFFF + diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..882625c --- /dev/null +++ b/android/app/src/main/res/values/strings.xml @@ -0,0 +1,33 @@ + + + fuelish + + Scan receipt + Receipts + Settings + + Take photo + Uploading… + Preprocessing image… + Reading text (OCR)… + Extracting fields… + Something went wrong: %1$s + Retry + Save + Petrol station + Transaction number + Date (DD.MM.YYYY) + Time (HH:MM:SS) + Price per litre + Litres + Discount + Total price + + No receipts saved yet + Export CSV + Export ODS + Exported to %1$s + + Server URL + http://192.168.1.10:8080 + diff --git a/android/app/src/main/res/values/themes.xml b/android/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..bdfd53b --- /dev/null +++ b/android/app/src/main/res/values/themes.xml @@ -0,0 +1,10 @@ + + + + diff --git a/android/app/src/main/res/xml/file_paths.xml b/android/app/src/main/res/xml/file_paths.xml new file mode 100644 index 0000000..8837e47 --- /dev/null +++ b/android/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/android/app/src/main/res/xml/network_security_config.xml b/android/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000..4c7947b --- /dev/null +++ b/android/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/android/build.gradle.kts b/android/build.gradle.kts new file mode 100644 index 0000000..445dfbb --- /dev/null +++ b/android/build.gradle.kts @@ -0,0 +1,6 @@ +plugins { + alias(libs.plugins.android.application) apply false + alias(libs.plugins.kotlin.serialization) apply false + alias(libs.plugins.kotlin.compose) apply false + alias(libs.plugins.ksp) apply false +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..a40f978 --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,11 @@ +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +android.useAndroidX=true +kotlin.code.style=official +ksp.useKSP2=true + +# AGP 9's built-in Kotlin support rejects KSP's use of the kotlin.sourceSets +# DSL to register generated-source dirs (Room's annotation processor, via +# KSP) - this is AGP/KSP's own suggested suppression, not a workaround for +# anything we did. See https://developer.android.com/r/tools/built-in-kotlin +android.disallowKotlinSourceSets=false + diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml new file mode 100644 index 0000000..fe85691 --- /dev/null +++ b/android/gradle/libs.versions.toml @@ -0,0 +1,56 @@ +[versions] +# Version-lookup tools (WebFetch/registry browsing) were unavailable while +# this file was written, so these are last-known-good pins rather than a +# freshly verified "latest stable" snapshot - if Gradle can't resolve one, +# bump it to whatever's current on Google's Maven / Maven Central. +# +# kotlin/ksp are pinned to 2.3.x rather than 2.2.20: androidx.room: +# room-common-jvm:2.8.5 declares a `{strictly 2.3.20}` constraint on +# kotlin-stdlib, which would otherwise silently resolve above what a +# 2.2.20 compiler/KSP understands. Keep these two in sync if Room is ever +# bumped further. +agp = "9.2.1" +kotlin = "2.3.20" +# KSP's versioning stopped mirroring Kotlin's exact patch after the 2.3.0 +# line (no "2.3.20" KSP release exists) - 2.3.12 is the latest in the same +# 2.3.x line as kotlin above. +ksp = "2.3.12" +coreKtx = "1.17.0" +lifecycleRuntimeKtx = "2.9.4" +activityCompose = "1.13.0" +composeBom = "2026.06.01" +navigationCompose = "2.9.8" +datastorePreferences = "1.2.1" +kotlinxSerializationJson = "1.11.0" +kotlinxCoroutines = "1.11.0" +room = "2.8.5" +splashscreen = "1.0.1" +okhttp = "4.12.0" + +[libraries] +androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } +androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleRuntimeKtx" } +androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } +androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } +androidx-ui = { group = "androidx.compose.ui", name = "ui" } +androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } +androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } +androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } +androidx-material3 = { group = "androidx.compose.material3", name = "material3" } +androidx-material-icons-core = { group = "androidx.compose.material", name = "material-icons-core" } +androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" } +androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastorePreferences" } +androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" } +androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } +androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } +androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "splashscreen" } +kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } +kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" } +okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..b1b8ef56b44f16b14dc800fa8103a6d89abb526f GIT binary patch literal 48462 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 R2np~f1o-$>Q}y+){|A@R9n$~+ literal 0 HcmV?d00001 diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..a9db115 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,9 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip +networkTimeout=10000 +retries=0 +retryBackOffMs=500 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/android/gradlew b/android/gradlew new file mode 100755 index 0000000..249efbb --- /dev/null +++ b/android/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# gradlew start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh gradlew +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat new file mode 100644 index 0000000..a51ec4f --- /dev/null +++ b/android/gradlew.bat @@ -0,0 +1,82 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem gradlew startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:execute +@rem Setup the command line + + + +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel + +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/android/scripts/gen_app_icons.py b/android/scripts/gen_app_icons.py new file mode 100644 index 0000000..f7d6259 --- /dev/null +++ b/android/scripts/gen_app_icons.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +"""Generates the Android launcher icon and splash screen art from +../../logo.png (the repo's brand mark + "fuelish" wordmark). + +Only the graphic mark (not the wordmark) works as a launcher/splash icon, +so this first isolates it: crop the upper portion of the source image +(where the mark lives) and auto-crop that to its actual non-transparent +bounding box, rather than relying on any hardcoded pixel offsets that +would break if logo.png is ever redrawn at a different resolution. + +Produces: +- mipmap-/ic_launcher_foreground.png - the mark centered, scaled + to the adaptive-icon safe zone (66% of a 108dp canvas), transparent + background - paired at runtime with @color/ic_launcher_background (see + mipmap-anydpi-v26/ic_launcher.xml, committed - a static reference, not + generated) for API 26+. +- mipmap-/ic_launcher.png / ic_launcher_round.png - the same + mark flattened onto ic_launcher_background, for API <26 devices that + can't use the adaptive-icon XML at all. +- drawable-nodpi/splash_icon.png - the mark on a transparent background, + referenced by Theme.Fuelish.Splash's windowSplashScreenAnimatedIcon + (see values/themes.xml). + +Generated output, not committed (see the repo's root .gitignore's +/android/app/src/main/res/{mipmap-*,drawable-nodpi} entries) - regenerate +by running this script directly or via Gradle's generateAppIcons task +(wired into app's preBuild, see ../app/build.gradle.kts). Requires +Pillow. +""" +import os +import sys + +from PIL import Image + +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +LOGO_PATH = os.path.normpath(os.path.join(SCRIPT_DIR, "..", "..", "logo.png")) +RES_DIR = os.path.normpath(os.path.join(SCRIPT_DIR, "..", "app", "src", "main", "res")) + +BACKGROUND_RGBA = (255, 255, 255, 255) # matches @color/ic_launcher_background + +# (density qualifier, adaptive-icon canvas px, legacy launcher px) +DENSITIES = [ + ("mdpi", 108, 48), + ("hdpi", 162, 72), + ("xhdpi", 216, 96), + ("xxhdpi", 324, 144), + ("xxxhdpi", 432, 192), +] + +# Adaptive icons render only the inner ~66% "safe zone" reliably across +# launchers that apply their own mask shape. +SAFE_ZONE_RATIO = 0.66 + +# Fraction of logo.png's height (from the top) that contains the graphic +# mark, before the "fuelish" wordmark begins. +MARK_REGION_HEIGHT_RATIO = 0.60 + +SPLASH_CANVAS_PX = 480 +SPLASH_ICON_RATIO = 0.75 + + +def extract_mark(logo: Image.Image) -> Image.Image: + width, height = logo.size + region = logo.crop((0, 0, width, round(height * MARK_REGION_HEIGHT_RATIO))) + bbox = region.getchannel("A").getbbox() + if bbox is None: + raise ValueError(f"{LOGO_PATH}: no non-transparent pixels found in the top " + f"{MARK_REGION_HEIGHT_RATIO:.0%} of the image") + mark = region.crop(bbox) + + # Pad to square so downstream scaling doesn't distort the aspect ratio. + side = max(mark.width, mark.height) + square = Image.new("RGBA", (side, side), (0, 0, 0, 0)) + square.paste(mark, ((side - mark.width) // 2, (side - mark.height) // 2), mark) + return square + + +def resized(image: Image.Image, target_px: int) -> Image.Image: + ratio = target_px / max(image.width, image.height) + size = (round(image.width * ratio), round(image.height * ratio)) + return image.resize(size, Image.LANCZOS) + + +def centered_on(canvas_px: int, content: Image.Image, background: tuple) -> Image.Image: + canvas = Image.new("RGBA", (canvas_px, canvas_px), background) + offset = ((canvas_px - content.width) // 2, (canvas_px - content.height) // 2) + canvas.paste(content, offset, content) + return canvas + + +def main(): + logo = Image.open(LOGO_PATH).convert("RGBA") + mark = extract_mark(logo) + + for density, canvas_px, legacy_px in DENSITIES: + mipmap_dir = os.path.join(RES_DIR, f"mipmap-{density}") + os.makedirs(mipmap_dir, exist_ok=True) + + foreground = centered_on(canvas_px, resized(mark, round(canvas_px * SAFE_ZONE_RATIO)), (0, 0, 0, 0)) + foreground.save(os.path.join(mipmap_dir, "ic_launcher_foreground.png")) + + legacy = centered_on(legacy_px, resized(mark, round(legacy_px * SAFE_ZONE_RATIO)), BACKGROUND_RGBA) + legacy.convert("RGB").save(os.path.join(mipmap_dir, "ic_launcher.png")) + legacy.convert("RGB").save(os.path.join(mipmap_dir, "ic_launcher_round.png")) + + drawable_dir = os.path.join(RES_DIR, "drawable-nodpi") + os.makedirs(drawable_dir, exist_ok=True) + splash = centered_on(SPLASH_CANVAS_PX, resized(mark, round(SPLASH_CANVAS_PX * SPLASH_ICON_RATIO)), (0, 0, 0, 0)) + splash.save(os.path.join(drawable_dir, "splash_icon.png")) + + print(f"wrote launcher icons for {len(DENSITIES)} densities + splash icon to {RES_DIR}", file=sys.stderr) + + +if __name__ == "__main__": + main() diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts new file mode 100644 index 0000000..077e335 --- /dev/null +++ b/android/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +// Auto-provisions the JDK 17 toolchain app/build.gradle.kts requests (see +// its jvmToolchain(17) call) - this host's only JDKs otherwise are system +// JDK 11 (too old for AGP 9) and Android Studio's bundled JBR 25 (newer +// than Kotlin's compiler officially targets yet). +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "fuelish-android" +include(":app") diff --git a/build-image.sh b/build-image.sh old mode 100644 new mode 100755 index 4a67757..f3b63c0 --- a/build-image.sh +++ b/build-image.sh @@ -1,7 +1,9 @@ #!/usr/bin/env bash # Builds the fuelish tools image (tooling-container/Dockerfile) locally, tagged with -# tooling-container/VERSION and :latest. Does not push - test it with the numbered -# pipeline scripts first, then publish with ./upload-image.sh. +# tooling-container/VERSION and :latest, and pulls the other image the pipeline +# needs (ollama/ollama, used by 0_run_ollama.sh) so all images are ready before +# a pipeline run. Does not push - test it with the numbered pipeline scripts +# first, then publish with ./upload-image.sh. set -euo pipefail cd "$(dirname "${BASH_SOURCE[0]}")" @@ -29,6 +31,9 @@ docker build \ -t "$IMAGE:latest" \ "$ROOT/tooling-container" +echo "== Pulling ollama/ollama (used by 0_run_ollama.sh) ==" +docker pull ollama/ollama + echo "== Done ==" echo "Image: $IMAGE:$VERSION (and :latest)" echo "Test it with the numbered pipeline scripts, then publish with ./upload-image.sh" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..851f146 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,35 @@ +# Runs the pipeline as a long-lived HTTP API for the Android app, on top +# of the same tooling-container image the numbered host scripts use. +# `docker compose up` alone is enough: the api service waits for Ollama +# and pulls the model itself (see tooling-container/src/server.py). +# +# The numbered host scripts (0_run_ollama.sh, 1/2/3_*.sh) and +# test_batch.sh are untouched and remain the way to inspect/test the +# pipeline stage by stage; this is the additive "serve it for real" path. +# +# Usage: docker compose up -d +# API reachable at http://:8080 from any device on the LAN. +services: + ollama: + image: ollama/ollama + volumes: + - fuelish-ollama-data:/root/.ollama + restart: unless-stopped + + api: + build: + context: ./tooling-container + restart: unless-stopped + depends_on: + - ollama + environment: + OLLAMA_URL: http://ollama:11434/api/chat + OLLAMA_BASE_URL: http://ollama:11434 + OLLAMA_MODEL: qwen2.5:3b + command: ["uvicorn", "server:app", "--app-dir", "/opt/app/src", "--host", "0.0.0.0", "--port", "8000"] + ports: + - "8080:8000" + +volumes: + fuelish-ollama-data: + name: fuelish-ollama-data diff --git a/logo.png b/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..4dd344cf1675d1059a00477b6b8c2d02a2d6b25c GIT binary patch literal 77472 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⋘E^!JOp*2)M;yF@d*@RT6zh)L1kcF?GT#k`D*) z`<6?(-`~!5yW`j*w6sVqPO1B5R2K8p<*IYju6x2%X36=oOG^^a#~h;cV=B>iIU&HU zey~v)0zu5SgfIKd%EsJd!C>T?o%z0o;1%hhD>|w#HsaLqSL_z)VCbw(tFB_28 zM_OB2VOdSaiR^(b<}JaKY)XJ0Fm^~n4B9lt!#<^CsYBYUQR2W%J~@!W2js$gRlb=12nCx+|ld-4OqXjU+bYW{7698l$%aWPGK@1y1}Fh?@iX z%l}|__wxPA@4?q9&x74Vo8V?+|1{Y~JR8*c#H$Gz z(kD9M37f8~Th~Lx1aJkO$m{v$+fcXP$^dF)Kx8R2WrNKiU)ZY8v)j`pcNlnkzSK7yyHd1xfY$ex!jNcU-B1p- z%MuTehAxWPaglCUQ6$+H0V6OcR1+I>{ZP!k)!G^tF^xW-dgZGmWzkr}RFA(R94!M&y_ zUyOysp`+uRDZ>t!;csb${9o0dsL01TdezGX(ko2o3<*afCH=pgZ#%nVd8Ch8M$dbQasnIhVhuB%s1HN24z2YJeP85 zOlivv3x>Ow94JcMl&04GK|Bs8CZQ_=w!*p>sM1>;g9X^WrC%3G6UJA{wYfB*Sw@P6 zmsH8q4s-V4e*K3_yf?05T@Acm{Ycawe3kXZytK z8%B&?hbMvt8EBoFhdWBLdJV*P6YjRYlRggA8{c&?Ii)oF-<{lhW3!RHs8W|D z`)%^u&TIjO)C;u+_ZVF0td1mql8|e*Wk$DqgZFdx$Bff6MA7mX^!X@P)& zKVGjn_s0^zCa;R`7oniy0fSARM`NZ8Nki)H0&4+NL(+hd+E8?1%b$3Oo~C2w=B1tC=SUZhI zyxS}u9y3ISKfhmXZxa1bZoi@Zi0L|_hXi+n>|rxXP*PC>gc1Epj>noP{wx4a06))I zCemzMVIxLfX7>%GbBjo6qC8;9Y6i?Clo>$aUZ!XM@6zZl0AToA5+hN>AA3^J!PC0M zse`w_CWR;B214rtr7)VVO%z9_XK0by*1Nq>@V-`Lq4YmY8^zjCmiQq_^2sm0Mty1u z?RpDF+bfmrtpR_O*ZB-{P3w3@?cyV=4Tjq`@J}RN(wp28@#GLYrpSoU4ETp8n}OnG zIoJs1wJzaqfvrl)eoLQ%)RJBsN`w}mI^O=}r*Ef~^S=(?8&TypcR_^s@5^0SzF;`Eq&IW@u$!|c4$iS~A!mE;u4p;{1&#|Y1~B6x>#;Qqp7Wt~kIW13ug=-_pJ zaC;Z`#Tp&Q3y7S|zf_40eeq-MmbU(d)Z4%JgR=4rMJ;_sB`304Z#d>HY&a`*yq&fs z&^=l8kAyl+ruo4sDuBk6iX9gblv>U(QR}G}+Zdn~ue+=wO&%8f*^s?&!F8!lB>l## z-pW1sq$z=eiN(HE-Wh{3#MD`r+K$syJ>IB0#L5UQ199j!LpqSp- z(EcojA$!;iS04u({EKLj!2|wM;z9zTz%)7>D%nS_o;#H8hc2>N^*jEfOnA<{+(eXg zi{9TiuS{EuAT*Fg$;~3+NRKqVgNd0ThjI4+%o}_oDG@o(QjaG-gR19|Fl@~fk51H$ zyd_`-flFaB>I!%Z_QDIg!uHokb$QnQaj$+|<`k7PJfmreQVb)99x#oA`LQLu@S{-a zsRA4Y^Yt(m#V31Pz(z9`p%*?GYE&V98zw?;V{;r;aA2v!E89ACD;dc}ec%CmGjlr% zCZoo)CS=sVwrhi+MUoDHx9uNXgwAM3&aHuQI(>;1IzMyygdKOZPx3h0-1Z>H1Ji-`vK3({EX zaYPAFd{J`|pDMGQL=@}MgHi8zmoD_K#6{w8xO}ZRSzC|_YZ=Ll2KtN?K1y5`(oDLQ zrYFqmXn!Q6_u1FRSBh@Ola3hC3YMA5%?;+U%J#A9JRAiYeYC+RV{2IU*eQ>>>65jr ze^;XBz)@m5#I-6M<9eiRu8CCX_v&aNh@V^!o6;ju7Ik@R;)$9Q+-~FLva5g!jF#9A z#v$H77wQSjM;5IE{p!B>y2gb>b<^3miATz3G@uO(Sm{P?kWQAU3^z8Y;Nc1!yAln_+yMWtZQaz-qF zRmQJbqNY4?x99Y(VCIUtw1igpj#$5FJh7yeQA>?El0=iAU$o(f1)VC7^Ti@I*q%~R z^TtF5IwE+FUj!C~KWdN=#+)OtyAIRh!j}AAqEs^z2~o#!y}yH?aQeFkt8%#Z%3c`E zSJHs8uELLUP#Z5Q?T^bX7gk!lBNW4sG;FLd{-n;V1Izdc6aa+YL@!E^3r8{}MslkEn)krH|uRfJp ztafmjS(~42ZfEqZC>VtD8N?|BTh;c{6~hSYU$!hyUbvg=jg#*moz~$lbZH8Oh_}i zao@bZ*)W)60?)>PG;DuX@#+Ly>7vd5E_r5aXrM^*TTvCpQ6|cyIZ{`Bu^1U^66PEi zTg8K9E$REpr|a7vu^BMZ0+KLiXp>9&zW`Mm3gW0j&@Fw)-K^@a0E-DcE!H!Mck@Ft z!5$bC-gi7X8A1^8!Ea(8eZTq6lLi@L`;CW`am>Xh`o9HY2vW(lj<#gUjrb(_<|6RX z=JuN6Kq`D0)*k>^GFPC7Zov}OMz)0>C9&6*`jY}~T`s*RJ_AyE%y1k9ocWKq0IT%d zO7rCjf)Db|;djw&e*$2vxXls^>FyuSOQwF7$i@L}k4aqM09z>iwi4cM!grNdU}nf zZvU8EF`k@l_3wh+jf&e-ajeb{LvW@Bm_HEj3bHLxCFXEoEqb zZ+`JSo*uWMZHM#E>pJ3GSrde6& z7(L9M%%Rb0`(0zWp+!o#IaNfoOCZG@q-(qb7^A*?&V{i*3Uj`nBGA%c4I1LQtrmi9 zKDbzNA)#z;2)2)~kMQ6`Nm@t!r4ger(|`5-O9t=$ z0vBX$h_X0h>Gb+_wLQ}g1?^G)8OSU**`-F3HpFk&{o4z1^Q?7tct?h{hzVvu80x}B z#l&6ck7ZGTOZaE{t8{i)1J|kBo7pq5AI-TKtJsQSDrf4E6f`Fo@e1C8?#KPpkNlf* z+_B|)PyD`T06Er{@SAUR zYHdrI7NW?~4#>}u7Hl^%=@tvD+f!Dd?rFcw1(*JYiycwdm?1lzGW#v%&h~ zGN#>sK6@pLlC@dr-2BJHDu8=14_a71T}2G$z(@3IOHf9xd+iFa!6gfWl2(|Wuz%~J z;r^(aPI#xACx@;10pl_!&fD|U790O?-R%)yPq0N@5{&x&A@1_zUo?Jb1mXF=%`!ob zc@lhHbI`MZF(F(vy1NgL&0Sr-@72rP!ZLH~GW8&fny|!=Ts;{~T>AsE9yfR!2QhNL z9Z5>^@-Y#rR(7?vp{1m1$T+yV7El^i2~3U%B^cr!y}=n z_LE_NxeIJi3AfJLd3jdtKOyO!sumRG8KV)T9e@~?h}-!`5-No~zxOWM2m0B!W|dqM zHUd~vOSjV(nsIgWd%;cDsZU)!-jH9mnL`)j*F@lHe}Xh<`Hw6e%{l`ir}PvSS(M~Y zUYC}MCbn5y82z;>EMBFgg}%&*JQtmpS1%EEY*bl$Slz$o5>53J4tL=JM-9bBe45-- zdIk_!Ckw&*Wro1|*bJ!tMOY!szvk_$;m^Dvq|X*8$^TSotbKyggzP@PAcdgYm&SJw z=G{C2+vMM#DHJLz>TE&@!zeJ^vzU3UUYRV~Z)aC|Fy-Ccv%Sn2tvjMc@`A>~F(mR_ z=Ys-*ToP{i-ydNJK)K|06QPqFm+Q*D5yM&VS z67!2bLv2Nx0Wh=5mu{#PY)y%Jc)Pz^4vkEiMS|u2DCQ92tqJv~#RV$9f3i%Kb7M&V zR?#mIilI`!Y}zC;x%NWr(;m*5D8;eRYCapupx+lpv<*tCF%*h{9eKf|2$d zh&8RO39!C8P9b~T0-qfI;`&+sJm(?$AduWxi|l5&i@;-cvcz?bTyMlCwE!S;{-ns* zr)Rn@7LJv`mq;h{4Tee%F#~6{G0^K~Lx;Kv0-*>qClPQb^mO+Or-GwHuH=qIOhxY( z$Cq}|r!&=nwF6==d)AvI8n=l3EDbR!?Hj~??J8=KIuarkSER!dqcinlCI2h&*#rDn ziki9an)1O5`sz)zB%=mm1Ji++Spy&mOa74-YD7&DnP^c-aUa5)PEtSa_jb$M<{@U2>m$RFHo4d#j1IzKCI!8?H3bsp_RSt~tS>F`O zrq8Fd<3`w+{XSmn*mB}1T=o98A9uq{@@C2UL+CzrCrbC2cp>NdC-=Wpe;7_t6%k$; z`HBpqS! z)kptkEEjL^^|Hn+;)Cx&@?vz0mHb~4p%P?A^(>!^K9`CN$ERexK?RNASc zRK$*d*99g$bDtcd1cFFU#K|X%>c2W76v(zdsrm=HXm36?2fRCB*a@PV-37pgO{ncZ zD0c~yKW7i(>nYCQUA8@Zv}r-z&&n69gwdz%5*(QmxES_04$z;L$v9IGiDC4AWtD&iK2cd%(OO!O3+$rT!p0k05Ato5Ur?62}F)sLfc zW(;zvt2n|sh0QfDi?n8F5=9;>SOYf(|a(N?-utLisvfq>4t@i9EnMY(3YBUQbZc?=72A@P@;uBY&4Tp07%8^#55 z%E)MvUi}X4D5f_SKJwbkKGv!YgA;aIM#gHv!SAfh7_e;ppV6d!7i@lPz-r%lVa?=j z#d7O$q_-IEMwN&_jQv0fK{ImjJO{^__V z8Odp3rpnJXWNzY8xAe3J3uku=4sTK_Wm3Jz9?w! zJZ2@EZHtSX%B$wwv{!R#v~619B18p9TK?Xt$@G+8P?nnoYOi{2Ktke($2aF*M^;TF_rZUKAPB8 zK9^A}0w>|h1?v(NAS;uIUMO4AsL$Hy_rWT72^Qmh7Dj*bmQVk)g3x7Ib9%^N$#R#` zC-V>9!TQ%)9k^ix_p3wmUpvCCIVCgCaQ8b+aUC3yHp=Vt;7ymx zOQfOSXy1{rk&#R|`4&ZOo@+p=42e*mvsS5Ifw0Jrta7aAPyjmukGm%xtgKPm`pxy3 z-Tjp8)3?tut6*u*K0nX+xT|z8p~&Wr!?O1in-_4sC_@6@WBxucAt*nu6c{Vt-{!9t z_KCvcm&j;*79!;)wL(tRN0$-{2y+q@Od{wRcs|7aD)93^hI+ex^m?za?dtTlv_uH; zu=+|wU_eJy$PBBccL-RGmVEwoYW|vY8e7pV>ScddG-PGc^S&P3S>xZC*u?r4WRfis zwxf(I((PPr)?Mie)isNbc5*Q>1E+5HO?R{c|L!0{F#E@RNy@nfBx20WS$6#UG7~K|B;TlPk!?S8qr(sJGpn*2j2*&DX!XyfZC_76+dv zeHb;)coqim!8Hq8K&vj1!DF0twue4H<;CUJ|0;tfFlm{IO|^Wmv9;+_W<;5Hhg4ruuE&vJbf-;0~)3}n3#Y8s22FfnQOx9fyZ z#iq%)f(|>*@=A;$f?Kk)f7nd@)C9MOn#Oum_*=MQ^hpXO`&Vz-E z(2bdC-^(Y5H|X*t*&_l3h(XTCd#Li7EN&F}GylbQrI^Xi`P|Or20nCkRrCp`)#v%t zrhRn&d@D9Yo~Z6Uzw;igzT(f!yQm)o%dvFI2xOc5^CvW8#AdJszO?ut#xw`oNTwnnSRzPF-CNyZ-`#OaB`Vs?SRY?GLP8EAFoW z18j>4N!pPYVGBvxgb*P|--u2x1DEba6Sp2BNB(=kZh@fw**mkx9zdD0nsJSE%63wc z32M-LF^yk!S@r%3vhqv9eaF{NYpy48*b@bqq4WvbNJ+^`IXsuC^R^3q$u~Rb(Hq3R zY^2F8cd<5gtFfCXbM2>&X4uuGwl#Jw0@k*L{V+NS(yuEHTL!F5KXv*bq(!ux)x?#4VS+%Bqj`3X6Vt(X!_l zZhY}I*D5X@QVEx_Z7NSvZN(=T%L1UIL~QT#6jF@~)-=WG?K~X51Fn76ca4)#4Er%s zu*X4D93M2^CfV7x8I#>wn_B(6!4%<%&@WUhw|z<9>cN45f5eUI4KZ*ETCpj{k0H{% z%}x+H_OH+gD}FT&3-4Qkv3d9Ld(fmBmbz9{~5P>HRRNGo_7}aFh!KMzqTwhbN4-H zpA3ANfDhaYknyO7%scrd_@B!VDHA#twXD&g&h!+$>p+K-nEud7vmrZNhVn#qQhVK&+6{H?%n0*oUd zpO%Fafyn@Tu`9~FsVQ=|x5r-N<{zxwtzWD0iLpBeeFXUf551K+EvFHvhOV!^texAW zf$k??S-UMVPtMmz;;7u#__gXX53=RgL!}~XWzRF@gDuvqt!sz zl7+Z{>SVdO?&;sd-2tAf50mNMSz@j>@1&d#W;tYQcI^?d8$Lt9F`2?&2eJ|~&;rqX z$;w4dJBzurnjVlupU0_FhM^ecTrinc)0$K79r>Sr+PuB;RKKRQADAB1VQI4(KSN5Dqhz-OmN`!zVka#7@Z;b>mFK8=jqHxacE5PS7+I1|Rn%wpB~r^- zHZaAQrb^ouG^Br>E;iC7JWF2I7XQs`iydF=(EytA43U(9=L^*#oJLd?pJ6PdO|72q z@XdAqVojp=1Zs89%1AY!SBp)SZPVbuy3vKmx`%kuY@{`0&(6IvM;osScbjkX5yxl?jI*V1tZxos# zY7!wO^Vz%6?KjtlS{_#m2*u2qL_{SLxm5QeikMM<3u>YZw1~EfYxSy#8dZ(Nw?9{h z&3z2H-RLZrL1-ZU>#=Qi{Kujb83~|kkcQ5A!zzb9jW>fW7tz=G}gV)!QXPerhC{UY4j<-OFXP*!#i$96+aH!04O!M z6CvB|dRy!4q+wcx$UnUUT-iy?%y{7pp52wDigbcFWLi2na^~NnY+lm|7M!vy+pKJ{ zX~OSQH+DM^H+JNIYa)#V5333E5?y?iHp2(}mSp2biga(>pv++{9rN&An)z&48S3x{ zH!dO6;ja)D2_m;aG*DQpVLFLD^ZR5lrYFIl`blNM+R>RedLUMniU1Pth!-!P#7w28 zxBpZNK(Pv ztch;+Z-Xh8SPr>FBy9ml5}YEjtlRqD`PS2_IK^zM#D$uLrc6STfL9JD?}@## zMPp}kaHFW_w2EUT>se5m9Ct{Gy8~P@E!(EoBTUGnC~07>KX@Ri91b(|mIf}|srlPR z{n1b4pod{5Ka~XI8>mjaAhpZKl+E&uo4p`R!yqw>N^ypRmF#f?@REaVnmjSh$xVo) z=W0Qj*W7X;Q`~XC$E;RS XUEHcJn@n3d04PM+MgDiPX#&B>Vv;>B&Pd1#A%Od= zaCfsxYp*8f^MakJzkdJ;FTqZ`yYD5d{g+=5o*mh~-+d|M)P(9ZmiijQ>QC_HH#+5- z*Qsy%5<%%4x^iVi>}Va=Ml3Hr8aLs;-LKQ2SgzG;Cj8PDP)8XNIV;&mPo*hC1;Ix7 zuQ9oID;YwiMQiz0?Fs8T5NPdh6BAcq1)l=zKo*0@2~~9BF*wd=A@!TLccQ++c>d3W zU`UJoG9spxOWYN9rR^O_NT;WvggVePn}twAW3KT?+ZPb7Lt4$nDral+x+b!5P!%Dr z^Gwfb0*BD}A~M2aogT1V*VDQbKM?%v;+ zcIfe?FbKt|@Ihe|HSx-Ru*ZpM@GKq(J!jHA{8IdiHXa*ErQ{YlKT8X<<;_d?xxf2J zbSBT}442|d(kPVTE2Yz`SIVaS?fVnf_tfe|dP!L6u9{9o-&gjnpshw8<7E{uyCY2a zE8Hm`*N{^nXZS=py@cE{HUIL8FzKA%bKSp9TAifdieKBmec=`y9nE{pl>YQugPu#v z0xH;jot;Q#dY7r*UecIM)tGpEAkCAF^{sTqCT4=}*zR7fg}VfkE%(&f$xRLRx74Pa~Cgx240>njw`7b8|w z2DJ`OFe!WgipPfkb z|Co9Q_DrCq32-J8dt%$RZQHhO+qP}noY=PB*tV0+z4zOF_Ai{LPot{3T9}$qX(7lW zML`@UeU6AEuAzgXR!2^vgYxuBx~MZ1LfwfKD)!7}PHtFI&Gq=L+jT>1a2#kp&!u06 zGF9(tEn*cxzK@=Yk$m6riNCw`PplgOr3mmbRS(r@(b&7L`%b~y;T0H8zeg?s@BlVB zl*$sN?*m59?ayj$v&B$45qX|?Q{QyV&EQhT2(K6N*aexzIg*bZYAe9S{9fk z^i)-@ZgfkVPru+cf&?xFjtC`7j5YgP@QlaBC@Sw~{=07G!XFLHLH91ozOs)$ww_Zi zj2Ess4}V`{XLQG8Jh_0*xyWKsheGD&?qfZd8e`aB@9sUje=6{QGEKVu9#l>R?DrFO z!JWlPGE^Foamnvz8}s^OJ)9^-B_rqeywT;cEaZE2vp7>2J|re9V;$ zA+YM#X1{?ExoX*BOD5 zth3QUPqp<_g&CxU8X+lw^Noce+t)d799++hA6Rjb=h9^+npQXFvU1?-yNe*T6^)vGOgn9F=I<|5v$1y1nSpx|&)K=!cO%#Sc$?aj1>TuaUs+nkb zM_Z4*j7?uYkL(`OFv0%SRwP{twB9Ib+AwN(W4X5NaKhC06J=V*>9@wA4wp!9G|cDg zAB`-4+p`JnFP8)*veY){!MmWCk`q&Np~8Vyn%z*J9D! zH*@ySv;ov@;78S`XFz`TP;yGMck(BzjcGS&?jtk%Rns@ORVla2cUSZgN4r85F1Gu1 zJgw()=}g1DZWCOhYWcl-SSb~KBa5ipkhm43;Xr!Gb3zrG z8}L;3DYO>;9x>ob%&CQEKy>qSG~))9*gaOFS;I?diYfGBrILbh1?mMR_#PuRSWApF zxk_Drr?hB^2qFa-5B#(}!n{H3Fo*CiR`Gokl%zg(HJQW$g~YlrW<7E2nlYHlvSX49 z8}l_%t91!Bum%d**a&)yJLX!<^_zfU3td3+uNb9189`?o%$$-X`(#AX1Y@b`SFpnz zK)nxlbivTcjR-?FUA=KsJ*O!ucgtHDDg9yMRrC6MeN_Uj$Hm6XN0ijEO59Z^q331u zw$D4xt<9-o|K;`vK`!s>pf%rPL9N#<61b2!b$}zR{BGk?U4)1t*@$UbWzak*6Dvhp z{VH3<(cN#BCWM+UTToSP5&S+UQOp4+!z0qzE$@@T%K-L&3FBWiO-AO_6-vwWa~Q{15OPV@ayA!HuD$u9LFYrb*xdT*I$Q_#>-w}x;go-G`G=L5#*5UT zF-c46oQ@_s&COF^mf|fQCnLw5_^ARi)kgRd>Q>g3xlVUE?}D^d?^d-_8&{?UxeS&N zTr?C6)oaQih=@<-1$X09Mv_rflibeb=5K{p%nIqshf6~U<|ZiyP%P1G$d;v3HW@f3 z9x+SXEw;#byAA*F_B{S)YU|eW84>YuIVP#m;q8Zcj&e9oK_6bHTPABi2S_8;C*iFB z=9i{y@~w(RlLIG95Ul<79)76X(dw zH0AZi1>rK4Y}Yr_amUVIUv;d!pYuO$KYx4eNY|MhD|BPBng(=3d@7OK}? zbtK{lNtPT2VuLlllFh8veRVCB$5(03hgti_o3rdHaiuQiI+R``j}MQ2yS`CWDK%nF zlZl@wERDW9i)1l(qy?+IxXgTSg08GukLwCbi;1OO5x71hD$uFs7=k-Nlc@I1sgQIymS-PYa1aR+C}VYg^ur{igad#7S^^kcbGlia=u9>k|R zaG;J~f;^=Txx}%#$HeFZ7GiZk3i5hA7l4L4*cEA6Uml#q!rRkna+4#aL_@e7R)x+`;$^w&&%JUn=ea% z{rvxR`|=;}k~!6Ym+VZTd5mc=$Am>RlJ%V1bWlqQZ-4Ka0WJ?J954jsV-GQ3n^!$F#@<{P|S=iP}NE`ohZ*EzRfXf21 zj;qR*&%OUFCt;OgRnMe+QdibbRi0R7V;D-_hiLIdFP4z!7>b;wiPTi@{&?61Ir??& zxe&Ps3)bb{LAFjra)S-{*3C_X``D;N{^y(Z?CpL`uB*4Sz9hRM5!mYgzPnDx;QmH# z>-m`JDI5B~D0iN#rCylMcaCtzF=mp^rE6&p*Iq-#>Lft7(L7rYNVlZjibAUX6K~yj zk~-+qm|*P_O*e})oO~ygc#M81g&7YdB&LKWIe{oxoe*ydu|$=xF*b%M$URd=bWdm% z9zqmT6`qTWiO4}?8^9KOM{kM}8wtd+cZ_@@G#M})5bJoHz3c|hAFq~iGMPhJ+wskF zy;O6w^yFp6LWM~Otpcr?Eu0R0?VV;TOTl&c`u!0=N%OYOiaY-9y&7IBp;$^}FG{!v zuWC1sADMbes!3d<1Bb~$OEjDpNixN0>;7-$^@iS7iS+yVd)&TMx9)j1!nkztJa7ii zgpH;Q=nP49s(oa+y>w}3owKxji#^&oMWhG#7||&~FI$jYx*!o7EOOX5*8R2fz`hgq z?pAU8+3(-i%=gX9SFG=QM>_T!#b;R4JLu7rIlz6f6jr@sg=Sl99$FJ+hsxdNW$Gnm zrspcmx9gs9M0b(0^|=7-r2FMO(wN-JnpJ)&ulWcoPhq$O14*SU$QKr8dk&;BaM40j z>-x)+2}4jWZ8}Z!qtKhrB|OSah>+)?r4kg3Y$#6E!NOgJTlTaP&7p8iO27muT!p_n z<=3VJ(OX4loH?SdrcbVhF9`Q%)sOl_`M{5AXsxI+`i)0_5jhETz@+vEx-35z^8 zsz7cRp?alP>I3f4!~TbdjowlMkH|3OJQ37gos5{$u%hlmg{_wT+zO5Yo%;dfSNyrA zcoPGNCex{GtFKok5>XTAECw;8A$KYE+unBj^giIh;qhr-Hd)fgeMQ-I({p=5rB?m% zr|)Gv$J*mk5fBzT=UE@0t>d3pF7>ZE&Jre%Ud9MMy{?4qx&qk=?j0HxcI|#&`d!5P&~7{7$$PgHS&^&(pHq)g$Zag| zM{)f&c`N$xuL~8<;(JWrz2=y^htHipg8C7P}UoXa~Zk<30S(*>Ar+~ z?~kBPIoQW22TEH;c+8IYi#pgZCz{nZmj>hNwUWgxdM$Z*87ZNg@qDi|5LJ6eeEP&; z_N2MLIgdbitZrIyQN#!*L>_6by&l-5?J=j3LIA%AY9PBk@@_e`e7hVMdAZ4{`mWmH zSw3%_x8!LPv3%z$yP^o)P3|FLR%;>Uvw96$v9+DQ?tzK9&qcg1@4n-*w(;>Tai-+T z5;9(IKY-{6r;lBsl^k;ty+1q~YoB4NGW(HuY}@vA*2luRkzor`rb_*uxKMtUsj0cc z<`>*|;PUHH;+Nud@4>k1r1;khvbQ%ae|lA6G$p}Sg~SeZ(DSC*0yfM80AB*KQ0gd~ zGQ-&Ze!0F}HlsuR_Rf^+RPfmM;^e*hxQ%f-Rrl*`Wpq$G)QeYzM}_A;jGjJl;(l-^ zyO=8DAbq#zc24n!c?^topV_57=l1oy%@eV;sz&vlFCz=RV;OG^m=76K8B1A1KxEzs zwuUeOCo@KQZ5z&Et$(1RkSL@1_@z{abKeuE9?8h(Fi;a?u1q9?WTW2Yrtjtrx>4zX`NaJf+MmfY3Ii!K zW^lBiBC@vUyK8G-$J?pgpVXAgwR?GH-l_EDyPN33>*V(7{mz=vxUDQ9x9&&p=F~>3 zfj_J_&t!qYb`!G*98ltj(=_&0@#yUea1WKOJ;C&vc-`)P>*jjWmEk%fJhr)CC&hIy zdjquNs$kRk0yKFD3K?yvB%uMxY^r)pY^rx@%-*z@{_eCm5hux;p0}jY`C5*;okCKx zXskfdKp`WhamvcG83mU|OLH9P_N0#5bO%d+DQ@@VIXKGZ&_5H(Fl!9PG~>+z7xKYs zgRjIkxE8e3c3>8$(=GEU(J5}P{YOxSmy=lNr&h?eu#4eR>JeBFmjzF`jm^!IiH%Rm z#uG#~=4q&dADbjZcdgaZIOZTt8Qp&mM^Sc(QbymQbRA26yRR4gbDA$!9jh+ZQ!-;-EtrCR^y)m$uHRt{ zZbY$$b_;c}p7wa&Gd^|Aa;(?#m;eRbHd7wD~d$-P&KX)p9+7%IV&Ku+63&`ren+d9Clu#&JDB^>yH^RHgDwa)q^r z)l6BW2!T0g<++EkO|5iaSC#*H-C?Vd0W)67y*j@t4W(_yAhFZPe{bxCm=5^{1N`76A$c8+d=mzMlP3q;%bv2MBcQG}2^?*Bsp}G#i?+IWLF9WfK zBqs5)5_I>1QANS#gm_X2_k=i-5UT7It7pJai}x>=zzD!~wgS&2*^u!{&3?$PdJ%)) za~yd-dry4myFSlb$TqK{*GPIUvir+^TovwMB2r`4Qx+-J+k2?4SF_X^hWR$$xMmT( z;9cbP&1)jSv3EcAd-&Pd>(yqpN?=1{elX8E^Ux*+j@vm`c;-7PaVGI?JeyoXcA#a| z+MV}oh4&ypF;@5CDc_IVQp0KA=e3G$}0s-e%aS=Rj!^t zeh0N~9-^cD+3;BH1t(JSDNT}+=G2${^HkK;E}0^o!PEnO*{FSgQr549#Sn%X zdLVmNBWlQmu!wNONP^IvNE4DwAa!LfDye%T(XEvb>BH{TJAlf*B5)+vo8RDC=PjQwr@xU z&lmCt&E>govP`2WxwRcZlEL5WvAgm0Y*#ckf#o*BELUvfqS1c%Eb1GBISnxcw0746 z#$f_1jU42S0KP-IR-gj2{cBBds3%ydW%hcs;Bq@6hJ6b7DtoxwAC8f_=(A%W2C=wueQlUvG4rUKg)T$ zWNOmu___bNd(9a*_pV*tDi)VYf!y+2YIDk-N$`9`1fqIpV=hO>seqb$;u;4cb5zgI zLY$X59{i|zjH9WszN<#Iub*YP?~?ufzt*4*0iLY!BpI>2*94D;ve|W0_5XQmk2T8} zv`%kF`}@b#kD0}Cm z?0x%gkCXT2$@fxO*mLgB4-I@N!rayxnI$FRM!9ZhCh1s>f zOeNeE%RvcwY%OIfjXEh@Su@gx9NP?wJCX`{ZibSgB0oz8xh{NSp1Q0rOeP7n7wFr* zj8-TfMIxJQ?OWDPiA-e-G0?dLlQmt84B= z$Fd^aY6LHJKJ~gQ&r}-Nuih^0D{F{lb}uChMdQ8$=F7sjm z=OM_9$Ea;0^ibV;9hdwaZ`rkbAG|lfO3#Ta(0S_g>msC|tM^Tz!%*#2+xxiXigHS| z_WhJau}hpxq7;^zF6bj`;a}8Ygur2Kcoc=2tTm_YVPCehB`}?h|8VR7LG~T%3u^ms z%rcoO6(gY3+m@qbSj^c+qamRUnRL8cHHz+%TNT$MT7#}&S8SxY~5g8ovn&ghVi zA61YEETAh^sxrJ;LTH+76sd!d!WQRQ<7T{bYJ+!_2)-6k(bY^H^SWIW=&>+t2~m8i zL1cm40$rl8YCQ}MvHnW^Qs{B|^hPtTo8C(jsRX9X`Rc(aBB!1sdi?FwocnG;<%lU;3xik39AuIE-`l`IFJ74j!)tl2=rNKFR;?%AJX{t@P7VgXrdc z5Je>}mKY~rEdC%6>$io!d=7i4B@EUGi|ymjh($5YC@JTNc)0KM_OUYQ$**+bQ<>*< z%t=BC4m&Fz5eWiut$dQoDe%4QQPrGe>_68pr;A-|nBHTU0;?UaMl3+hNQGPGu=1jU zfZgC#$RuK!b3eYK<8tbr%YD+<(He^F7|>L}Tt+Kdv}F+b_OxV#A+swa>X7Tl=0N^B zFZ$&z|EtU96U}@%HgQL=HVRhHLT_wVfew@sry2p0t7Lg5WsQWl{g&m4`wOb!xPb?! z_1xlTaq62FO;7ftRj%gyrnHwJRo4eO_3LfA`%)z@<1#pv1qdw}j;i|N`(R52>-9vG z$7Nd<>w9@YmSb-ZC+&Ze>PFO$q=&!zF3U8+mF6KtSdK&gd=qOSIzYKODXmT9l@Yf} z0NIT=A1MnngnqO6C!fQ;I$Y>SYeeKs~9}+Njk-3tq(^c#7AcTxHf!Vb^IGNxnRpTWKPBg0Y zM#t<(YoBg+T~2`$(()cbnI*aVolfUMYVpen2%uT@**~<_>tu;mmm{p(JB{mFP~6U) zk>v1N*X_hVHRpK^>=N*_^iF1lxt---B%s2w>M_L}iSzy>n?>D7ku=2}Aw}smXQHfY z-Fd9bDjx^?pa`CaI4H0yc_$Gp(T#Ey$nc}8fRYT+cUwT4Dj}Xpv3qyR#AMx^=J|dS zU!!XYPra;SJ0HOP2>kWT9ok#u`5|uVr7~EAmN=V}d6bd+y>lbk>jRMI?Mf*0S$(hg z-vj&NpFv1zK#^Rx3}+V8F)Qw*uFRd3ox>6bL`)kAEpzIqamek5$8KULlg$eymiaA` zL=2vSuU8m~Q#cjtdVk0g&0cn>v&1XoO1@(Qd6glQ)j(;8rZjWTlOV9*LuD<<ge<<|LC4C!45A`YR8xB8&4~BbU>2K+$e#6x0XIWB)N133PNfaxqgS zO`h#EWfw)=cDq+g>v6SD^AYTIo-$=td@~)023)6uc7`i-PK5USl81>0 zicWnFZ{3& z3Bth39Nyo6SLKlbps{yCxH37-MY=0 zNxk$_^l?fiET!jI&Zm+R{a6>VdaMi<6KbF9n~5iD6p&94U^KHyB{aBO9O1Y3lJdCr zZbLX_W)dq|ik$`yWHl}(A2yQG@<@JtZmc9KrGUzMTz3L)ImFd^yg&emUMJCgsX0Q& zQ8CsgCKdd`q)62y1bvTPYiYZF-7d$G-L(d^Um*pgpX-slGhnss;rGOQ*ab|ma>H~C z?AQolC0 z^iSK$hwRxqJVWeknlDZADV-24cL9=UshV`(S4e1ukk5iSr~78DA7iQV|7X<6gH|`*r&=$-5zfr0QKX{NZvb#NOV_cU*8LG~;+QT{F`j zEm2X1S$NX{5kP`_=E&AAQWF5w`v1Vg!n9wjZBL%tWL?%Eez!Q5Ak_#Vq$cJ`{?R7g zLbx}-d?e6kyG?^F$>O4m_0StoX| zv?DDx70;-pDC;@v_R~zfsWn=K5l!<&#%QLr)!*1B658wdi0dNTVzULi`Vmc{+<|Si z4#c?Ua-lu@o*F(6oB>nv2TQZ(ncbh)=5(IiN+<|EIk2jM}(J_P=*r9$9k zm0+4mXD=Bh{d3$O=orA3PqlH;UXZ&0cskQ*??UxeoTF_25m+LiW;>dEeR$`RiaUcJ z0|)P9+s!Nz876{Es2u(lHYCCH>h%WRfVC4(Wh;%&KMa~I8mW;xeRG44XlhWJAT&i~h84QLL6C3jLJvU#_0*&x?>6K) z{np_15>5Nr{m}o*X~y=h&u%!qfC8;Cr$!?;qb$*yb$mBZFfGW2k_J9^?Eudd zg&ZPgtMsgWO5KjQB})~Dn?5k%SUX&C3;umA;b=z#_O8r#(VOI8?%No`DBx-i6Y$LLP})Hyv*#J!jK!f95Y7%lqiro6zX%fN5T_ z&m6)cAfEzVqb8v+w(sS$gW~pg{gOqq)rwe|#O9*^A)wh><>00V6?b-G`oAgmVXrTu zS@L|7V?m%4^cqo1 z#fm+Xuf$dvQE2PuChD65O6zz$+XGGEXx~#aFpS#F*UMc)^KdYrraj_cGw2-dz1Ls1 zt>+s|)bK~hJPYQy!*k)~+PPPzdyk~OIhilRUwkh7ck;YL%5H@eXJ3ik>?LlR@m|*=fzd9YiDA#Pyeyxpc;1ly!EAarknH|0zyL9aBWnM&?sJk?(u9tHUib>&-za zcQ)bn9VpIiSav-NS0DE8w9ZeX4ss5t(Gz?_9lk0}Gr=?8O3Mr37W{MZR@KYEL&^rW zI9j$IN`*$eP*ngJ-#kVW|0z;&!msrpieP70%fO+EN0zc;_t@x`%KP*D2{%-M*xvP2 z`6;QmDdcdCTk_0Ojoy_{jb3MrN*L&*54%=QJSy@Wn? zU*BELOBt6>UPFP)Z**W+!vCNwU035tc^%*Yj+@~hZ*P_9 z)=UF|o1Jc2@I|-rlPF~8@pPmWmaH7trv)wk2R5}{_+gHpgg#D?pW%xJN^PEbNQFFii2T(ajew?EsWashn z5U0$$8>>z#Y2GrTjXmSyTyT{oD4WcxoT;!ee!kVQwj8n@tk8lDvXqQeJX#<;a9|Xu zE))NNXe`HvPx`Zb?njrDx^OvT5eJ{T_PCNWd#faFsPAn9PT7AO#gm~wLvXYo^XXFs z!7mJFRgl9(R@N|G{ z!^{Rxry*_uifoaOJn)-I{N&No3QidWBm?J2k@__cQY}=OrJ7aS4asxqpc18Fifp%A zRf~}kE}#3|cS!|qBkAAoetFJk$gJVg6?qB&P*8D|&WXY%LK9m0 zQ$d(@?S_JNc|7ayH+km3)w^BuZFad!%o?<4c?6zS)YFy5TbA{=pOAM#V-VGAYvy;s_PP7}e{>^MYE%Dt9)HNANooeyQ9+E73wuVoP+fSND;gsQ}hGcCg!&!Wsy1YvN>3&{e4+X>06dacbK>ifNz zws8gV+3;~(`9raL{O(YY44XU%);5sEuh8?SMQ(W;m;77tQ0?Q+ zw9Pedc#pOFH8;_6nR2x=xF~LpNzcEyTl>&F4Ug3@#t$ay3Q0Ky+VeCL>%oOaE8X%g}afiF!KkYAvvDpeY&JB-sR+?b3 z_bS`|R&$Fo&S9X2<`9{ZI$atS?DR;^vvs?&s=Rqf@+htyj&=+Cm%6WOZ`JFnV=sB> zHdC|=q88DBI92sC%(A#8^7yoAKSvXpmpTIrL%^JV)a<+772z@i2dlJoN85IXj>|!( z@n;IeB{QngX*P#ZB2OEVG=prsbC?5m&go-Fvq##$E=grP&cW8H-t zk$+iI*{yAFu#&G|29V;Y^E`mES=gz~GhKaqh& zSq>NmNyuLY2y0Z;bH^i@-S%RvKu9lx`=fhF6sm!Aao5Z%k=z)Y#sD3 z!TBFqenP99t#QWZE$_J%_T!|)Mb+&O1LK;PdEr=GhPJW4ACp8C%mWr0-L&Sk9)5sG zoe!RRmnDVqf?!vkCvU-FHXdft&L}u5;A-hAJ18CTUjj;T3d$KOf~)#VkS@Nf(`>j_ zV%7KjI4Gg2u;(_|TBFRyv#=YU;NtfUNdg$e*QwJ0R9OOc|~s z*4qDHBzpSX3Tj-44RH!V>wrxpMDH(5Hba4)CDZ8$-(aNHP-;-QV@m5=qpds%i0h?z z{0avR5=eP>u zakF45r!OMmFH~c%3o8Cpwo;EXS_2yDh!Ka{3itEx@u>i{S|gdmk6a?rFRAw~dB4db z#+)SAVk=!Hd8-Yr2%4v8lC~PN=KTT`9H)ta{!=1Er&rOQW%IYZ0{q*cY1c8SeJ4N0 zdbU05P6ZGhe_xQCc?Y^dWU~K-=y(D!);XD*1q!ur+rQ~sSOvH=N|!LXg(?UXFfA-F zay{x(zTDy7v~%4f&z${s|G!|wx1SyxDJR8!N4FQoG^Gf0Gz+R9V=pn0PkY~Qaekat6*5pNdl09!nt z_8IEqyYtHlH6W~1WvX1+VK@>f`5+&+Hve?zwbr*%%jt=<<#)q=k88IU>)ddK-d04! zwZ2k)qk>C2NV%-rgQG6w2(EMQdsjKN7&BC%D>gB={0Y^_n@8nUMoyQt&1|Ei(@CQg*9Lopy6~kaK~3DCKEWf@kNImT-*SLO?^a=S$!H{5{w&U zH{(BdsI(UN$|Hb#44XBIP30 zQqPN}C`!n)P5IEWB}Gnu4YEx18oLD)*|PH_+`&V9KC`jbf)8^@FcF97R&DoFPIn5p zXw(0ro&H=E=Iz?a3fy)a;&^>jE2MNGr0CLxjb09^0?6u=FR<##7ogeL40bs5=?KwD zD0;{i14I?Z?Dpoy@s@wA*{4p)O-M+`l7|&s%)@J}{QVnceQas>uZFh09z1@ON{u5l z6JTl9)4N$P6lOAf(9c%u2;+@^hlwbPlcz!C!q+rM7m<12*P5Z>V)L8x*Uapv+mCNE zVG64>U0HcMK90S1Hf!sgmMlV2YK4+rob&~w@uRafJ}WFA?F!uJ zBFO&PzC7l7lwwS?c#D=pmVEkdd_J;IC^pW%e%795uQxhS`Pm58R_>mM>Rxba9F>7; zV*;`%qf$NgQ4(q4z5C+B$ad{V> z;GjOQKYlJ>xitr3(By$oQU5$*WTJn^sKnIr%Vx&2Vei*d-Crab@vQqXI6NG)tlI1~ zfr$FYFsgwT)6?gt`(kQ;&%W5M8}{#gsND%umz$0-qLwCxbdNK{@EC=BPHhpx0XHpa zvC!tq>}Hf8JsW+VYNZ0-Uo`Ml`;)hcWe2gg(L?BwC<#lKJm0(QvTlpdTQgm!*t;XT z?~*Xl-TVvrtY$Zl{a?Z8KbKyvP@O6$71ayffAey*dvdwcCP`VRH9 z936j6^_pyNLO=7|(2c0b+$LPR;Y*lIx@dm9^u9yXd`+UdeTROfS=M%D46V6?Qpw!A+UJ7_&mqF>g zeLWUJxh46%>ZFb*0U!SkR*g?aSd@hkC)i9CiuBTrr979S+LPp93JGH(=uMx z0%lcfjVI`ofQT>g5JlhQJ%*`syD54({e9Bts?fr*`JreFIpeu^*mZQcz3g-glMw*O z>)Qq^TvAjjApphyU53jPE)Lh3@1YN!jk?YM%@095Xt89A%xWj#^!W_ z=eqgD>XiFw#8Hh^n@=@?rZk_u>)ZL-{xLhD8&(M|+&Of#dfSi69g0Z$qi+o59slC? zbYH0b0*&?D49?Oyzwh3@&!(E3+7~nkEgj?2P7-&fkXUt5|0i>9Vc;6dR2=&(AKEEp zr5Q*{76waWKStvsJ%3K48de6Ps^E9?LV>X;Mvj2Vt$q+8CWMH86f^T^0uB&%&qb4UCu5dUa3*R^kUM&sub+$ou^i8?xei!BVs78pdquHd1=EN6vevJ8<}J>UMnY(?EU!^Cez@-p=|WIzsz3 zNGWtxY1DU?*rR4=U>TY(&2y2M)L0Hom4^Uwnclr>5dUreF8I;EvAqO{Ar zH4f=YM!brSF0#*8n;0OfB&Cp$vtD5p_TiZflD2ZjNAxp`{IgiP)WTEL`D0vNL`LNq8)>OTu_JaKB$(_zV;Yf9k!;^u6^dH zkvjfGVb^-CNyaM}P)vMzzq<`R`@(gM!h=`Ylg)o|2#v5QsA5)R^xUSHEez?p=nI0|dPxJ`q*P@a+jbt0SaqEQEn?i|?I9nuC|13P zqY2qGZ>H>r+0f27=q4&!A=Z#6?kmGrMuzAplb2jX+?Ng+Z+t69076LXHXqv^BdPn8 zadQ|dtI*VeX2}n!p-zdT;e|I!Hh}2on!}Zd#bFD9q=eHTRlj&v*93Ni(&_cs!)OIp zS*8@`Ze{NhNA}VOHQpD-?@OxDHGWcD8%O@NypS2W)qW34tgM8lHnX-*db1DN1d0kz z;4y0r=i>HbD7s6C`?mhPUT+V350yvEp4SST zxs2z;GiTZffAZW4@!s)ra7Z0Q^x`PGC;=JONOiqEGNJqQd9lVg(smamN_ceKp|p=+ z2~+Lj&!^`leH@{W;~XX$2SmOnlkNengLoIVt@`Ul?V%fdY=xU{%(N2qsN6U<7Z#DF zC$nzv{QXYK`;^aY>~x9wiq|K^#TKrBTQsF#uPBOvHdkcn_6xd%${Yes8Pt&K03$XR zW%9ri7Y-wbD|ONZQe^~|T7jlR?q#R_@(X#mL}(mq-wQ(kJgYGXkEG{@fO}>KOVaaa z4~_HB*uN2CO|@hQmX+4i>j!>|prk~<%nabt;uxLt4Z({xp-6K#nTKoFCQj`2ZuF>T zdPrqq?GAL#|Ob@@eGY5v&m_U7sFT7_nfKBv7%S1F5FJ+<`}#z z9&ivCxsiGP4jO%MhZ{;Wx5#MU^Kke6B8z@Rz*pu%3iDF{!UtN$%%ZM032G+H!X^yh z-+}KVEFKK^%Y=U}>GaI4QjC;hU|kpnn;}AC0S;2AsO(Vv z^njSoC&tp!AyO0{S4~9y4TZ()$YL=bJm8K9_xtpH9I5k6LXa6}Bw4aM1pKjvn~sq+ zRDmL4CjlZ>prN@(ri_ZBnkfie?Dv4f=+yD`E@AKUa&hk`&wIMAw`QG4LOHZ8R-4~l zOmnrhP0wBbk$}~#(+Ld5hc`Xz#?X@_(Oe|f^;knx^wg#JFR@9Ezigz6s2B1C)SG@@9-2zVsTH!%q3BfFEBBU5W| z2v+{Hil3XeUzA;)%E`@Gg$GVoxk%66zhb0p0%F@U)4?mR5-lH9mGgr`v815kwuJ+m z*4)c^k8SAS7OKeF!S;NbtZOSxg;jPovgETIJ4-P%cJ!N$Ok+rOdXp$xnGF0#e_<;h z9dha%Df6HJNntp;q0P~FPp(HAGhz(BdK>nbXv$Z#L0XuN2SgdjQ|ob-{P9}YHG`wW zd~gqChK#sQ^m`X3un*Mt{*;ylMkcT%@29DK7Oq_|U;^ZwbR;RDn3+3K2ea$HbaylF;Uvy-{~`u_eP3guoKOdzs)gw-xin+;yWdG`R3P<>cOzwDBx zyq>c`JrL%PH*ZQZE1u9z?~!5<6a6I)$f9ehReKVQHX zEG7?v!=Xk3fQW4T5ipi(W`gx^wYS>NzQj-J*T77F>LV;Nn!RRR2Z%^=%VQsC?04^X zv}b*vBX56|72dmk*M_p$+Ov1Z0s%=X!@7M~O`C4Nhlgwd^2q-ih;!Dc52Az~ErV5Q+0NuylsivZ7AL|MI~ z8i*X&b=wm+xA!sfLRY6`BfF3A$B%|*OwqQ>e?;3Yj$3k~9DXRNQHg_en^kJ{=@{jK zywLj%&DEFdDZdlavm+WQK@YoIWmvJc7c!qX+lA649{(yQ7#t1Z+ouHKIl{aO^VZYivimUGg-Te~UvusvYyoM!Lj zl*g6JEs9^c-rEEj8JLlJYEvrm&-=5S5j$liCOEl)h7Q3~_@X%&oB+fyr(ne#nmAEc zyOHO4m7ozt!tNl`%JjP%*@l*8w3JKozW2v#b&cH{BRDgk2Osp)GeyBigAqyZvwptdSMLgSJArVB3n!D; zA)SQVp527Y`dFQnRHHJ?g@~}NOF)&4V7Wd zos|CYObb>;Vtmw#MvNev9p!IJuzR$atC16os}hV6G|s}a34yvpe1!63dFEp6?><+FzF zrm3vWSNYA)Frw|rah&AjQS=;jsJuloCy<@8U`{uF!rrRqW%7C{$minuubMhIup=-t zGwc+TYDbzK*`fUgsXKkB&&a8mUR zqstq;PlRXq(_C($yt+P{s4~uafj3f>n7voy{}80l;*daMUG%<>)Zw&#E#mo6)vL_ewcadkSHgA~S+H%lrxADoogp|ZVZUnt0|h{o3=0cT z$fJKX-b|2N= z9nUqnk6h2{AkH+XysYc7KCBIGx$&-(#?x&BHW?exoVw%&sE8ZIoI1~wovbPe4;C~y z7A^g|z9S@o+vG=s6^9~`8>KLl6d@K-XOX3u zlO6y&CAI0pHhGB#nNs%Q5}4m2epZ}Thr~s4IT(Z_`6FJxzNsUinb7{jl!Z8)61b6p zqkZA_`dZGLCd&FU#cDx=sx+sCwG0q&@bOL}>V7Ge@U^b7ky&X4D1(BANN^2gTp-l) zvMuO(BYbM*f7h-a=fr4S*3c|uTmi5FpDWDR>S%#!l7r3L5W+k@zLav5jAC1KYd*!r+DWh=^zMT{n)^`i$%9#6?H9***a3yO^Cj zL^V@$dQj7tqis~36o!p~Q;Ax%Jos$Ut!%SLT39Qt)mt^WIGC9-SOa#-_%_;7wkCbak}B$RPwhGW1SSH}i>{Y!cdgR3el_UcLc_k>PTFbv=;I9=s zJ41wWt(|HdN<=(}BD?^}qc|~;x_5y2E18W|BYg1vbrT)bhmAqU-EAbDNJI59GHjS?{Nh`XtYzfpF9)=67Tm`dw6W^pq=8nvD6+r)PkfN{fdO20&1mpmV-!+h z3u!V1^V$Y$`SwZ%JE;UedUp10zNC)X>4gITj5xFns7=86+)iD6ru2T~zE&7~UhT;F{Xz;$ zecbVp9CKtMJnt)bvA8)n?696izlTKVx#apjQ!h`>Fbf}iEN=;{{!?+mP^WP1x*XJ| zAaY3}5pIy;;>OUB60y`r1|{w&#l)L7(bIiQtnfp+r6K4!#i(WzD!s5k7#d}eDAD}u z3ZY73V^OQdUp3WnNdSuq*>&5vs>sG_(;txgpm;Yv_fd=uI}&A zO@MgB&g<$=QvvcoeOs_AVE#8iVelD648RuT# zUrs4|i6$8X!x2V%~2 zKM4jSMbKQ*OCPn(*R$eZ2Uqn*$TQ-^4L7giXwzngNyO&#LqaGUq1_pj1_eINN&LF{ zgAb;;wfcuEN{B3;x&npJjpH@FK$z;#%XD}|BsDqm-_Vc?Q^ZndKTjjQiG@i@{}Lv{ z_guReTCVBk!kCkM?IY0x|3ie%)ovH~L{da7a=?XC7+o6A`#N3jR{Rd`f5|-i@3p!m zPFTCo2!Chj3X=xR%)e8hzi&tBFMpdQI%!Okq(r}3qqr|{1UILtX<9f|nAo^?qVtGI zg(Qz1@qwR-;0MLYNSk2eO_Z}l(}|ejr#v;ZNetXkYYgn=MAe!B^Mi~3&QD@6+{!Lo zpf^ValFp3YMbk%-IWdHbX*^pMW^o$s1+Fc}*i2y|M`&uGU7Y^XIAIfLl|c;1(52ld;fHJUy_WzqA zk&v$=xplj(h9X%EhCj+KQnSRwI<#!jzV~X%=5vjF_(aSYr@rOzz^;8lVQio04ogP8 zB3RPyU~M|?B9@G3Km`v;bM|{oh~7jX-$xF!+d({DeW&IZ*PuJ5NA{l zFE)xQax%>9!#usz9((`;D;^}>QzfCMB|-w5hOC??4k}r;D-e6%UCo$!^;gENfXMq? z$Kq~e-w)(((zG5c9?XJepjYTT`FX3Wj3bd?hHXvLeQ1CAE4U$U?+JMx&{r0u5t(Y$ z!ULT{QetZ47OH**^s=^2pWl@p89v1gnwr|LAj)&bf0EGBcQ*pfIKV!lycMMZ52`_c z)X_0T6?HhNNA*+R6ob*8%h+Lc&oO?qQXsA8nK zQ3!sKZT)rmKTySq8mD99ytNeGHJc>o*wQgwp+a}Q=59plTB2m1y`ub7$IdMW-S42= zz>3d|oARBU$DJnIs+p!o>eG@LYE>K=8v}K*sA*<-P^TN|i$B*8D<+}9X}|JPOX!6p7Eu6@%!ARJGY;aaOc3FuqnEU z&#D!e3=o;X<`l0S{V}T*#cI*Y)!resbZI0Keraor3wq`&WGz;;Va60I+N*0_96vep zLot?4JWFIcmnVkR9JLVjW0zuBJ5oK(tu;zU;6Yj@5aD^e-t!Q?U)wZS*umDW#xTh$aFRm7;K1E8HJm;2?}j1Wn_rZD8^wQO;U*cg`c9>$HC(R?ea=Tb3KM9h z&Hd&@5DZ7>==M`8YCXwUBtUQ z0LL_4m<(BH=U3k{WNCrNP>R{XeEJ*lXGC4d6Cw&Z5cY7Dz&sK&*MA9zb|JaQHk)SW z{=~p*!g}kObt+})zs!|S5!~x1#k)b?A1qtwNM!VV93(*(c)ajNY81G{mV?@r#bmwQ z_Z}odO>^^7na!U{rkSK;J<0QyG|1l$e>?pWG?{k|Z*cu&K~!8szT;y4Fj;G2K<#*y zf#k7#V)2o|K*pklHPNp_s-dX9{rrBHJ`mF4BTQ)J#>U@6B4w@d9mLQWHL4iY8IM;a z$oMC3(6sheX53}!zmJJFjlOarq8Ws#@gsZR*K2&vWu6-NT7|A}nn}A%Z0m=d^IR~F z3>@McE0v3@C9pCdz71mXG()QUJfd!Xbz~t0Zi6XXYu998Yn^2T52$i59U~!^91IRf zBrxVTVTa(d`+?QvTe_Z1F%;(&q-&l*T4)(?2hm7K=AijV7O>GYFbw92@L>b0>#a&a zwI!R!3rnhz3*{>x2?n_TUJMN72qeyE5dLzf(zLsZe7rPB@v z^Bg584>{ojCr2^Cq*$C2n0VKUIJZQiEX|eAiXgrT*=aEoT&;=bvqXl(51H_{>*BDY zr2;ZzcbKs*u~8|#)N*W6k#GoB@{#4mz>&8a5$)0u|C4tw<$rV13n$b0iyT^W!+)W| zn$zlRO`CL=)omov)z}NuV6?>o;tg|%f3Qg?*k1pR^GhANgclw+&7gHGEzBB-QRt$) zY!N%pcg3XeK_cXrGvk+*%9iT9Sk2@hx9wz)mH;n?EMbxCIoq2Iv5HBsUG!uFR6J)BEWI4Fg(;T{b9aDd>) zV40!KSR*}QU@+9x%6AbzEw|igaYees9qpXpP)2USNk=Hqj*IqJ*(s}aj!Xy;5N^fo zLG;a!H#)t-ThmoVyN9kc{)v?icC5x1#E5c&9{Z4!Z`=-(tuTTs$8g31^lhlGn>;Lx?qg|)<(v$`er(wb$v zrexGP&{^}0SV##yo5wXYCI+Fh(b&v22#&~|8ES^N@9JxlXj^0(qs|9MtZLxMt~buQ zwmo%f=f)4fNPA2;xJ)E&Xa*4>J9{@##m(IEGb%}kRA!j+uWkM_x*o3*U0lveQ*Q%N>>BJ!fMty%L zsS3WJTXw%;7_g!|35n3yiHGr-{3n?_T;RdEVUR}mKF#{e<{|F?iltA`t&{?`>HK71 zusIDW@AlQ-6*!|5XK*V#m? z)9s*RWZ}?4-W}g%=ac^D_16b^RuRdT*T-M_i>;CI6O2orYc8r0iWwfKz!AYumfGHD zF!kx|gX@>D0JcLUBOpB7pR(E(J22mQ>t&N$Y|_1M*_^%F1aEDq_(G#@XY`ESQYzgL z8E>nM;WrZB8p+=k0$hEA4*_AqT07vA)At_x$20=Bf`S7+WjT-l#|QtX>Vq^ZvuteMU|7_)JY!gnvRB)QdkEf^W`B#i|#V>*MpRT3eE=_POs5jTJth8GE zu8=uE*HXe~nrSs?^TR`-O6mYIc~*zii@$31kSS)*+m8JSGm}hy*L)p!caxU}hl2@Z z*<4QOwzMoLiY)S?*HXQNeAB!R80k5k22Yh&-Hq*SxbM!PKlKA4ab&D)A|YcADHA*>xg2)^L?5FPFULe*FhrlDd5z6& zl4Waq4E8U!JUNJ*{0~+(_DC3&e9)e3!wQaY`rx5K=WB3%J|FUH}Z#zaK#{8ER=0{Eg4g97@x|)wq z5BH7;5VPZ~!C+{)f|~&L(9{0z9$4UJ+3Z{v4jher{Ihl~IBB`dU#n|c|7bI;To=yJ zqBhELojyIIeAXCd;>JNN#%|%`&*Aai*YA*1A|RQH8jY`dy72|Rn3|mVv*U1Gh_`i;wn~a9ed7nq9WA1Rn0D z{{G$0W6k`2RYve@yNHVoUH6X%o9f)d+!ouOt2cvjR}KB~D1&$4=lL-V2gh`yy%iFjnV18l z)b>u%h^CIt-e81^&vBE%&T&H#{rSIvt#xN2i8#K&k8glCyd}Z2E({ zdk~1Pl$5<2D2Zr21|q&>WyvzO5&rgKsfcB($lqzAn{nKbf^4c>pgP4p>cDuNfaI5K zGBQ1HjlpGcvqTE)`xoiPd-O8nP05mkZ9nT73TKGS=kZQ{w#GXu%laFmWAtQd289iM z0~1TvNdU{h3k3@4g3S6rDJ?cU8w`tg--*f8#Fc>tb~jD<)z@X&F?< zw-C?&HKkeC_I~RD_C(GZMo+Uj!@(gNaxo_GNF3q7Bm8whK2Vh22`Xd3@N=69T7TcC zx^2d*Gqa-H+avnG_C~_rg_DTAwfDQ3@_BgVEjsEd_uXD7QRSTmX%V(Uo~vOb_g)=M zPXjCt3JP=#j=}8E2ctWo4h~*2ESj@zmN7Tyly<^MUBd!+94? zRdsa=jL# zv)J;SX!g?6b@GA}HmsrWm~pU9x^W0kq^%_Z!k3VUy1k$O!S6`f3lxzo%spY)xppOvExJXt;LkeC(nKqxnAQRiUfUqN`kWzPabl?j`Jq_4 z9U~Ardt6i*5LTFAy)RzDrhtOh0Ne#!691}LaC;ac0TC4}RQD${;pgb%`9)?`8eyhOWVow(2>{ z${lS{Xp7xk$B**HJbvPA-;I>%W?4`}rG@b-RYT^Y>;d`P0# zHq(p_Ef68J-$V}R`Yj+xVibmM!Oi06tdU)|e?c=+yX3Mm(tVRLMn<3&7*=ocCj|Xk zjmX%sa*JbR<-;Q1O@p2Ahm^mf`7&1aQfB5kWM{54=H~U&f%B>LxC!zmGyvKk^Ios?P(61 z*?Kh-jpzGXvoo?#y&ZP%COQM(`Wn}mKXJpx8IUn}MKwRf6oR#TiM22KdtoR$RFbAY z8N<2h--IE}Qw$qDfl^XaPePf`O_XTEAieB!D(ai=b`YG7bE$H4d4?hU%A+yTnd?k+ z^LS%oyvsoqC>n}RmgThSqJKS_u=n2j^+{-si6tZ{t0pGbt*ELdrf-@1d2TU#f!<)9 zI%if(Pw*t@6L^szd`8bRAJQjVC(*eUQD@;#OupnDycj(aJGVk=7U~YF;Teo&ldDIj z$vYP}QVgH?|7QWbNq45Sqp$*BhPsK-$J#M=C~$@S%tGd-5w&SV9p(&(q0Kw^1DjUn zu%ksZgE%p;gPH@HB;uGi|9!8Ql>z}_lnzecJu{E_13B-_{FDFm$43U^e%;9lzN`_w z?$%*?UQzLbQBZ8i-a!$)v9)$RuboVqN>W&2WEyFSEuau7|8ge}<>u2$_-^au4H;Tw z$kgm1VFhJdv{%Q}GLm|6>aH<9%z|bN9IQ$xjV& zq0cLQ_dUi|PPElbvGo*qRPW<}BOz1?Z^9>k$ag);EMHxnfTd>>8H1red-gcmWZXE? zx-?X-lGD_%IfCav%Y>VXF3h@m+SJ`3Pom~D=p3H!;8FcBd6`l6_re&6AE#qMqblFR zy1k!3X3B^PBP&S`1l2jSjvk??`N!P>ch_ujG%J>ThM+F*^d9gwH%%tmm&r2r7ymMW zOt5o<*H^@uo&7m2FqC?JyCLepeyjB;a$<(qxRpW7@M5~C>yY!4f5zuA$#ID!+0@Ls zZ$(@E?Fksvx^#nwWMeEtA2cGH(r$C|FW35*7VWQrXb?22z%0x@uu?IJ_V3$Dq9*Y^A3Ggju< zI@L5#iue51$y7NjYd_hBH+b$|=Iq_WeUB0*)3N>*7-CoO9fs1Nb(Jb$pB_=IFF>3` zci4zg5QPp?%|yZMJ)?kZe+#Ss3=OiA@jT&DLt=%ilEmcCD(HtGuIFdcuA zy;Hxx;rFcnjpQOjLLjo8=kmpd_S+tQ#4QU8hyei{`e%)6R$I7_?S9u+|% zQxm`Dpe&Vw;UFGpxBr4SAdP)lh2-A{KH9L&E5&@gX`Ip>cJlWcFuNJnQWMw$B6Jgud>8YT7Wkdd348u`~`WaZx~>9W@?zDNyNdUmLCb!?mprgE&;b%f1e*+m|Z~Wba;7z0X;{U)SpZfKh{d-7Th#N+tWf*M0^g6uXBUYZpw?bwOYoNP} zHh25H2NtSUjB{kiA}p0L%2Rl84rbCW5zNnrHlO8@flKCguCP%Kh6&eNPQR!&%#nwl z+>Nry`>}mPCrL?XPZY{aN0R4#H%tbtkno|o^hEjAfyYPxr>-;KZx9Kkm7XQjPs0J3 zK^?*#T4PeyevoRvQ#iqBVS^xSrYeB!GF`!nx9+zO5K}jdma1@FNC5T?nQ|ANS6{9K zA+Im0f-k73};~Ey-ek8EbDS3WFme~(~n>aT0W4kZ2Oenu40AVLpGj z`@}!AplH*{npt6F%2N$^<3*Vpw?{RH)eP`-xhE;LJ}_RUBvG%1RdDbXUp{A}ctJOq z$mvyWiTU3xK&J6j`L^1g1@nJW@4fLfi^>AE)5%9XuSSatEnC`;>@U9u%zoizS-jD^ znL{rr1j-dB1ME+bUiq%t6%I=+_=5r}n1q=)FvHBfkK68g!k7??RaH~|{gt~?AH8B} zkwODUDO8Hagbgvgn53C>2$#Moc{mY~41kLzM(^g2BHm-<7wGZF46OOvWK&y{RAEV6 z6dZkNFEE>BU~1?1gT-iX|E?5XAfSr=iQ1tu}11!X%skh(S;P!rmQ$y~5Gqc!3 zoPJ*lVXSLwe(1U`h|beW`wZCF0(Ta9k%!bgUQG`jwyL*~Sc%krunTsaZi9|p_)jga z4SATFdC^w8pFFR%%VJo4W2-89xvaSga;|)m#AtK^vJ8gYt?>#`X2$I@N(!49=hNL( z%MjYF&x_v^p!PivNI3R5hfN%t!y@5A#36kvkq=E4p3_ae5qNnI^t7NUchR)qEEcBV zPkau+H94p?&Y?v&roip$7$|Th?~ylWLhBrEdD1%Dy1E5}weUmcX$7|}U^v$&R$xd8 zg-d714F@f97nKSuFMfmLA=@7c>n%}rCPu}{`*bbs(x&>s$v^t5-CBY1H_Tdi%i!On z$Llr;7kO?{7M{}VoL_KWp?D`YnxR}d;{rUhsd1YZ$mH!i{_#kEM$y;OZ4er`Oc~+-7|}%sO#6aGz{@>zH)57 z=v;+KE1AL`AppXz8&ue9iw*3Y?^*@2%6#wcge_E;Sbt-lQ3N#{Om`CP?`OD1b_F#h zxVRs``|5gbn9&oh2Tks#ua}Gk4zVD{TWODZVG`|5KyLGG!ga9n zd%@k^SQfE#ffKM{&y3%gs0NW_B;Q=`7M;j$<``WC3PX3GE8y$8BBr19d6DD@YsKfsh}WIk zBd2xI;yKx@Q#S$%rbwIEQ>?0KP19`t>sKt)rKi8(^M(6$T3&a|%K<+%4JAs|c7n8# z&Df)Q9v0sd&iehN1tf=k^IW<{6SnOM% z03#D^f6O*@A|J?!P%8jnI+%AJ_X*me$tH3t z`96uKY6}5M>H$mzbK!cL|Mns5rjXlNkJ*OG&@9NZUTpZ3vQ{}hAM!r%np@)GGY52T z|FpeJbM^k_itV4Yw6YD7V^IIxmDjyJv2L)c4~_j@$;%Xf7@8;qzqp6Zp0#^ThS=Xz z#|O>v15CDtvo|KeV$pnbk+d?I$@i75S#|vQ zipWXC-l^nwyvT|+xXx0YhG5w<#&Q++(+r6mXU#Ia2+WBJzg7=&YPiogZ zQ=)_^&7);%++{3kIa7dD!IpQz*CUsWBRgsyB#HaJMz%x#aJhxs72!qIg$)Ts_g&Oj zIZGT7Nj+UHXTN=5stOGU}W8f16+^eLfou&O<|3$qsfk1ftZ^R#$F3t_%|}ialC1)#@r7Njzelm;To)gZ45% zARi%-)6+Y_O{|Jw#lV?Y9NChla|;KCa35&lPrTZ<-R0}DHzM(pl0dex_!62`I+V&3 z7D;-^Vw_s{9CJX6XH-@so}2Ri)Ey?PW5jCd&F16sUiNKa2|i@|iJf{MxV%_xdA`B| zAhCp4O?7QGfjsi-<7CEh=a#PF)*UIosSFPkN`71oVi+46w&2^Qm@T-vJCH1v8<+R0 zz3hyYDu^7d7>JB*v%V7bxq}-Wk(sn<^D|)xr?Tb(dH_1Jom0A!37Uz8Dbm%NC`hU7 zSzH}Ckm-H}68_<-s2o?3j#?Vo5W_T~*8mbANl7{TwPG#{k4Qp@H?Vy;6;Cc6-k8ZIZ?4y?mzc*5=2; zmrElbIdb;|H0$LgGHvf@tC@F0PhS&;9IX9k&1Od|!;;-}558it)r~?00}@3|PcjV{ zKw~G*fPgTnWlJrah$5j4nVUMQ!1}vqCyC1+gUpu+E|z<5T`3>MZx3!wqX7cPcRljv zIF@DyY^)%%)muPz0#Q0@4}uPW0R{VM^=%Tl^S9268}nn2PN`B|DbA!G#)tvLeDMrM zz3!jC7e8oNf{Gs2`*$fn%tsN)Y)r6VgXlo62WW-s7IV7p+_5nJz3XZWj zG9ru&2sK=Q7ZnnU^x|6;R4394Fxc`)^`qq@3`1)7{;bt>8irc_y6k}!cbl7q4qsKN>p7h9!jWFIj)FRZh`LOhll<6&%i zY`%)0@_D2B1DfUHqatp*Zi|(u#*rXB8}QK-BgUCZga0D&1rzyQNj%SM`1uo~HMgn> zD@y9a4^M9Q$r0h<5QSOM?8t=?MRx1Efyi^m%is^RBXUX zoO+qLWjPi79{`hdY#aD=a2pR({!8t4)UD5d(!^vgzncRx^aMR>v!#c9d)efel&VbP zd@;BC{JbzaEM`P$?Edg&a#Q1D0(CN||n zC^Vy`^6mnYYYVD5!V(1_5^_P8$@h_};iNHmqw{Sz+$dQSt6(d)E8_H?R|Q;8i+k^% z2R9x;moJ@<^J#ucGyEU?i!YHwbScu_Njq+&X;}>7aV-A*1xCphZac~Dy$IeHPs+)K zsJm^TXgnDU;;2Qb#iOFa=emm16#5a0vKsFC+>|2~m`?RWWTI2d<oV?}ZNkXO zLI5^O%KC$bsplP}U(s71=TAZ{5IH#n+di@t<+g;<+2dBBb6x}s9lY1K8a-fek84Oj zu_LC02J5AvgP=vDKYKv=I_y}(;rjxTvZmPQG2lt9#7TlT@-nbT>4uZILP-MTSVbxO z1{DQ4_->(rt~;(Med!>k(MjeZO)VAFZR?R%O-BVKJOqHK0y%5uHcxP{7HY@ZHmZ3W z6>G0UVq^vG&vH8QWO8BMI$`1@ST9=WhbnUzlMb~cvK*QP%HsP(^0pJA!jiQ z-}XP&80e~s7a2j)7Dz0V09on`coQ8C!{Y{5L#Yi)6?AeA_IsuZ1BG^u9?cbcG37jv z!H`3CjX!W1@odA1J73*=x17Br--zKzOmak+s+vRCN@JhIUMWhIL2}(cg%~vR0@kNZqwvLUL|y^M0!c%$nB!BNGXScYwrKoABM?B zC;*hEzcg^J--z$^SiAc5;_CXC%(}tT5Fc9RxLnmRJ;6+CA-@A>3!W}e9F;7 z@CsZ%l;CNFzz}Gh6CO=1G!7kz+#((*LG+*32{MEN8! z6g*R_*b#w*=I6k`O7o*qyy>MOS*FY%%!OI7eJwjp5>@ZRLf2RLLBnz*Y|`YILgr?e zZfl~@1|0Z?g}}01LsJkZU)bu!_f3wNx$V8u_FnbZf)A)K#I z!Jh{H_+l0+UkF)CKm|&}zyh&TvxQ`8tuF63M&;g>9Yf69d9-z9;>nqZCB&Il*1{gn zIJe!O`21Ut1R)ny@)>|K+&~)WKJ@)@Cp&$v2Dnp^4fq@FpL6^sxR1QoAm?o(gwPWy zd|imtA;^v>0BECpKj^IA@NH+~CFU@oE9fuBT}c8z-jga)p2V=Tupz;u(1I2Jlj<1o zIEhE2z=os=QGaw8*H9BOYfm6WHqIq;p$H$Y#w2b(zD_ z;_MFMT(U_i;^6>oeefS{VBqaG1E;#dJ@VZ_PPPGk(M+^O#=tTViBYOSjCRk!_$iUy zVDWjOc}oxm4E*;>zrHRAxY_xCFeHH~T_6ew)gAb5-J5->n*!0%e}zo@N&!=USJHv; zV|7}fit&O6IL(q|#H}~&C~0_Ne}PKJqh)xgl@6TV8BztwM4ef38iu<%2sHQa*D-ZWD#kF zrCs%c|MI>w`_+5v(Ix!UrOEwOS$F!6swbwD$mP#Z#3H?@3ajZXv!fFfk}EG@>aq{k zkPT)e*A%i2#4fI3O;cz|BHnypSALZTfy1UC8Zx91_BVEd9wqjlfIw?6pqFlM)M1Pm zA0(zQ%_M2SjG#T7$_Qxhyo%L(T!l;djajTtT$tqf6D6LdQS}>EhwmgfBEv_++#Uy5 zRSuwKm=6hHU|e_>*ZO?>ZelFU3hRkv+E)4s2=&UQuMKD9)h*=l4PN?{Zg*+N2|9*TluGQI^_5hCq)K@)A1PV7MH?HQGi+Go1%p?QXg zMM@*7x2Y4E?a6AI&jf(3;Keo#EM^9^W`O313*|DAPc)v$Y^me>w5{vdTZI$Yf1-&S z!E>2;{3E09cV2dNvMjD3adc9s>%=exO9DC1 zI-kjbbpV0)WzmalXuFT;)Dd1Y#0Xpj5)&-R`|VG=FqOYiwtDv=+TmzOZB{rbu{bvJ z&!U0jje5+71{6`7Y@_J`?8?kIGc(f|?3iJ|3Nmd9rh_8bt$+)A-=ZqmShAFp76*4w zE>5-Gh1{oh>YyYl6`u-Qt<_vrEcORd-G)V3x4C}rMduO^Oa1G#1~|r#6M9byQ)iFDu5edJ<1Z zZBGTR+#sojk}7>{X)m&TPv|n89dgp-E_8H%0)ob(RGip8W+Ci9sm<9O0h zOZ^iK0UNeIl-M{ckls8JTLvXFtxH>9bln?dATdNOB*~2!5`80zh2rNfmtt8ftB z+%DGJ%#lnn0=+tLb%oj{wS%2hTr*O!qB#IzAQ3ne<6|q%w%%0t#E`MQ9Et06T^MsOb)nd zVR_8*EOuRil08=uEcgH@2%I(5TF))1<8tC~ZIzBFaw8~?c%~UL!%x8aY=(b;5C)l! z*IcI&I6S(MVdj8I=1<#p5cok-tHpTAhr=#sce!99-i9VB|#k$^b}e zq#39|#ViuL#RN*%aLuQsEZF-$A2R4hkI@>XiCziZ{bY#=C@KoP2;a+MrZ)F^Y+?4|O#S+S+>4xd_3{@p+Draix4tVvgykxKfegna0LG zU=LpKMbWbWMyTAQ95sea{=F{S|33>b{?WEe0`kf_4_3 zN>H(q#MbabIySEOn4-!}Y+3L=I}x@>q2wt$xg-CKEgW^;B~Y|p5gSp7RxF&>47vT& zgTlS7eyHoN(48MErb14ZSnBlB!ARDbo}mh8n|)umS%Jq-D`#PSX=;6EkX(F&jg|mcj*NaR0S+OG&+Du7}_LK>p(28eZmS3 zJiDq21)K>~wqQ-3F^sX&j;}%iLj zhsq(~5~@bPOrH zZXHhoTnd`A-6RGr4m`9~cYwGrel?~)*NHkh4U`nTa|Lw1GjhHvW07iPzM-aT$_sG_ zb96kmR_1pw=pOgXj#G_CeJS^uOMR`ndiQ?i)cGCXO~^lZC;kU2jIfRslF6TTp5-Dy zO=Y$&h-7rgv$6z4B}t$h{?HA_adsCSh)g0xh+d$Xi9x_~tFaUYk@>AfA#XPcBkEcu zF^aLGR7*Uf=(;t4vBbTXM$Y`R1sC@Lpmo}1hjIp-CgwCpR{`E!md8WAIR{B&zVK;i z$RP%yg8QDS@AV6Q8u*E#N2JUtTd!qLUlJ-}8~eE6bBrmnz*ADWPv8^7ODpD9&s%_S zM9g91fmcjeS#ZdNvzeDjs*0z8s71nOx&1%l-m)vMuIUry<}F^thuVjYO$?=(X^2of0Eqr zk%($$MaOg4I(~CF0(7jl=Xc=gtUg;h{=H+ah+#E(yPND*vz7e6Q;;{@2odjj&JlN1 zKF{X0+`lb{S1Pd*2$j%i>|OWss9%C(N9f1-t}GGewxg6ue-I;`0(Nk#<)_G5m*!PX zsy;->HxRMqmdvs$<0mOwu%w4p#D~Hoo-!TI@p$p25UCzGyWiNGFR{`Ev~9?6c_X=3 zd@+wG9A4b+?trW=_u~nYR@PSxp~d%?anUo#`hA20fT(yX&g(3srWW>5z7LfpB}+EW zH>WG6Nm$QISUUU&kl*q2k_TOpn9mgfGF13~BuX;k%w#A}JBQ=^awn#YE+KGR5A9ba zrmCcDW+0AWsdPCt>~V%*b)HQB1G7DcZY7B1^#H}+g8W~_K9i!Yg|FQKJ5$Cnt4!4~ zhAD^@$k9L?OEWWI^QB}{OA}>0(}-g<%c#0C32nFo70nXc(4gNRZvcFUo%OcVnLWPU zcdGK?H+19V{x-*|8a->k7enH#Izn>#C0{Z+2| ztAF*^xJwc`I{Pveo^L#wzsoCmY;l#9mUiPk3e73)%&v>D1|&2X<^7`C(CfLiJUkix zMml*asV5bnNT6t6Tu_`#iHOZokf3Q>{Y2%E39C@uEezMEU8|xllrn_Ro5sJhP2k&$-OUs`nfLKKv5OzIrryYBu=t3c>qE7gJc`LR2!oBY&H}2A z%3uFY1Rb<_dzs&6-m{HHH+S3-+SB|Zbu6HhXsp1C9m10b+n03yeR8lzWJrd{8j_3n z5zF&fDxo^K@70f@tx5gb?=QTBjK(KT<@{y@?=1%knKQC4R($NrGz=i!suHsVyaz`p zir}O#+l{(L(-?lSml9wwyh;{?2d_wGxL7SI!Us0>FEJSQg~rj5Iw6L+z= zh`4u$lrVic34my$ftXbH6_qxEsd90Cl@$O)UN5xg=ghW?1`XJ7l`$2aJ4(PI@_m}) z#6RLvR``zmT|ln)fmACSXfl%2&yOkyvYU7zxe)~yym)= z{uRaBceJm#psBP1BYvcgb6xs)bt#-f!YtH3=;`I3X|Q=RR*u`)eL9=i8mUL}aKIrs z*mQ{T%_!zS3?9wjVnJS1fiel2sTx4piIR61vBeAs>HFtRH2Y^QK$4#+asBOA9tTMW zIF&U6tS+AsC^a#{>nHp9^pVP`F^IAf&7TpVW=$dE`UNBj3mFlvn56u=_+gTuYO-9> z#_3I7GVO}CBiE0U5JD~*a`M6a^Up}tOFu@`M^|@)qY{5A+d)FDo1kjRQwSP(ryQ2D zyzvCqOf+aot$sH&+phes(<*+34vSw65{AgL^d$`$=qLNt6S>={S3uSK7K9IejPyfLD z)_HmPkvzA>#dVMH&LX>p?|hK6g1V^2%tVeh0YW|bh_K@i(Xr;?Fd`$T00kP*u3dO8 zUYCrED+1~uRbO8Fgmpj2*gf;%5Ywil0K6T|1|FBXJmmx^0G*sr-_Dpfd_3rUov?m< zKu^aZ=zJ)!+n8fl->$$<%ACZonH!imQ?w7OOI*3$H;>8Pr>Sb9!bWFSb;lp1#F5B7 z{!BBTgDe8FbFk+|L?qI8{Olp#QWb(&T?}k~yw8>C6M>ga1K<zp1Yf%~_7zy?@$94`78&c(vdB^x@4v zfwDg2n7EyXVBh(UJ9G$D4jUpN;$gd`{-kD$>fn6mAp@XWb9u8IDZ)LTgE8O^8TOY$ z?ritFeP~|wR{MV7uk@#$xyoYK)|4&N*X{v=5Tl-+ZQQq#BLDFOzImo5*NeFP>vy-6 z`z_t4%WCo(dkfpE{bPeA^eExZ4rlr_^+6iiD_*4=-tq?%g41OfwC?^#bfNf?HmqC; zfgEU(9y(1pEXe#~@hZt;k!jPEDkP-bGs_ufY>nKT$1g!4B`f~Y+I)`w87i#|RO6=^ zjHz!8=Di9{YR#>__^J%z)fbK!5L*{w~+KY%}&@5zNNWHV+6IC`!<>_{C-HVS%GY&3d6MYcRyWOnY5~uK5-w+4wJ>pKa z>(_Sog8y6Sx8HnG%4rOY%JX=%V1k6)Y%eO^`Xc$@nXqu{rSdE)?+j|rZ>}teISl7c zuL_ct7)5`RWQu2L1X=(|WATA3O3@tceyn^|en4y%1zN@Lp1j$v-JB(eKoYm-mr#{t z^SRs=&m?@}Fni^p3A25sH;0&K3bD=}2451ZAg9yRC$kqC7FPH&M>xT7cKa9H)A9uF z2CKdC>w;&Zkw`A5hh^Tmig@R8<^o%YeIi99(3&;Z>7M87PfJ0N)$7;opnqO5o%K7m z;*Wi?JstngyX#?!p}4*C=nfdyn}Q3SLfG<)K>a1F3!sf(c3t3*WeT0-(Zy^x6cSQD zJC{62{LG-U`Yr4m+BX|U%6JCK-_6)n-9Grr1j)oD42s_ah;8J6G9KEER%RZrN9swO z1~BZBRv7uAW-RpAUT(g z%k7XFpR>*uUI5+s<1rF@rZ{xU%Yi)W!Y5*QN7OUhc$9 zJWA>G&HXu6^DVcM8t*dzFRguTRapJ(P==Aoyseh7C?bdyK^nViNZ0!3%1s^T#&acq z=hf~G7HH?nqh-n`-(!XFHb-kq8UQ?hZE0jGzLFgAnu}v#;#oz-BmO>9m})ArE1n*x z)Z=B@fvzy;(&EU%mzCcg%>Hl+BrWTJcKV%TAcgUs+XMm0qg=9g?%E|>O)B06sG{0g2H_6F0 z%YM3Emqp^8{x5S6Ad35C+Q${O;Mls05ozJo)vwgJ6H#M_7t15~ZIEnnXnx}Gf6UL0 zA3@;hti#O@)0cCx`vITxKcA8XL5*Fvu?!4ntQGY*vH-n}Ef%Sr(6U6X(1}&n0aE@h zREPY}d(cAp4_)_*ko8V@fRJhMX{l4zy0ABNk~z(|MJhe7eObJR{69`D`C#ShB{{FO zE5d8teGe2V`k%dR-UD*=8KLz*$gCF%Eoi-?TzPykHHKx;rW+L>1Rt)H7YE&Uf{*`D zcySM;vziuuQiBey@BBa4+_v+dF~vNEsZV*pS8s7gx#lkaK%(XkQXeL0_L)8!Zne5y z&&ZRvL!`Ibj@Nq*{Q@=$Vi>P|EAs!#!x8e$6h3+QTI$~^ zx>#vFSA+7jj??gJ%nO!jdmw15^nc(+{-!S(ep(v7EfGEs;vTswgGrx}M`TbKu#OXYR2Dc#NT+=I8Q<$$WRBbmcXR zUsW>ND6D*p&zuvUncIj05D?}BRrFwqgy>+3KQ60nb$=#p?AXs?wEtEPuM8kkA8Vz9 zQve{zAi;5dZ$nmru^YWMl7g=)58w96%deft#C5Nzyals6KXad}HX?T`~QjpC5|?mMm=?@XxB93VJ0Lcm~o4 z04ealC@p|Q73aL+NPX~&)N%BW@93h_ml}K*E^F7IF-t~G^IN2AR4{Vy{$rvAV?mT} z<)LxzmX(wMM--qf=}#0mY~<)iJ+kLVgu5Qc(Kxl&ycO8iR+DRAxXXeTIv-p?s16b? zf~)!6deuP>AS@hC0;g)F^KSBf<25iK02U~ARqk+8DgJ^Wv?`pUk6srZTTpQhkU<3Q zeWK$4Bx=kLKX${WlY2ZPdj?F`>rBIY)WkeT37hx+?$qcvds{j7O@I!MPovdi2jR=% zb#p;-myX<$1u8Ty_;q6=Jdqh(O032k!jr2HPhZQ<7(Nx6{yoF1>@)fYWPAr?qbdR_ z6~Y=BspbITC}~O~X5Z{ky-@e9?~a5+ZH@eMTa-_P=FlVtL7eXI5UJAT*)j04pmqHm z2$YDDh+ph#Y4f{nIu;#0XpGt0S<#%N}rTvo!_dsjddv=d5ymVR4AG6Mn!fM_r*p&vUe z(_9%)7ltG$m~jGe{%^-{s2VjGxx7pU>2r9{jr^p<0}N8`@JWV;U#V!YO1PRNHK6xN zC$P7xlg&H*sz7tl3T1rUQwv_cVD9QVMENNoUUW~o`G;;k8kB+p?x3PlQr;vq2Z^?dyr%9ffTZ?>JFmC z!5!Gxc%ht;VV5&H^5jq7*kUD~9Zxs?9pFYc-fD z>4r z1tM^X*Y)_AZ<$Q&VLZ~fd^azpNHxfoIE84~cF6XOQUqWgpbQOzLyzF$KmJIm$$=PA zRJ1gezi>_%AWR2HHNyWuXoC-kSa|jiq!prS1P_Y8+lO#rNh?0LP?{;&P+52ku$nOl zuphcZy;9T&fe01wItf;Rr_lky4?DFk$>i-i2Tr&k^$sT99THi|!u=G%v4mZH>%pzJ zin;+Nl#U--G8$p3fS9^5l`TUL@yrN^TZaUnwHLL#O`o+dR`$!+{FvKy&NX!CuXoh-9jK^()TT(yw*$LS5^u^1;gQd@<>oAgb;CrQrjpCxHw6G)XYF)lEA5w7!NsxY+eEz#Ig;BK*sk)#av_G& zqD#(1R7~a=xP`mi6~^}h)wv?wM^HYSqcFTfHkXKnmy#gBQ7cGjd`}_zPrTEoyUPVd z=?Aj$?OWHed5qiR#xHU0<_|0LpOI0BsS&K)5Gn{J(7s76!2f7ndbAd=nBLEaQy}me>^O3kCs1}r4 z`h4uuwo-d{cV2jar1Pk);pr~A3nUCmw{>3Fx=Kdhay^>t5iuJEc{nr2q~0mBINBI5 zA;*UcOEFT`F&#IjB$4vfm^S?jqj2nbFw>pakwaR|TXR%rd)&di(h_?&mSDpA%^+VI zO5T3pG9{P~`*nBsbqoGSj8y48ZADb+Zi9^+J`+Lo2Hlq*s0vQ53ouN-&(IT_#G2l* zS20~k=Q+dEV^6ZEP3@PNBNzWaIh)dcmK+sHr4{yu{TH`AqyTUr$9n--x=GJCQnXbc2r7WNRBMYQUUT?E9!Fuqcz8 zxvwSBXQP2E7`Y!U`SaqK+Vf4;aL`tEO^3gjD=)z8b*X!=`5j!y(TXG!Js zw`l@A^O)LqS&DXtAG5y9<5w7Mw}$-2n18RV^Se}CUPfJp&Y$O2^}+`>dX1u*gX)e3 zxP88eJCCkyxgLW`yKFmpu6ON|!hUtf0iulFGRFVa+690alW`w8pSQeTnb7 zc({JvsnoSPtb6ck8sz|goZ(iq=REj%i%`w9|L@xsqlX$CzL*; za>$i!&A?Y`Epi4$9R)*>xB8_m*ifzG^@}KYsNg^@l-%W4Le8Tut2Gd4S9Ltl9+G+% z3P7i)pwUtWiA^QvA2;JF1YvHxp$qz7TrO_J(?K(VkjPX?kVSe$drDb908W?dbe-Vb z`$%5xlI_PqCTU{eDcj$swWxS0!J)R68lA$e&x(d3(uNw~uI$Lf3vSisY z63*$$-#C_Q5R1V*T-1^WTbP7uX>7uFL!Mq(oMCrzuxQF?g|)LbX0vgG&M z-8wzp&!qfDu&e{WuA`$L7nXmS^85g0N54iA!{5 ztkaroMTiMHKAXV@{LV&h-$JswB%vY*Uq1)NZ{Ec8F~<>MoY1N5<^5Qb*!YV->7(_Q zBmaIt_4p7sUZa7SvNkdzaZQyJUAlXf+3RSO-++RFD&~JBoS$ADd_=3ql!?MYB}2;& z+!moNH1wmdf*(NsY&87r^SXbRop+$(!*pPLs}uCMPu>-B>_rM$xz@&v|+c_YE7@7gXNH<{sR|WU+fAenM>D?k^Qe`>*i2`W)DfS-)_YrUG9t z!E0G8un9Edo^py1gDyBMC*lq_4@X#cpwDa4!n^KPLfq8PDry=0x$}*VUCmrVdDf)g ztWO3)M9$+usje?I+#Q>BJEz3UmM>r1&K6y|p^GjXo!cO^?B!0BmW;^OR9g1^We2fO zn;bIu$ZDcFd#7r~)liW-WFPTW*lq2T>1$LV80=Fi)3+YWsKj;fFS+!{UVm-rp~A>xjr6z%8lN9@*MAMxwyp@)BH{LQ~)CngXc%h0K+Pt86zbJe-vlP0^ ztXx|k$m3SNC}b286ZGO_BCiMDlPQtU5YAvwl9hum=EClkNx@ITm-y1AG~E{R+}_15 zaCZ3knZ5@G>H!mU?KPjVl0mp7n?~T(Q?WyZV|iMrX=;Edz*G$#V&3UCJ^@!hQ+&q% z(*i8YZ+YytUyI#5q5U+vh^jrRZrzF#cRuWKq%8d69B_NDq|HW*NojQ@H6&s+Y3NYZ zz*IG9<`mD)pxu7+#B%8!KQWAx^hF5cej4t7Q z#5~!DR6Hu|#rKZw*Ih0GfARAT-DkPmTYgM+O4L5BP;fS`#tkyDq>$!TAV!_>59&k> zDUno;m1)Fl&&K#TGzRrN_jw50o#Cc$6>Pm8ky(ZulbM*5jyddWubGk|!olN8NdUN8 zn0T4jO*J^MW$@@iTB=kE0mn&1MQ&#=0!ff#M!U{~uiTp+f4kA$HjltB9PWJ!rVG}l zAg4*yxd87;836Cvg!>3q(-?!?Qrx8a_xgHRPl&A78pLM^N$&^@F>2m_>8?K+l}RXR zZ6YI&!bZg48fxyu&Ki?K3xn%3KxYou=ek)M(u?|hCSSX0d7e2}glF1q4{BxJlzwaK zu)rGJA-nUS$;3z;%On%p%Q0w!A_b2GH_IYxCbh)(Q9=%0W0cwCJ)k!psh%1G{v+QY zP1lau7qJWK(P4GXM&CGqz5`4eAyH*M7%ysv{u$(Oc9i3-3cdLpgN`DVL;4ZP%3UH+ zbwx&s4x@CXdhFkIgZIYS!cM7PX_t-)DjL5Z=HDlwkNM@f5Vv2k@DaoFHyM$+afP*R#6+1egQ>l*;Y_yf)E{|JHWZ!RvCt*Crrqla`S`b+f@~uQBUdGaoz{$b9 zJd$ixn6p~wz1_@LRJMjs8-A*7GLhaoo}_@^D>*p7=~(SC$Q1TsP4RkFoqopG z1)CQX6=v13(MO6U=7WIrWVxA&nI_|6YAC14zR51gjw_=usN21t-dt?~R-o%3R<;h_ zk52a73z_)u7RtZOeF|>-q(;hS{P@rIMzDgQadOm){M+?(HjNigZS#zgXY|JXnr7Tu zX>dL?AEX@O2Z`3ne|$W%1w+mSLrRI~(hP87k=D6QZfYDF=L!`btHSlp%a@~7DWy`;%2=nQ+zLhW668nNI8&WKu2^-_)(8_EmDv*yD2Gqt z)=}vGhtmNWr9>GSjbu6~7lHcI0WPmS5|Wr@jkA)AHGDECkv_6xtn;0GL;VJK-k<-@ z#Ajmk6124CzxR}P{zJ&%y*5!ftwLU)cl2~z9fN2l5sH(wKGIKl4Lo!0zC4fpJYs%2 z*L|<8q3qXdmghx6Hs4@Am9bh}LsCyO%_PT$g4K=MCzkJr@Zq7^@3;;S$MB4Oo#eO~ zdcc$zM03|hos%>#*UH9^e_YIyGlg^P+nIoHIEIlgj96dXuG>*$>q9hv4CmmAX*+Cu2j-HsK}}nSS!{PMR0~+mEc(!c zc->Ynw}&{g0c2#xWi8D+l|60mYJ=Ap{b73IJ4uhG#HM1mt&u1@1S3Dq%qck@=;VQz z5_FMXjfuI#D^&t)gQDwfQp%uzB_gj10NhAoGXz@X%wA$WuD3w(Sj1@@ISx3O`AGi< zD9g60rV_B7BP*kMu^J zdj3@E0B3l7cSj1x`Kdx)GNnbMa&|2aAtOxP$>73>8tJW|p29OPe~)fEHQ=&{0Y&Hq z=>_fy21y&Lq52-XR9B41$mOeCIXl4Q*J@fm>eKMu;4yl9HKA2r95y*3AeAKgxQ zee}x8De-&Te|=+wu)eZbt)=$;bnwW6s+f}hhgTJ~%(xXqWvHmH9b^FcFmnH4;$?O+#<`_b%Rnk?(D} zGg7<}FRM#q;jT%R0T4ZQuz3-L9+$sVD!5e_TVgWPGGumjAyaYbQ6BcN_yfG?QWFS( zoi+NnXxqzDqcI!Ihqzc%CoJWSl=1T`jr=U6*TQ56xuJ{z(0=RAENY%@o+XtY4%fc& ziTTbD-hhm>9ve;ReDXEPLs`$WZMgw5_{wxp-%@2PTtcb>BZ<)?_BM1&IwI*}TiBi0 z3yA$hmj4MQ0Me?~RXrQU<3WKzIFo18)18qnA^XM9C|)sMC{1MJ=a#{U)~h<(OBalP zs``}#1W9v17j`zS;NF+hu?{G59dgg?Dx-2&0c0NCfBd`5jrOq zUw?@2Cbjb)uE8;(j1t>wpl|H=X}773*k79IS)U$lJEmR2-hsIJw4 ze`n2dm>2QMDq_j@3z;QQwH5q?4V2C)2{0&1@q|c&VmRn@uH%4)~Hp zg?Tv6ueTH$&S#;r-;z7M{@sq2`3IZ1!F5-AlNBq@6mGk%8YEAV`C^cU5-HMgm=k}{ zzO3ZkA$_)iynpnrzyB#tb~7l4N4?EAL(Q9ZFwtD5&;H_sjkNQ`slB1(ACAwVb)EM| ztExsRJ-=U?{@39p7r_Gpm@QxYXw#0{Rt>C06IC&^X2Sgs9FQK(py`L{{DAOP#8bv# zbDG=fF64K=!bdRd7#C}Ny81@!5H;!UN0nc&L=oe=))E3{8B|>i?|fdu*XL{Tzqu6c z7wmk;YFC&oes-kjRlSX)R%Oa@7eG+F)5UI**lno_F)o^j2iCJ?x^WRz&Ec`MksxIyH2U}~E8GPO{8si!u*xL9shL-uxx7It{={LZ&luuZA+Av4WMZf2nHp)BT1$ioU`ER# z$`BCi5*pzE;lhA2GBT3Z_~v-#HW`00`WX^_ag_a)LTNx8>1N+J+S~M~$lIdO@k(3E z{oOIQBzYZZV*sz%4iPL>6}@%bDpg8-77uLkJ+hd7T@>~7F1(~H9F=a>8;^`sIc5xP zsJMDWvzm9|IQl@S>R>bbV$n7y$fyJhYVAtFv$fW0*U)GgjhYnrAQ9N(E=|A&=g1Nb z?sWcBXIOE8NzSo(J=5rCv&8l~GrJxaR)NI){;F$Lfqpwm9dwC*wo0hIhn(PHQ`W$` z72{Ryg+Di80>>f3y_Z1TC3zVkBtd&B>?A}Wa>*j*^>8cPOO^a$GTXdPM@g0`<3y%pbZHcB3WkEnOVKLl%e6c)0j$lz1G#eMa zE8~gAlq7N*R@&|SF`%dVKs)PJ?uE0uaZ(zHj#{s@6+g4HluX#}6Ve&cl$7 zd%SFDLBz1iU&`cu=X<28TfsjU@JI8$nCu6q{Wb(Ej3Vd>*6f@EHb^4zEx6nJOt8Zo z2QVtct?Y$$e_lPdEnu^^y^Uu_^arys`OB?W^P#NHwJI|&>Vx6FH1d_(f!rdvVS^R_M*{nY>|?G z%RzzcJ-4P0armMPyv=>zy|?8bfFT?*-HEw)2t)lDh!-;)XZC9asG+>-b<=~fD6v1K z6d#u1Ia!A5_>c$Mp!|r9qy0qmD3(s|eeEV03GokllXq(#jD<~mwo49O*;2qvp6Co; zs;_2}a^8{uLc(x(U&Z@!fkK3t(l?Yd{E^Vo%$mm?oNR+=7Yc^Py6u_@Yv$$Yp=o^%vnP>Uf?%<;8bb05cmE3xZSQ#yis6#E=U%I*(17J$AQ{njv=9}+YSfH`O z&Td*Jw!{D(XsJ-iYN1pwz3S)Zkyy{$_JPR<62GEyq?uKRX`TZKI>A*ExrM3WoOIR3 zj1Ez4tI4cgq)WH7m*`!5pXC^-GStsPuhjXO;P(yU$&UXc~LgP&bF?m4mo)(0XZJcGoM zQY4TLQ}sbJSM$T%TbL z|9S1~gum|g5YC5!i_&*6-gTZb5>)1#BsmF?e%)(9?h3E_+IDURe7ut;88{*b3}s*w z{>gRS2L-NCEL!xOAC(cGoKw9irg<~ID<}RT43;%0`i(D8vwd`6Duks{VR|XpH&@~Izhqa zjAh;a==Qufq26tf_lrx}B4{{jOSJQ&3owy>%I`ICz7K$Fk!`lWI~oIqCC8AbCz)bA zZqb!QPd;aJR#y`v#DSCcvOxGe23gtWI0)$8<|^^Mj?v2tf4xX8fs9eBZx>qadABU2 zDF19;2=KQ4)k(#tQAEa;^%f>oWbm#(D$lEiPw{%#F*;;z>I@AXDo3Xumr_`C2Ipw0 zZa89yA63;F4YWQ%A&?EHtHCb&A&CY>QK}e)-qG!7)i9 zC=r@;yR6D^4yB~aU#~Hp6jlifJviWHlX)@wfRCpCX;oze)VFu zIl6dQ)o4qDb&Bt^{rN0j#3`cG^6&p?IGJ5fc|AfypnY2TM(xfElN-5Os+zG+1B>17 zR(9Koa|+!J5UjNw?I6jM4ntnO=#-ALxDmqK)}Y zf@0BKXk}_!aK3NZTkZKIux+|rC8B5Y1frT#9YDU*cV}hip1Ox@*_?7$cRkx~^_-6| zlM}5y=6GDC9Gzu{>=*|^)hLdi&$z)9ZQ%2c%`M^Skk?TP(Hl!Ow)@S)9!^H_qR>`< zIe13)(VPY8HU-tR76w@oT6kR-qi5}q>c{y~v8TKr`#~?b0k5dN`O%*QgP<{iw^+ek zAsYpsJD~H%wX?-^L=>8(PK*>bVu;&=7RQ5%9(e(Zn*aH5$e@es1ILiYAP9Z_N(W7r zECWo*UM{u`8Ebg|Djw0y(iFU1gcSraNzdaA_TPGN{gTm%U(s~ZZg4#DC1Q24cL8X> zZa^!#chyxh1Sm=i7JZs$zxO()31FI4?6**Dwrx0Jv(hs-M z8l=}EYOe?HU4%{t!Pl1H+O7Zt=*`N)pub!4w|6aKrJ2uUPxyjv5^O8lU z(FCZ2w z)=On^W-8&oTlT{8gI=>=IZPb9rWmK%OkHoG_b)Lo2Pp^VuopMez?bFU0R@?b^k4+wkUTbdWq=Ud&5hB3W^hu@rZqsz< zw&N_!z#=!jW&sEhd3ELx1lc>V3K<$)PJCjvx^@9-?!DOCUZ+tobLfLED(KuFobT+7 zbK=ql`QRL+EXrr!N_g_Z^DYq56t6DYRk6P==p&EbaS(Hq7fNyU)%mSui<>0sdOU5C zvK4=Anfknt5j5V6G=iZa_sI}0RZ5dg!ez_f$ZIsN?J`^M{333@+pzX`7oE-~kHB=q zcRC_cn)-TZ%agBG>XroDPoC)ow=sPFO z-1)!h3D$4Uq2%s|8B2&McUeG?bG#A1Ro`Na5b4!%aNTsgnSI0?%tf%1LD`IaMg@x= zS(Z$EF>`%RseEl#K7FxcddSN44|?4W=$ojsf}2z~*ok_-Alp!%Ah{*@&+4&ZrWzDR$!FY_J zeoN1xmv^|3aJGo2p+AKJp~k5ZF7u-$o!C2jDx|La*pLBGX|U%y+si!-=E}()EpsoO ze?rJUwpLH=%=3 zs`<`$ls z!FTAQj(VMC`mYtoQ1l);(YK@QqZCmi6pL4N+n9E8rL&7)9gC8c)3h7P=@2&z4T_Uc z`H3qrdUjVX+8nQ?kuc4?UiOcLY6wh}$@va2;N3V&)5G2qU{7^-oWmpBvgjwL4g!`a zouD>gB1l#9#X`?lk`ly)ySbOduj7)ZVOviIj>OCG)j#zKm1TZFfo2wY?AHB65wjFJu(wT)Rk!0LIhuDIwOaH~GX z#4wEe4$7}ZQ*(6JvR;&+zB9sjoQl;T^Rs5yR$g2Ctij%h;@x$iKCR@n{LwBEpB8xq z5fPp4M}AGlBqh|6sMXfYXGGdoFIKj|ymHAe!_=@vv{@9>`a9n4&1J5O+d*g~`BFtf9h9xn~k{kGvmUMz*R z+MtFTXqfPW1FWdPr8lkF9Gi55QO^_1&nn!21=4%7ZJs zf4wm+B{m`t{SABa`GIoaN_X)B^Ck#7*7%mVY@K;A7zSpC_ z2v}Pp94OS$;kPu2*QNkRf!)e!fp`LZ^U;O{;9vnHP{xRs>D+p7rD!~$ER9tkE)68K zDXq~2-$YFqT)h6V!as8yXy|b{qg15qZ}3|$on|Xd^MhdpfTq&@b?|fk4t|8l;cs#2 znQTTPHfkxwBc3PB(xc6rKU!k5-|7admx6LJ{myF27ArCh4DMHy%SEcVwXs>4m7ScQ zr~Cgf%e$GXuwsNWejqq@M8Dn_Sc;>O zlSBVd9{JOpWG&2-MsFbdHJHMYoJd&*f!mmXcm4wYi;FYc>nW7|?(4l^E^4OICtR=9Z99 z$Fro1*Ik0n)iXi8lNdoaRc=w=Pcn$s(k`y`Uu>S#!ek-S?nLb6@k(xv0abQCZ8<(B<9Dq|nWk;e<8Bz=A7vzJ|7| zeqK-}x8kiZxBAt}5J?+1(V8J%!s^OD7?i(r419#WlPoh^BoEkXw63f5-Uv4p4{!Yt zFhlB-Dd6)O1ZC*)u08U-Jlgt~=1}{XkWh4gBh4GY#^h5lRm{W^_PneIH2+i`51m-u zDfokxc>V6!{`p%yDvwS>@S=Ip9~P2+j2jH?rO|yhiY;xpr0QB3W;efAwR4to`Natt5*Cc-0YraQawbxsBf`Yj z(Trl3K_s;W$2)C9s^RHXeT*EU2AL>leS&kTvyTKEpCCSiXznyv17{_&OY3==R@4(<22(SpVpq8YEGP{H5 zCKrps0JD~TuXEsO{JZL|1!c=lzI5y2UF1c3_OMBn|9FDlZ)=^QyDu9Vv;-;aquc*- zIU|I-2#jMW_hvu0W> zH~k+7QC!6Dex3KR#^5qJn<=f1o4md-P}sSH69Dspk#X9HWhmI#?aWT^7`74cxOK6H z^g)5Zp1_xtVZFPTeQ)g_))^0d=QYH*2i=gQH)Qe2?&nR2yxR;mksuYE~yll8u zSCPcOM}7w*VDURoE=`68v>nuF)%ib?ZlQm&C%UsQV|{kw{`Zn({K; zI=<5^MguQT|m9{}1Dqmg=cgDPQh7{w%f- z_kA~g>nSENO(6F#VnlZ*DHBxmK8X@KhKX+4kBZ@%Ak?|+tk6+olpm*U9Jvz|{v)5t z8?X|h-y0A^s`m|d1Vkr5^YKz2Iy$?om0~72xU10S?zL@PQNP$TL0WA#9#dA)@zDMp zV=9^NI^Vwxh7K+B*^b$c#sMn1Xuz4&21JjNc5oYqr#t142XVs9>%)F|I<-R~fv>C# zSDg4E9Fy7$Plq#(O=kXE*YEVkcc;uI)GIKGl``gM0@arWYa%2fSIzK;27Pai@K4g5 zkGg`kuyP)EJFo&b;I=@!9>R{xE3W6{t|aCtS5psujWS2p1=VMg%9g~)uw@~3G0w#0wpa2-Sqs5uFGm#KofpUc-4`|#ROWdW zDn1ETP7cTQXoXA?a^`T=Z1nhWZdReJdhdaf6!LZVX1H4<6~t`a#uy#5!q`gh#C|SG zJ@XN>wEk)@aBtEx-86&Fk#pNkqjrDqfTBpOyCQ5r`>v}|U)qgacy79-uBo2_@PqWn zSS*mVpf;_b*2c_}Sldq8V=a;qrkeX=Cr$Ql9kx84aWS=18!*gitE=`fv0k6=Zig4L z5N#hU_e1x+A_7l3b6qzdZ8mdg_1*87gAd8n&$O|Uui0FObza0{b>$eSNFC{II7c;? zZ1HnSUdXIBl!MrO7E`(7neMXo?;Qo4>tP?x^XDr{`~TWI z%dR-OrfowA4k5S&cZcBa65QP(xVyVMK?8#&xH}mJ2@Zq1yF+FO5ZvCm?w|3lr&oXP zUbTDgE;;KcR?K~kDi_=7IZ}X%D7vB*72mF?zpZiAPE~2%fsA>Xgds=-^j7hM^Gx%h zH(v#0#h9s>WwCyJHDEpP6&D+9nB(|!hE74={R7vrG2vRqYw$UVq~L ze<1^HHfvmQ^aU@oSjPALb954)QS>t=n;7adns)2Dgn%{ElUQ*XM8iZCK@HS*pT_+E z4!`4Kp{y>ht3%k#@DY7EOm06OMkefrByofWJ8>YnLR7E+Iu@C{NGg?rNH579G%M+B zoJ)+t{Bu!K1D6z|Pqi#(hKer$sg*x_y6K~OT9z`Ovj-|k*15M5YDz2jM0gZOx;(_3 zc9K=kG zm(LzpdHuPYLA;=b_D@ri+F>k9pX!{iy2o3yPSD%;$Wo($6=d=ZMYeU`5@jKb4G3o6-|r#@2^2J<+>mvTaw%vZ-oJ~ z3CY>;Fvmv&JMg2gjxHWzGe*Sa9(Et)_clxr6)e8}LZkIRwF_pTM9M_%lFe>`bOY8yVr-BW}7Y%Bu34mOFjGhO_8Y|+vWUdp>bG_r4 zu9no_SKi~tu4}D59c?&N5n6&YD_%$<4@DXR66!OdwuG;DY7$HpG>*@mYKyba!uGpz z(9b}I@0=wzD1j=gh-wWd15c{Y8z=R{hB6q8;8tu#hTmEu_bkicVc}x@#%}&0m6w_& z(G{U-W8gHgYO#~VM=TSJM^*c=MD!vLs_n-Dj)PmCAM<63rEVr$0vRVR0UsEda=m2?9>O zyX{p>0Sn(5cO7li->7lP+leodahrs-Pvo5RdJ==L)4$$H@DZ>hW=x4kQcJk2UyMub z9~|1@a|;lEm&96I-<{JAkf^>Z^W4|&5bN`KPqhy5cZAxy?QV}04hvpP;M&dq^qqKl zI%R`|23^Xgk;x3AQVWpzu1pO1Ah@dMk}0dJmT@R2>`D<0klA~-EQOb(voaz|qj7hZ z@7ji%Mz^qc`y3FK;^7=IAFGnCmi+Ylqn-@=Fg0diOn{dnX)oT3P28(;g`IP1{m=2GbOD$KwcpMPH0t+gvIW&#vhD}Vp#i^{K6!OK=2#=ReuNdt z97fg(DG4%QB@NA5cJ1@-HubQ@G{WWDub;K9(7gMaqdB>w|1!ZVlZ2fS)ei5!8XRr^ zbrm^|2`!jDC|46Sx!&90tu7iI#!y)pq!B&I3|kpChqSt9Hb8Aujn!HSNwj)`nW&@v z8nH6g@PzVN6dWR5(qj0Hz!g;AqS7g=3+V(%@yr_+|WDYA!vF-KL_2$CDWo22kX*Rp>*lB9wH51Tcy0^~vg0%aw z(3%ozd@y9)qhAkZKk*M&OQefl((m)jrlre9Nu8o}uJ21MvxHaS)LUFAak zfk=$a`P|O$cn7(CQ{}^%whLw%Q&*_39EnAYE6Ytk=SnBX|0{u|wTMmJ^#DVCf)kDv z$IPyqr`ypkxb68|Tl-pB(CW{zsze7HsXSMTTS?4i#>whcJf6j&ZX2#+<4Ss#9**l8 z(Ja;xmihU!s;5V;V>fCe6OlB|(iKzLp_Jp4kcu3Gb5ZPwXq9uKB;NW#Q*6_%xybzw z;_nFy{3mLr5FZXVX`Va{mLuxci>lHrce_d$s*{G!nUx3Ev;R4|@G$U?fuLKykP~^P0)#`wO2gK5)qM&GHceYVT;Sa zukCbkkr!JpKHiJ6))?=|y%ypb=7z}v|I?+Ss09?)iFKcIp`g`H$xN?|qUA~ZxGbWO zd3we=I9w@4ALIVMx0;&d)$RV;V8>s~M_q4aeo1t>6GmJY(i55__&=P3bKR|>r$f@K z6;r)Y!XR7o!vt?X`!SE&wO^O%_@bAIqPpIH=GOQ?Rhlunbo%285%6kQ-N4(hk2>J) zUL-V9AdlzkUC4x|nE%3ULyGap*sV1FF2l#L;lmk-PmV~1)^f95>cM}GK;|u=Mj=@P zd(w@K@71I!1eYq&eftIveyO=;PS+GZS*O~oyvwVK*Hi`l_h2@G$lctPdoNw6Au!~% zeiJmoHS8|}?o=<=A-VCME1{5(EK%+ZEZyJ!0BA!d5c~TiyQxCflElANmEAp=@FT7u zmLUH`d#!I{#zg`Qa9H{ z!$yQ2NOJq}G+$Enkw-!QkuZiwXX6j_^5-We4u*4Nv3uZLpK!;ZeXcNGNtPGtbp5JG z{dEER!xF^F%ig9`(cG&00OR_ty;G8J?+B1LwmH&bed9l02C$-f*{BkH1W%#;bTarOB)0e)k~{1b7j?`nl8 z<0Q9FE5%=8-c&@H6r*kW8K)DLHhlnX8|y}3qS{5{xbG&myo*(fhhUSwY`D6{A-L<_ zyM}#&*q)ohqHWMq=~&BmFTVLTr5P8K4cZJYvMQNV{Nn=YP5f%2d`gZ@TrkqrDc0}F z;`UO`7hceo=SsZUb4@acMHWaiqaI1Fi6vv8!=J2!&AA;iW+XU?gOEv?Z;>Jwj|Js4 z6Fe5^&+vVrYz|Jnb1~gkvNo0tdOVs-yE$1~B1S`FSx#lG`-IJF5!ewMEC+%QMxBH_2&cFFpXscO>TT;a=&hd&)yOl zNtsRW%LVWLbp#*Y8VG=FnbgM)sf*osn3o?p<7&#l&{4pkZYVc}TOc&8#g%JiT_|T=xbZn;m9nUA!{~$Jia5 z(`nbAM_8o&QAp(`eXnxAH_dc^dD>8Ss299E|D^>>)b5Mg1tiPk;QFvj$2DJCSeyuf zCV^bzgpK$Ip8j!>FscPTqmubwbDH|aUq0Q6%v&<5ylFuxX~o60G1Q2fe}pyRSahJU zgrq9g=8fbrc>5X20)4VqXc=8uMrGf5ra~1Wr`Fa!`n8H}g`~kFJH-;Y>1x*QR;OE{ z42ZABwjKAM)SI+CdIAA_r|+YreC-6SUb43p*lXXdUIlklaKM6%XAl3r&|^0%+Ob zww_h$I7Xuev-2KaxC-H^lp)0Yzy@f`tsN15SVO{4!Wc-toZ)q}Q^FtezFOFAqeWL@ ze_xaej?)Cdj{CLh*h*{0s~>=4?UPTpvfMTR%z$`H?ZCiLiC0?HhZuAA7;BT=)b-L@ zom@eG9QL=@Zh^Z!mXX#Ha`!(B1gw4`<0emkx}Sox@hq*hs8Wk7v)=@Gl74(2*Y(J< zJGeW1=1wVFOJwkerGi!Bz-+M4pM~^DsqlVv{Jz~x^l6v3t(|7X zyX>oIJR)yrfb=)a@+Nb)EPtl~gM?5vU$blM@P9plH|<=*8x39eO8@fS?q-3puk*9E zd}0fII)})_#8!#vtk#}~WAdQiXcIKV#oYalSc;+GUIxsoI5q~!k`ibO@Wh~9?@)e+ zh(nDHw!t5g{1Hu6c~!DvIl%Z#8dH^|^)X{+`$LDGQ}q893!8?+oc9JCfCz{K9cR`E z8l%%p%VnQ$VQ^%0@?ZkO;Qee3$gtjq8^1Wv(0d|x=7tTQ_vg`dL;{MCGEZbf^D!Xc zJNkK_=IH7537^cg(e~?_3IYssNG)Y}P^wTi9oEg`6!L8Q2?qPa|ML>AcKI4x)N`w3 z*6m^}I0^YP#7Db})gLjwO@pJC(|XB8Sj#)G$}$x-rt<7hIi@y)3K3&uT%JPtMQLnam< z)`^fK;|ST0kv%Pa+VE1c-XWP893rGwIJlN8lbzZmcd4Jf1CEK^>yYYNSv*jM(feW- zcP!flfqI%~1Zanfq&S-~558ha8OJW6UI%pBJq6-^bNUwk*$O{#ln6@@Hguruvo5mp6-d)UZO-WwqlamVEo!Pa9E_ zq*jTM*Vo)rv(V#ovv=6NsGwuO=uD6>IMVE?2lwI;l6cPbGf?jz0||`Hiu~*|CI`45 zV(|~k&JVZj&^SilxBFF=-Tq!^@~;M|*SSaa0OO!-=et+G^dd7cg8hG^4TVy7gH`-8 zY#FzCbE~3H480-kw}WW)Ev-JRt@hUcHPOU-K=*ficxv0OHR zJY|v`jB6lEqV{;{na-jzi0ubRkBn$TKaw&%8U0^$ zIa@OT^()D>$=$5`bPARGG@2SPosVU(xZHv=s8AanenTp^rxpGM|Q; zxJ_Fn7WTi&lZ)THFo*|D*dAX3^)`N+_1u0C+wXV1ei9E6+pnYOeTi5n|Mh^S6xjH* zU?QP``^^nfJGYs1=2auj$bAB;S;)YsFJP%AVXGkXm^nS?uMK3@#8BPqKgzuD!*X^m zMJdJGqm|Tl=o6bSx9W2>3qV6Azh2EaGUh$Qay#2-`1~O^c%dLw*t22fC^{hS%yCaXz>i2Qv*@kw!Qh0*YV?Jh zaO&8MZ8fg^$Y%j3MwXZik^zR0nIOM8>|Pz}y{_-uPCm|vb3B2)J1+jd2w&AEKExj{ z7X#bK+6E<<3fi5xtVRQOmzaLKHa;HoaNm*NT(9OeO&7*18R)MQA%G19%YN^s1P(ye zpe9KQYfB{IDC1pc5FiAeyY+{ck5-7ZTksC>?Im55qtIpliWcCPU4Vi452hu{#& zRasly7Q;n<{|SZ7c@_6+)!U=w&>kkxXjHO2hJU-sKhCM~1-Q+x`{sMho!{oB5Q9`@ zwV0%gA@bS|5t?*PTXTtDYxMRW%4#lGnv<8`mR$)sirUu6eXBhXn+~<%)#TBfh}DVSu!Zcq!+v zMQ3q{CtjF(6Fa{&{?#dLJ}39P%%b!A+c7QndTy)73N4)LUN-;sJIkJ#@N2PC6|=P` zv2o$*kBYU>H-xuK4w`Yuy0c!b5#Q^U!JTm~X?gufwt1HDPIMpl={%+ozE7o5LovwO zlQ6R9+Txd@+YihVE4EDd476_v{!0CoBp`#+VUabfExz-4tXF$~*X4sMNaPoc3^zVi zoRAB1X5}-m_cTaB5P=difoE-<`Q)#K9(q{PcrWQW^YE7k1F$Y~bdvVjA~B`en=)Eh zc)l2PVaSOOjXiTymuq+~Vm{?@=Z_%&tMKi*w;t^hHP^#Hlx)wmK-SSDelD?NF=Kz! zZT!6Ox{#XTYW_|cgPG*pK1Z?mmo&GC=7t>?jvQFGElmP=r6I6mo)~ROKXDCrAHHoAj4Ln)8T zW9E?D9#z5jfk|GtCD6-`WRh^m--R^wN$Lac;Pj^Bb ze=ARqFBQt1YM0IZog2@kP0> zt!JOtj9OjwBk@1m=dm`Vz*vN-+mz$-!sMRaXuj* zIA^eDN;&p;<5(h5hEhi-73lc%Z!^w@`z!aEpzr78RwVo6L4Dfza719HDl`wpMTlCf zT%Mvne6^TKQ@_jQ<>vT#Usx^lY%ASFtKYcndGx@U8S=zy1v!G*I01!DJ^D2EJO~$Cn&~%@n-ty{9Nn=K2+>60|qoWoND*EAu5#;f@K_f=DjQGGclVlD(!qr0!0IlG*N1ks8X z{6zE@8n~Hrak!Bwx2X36du{j6Wy?uy6cZ1AiiY3Q0#*4s_&tT}q-&ZsSE-I`T0cZ4 zoJUoff0PF!q_PXu*0ufoLxR^;a%f4VNaXGs1c~*=MaL#h%xj9nG_Q@YK$IpKc7QSC zxs&>;C7o(PEH3T#RBse(&=taXE`Xth;GA7urUuzb^zMB$%W;T@41Zdkw;|LhzD9uN zT>hiG-!}}EWP4mIN9^>oQ5FoFn$J3u;fNqRL~aH6EQP4AFvUlb9cY!99TQTfh1cqe zOr@_|)v5PUw~9-Lgeg_flUh%Y)y%uJS%*NOfu*n3HpP{fs)71+AN^y2BC zPJn=j{-bt`ZlWj!52nXekT)-bE936No|`)N_{Ipw#6Oj#!#81r?6+5VF_F6=m&NuX zdY-3<)`+V7Nm!CBi;QuU8WVqjxuaz9=FH=aidj77a|am-nzZr@zt{31M&7%Y3zK$& zB_iLqK7}ON$#?mIPs3ipeEb*2Oj){YQJ99u@2&Ah&!g81HU0kqF`CO&j~Pn!QKC>_ zz&W%fGY~oz_ivj`g`PM3ZBWMSY6w76NYu%|F*wtj<<-fK$uoPFlsbkgg6^;|9R}hd z(&UUX1|x?c1uVhjRtkV^EKEx3Fv0LPF6Ga0Td&Ciw_#O!$^CROmk9Zqo+kI-4WiJk zEl@czwU-iKoO0D?P2&ph-KQwUHx!sc)$k0N@~Hk-&z}9^Tchq5v$NXt2KMiGu+s&w zYGDS&zd>$Ew;#W!_FOC{W^+i&g80~P*%ScRr_H)X{MdCX+DTpu_P56#%lIGO1Ht~X z^$WJeq38~6@Vy4=o5s%d*9nofBbCh%js3=hP~T~9GQdB!mBz38)G7pYh;|Sx}O$$Ts*1z*78$i930+x03pR zjBi85S4&((+H%mht{Src zZ_0q9&Lq~Ow9h@_wI`&pX-2Wk`vXuwFrmp$%H15fS>02pt-ey5bPZm*k1X&)gC$yl z<=@Sn<-?KpviRdb&2SK)WulO#j{^=41O1DPq_$Cajy*}Fx&2O?64m}5;mJu}$1-mxZ;A5w`=E{AGD*G9>nNq7N6WaMXC=g)!p z6u&c!E`}99{O6{}Lr@K;p`x?)v2EZ$UY9^$p^FI};i8}zpaS+=oc{va*E>X*aWd%CYp7W19n1^DaNGtN14;ZKu;)qQ<= z??Bki-dm;w-6+><_+Vvx;S?O5x0@H1Q<^tl`qx%V;uVY^-DOGNs8?veES>=#;^1Vx zqa*w&l&BaYkr*+x8te-L;+EF=2?zBWD;dovW7<~g5Gq9B3ZQJv-BcRfor=M(vGG0rFQb(=T%(roU_^4M+o)#AnuOHRj+Rn)+ph zC+vrQsaXTBz@5_eL_<^F$gJcZ)NUm%{O4g7g78$5T3-g19-ACq6ai1^zPsYCh2m$C zIdg|z^X1RBKUvQ#cHc+nKh(G@RZG6cgwvCkJ_dZZK$k)P5cj`)Y!Z3rW^LDH^N0WdDS+IZRaJ|itZ5|Os9icRaV0sFU?41qB}Uo`I53i^-mPgVFc ze2O{NWfu(?i&?}9v&vKe#wDOidpqR?jLK6)IJy4frb>eLPd+fpsx~4M3=hsImXsL} zON3$-hMcAPjTdVDJqI;x?#d+ca#6cMB|dwha~wSOsXH84*(c7zF)&y1%srw-+|EC; znzzJTmVDF74{Q+rT(&5qVc7xCS8!0In;m9z_2h5^;QHp;7nRszagGwR|A31_!| z*xHs6Dj4RhqlVknedYrOlK3qk9~;W1UDFh>z-G|lr~V&-3y(a!ul;&Sr0ASf;D6u z9Qp#styaVA&-(`-0!M{*X;%QOE&@LfsL2%{qzo5{QvJ-$;f86IOu*4+Vl`$o#kJW; zVG)vS>0F?o+ z-p-PP4hygVXeR#i(1L;r{*HD8>ypeE6`YQ6NAMsQk6^?28|Z3zak&>!iO;3q!gU36 ze%{y%gVSI7G9mX#nd{YDtoS4L=;G(!YMb`wgK%I z#Fbd2;wL)eCh#)ITSp1k3>MTb67VFfEi%^nn~c}JbAnUgc7D|KjQ1Mce}46xZ9Y$T z^ENbEPiM=@K0Q=iz1eZxuG?9sWFura(_nVl|6ts=Zu?7`i1lY^BZbm(1L#*xch`2) zh5z**M|KfUC_hE76*NVxh?ZQYrCF5jkT%nDO2y-Pj~#06R2)|V&qy1WaX%DuPwMnB zyoPQ_p;l5o7R3MuP1+Dv4kt-_w??zrr0C-d%bSx;KU<&nH;aAL^^Md)u7$vBBe$Nx zx#Pn)pV4YVU+PxLpI=A#vs3)ezr2xB+<`d&QgEDi)%g$dcQ1QTTQe8{-Ox~+r)usQ zy7bM3q1i6k5RMvS7c;At z!}T?G(p6+ny%dqJdLY=}br3VLDh!CW0(&Zy!d1v`J7zn_kwYvWZYMFvWI+6Fv*kSG z=_*0%_bf(h`?IvI_Ey8sD*J})a;&y2#HTfj~64^Y`$6R zN`av}$=t5LjzP;s&!cZU2l632uSMM*wgW@xdW(hODK?ZCvy}Dy)hXH-v?%+l*2GTF z?+DoS1>F9g%LSV~1@c*3hMH|h^HcbXSk^}X&lW?Op;5L=0UaIhl<xv7;6^2uIn6 z0E$VLKawWGUN|+E%?K)G9Ng?|c@4q}+&@1q0KTnr06foTy(R}bQnq92i1I1AZ$`z* z-3~4(tAF3m@M<8=n4A)i=Cz?%K+b*-_`OzOj`h`THlo#pxR1x`k_kL95-q0pcqTBP z2HNW&n=ieyVT-HbTx9r3KUTv3?wb)MPo7%v>E}XDf5KXm76JqWgq6?wvTUjnrRsAe z&TNw>f1CyupzN$B&$h;QZA!27%ZGH^zv64+hro;HjXp0nwd;-kn=w7FPsG}@ioRJi znx- ocr -> structure) to +the Android app. Runs the same in-process functions the numbered host +scripts drive via `docker run` per stage, but skips the per-stage +container start-up since a live server has no need for that +inspectability - the host scripts remain the way to inspect each stage +independently during pipeline development. + +Jobs are processed asynchronously so the app can show real per-stage +progress: POST /jobs returns immediately with a job id; GET /jobs/{id} +reports which of preprocessing/ocr/extracting is currently running, and +the final result once done. Job state lives in memory only - this is a +personal, LAN-only, single-instance server with no need for persistence +here (the Android app is the system of record for confirmed receipts). +""" +import os +import shutil +import tempfile +import threading +import time +import uuid + +import requests +from fastapi import FastAPI, HTTPException, UploadFile +from fastapi.responses import JSONResponse + +from extract import structure +from ocr import ocr as run_ocr +from preprocess import preprocess + +OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "http://127.0.0.1:11434") +OLLAMA_MODEL = os.environ.get("OLLAMA_MODEL", "qwen2.5:3b") + +app = FastAPI(title="fuelish") + +_jobs: dict[str, dict] = {} +_jobs_lock = threading.Lock() + + +def _set_job(job_id: str, **fields) -> None: + with _jobs_lock: + _jobs[job_id].update(fields) + + +def _ensure_model_pulled() -> None: + """Best-effort: wait for Ollama to come up (compose starts `api` and + `ollama` together, with no ordering guarantee beyond container start) + and pull OLLAMA_MODEL if it isn't already present, so a bare `docker + compose up` is enough without a manual `ollama pull` step. Runs in a + background thread - jobs submitted before this finishes will simply + fail with a clear error from Ollama.""" + tags = None + for _ in range(60): + try: + tags = requests.get(f"{OLLAMA_BASE_URL}/api/tags", timeout=5).json() + break + except requests.RequestException: + time.sleep(2) + if tags is None: + return + + have = {m["name"] for m in tags.get("models", [])} + if OLLAMA_MODEL in have or any(m.startswith(OLLAMA_MODEL + ":") for m in have): + return + try: + requests.post( + f"{OLLAMA_BASE_URL}/api/pull", + json={"name": OLLAMA_MODEL}, + timeout=1800, + ) + except requests.RequestException: + pass + + +@app.on_event("startup") +def _on_startup() -> None: + threading.Thread(target=_ensure_model_pulled, daemon=True).start() + + +@app.get("/health") +def health() -> dict: + return {"status": "ok"} + + +def _process_job(job_id: str, image_path: str, scratch_dir: str) -> None: + try: + _set_job(job_id, status="preprocessing") + final_path = preprocess(image_path, scratch_dir) + + _set_job(job_id, status="ocr") + ocr_txt_path = run_ocr(final_path, scratch_dir) + with open(ocr_txt_path, "r", encoding="utf-8") as f: + ocr_text = f.read() + + _set_job(job_id, status="extracting") + result = structure(ocr_text) + + _set_job(job_id, status="done", result=result) + except Exception as exc: # noqa: BLE001 - reported to the client, not swallowed + _set_job(job_id, status="error", error=str(exc)) + finally: + shutil.rmtree(scratch_dir, ignore_errors=True) + + +@app.post("/jobs", status_code=201) +async def create_job(image: UploadFile) -> JSONResponse: + job_id = uuid.uuid4().hex + scratch_dir = tempfile.mkdtemp(prefix=f"fuelish-{job_id}-") + image_path = os.path.join(scratch_dir, "upload" + os.path.splitext(image.filename or "")[1]) + with open(image_path, "wb") as f: + f.write(await image.read()) + + with _jobs_lock: + _jobs[job_id] = {"status": "queued", "result": None, "error": None, "_created": time.time()} + + thread = threading.Thread(target=_process_job, args=(job_id, image_path, scratch_dir), daemon=True) + thread.start() + + return JSONResponse({"job_id": job_id}, status_code=201) + + +@app.get("/jobs/{job_id}") +def get_job(job_id: str) -> dict: + with _jobs_lock: + job = _jobs.get(job_id) + if job is None: + raise HTTPException(status_code=404, detail="job not found") + return {"status": job["status"], "result": job["result"], "error": job["error"]} diff --git a/upload-image.sh b/upload-image.sh old mode 100644 new mode 100755