Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,18 @@ jobs:

- name: Verify App Bundle signature
run: |
jarsigner -verify -strict -verbose:summary \
build/app/outputs/bundle/release/app-release.aab | tail -20
# No `-strict`, and no pipe. An app-signing certificate is self-signed
# by definition, which -strict counts as a severe warning and exits
# non-zero for; and piping into `tail` handed the step tail's exit
# code, so this check could never fail either way.
jarsigner -verify -verbose:summary \
build/app/outputs/bundle/release/app-release.aab > /tmp/jarsig.txt
tail -20 /tmp/jarsig.txt
grep -q "jar verified" /tmp/jarsig.txt \
|| { echo "::error::jarsigner did not report the bundle as verified"; exit 1; }
if grep -qi "CN=Android Debug" /tmp/jarsig.txt; then
echo "::error::bundle was signed with the debug key"; exit 1
fi

- name: Report signing details
run: |
Expand Down
19 changes: 17 additions & 2 deletions .github/workflows/sync-content.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
name: Sync content

# Re-scrapes the three bundled datasets once a week so the offline copies do not
# Re-scrapes the four bundled datasets once a week so the offline copies do not
# drift from their sources:
#
# assets/data/guide.json <- srb.guide (74 articles)
# assets/data/places.json <- stats.srb.guide/map (relocant businesses)
# assets/data/smoking.json <- Google My Maps KML (non-smoking venues)
# assets/data/tg_chats.json <- stats.srb.guide (chat directory)
#
# Everything is validated before anything is committed — see
Expand All @@ -30,6 +31,7 @@ env:
FLUTTER_VERSION: "3.47.2"
GUIDE: assets/data/guide.json
PLACES: assets/data/places.json
SMOKING: assets/data/smoking.json
CHATS: assets/data/tg_chats.json

jobs:
Expand All @@ -52,6 +54,7 @@ jobs:
run: |
cp "$GUIDE" /tmp/guide-baseline.json
cp "$PLACES" /tmp/places-baseline.json
cp "$SMOKING" /tmp/smoking-baseline.json
cp "$CHATS" /tmp/chats-baseline.json

- name: Scrape srb.guide
Expand All @@ -63,6 +66,9 @@ jobs:
- name: Scrape the places catalogue
run: dart run tool/sync_places.dart

- name: Scrape the non-smoking map
run: dart run tool/sync_smoking.dart

- name: Scrape the chat directory
run: dart run tool/sync_chats.dart

Expand All @@ -81,6 +87,10 @@ jobs:
places_after=$(jq -S -c '.places' "$PLACES" | sha256sum | cut -d' ' -f1)
[ "$places_before" != "$places_after" ] && changed="$changed $PLACES"

smoking_before=$(jq -S -c '.places' /tmp/smoking-baseline.json | sha256sum | cut -d' ' -f1)
smoking_after=$(jq -S -c '.places' "$SMOKING" | sha256sum | cut -d' ' -f1)
[ "$smoking_before" != "$smoking_after" ] && changed="$changed $SMOKING"

chats_before=$(jq -S -c '.' /tmp/chats-baseline.json | sha256sum | cut -d' ' -f1)
chats_after=$(jq -S -c '.' "$CHATS" | sha256sum | cut -d' ' -f1)
[ "$chats_before" != "$chats_after" ] && changed="$changed $CHATS"
Expand All @@ -98,6 +108,7 @@ jobs:
{
echo "articles=$(jq '[.ru[].items[]] | length' "$GUIDE")"
echo "places=$(jq '.places | length' "$PLACES")"
echo "smoking=$(jq '.places | length' "$SMOKING")"
echo "chats=$(jq 'length' "$CHATS")"
} >> "$GITHUB_OUTPUT"

Expand All @@ -111,14 +122,15 @@ jobs:
echo "|---|---|"
echo "| guide.json | ${{ steps.diff.outputs.articles }} articles |"
echo "| places.json | ${{ steps.diff.outputs.places }} places |"
echo "| smoking.json | ${{ steps.diff.outputs.smoking }} venues |"
echo "| tg_chats.json | ${{ steps.diff.outputs.chats }} chats |"
echo ""
echo "Changed: \`${{ steps.diff.outputs.files }}\`"
} >> "$GITHUB_STEP_SUMMARY"

- name: Discard a no-op scrape
if: steps.diff.outputs.changed != 'true'
run: git checkout -- "$GUIDE" "$PLACES" "$CHATS"
run: git checkout -- "$GUIDE" "$PLACES" "$SMOKING" "$CHATS"

- name: Commit to master
if: steps.diff.outputs.changed == 'true' && inputs.open_pull_request != true
Expand All @@ -133,6 +145,7 @@ jobs:
Automated weekly scrape.
guide ${{ steps.diff.outputs.articles }} articles ·
places ${{ steps.diff.outputs.places }} ·
smoking ${{ steps.diff.outputs.smoking }} ·
chats ${{ steps.diff.outputs.chats }}."
git push

Expand All @@ -150,11 +163,13 @@ jobs:
|---|---|
| `guide.json` | ${{ steps.diff.outputs.articles }} articles |
| `places.json` | ${{ steps.diff.outputs.places }} places |
| `smoking.json` | ${{ steps.diff.outputs.smoking }} venues |
| `tg_chats.json` | ${{ steps.diff.outputs.chats }} chats |

The guide was validated by `tool/validate_guide.dart` against the
previous bundle.
add-paths: |
${{ env.GUIDE }}
${{ env.PLACES }}
${{ env.SMOKING }}
${{ env.CHATS }}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,6 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release

# Signed artifacts produced by tool/build_release.ps1
/dist/
47 changes: 31 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,22 @@ Serbian Railways' own site is awkward on a phone. The app talks to the same endp
JSON station lookup, plus route search and a per-station departure/arrival board. Stations you have
used are remembered, so the usual trip takes two taps.

### Map of relocant-run businesses
362 places — cafés, shops, salons, garages — from the stats.srb.guide catalogue, on an
**OpenStreetMap** map. OSM needs no API key and no billing account, unlike the Google Maps SDK.
The catalogue is bundled, so the list and filters work offline; only the tiles need a connection.
Each place links out to Google Maps for directions.
The lookup only understands Latin — its own front end strips anything else out of the term, and an
empty term is answered with the entire network. So the app asks for that list once (396 stations,
15 KB), caches it for a week, and matches on device: Cyrillic is transliterated, diacritics are
folded, and each word gets one character of slack, so **Белград** finds `BEOGRAD CENTAR` and
`centar` matches inside the name.

### Map of relocant-run businesses and non-smoking venues
362 places — cafés, shops, salons, garages — from the stats.srb.guide catalogue, plus 145
non-smoking venues from the *Lokali bez dima* map, on one **OpenStreetMap** map. OSM needs no API
key and no billing account, unlike the Google Maps SDK. Both catalogues are bundled, so the list
and filters work offline; only the tiles need a connection. Each place links out to Google Maps
for directions.

Filter chips cover the smoking policy (banned outright, or smokeless devices only) alongside the
business categories. A venue that is on both lists is matched by name and proximity and shown as
one pin, not two.

### Visa-free stay calculator
Enter your entry date and the app tracks the remaining days of the 29-day visa-free window, shows
Expand All @@ -102,12 +113,6 @@ Address registration means filling in the same form by hand every time you move.
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.

### Map of useful places
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
432 relocation chats and channels (`assets/data/tg_chats.json`), synced weekly from the
stats.srb.guide catalogue along with their topic, size and whether they are still active, and
Expand Down Expand Up @@ -135,7 +140,7 @@ Russian, which is the language the guide itself is written in.
| Scraping | `http` + `html` — exchange offices, the railway timetable, the sync tools |
| 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` |
| Integrations | `add_2_calendar`, `url_launcher`, `photo_view` |
| CI | GitHub Actions — checks, weekly content sync, signed release, daily parser health |

---
Expand Down Expand Up @@ -165,10 +170,10 @@ lib/
│ ├── deadlines.dart # reminders
│ ├── exchange_rate.dart # best rate, NBS reference, converter
│ ├── trains.dart # route search + station board
│ ├── places.dart # OpenStreetMap map of the catalogue
│ ├── places.dart # OpenStreetMap map: businesses + non-smoking
│ ├── calculator.dart # visa-free day counter + calendar export
│ ├── white_cardboard.dart # .docx form generation
│ ├── services.dart, map.dart, tg_chats.dart
│ ├── services.dart, tg_chats.dart
│ └── author.dart, settings.dart
├── service/
│ ├── exchange_rate_service.dart # parallel fetch, offline cache, best rate
Expand All @@ -185,6 +190,7 @@ lib/
tool/
├── sync_guide.dart # srb.guide -> assets/data/guide.json
├── sync_places.dart # map catalogue -> assets/data/places.json
├── sync_smoking.dart # non-smoking map-> assets/data/smoking.json
├── sync_chats.dart # chat directory -> assets/data/tg_chats.json
└── validate_guide.dart # sanity gate before content is committed
test/ # unit tests + network-tagged live checks
Expand Down Expand Up @@ -222,7 +228,16 @@ dart run tool/sync_places.dart # 362 map places
dart run tool/sync_chats.dart # 432 Telegram chats
```

Release builds and signing are documented in [`docs/RELEASE.md`](docs/RELEASE.md).
Build a signed, Play-ready bundle (Windows):

```powershell
.\tool\create_upload_key.ps1 # once, if there is no upload key yet
.\tool\build_release.ps1 # -> dist/srbguide-<version>.aab and .apk
```

Signing, the checks that guard it and the Play upload procedure are in
[`docs/RELEASE.md`](docs/RELEASE.md); what changed in each release is in
[`docs/CHANGELOG.md`](docs/CHANGELOG.md).

---

Expand All @@ -231,7 +246,7 @@ 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-content.yml` | weekly | re-scrapes all three datasets, validates, commits real changes |
| `sync-content.yml` | weekly | re-scrapes all four datasets, validates, commits real 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 |

Expand Down
3 changes: 3 additions & 0 deletions android/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ GeneratedPluginRegistrant.java
key.properties
**/*.keystore
**/*.jks

# Kotlin Gradle plugin scratch dir (session locks, compiler error logs)
/.kotlin/
6 changes: 5 additions & 1 deletion android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@ android {
applicationId = "com.alakey.serbiaguide"
minSdk = flutter.minSdkVersion
targetSdk = 36
versionCode = 11
// Both come from `version:` in pubspec.yaml (`versionName+versionCode`)
// so there is a single place to bump. `flutter build` can override them
// with --build-name / --build-number; tool/build_release.ps1 always
// passes both explicitly.
versionCode = flutter.versionCode
versionName = flutter.versionName
}

Expand Down
26 changes: 0 additions & 26 deletions assets/data/locations.json

This file was deleted.

Loading