glanzlicht / Meinweimar_Tourguide

Intelligenter Tourguide für Weimar

Verified secure showcase
167 Files 833 KB Public size 12 Redacted Dart · Markdown · YAML · HTML · JSON · GITIGNORE

Project overview

Meinweimar_Tourguide is a verified code showcase with 167 visible project entries across 5 top-level modules. The primary detected language is Dart; protected files remain visible in the structure while their contents stay private.

README

MeinWeimar Tourguide

MeinWeimar Tourguide

Eine produktionsnahe Flutter-App fuer Stadtentdeckung, Tourplanung und Sprachlernen in Weimar. Die App verbindet eine responsive mobile und webfaehige Oberflaeche mit einer WordPress-REST-API und macht aus redaktionellen Inhalten konkrete Wege, Orte und Empfehlungen.

Dieses Repository zeigt neben der UI auch die technische Produktarbeit dahinter: API-Integration, Offline-tolerante Datenhaltung, Lokalisierung, Routing, Audio/TTS, Analytics, Deep Links und automatisierte Qualitaetssicherung.

App-Eindruecke

Start · Entdecken · Touren · Karte · Mehr

Produktumfang

  • Entdecken: POIs, Touren, Veranstaltungen, Partner und Themen mit Suche,
  • Filtern und fehlertoleranter Trefferlogik.

  • Unterwegs: Kartenansicht, Standort, Fussgaenger-Routing, Tourfortschritt,
  • Favoriten und eine persoenliche Route aus der eigenen Weimar-Liste.

  • Mehrsprachigkeit: UI und CMS-Inhalte in Deutsch, Englisch,
  • Niederlaendisch, Franzoesisch, Polnisch, Italienisch, Spanisch und Tschechisch.

  • Audio und Lernen: Guide-Audio, Flutter TTS sowie SprachGuide-Kurse und
  • Lern-Touren.

  • Redaktion und Betrieb: WordPress REST API, Oeffnungszeiten mit
  • Zeitzonenlogik, Kampagnen, Partnerplatzierungen, Deep Links und First-Party- Analytics.

  • Responsive UX: kleine Displays, grosse Systemschrift, dynamische
  • Kartenhoehen und versionierte Web-Releases ohne veraltete Service-Worker- Assets.

Technischer Steckbrief

BereichUmsetzung
FrameworkFlutter / Dart 3.4+
PlattformeniOS, Android und Web
DatenWordPress REST API, typisierte Models und Repositories
Kartenflutter_map, latlong2, Standort und Fussgaenger-Routing
Audiojust_audio, flutter_tts
Lokaler ZustandShared Preferences, begrenztes Caching und Analytics-Puffer
QualitaetFlutter Analyzer, Widget- und Service-Tests, Lokalisierungs- und API-Modelltests

Die Codebasis ist nach Verantwortlichkeiten gegliedert:

lib/
	core/          gemeinsame API-, Lokalisierungs- und Navigationslogik
	data/          Datenzugriff und DTO-nahe Strukturen
	features/      produktbezogene Screens und Flows
	models/        fachliche Modelle
	repositories/  Datenzugriff hinter stabilen Schnittstellen
	services/      Routing, Audio, Analytics und Plattformdienste
	widgets/       wiederverwendbare UI-Bausteine
test/            fokussierte Tests fuer Services, Modelle und Widgets

Lokal starten

Voraussetzungen: aktuelles Flutter SDK und das darin enthaltene Dart SDK.

flutter pub get
flutter analyze
flutter test
flutter run -d chrome

Die Anwendung verwendet standardmaessig die oeffentliche API: https://tour.meinweimar.de/wp-json/meinweimar/v1

Web-Release

Fuer einen Web-Release das versionierte Build-Skript verwenden:

./tool/build_web_release.sh

Das Skript legt den Runtime-Build unter build/web/releases/<release-id>/ ab. Launcher und Versionsendpunkt bleiben bewusst nicht cachebar, damit Browser nach einem Reload die aktuelle Version auflosen. Startup-Diagnostik ist nur mit ?mwdebug=1 aktiv.

Dokumentation

  • [Deep Links](DEEP_LINKS.md) - Web-Routen fuer Bereiche, Touren und POIs
  • [Projektstatus](PROJECT_STATE.md) - aktueller Funktions- und Integrationsstand
  • [Changelog](CHANGELOG.md) - nachvollziehbare Produkt- und UX-Iterationen
  • [API-Integration](docs/API_INTEGRATION.md) - technische Integrationsnotizen
lib/data/app_data_store.dartDart
import 'package:flutter/foundation.dart';
import '../core/api/meinweimar_api_client.dart';
import '../models/ad_campaign.dart';
import '../models/event.dart';
import '../models/partner.dart';
import '../models/sponsor_location.dart';
import '../models/poi.dart';
import '../models/tour.dart';
import '../repositories/ad_repository.dart';
import '../repositories/event_repository.dart';
import '../repositories/partner_repository.dart';
import '../repositories/sponsor_location_repository.dart';
import '../repositories/poi_repository.dart';
import '../repositories/tour_repository.dart';
import '../services/analytics_service.dart';
import '../services/offline_cache_service.dart';
import 'mock_data.dart';
enum AppDataStatus { idle, loading, ready, fallback }
class AppDataStore extends ChangeNotifier {
AppDataStore({MeinWeimarApiClient? api})
: _api = api ?? MeinWeimarApiClient() {
_pois = MockData.pois;
_tours = MockData.tours;
}
final MeinWeimarApiClient _api;
late List<Poi> _pois;
late List<Tour> _tours;
List<AdCampaign> _ads = const [];
List<Event> _events = const [];
List<Partner> _partners = const [];
AppDataStatus status = AppDataStatus.idle;
String? lastError;
String _language = 'de';
Future<void>? _loadFuture;
List<Poi> get pois => List.unmodifiable(_pois);
List<Tour> get tours => List.unmodifiable(_tours);
List<Partner> get partners => List.unmodifiable(_partners);
List<Event> get events => List.unmodifiable(_events);
bool get usingLiveData =>
status == AppDataStatus.ready && !_api.servedCachedResponse;
bool get usingCachedData =>
status == AppDataStatus.ready && _api.servedCachedResponse;
String get languageCode => _language;
Future<AdCampaign?> loadSponsorInterstitial({
required String languageCode,
}) async {
try {
final campaigns = await AdRepository(_api).fetchPlacement(
AdPlacement.sponsorInterstitial,
languageCode: languageCode,
);
final now = DateTime.now();
final active = campaigns
.where(
(campaign) =>
campaign.isActiveAt(now) &&
campaign.supports(AdPlacement.sponsorInterstitial),
)
.toList()
..sort((a, b) => b.priority.compareTo(a.priority));
if (campaigns.isNotEmpty) {
final otherPlacements = _ads.where(
(campaign) =>
campaign.resolvedPlacement != AdPlacement.sponsorInterstitial,
);
_ads = [
...otherPlacements,
...campaigns,
];
notifyListeners();
}
return active.isEmpty ? null : active.first;
} catch (_) {
return null;
}
}
Poi poiById(String id) => _pois.firstWhere(
(poi) => poi.id == id,
orElse: () => MockData.pois.firstWhere(
(poi) => poi.id == id,
orElse: () => _pois.first,
),
);
Future<EventActionUrls> eventActions(String id) async {
return EventRepository(_api).fetchActions(id: id);
}
Future<Event> eventDetail(String id) async {
Event? current;
for (final event in _events) {
if (event.id == id) {
current = event;
break;
}
}
try {
final detail = await EventRepository(_api).fetchOne(
id: id,
languageCode: _language,
);
final index = _events.indexWhere((event) => event.id == id);
if (index >= 0) {
_events = [..._events]..[index] = detail;
notifyListeners();
}
return detail;
} catch (_) {
if (current != null) return current;
rethrow;
}
}
Future<Poi> poiDetail(String id) async {
try {
final detail = await PoiRepository(_api).fetchOne(
id: id,
languageCode: _language,
);
final index = _pois.indexWhere((poi) => poi.id == id);
if (index >= 0) {
_pois = [..._pois]..[index] = detail;
notifyListeners();
}
return detail;
} catch (_) {
return poiById(id);
}
}
Future<AdCampaign> campaignDetail(String id) async {
AdCampaign? current;
for (final campaign in _ads) {
if (campaign.id == id) {
current = campaign;
break;
}
}
try {
final detail = await AdRepository(_api).fetchOne(
id: id,
languageCode: _language,
);
// Do not replace the placement list entry with the detail response:
// some API detail payloads intentionally omit placement/targeting fields.
return detail;
} catch (_) {
if (current != null) return current;
rethrow;
}
}
Future<List<SponsorLocation>> sponsorLocations({
required String campaignId,
String? partnerId,
}) async {
return SponsorLocationRepository(_api).fetchAll(
languageCode: _language,
campaignId: campaignId,
partnerId: partnerId,
);
}
Future<Partner> partnerDetail(String id) async {
try {
final detail = await PartnerRepository(_api).fetchOne(
id: id,
languageCode: _language,
);
final index = _partners.indexWhere((partner) => partner.id == id);
if (index >= 0) {
_partners = [..._partners]..[index] = detail;
notifyListeners();
}
return detail;
} catch (_) {
return _partners.firstWhere((partner) => partner.id == id);
}
}
Future<void> load({required String languageCode, bool force = false}) {
if (!force && status == AppDataStatus.loading && _loadFuture != null) {
return _loadFuture!;
}
if (!force && status == AppDataStatus.ready && _language == languageCode) {
return Future<void>.value();
}
_loadFuture = _performLoad(languageCode);
return _loadFuture!;
}
Future<void> _performLoad(String languageCode) async {
_language = languageCode;
_api.resetCacheSessionFlag();
status = AppDataStatus.loading;
lastError = null;
notifyListeners();
try {
final pois =
await PoiRepository(_api).fetchAll(languageCode: languageCode);
final poiMap = {for (final poi in pois) poi.id: poi};
final tours = await TourRepository(_api).fetchAll(
languageCode: languageCode,
pois: poiMap,
);
if (pois.isNotEmpty) _pois = pois;
if (tours.isNotEmpty) _tours = tours;
try {
_events =
await EventRepository(_api).fetchAll(languageCode: languageCode);
} catch (_) {
_events = const [];
}
try {
_partners =
await PartnerRepository(_api).fetchAll(languageCode: languageCode);
} catch (_) {
_partners = const [];
}
try {
final adLists = await Future.wait(
AdPlacement.values.map(
(placement) => AdRepository(_api).fetchPlacement(
placement,
languageCode: languageCode,
),
),
);
// Keep one resolved campaign payload per placement. The same
// campaign can intentionally use different creatives in Nearby,
// the side menu, tours and the sponsor splash.
_ads = adLists.expand((list) => list).toList(growable: false);
} catch (_) {
_ads = const [];
}
status = AppDataStatus.ready;
final cacheEntries = await OfflineCacheService.entryCount();
await AnalyticsService.instance.track(
'content_load',
properties: {
'language': languageCode,
'source': _api.servedCachedResponse ? 'cache' : 'live',
'cache_entries': cacheEntries,
'success': true,
},
);
} catch (error) {
lastError = error.toString();
status = AppDataStatus.fallback;
await AnalyticsService.instance.track(
'content_load',
properties: {
'language': languageCode,
'source': 'demo_fallback',
'success': false,
},
);
_ads = const [];
_partners = const [];
_events = const [];
} finally {
_loadFuture = null;
}
notifyListeners();
}
AdCampaign? campaignFor(AdPlacement placement, {String? category}) {
final now = DateTime.now();
final matches = _ads
.where(
(campaign) =>
campaign.isActiveAt(now) &&
campaign.resolvedPlacement == placement &&
campaign.supports(placement) &&
campaign.matchesCategory(category),
)
.toList()
..sort((a, b) => b.priority.compareTo(a.priority));
return matches.isEmpty ? null : matches.first;
}
@override
void dispose() {
_api.dispose();
super.dispose();
}
}

Architecture

High-level modules detected from the verified public snapshot. Protected implementation details are intentionally omitted.

libmodule
testmodule
docsmodule
webmodule
toolmodule

Technology stack

DartMarkdownLOCKHTMLShellHTACCESSFlutter / Dart
Dart89.9%
Markdown3.9%
LOCK3.1%
HTML1.7%
Shell0.8%
HTACCESS0.2%
YAML0.1%
METADATA0.1%