From 4d0af1103d8d6e06cc5f5fbe23bdd407637fbf00 Mon Sep 17 00:00:00 2001 From: ialakey Date: Sat, 5 Sep 2026 22:46:19 +0200 Subject: [PATCH] docs: correct the README to match what the project actually contains MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Several statements no longer described this repository: - The "three-repository system" table presented a Spring Boot API as the source of locations, guides and the chat directory. The app makes no backend calls at all — every one of those data sets is a bundled asset. - Screenshots showed the pre-redesign interface: the drawer and nested bottom bars that no longer exist. - "It works with no connection" was true of the whole guide when its images were bundled. Article images are now fetched from srb.guide, so the text is offline and the images are not. Stated as such rather than implied. - The platform badge advertised iOS. The ios/ project has no Podfile and still declares an iOS 11 deployment target, below what the current plugins require, and it is neither built nor released. Recorded as unmaintained instead of listed as a supported platform. - flutter_markdown, `share` and the five-parser count referred to packages and code that were replaced. Adds what was missing: the Android toolchain versions, the checklist and reminder features, the local check commands CI runs, and why validate_guide and the parser health job exist. --- README.md | 231 +++++++++++++++++++++++++++++------------------------- 1 file changed, 123 insertions(+), 108 deletions(-) diff --git a/README.md b/README.md index d99bd20..466c425 100644 --- a/README.md +++ b/README.md @@ -1,94 +1,84 @@ # Serbia Guide **A Flutter app for people relocating to Serbia.** It answers the questions every newcomer hits in -their first months: how many visa-free days are left, how to fill in a *beli karton*, where today's -best exchange rate is, and which Telegram chat to ask. +their first months: what to do next, how many visa-free days are left, when the next tax deadline +is, how to fill in a *beli karton*, and where today's best exchange rate is.

Google Play - Flutter - Platforms + Flutter 3.47 + Android 16 + MIT

-Part of a three-repository system: +--- -| Repository | Role | -|---|---| -| **srbguide** (this one) | Flutter mobile client | -| [serbiaguide](https://github.com/ialakey/serbiaguide) | Spring Boot REST API — locations, guides, Telegram chat directory | -| [serbiaguide-front-admin](https://github.com/ialakey/serbiaguide-front-admin) | React admin panel for editing the content | +## Features ---- +### Guide +74 articles across 8 sections, mirrored from [srb.guide](https://www.srb.guide/) with the authors' +permission and bundled as Markdown in `assets/data/guide.json`. Every article keeps its source URL +and last-modified date, and links back to the original. -## Screenshots +**Article text works with no connection**, which matters on day one in a new country. Images are +loaded from srb.guide when the device is online and fall back to a placeholder when it is not. -

- Guide - Visa-free calculator - White cardboard -

-

- Exchange rates - Map -

+Search covers titles, summaries and body text. The query is stemmed, so the Russian "банка" also +finds "банки" and "банковский" — a full morphological analyser would be overkill over 1.5 MB of +text, so only the query is stemmed and matched as a prefix. Results are ranked title-first, filter +by section, and show the snippet that matched. ---- +### "My path" checklist +The guide explains *how* to do each thing; the checklist says *what to do next*. 24 steps across +six stages, each linked to the article that explains it. Steps bind to articles by slug rather than +title, so they survive a content re-sync. -## Features +### Deadline reminders +Tracks visa runs, residence-permit renewal, paušal tax (the 15th of each month), eco tax +(30 April), health insurance and document expiry, and notifies ahead of each one. Dates are derived +from what the obligation is, so most reminders need no data entry, and the monthly and yearly ones +roll forward on their own. + +Scheduling is deliberately **inexact**: exact alarms require `SCHEDULE_EXACT_ALARM` / +`USE_EXACT_ALARM`, which Google Play grants only to alarm and calendar apps. Reminders survive a +reboot via the plugin's boot receiver. + +### Live exchange rates +Serbian exchange offices don't publish an API, so the app scrapes their public pages. Four parsers +live in `lib/service/parser/` — one per office — each matching rows **by currency code** rather +than by row and column position, because these sites renumber their tables and positional parsing +fails silently. + +Offices are queried in parallel and each card reports its own failure with a retry, so one dead +site doesn't empty the screen. The best rate across offices is highlighted, the National Bank of +Serbia reference rate is shown as a yardstick, the last successful fetch is cached for offline use, +and a converter uses the best rate. ### Visa-free stay calculator Enter your entry date and the app tracks the remaining days of the 29-day visa-free window, shows -the exit deadline, and pushes the date straight into the system calendar via `add_2_calendar`, so -the reminder survives even if the app is uninstalled. +the exit deadline, and can push the date into the system calendar via `add_2_calendar`. ### White cardboard (*beli karton*) generator -Address registration in Serbia means filling in the same form by hand every time you move. The app -stores your data once and renders a ready-to-print `.docx` from a bundled template +Address registration means filling in the same form by hand every time you move. The app stores +your data once and renders a ready-to-print `.docx` from a bundled template (`assets/template/cardboard.docx`) using `docx_template`, then hands it to the system share sheet. -### Deadline reminders -Missing a Serbian deadline costs money. The app tracks visa runs, residence-permit renewal, -paušal tax (the 15th of every month), eco tax (30 April), insurance and document expiry, and -notifies ahead of each one. Dates are derived from what the obligation *is*, so most reminders -need no data entry; monthly and yearly ones roll forward on their own. Scheduling is deliberately -inexact — exact alarms are a Play-restricted permission that a reminder app does not need. - -### "My path" checklist -The guide explains *how* to do each thing; the checklist says *what to do next*. 24 steps across -six stages, each linked to the article that explains it, with progress on the home screen. - -### Live exchange rates -Serbian exchange offices don't publish an API, so the app scrapes their public pages directly. -Four independent parsers live in `lib/service/parser/` — one per office — each matching rows by -**currency code** rather than row position, because these sites renumber their tables regularly -and positional parsing fails silently. The best rate across offices is highlighted, the National -Bank reference rate is shown as a yardstick, and the last successful fetch is cached so the screen -still works offline. - -### Offline guide -The relocation guide — 74 articles in 8 sections — ships as Markdown inside the app -(`assets/data/guide.json`), mirrored from [srb.guide](https://www.srb.guide/) with the authors' -permission and re-scraped weekly by CI. Rendered with full-text search, favourites and an -adjustable font size. **It works with no connection** — which matters on day one in a new country, -before you have a local SIM. - -Search stems the query, so the Russian "банка" finds "банки" and "банковский"; results are ranked -title-first and show the snippet that matched. - ### Map of useful places -A curated set of Google Maps searches — exchange offices, non-smoking cafés, expat-friendly venues — -defined in `assets/data/locations.json` and rendered in an embedded `webview_flutter` view with a -dropdown to switch between them. +A curated set of Google Maps links — exchange offices, cafés, expat-friendly venues — defined in +`assets/data/locations.json` and shown in an embedded `webview_flutter` view with a dropdown. +Requests that the page hands off to a native app (`intent://`, `geo:`) are opened through the +platform instead of failing inside the web view. ### Telegram directory -A curated list of relocation chats and channels (`assets/data/tg_chats.json`), opened directly in -the Telegram app through `url_launcher`. +A curated list of relocation chats and channels (`assets/data/tg_chats.json`), opened in the +Telegram app through `url_launcher`. ### Personalisation -Light/dark theme, RU/EN localisation, adjustable text size, and a configurable start screen — all -persisted with `shared_preferences` and restored on launch. +Light/dark theme, RU/EN interface, a configurable start tab, and per-article text size — persisted +with `shared_preferences`. The interface language defaults to the device language, falling back to +Russian, which is the language the guide itself is written in. --- @@ -96,17 +86,18 @@ persisted with `shared_preferences` and restored on launch. | Area | Choice | |---|---| -| Framework | Flutter, Dart | -| State | `provider` (`LanguageProvider`) + `setState` for local screen state | -| Persistence | `shared_preferences` (theme, language, start screen, saved form data) | -| Localisation | `flutter_localizations` + ARB files (`lib/l10n/app_en.arb`, `app_ru.arb`) | +| Framework | Flutter 3.47, Dart 3 | +| Android | AGP 9.1, Gradle 9.3.1, Kotlin 2.4, Java 17, `compileSdk`/`targetSdk` 36, `minSdk` 24 | | UI | Material 3, one seeded `ColorScheme` for light and dark | +| State | `provider` for the locale, `setState` for local screen state | +| Persistence | `shared_preferences` (theme, language, start tab, favourites, deadlines, checklist) | +| Localisation | `flutter_localizations` + ARB files (`lib/l10n/app_en.arb`, `app_ru.arb`), `intl` | | Content | Offline JSON + Markdown in `assets/data/`, rendered with `flutter_markdown_plus` | | Scraping | `http` + `html` — one parser class per exchange office | -| Reminders | `flutter_local_notifications` + `timezone` | -| Documents | `docx_template` + `xml` for `.docx` generation, `open_file` / `share_plus` to export | +| Reminders | `flutter_local_notifications`, `timezone`, `flutter_timezone` | +| Documents | `docx_template` + `xml`, `path_provider`, `open_file` / `share_plus` to export | | Integrations | `add_2_calendar`, `url_launcher`, `webview_flutter`, `photo_view` | -| CI | GitHub Actions — analyze/test, weekly content sync, signed release, daily parser health | +| CI | GitHub Actions — checks, weekly content sync, signed release, daily parser health | --- @@ -114,38 +105,42 @@ persisted with `shared_preferences` and restored on launch. ``` lib/ -├── main.dart # bootstrap: prefs, locale, theme, warm caches -├── data/ # models + repositories -│ ├── guide_dto.dart # guide content model -│ ├── guide_repository.dart # single cached load, search, favourites -│ ├── deadline.dart # deadline kinds and their default schedules -│ ├── journey.dart # the relocation checklist +├── main.dart # bootstrap: prefs, locale, theme, warm caches +├── data/ +│ ├── guide_dto.dart # guide content model +│ ├── guide_repository.dart # single cached load, search, favourites, history +│ ├── deadline.dart # deadline kinds and their default schedules +│ ├── deadline_repository.dart +│ ├── journey.dart # the relocation checklist │ └── currency_rate.dart ├── screens/ -│ ├── app_shell.dart # NavigationBar: Home / Guide / Services / Saved -│ ├── home.dart # search, rate, next deadline, checklist progress -│ ├── guide.dart # collapsible sections -│ ├── article.dart # reader: text size, bookmark, source link -│ ├── guide_search.dart # stemmed full-text search -│ ├── deadlines.dart # reminders -│ ├── journey.dart # "my path" checklist -│ ├── exchange_rate.dart # best rate, NBS reference, converter -│ ├── calculator.dart # visa-free day counter + calendar export -│ ├── white_cardboard.dart # .docx form generation -│ ├── services.dart, map.dart, tg_chats.dart, favourites.dart -│ ├── author.dart, settings.dart +│ ├── app_shell.dart # NavigationBar: Home / Guide / Services / Saved +│ ├── home.dart # search, rate, next deadline, checklist progress +│ ├── guide.dart # collapsible sections +│ ├── article.dart # reader: text size, bookmark, source link +│ ├── guide_search.dart # stemmed full-text search +│ ├── favourites.dart +│ ├── journey.dart # "my path" checklist +│ ├── deadlines.dart # reminders +│ ├── exchange_rate.dart # best rate, NBS reference, converter +│ ├── calculator.dart # visa-free day counter + calendar export +│ ├── white_cardboard.dart # .docx form generation +│ ├── services.dart, map.dart, tg_chats.dart +│ └── author.dart, settings.dart ├── service/ -│ ├── exchange_rate_service.dart # parallel fetch, offline cache, best rate -│ ├── notification_service.dart # scheduling -│ ├── document_generate.dart # .docx rendering from template -│ └── parser/ # one scraper per exchange office -├── theme/app_theme.dart # Material 3 light + dark -├── utils/search_stem.dart # Russian query stemming -├── widget/ # reusable UI -├── localization/, l10n/ # AppLocalizations + ARB files +│ ├── exchange_rate_service.dart # parallel fetch, offline cache, best rate +│ ├── notification_service.dart # reminder scheduling +│ ├── document_generate.dart # .docx rendering from template +│ ├── url_launcher_helper.dart +│ └── parser/ # one scraper per exchange office +├── theme/app_theme.dart # Material 3 light + dark from one seed +├── utils/ # search stemming, section icons +├── widget/ # markdown renderer, tiles, form fields +├── dialogs/, localization/, l10n/, provider/ tool/ -├── sync_guide.dart # re-scrape srb.guide -> assets/data/guide.json -└── validate_guide.dart # sanity gate before that content is committed +├── sync_guide.dart # re-scrape srb.guide -> assets/data/guide.json +└── validate_guide.dart # sanity gate before that content is committed +test/ # unit tests + a network-tagged parser check ``` --- @@ -159,15 +154,19 @@ flutter pub get flutter run ``` -Build a release APK: +No API keys or backend are required — the app ships its content offline and reaches the network +only for exchange rates and guide images. + +Checks, as CI runs them: ```bash -flutter build apk --release +dart format --output=none --set-exit-if-changed lib tool test +flutter analyze +flutter test --exclude-tags live # unit tests +flutter test test/exchange_parsers_live_test.dart # hits the real exchange sites +dart run tool/validate_guide.dart ``` -No API keys or backend are required — the app ships its content offline and only reaches the -network for live exchange rates. - Refresh the bundled guide from the website: ```bash @@ -183,13 +182,29 @@ Release builds and signing are documented in [`docs/RELEASE.md`](docs/RELEASE.md | Workflow | Trigger | What it does | |---|---|---| | `ci.yml` | push / PR | format, analyze, tests, guide validation, debug build | -| `sync-guide.yml` | weekly | re-scrapes srb.guide, validates, commits only real changes | +| `sync-guide.yml` | weekly | re-scrapes srb.guide, validates, commits only real content changes | | `release.yml` | tag `v*` | signed AAB + APK, verifies the signature, draft release | | `parsers.yml` | daily | runs the parsers against the live sites, opens an issue on failure | -The daily parser check is the important one. The exchange offices redesign without notice, and two -of the four parsers had been returning nothing for months before anyone noticed — the whole point -of that job is that CI finds out before users do. +`sync-guide.yml` commits third-party content unattended, so `tool/validate_guide.dart` gates it: +section and article counts, per-article length, duplicate source URLs, and a rejection if the +bundle shrank by more than 25% against the previous one. + +`release.yml` refuses to publish anything questionable — it checks that the upload key is +`SHA256withRSA`, that the APK carries APK Signature Scheme v2 with a SHA-256 certificate digest, +that it is not the Android debug certificate, and that the merged manifest still targets SDK 36. + +`parsers.yml` exists because scraper breakage is silent: an office redesigns its page, the parser +returns nothing, and the screen simply looks empty. Running the live tests on a schedule means CI +notices before users do. + +--- + +## Platforms + +Android is the maintained and released target. An `ios/` project is present in the repository but +is not currently built or verified — it has no `Podfile` and still declares an iOS 11 deployment +target, below what the current plugin set requires. --- @@ -197,7 +212,7 @@ of that job is that CI finds out before users do. The exchange-rate parsers depend on the HTML of third-party sites and will break when those sites are redesigned. Each parser is isolated so a broken office degrades that one card rather than the -screen, and each card reports its own failure with a retry. +screen. Guide content belongs to the authors of srb.guide and is used with their permission; see [`NOTICE`](NOTICE). The MIT licence in [`LICENSE`](LICENSE) covers the source code only.