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/core/taxonomy/app_taxonomy.dartDart
import '../../models/poi.dart';
import '../../models/tour.dart';
import '../localization/app_localizations.dart';
class AppTopic {
const AppTopic({
required this.id,
required this.labelKey,
required this.aliases,
this.highlightOnly = false,
});
final String id;
final String labelKey;
final List<String> aliases;
final bool highlightOnly;
String label(AppLocalizations l10n) => l10n.t(labelKey);
}
class AppTaxonomy {
const AppTaxonomy._();
static const topics = <AppTopic>[
AppTopic(
id: 'highlights',
labelKey: 'topicHighlights',
aliases: ['highlight', 'highlights', 'weimar highlights'],
highlightOnly: true),
AppTopic(
id: 'goethe_schiller',
labelKey: 'topicGoetheSchiller',
aliases: ['goethe', 'schiller', 'goethe schiller']),
AppTopic(id: 'classic', labelKey: 'topicClassic', aliases: [
'klassik',
'classic',
'aufklaerung',
'aufklärung',
'klassik aufklaerung',
'klassik aufklärung'
]),
AppTopic(
id: 'bauhaus',
labelKey: 'topicBauhaus',
aliases: ['bauhaus', 'moderne 1900 1933', 'moderne']),
AppTopic(id: 'weimar_republic', labelKey: 'topicWeimarRepublic', aliases: [
'weimarer republik',
'weimar republic',
'1919',
'demokratie'
]),
AppTopic(id: 'ns_memory', labelKey: 'topicNsMemory', aliases: [
'nationalsozialismus',
'ns geschichte',
'ns-geschichte',
'zwangsarbeit',
'gedenken',
'buchenwald'
]),
AppTopic(
id: 'ddr_contemporary',
labelKey: 'topicDdrContemporary',
aliases: ['ddr', 'gegenwart', 'zeitgeschichte', 'contemporary']),
AppTopic(
id: 'architecture',
labelKey: 'topicArchitecture',
aliases: ['architektur', 'architecture']),
AppTopic(
id: 'museums_art',
labelKey: 'topicMuseumsArt',
aliases: ['museum', 'museen', 'kunst', 'art', 'galerie']),
AppTopic(id: 'parks_nature', labelKey: 'topicParksNature', aliases: [
'parks gaerten',
'parks gärten',
'park',
'garten',
'gärten',
'gaerten',
'natur',
'bienen natur'
]),
AppTopic(
id: 'castles',
labelKey: 'topicCastles',
aliases: ['schloss', 'schlösser', 'schloesser', 'castle', 'palais']),
AppTopic(
id: 'family',
labelKey: 'topicFamily',
aliases: ['familie', 'family', 'kinder', 'kids']),
AppTopic(id: 'jewish_life', labelKey: 'topicJewishLife', aliases: [
'jüdisches leben',
'juedisches leben',
'jüdisch',
'juedisch',
'jewish'
]),
AppTopic(
id: 'music',
labelKey: 'topicMusic',
aliases: ['musik', 'music', 'liszt', 'bach']),
AppTopic(
id: 'hidden_gems',
labelKey: 'topicHiddenGems',
aliases: ['geheimtipps', 'geheimtipp', 'hidden gems', 'besonders']),
];
static AppTopic? byId(String id) {
for (final topic in topics) {
if (topic.id == id) return topic;
}
return null;
}
static bool matchesPoi(AppTopic topic, Poi poi) {
if (topic.highlightOnly && poi.highlight) return true;
final fields = <String>[
poi.category,
...poi.topicSlugs,
...poi.topicNames,
...poi.eraSlugs,
...poi.eraNames,
].map(_normalise).where((e) => e.isNotEmpty).toList();
return _matchesFields(topic, fields);
}
static bool matchesTour(AppTopic topic, Tour tour) {
if (topic.highlightOnly && tour.highlight) return true;
final fields = <String>[
tour.category,
...tour.topicSlugs,
...tour.topicNames,
...tour.eraSlugs,
...tour.eraNames,
].map(_normalise).where((e) => e.isNotEmpty).toList();
return _matchesFields(topic, fields);
}
static int scorePoi(Poi poi, Set<String> topicIds) {
var score = 0;
for (final id in topicIds) {
final topic = bySelection(id);
if (topic == null) continue;
if (topic.highlightOnly) {
if (poi.highlight) score += 34;
continue;
}
final category = _normalise(poi.category);
final topicFields =
[...poi.topicSlugs, ...poi.topicNames].map(_normalise).toList();
final eraFields =
[...poi.eraSlugs, ...poi.eraNames].map(_normalise).toList();
final title = _normalise(poi.title.get('de'));
var matched = false;
for (final rawAlias in topic.aliases) {
final alias = _normalise(rawAlias);
if (_fieldMatches(category, alias)) {
score += 30;
matched = true;
}
if (topicFields.any((field) => _fieldMatches(field, alias))) {
score += 42;
matched = true;
}
if (eraFields.any((field) => _fieldMatches(field, alias))) {
score += 38;
matched = true;
}
}
// A title hit is only a weak hint. Curated topic/era metadata wins.
if (!matched &&
topic.aliases
.any((alias) => _fieldMatches(title, _normalise(alias)))) {
score += 5;
}
}
return score;
}
static String selectionLabelForLanguage(
Set<String> ids, String languageCode) {
final selectedIds = ids
.map(bySelection)
.whereType<AppTopic>()
.map((topic) => topic.id)
.toSet();
final selected =
topics.where((topic) => selectedIds.contains(topic.id)).toList();
if (selected.isEmpty) {
return AppLocalizations.tFor(languageCode, 'freeSelection');
}
return selected
.map((topic) => AppLocalizations.tFor(languageCode, topic.labelKey))
.join(' + ');
}
static bool _matchesFields(AppTopic topic, List<String> fields) {
for (final rawAlias in topic.aliases) {
final alias = _normalise(rawAlias);
if (fields.any((field) => _fieldMatches(field, alias))) return true;
}
return false;
}
static AppTopic? bySelection(String value) {
final normalized = _normalise(value);
for (final topic in topics) {
if (_normalise(topic.id) == normalized ||
topic.aliases.any((alias) => _normalise(alias) == normalized)) {
return topic;
}
}
return null;
}
static bool _fieldMatches(String field, String alias) {
if (field.isEmpty || alias.isEmpty) return false;
if (field == alias) return true;
return field.contains(alias) || alias.contains(field);
}
static String _normalise(String value) => value
.trim()
.toLowerCase()
.replaceAll('_', ' ')
.replaceAll('-', ' ')
.replaceAll(RegExp(r'\s+'), ' ');
}

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%