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/text/wp_text.dartDart
/// Normalizes WordPress text and URL values for display in Flutter.
class WpText {
const WpText._();
static final RegExp _entity =
RegExp(r'&(#(?:x|X)?[0-9A-Fa-f]+|[A-Za-z][A-Za-z0-9]+);');
static final RegExp _br = RegExp(r'<\s*br\s*/?\s*>', caseSensitive: false);
static final RegExp _blockEnd = RegExp(
r'<\s*/\s*(p|div|li|h[1-6]|section|article|blockquote|tr)\s*>',
caseSensitive: false,
);
static final RegExp _tag = RegExp(r'<[^>]*>', multiLine: true);
static const Map<String, String> _named = {
'amp': '&',
'quot': '"',
'apos': "'",
'lt': '<',
'gt': '>',
'nbsp': ' ',
'ensp': ' ',
'emsp': ' ',
'thinsp': ' ',
'ndash': '–',
'mdash': '—',
'hellip': '…',
'lsquo': '‘',
'rsquo': '’',
'sbquo': '‚',
'ldquo': '“',
'rdquo': '”',
'bdquo': '„',
'laquo': '«',
'raquo': '»',
'bull': '•',
'middot': '·',
'copy': '©',
'reg': '®',
'trade': '™',
'euro': '€',
'pound': '£',
'yen': '¥',
'cent': '¢',
'deg': '°',
'plusmn': '±',
'times': '×',
'divide': '÷',
'micro': 'µ',
'para': '¶',
'sect': '§',
'Auml': 'Ä',
'auml': 'ä',
'Ouml': 'Ö',
'ouml': 'ö',
'Uuml': 'Ü',
'uuml': 'ü',
'szlig': 'ß',
'Aacute': 'Á',
'aacute': 'á',
'Agrave': 'À',
'agrave': 'à',
'Acirc': 'Â',
'acirc': 'â',
'Atilde': 'Ã',
'atilde': 'ã',
',
'aring': 'å',
'AElig': 'Æ',
'aelig': 'æ',
'Ccedil': 'Ç',
'ccedil': 'ç',
'Eacute': 'É',
'eacute': 'é',
'Egrave': 'È',
'egrave': 'è',
'Ecirc': 'Ê',
'ecirc': 'ê',
'Euml': 'Ë',
'euml': 'ë',
'Iacute': 'Í',
'iacute': 'í',
'Igrave': 'Ì',
'igrave': 'ì',
'Icirc': 'Î',
'icirc': 'î',
'Iuml': 'Ï',
'iuml': 'ï',
'Ntilde': 'Ñ',
'ntilde': 'ñ',
'Oacute': 'Ó',
'oacute': 'ó',
'Ograve': 'Ò',
'ograve': 'ò',
'Ocirc': 'Ô',
'ocirc': 'ô',
'Otilde': 'Õ',
'otilde': 'õ',
'Oslash': 'Ø',
'oslash': 'ø',
'Uacute': 'Ú',
'uacute': 'ú',
'Ugrave': 'Ù',
'ugrave': 'ù',
'Ucirc': 'Û',
'ucirc': 'û',
'Yacute': 'Ý',
'yacute': 'ý',
'yuml': 'ÿ',
'OElig': 'Œ',
'oelig': 'œ',
'Scaron': 'Š',
'scaron': 'š',
'Yuml': 'Ÿ',
'shy': '',
};
/// Decodes named and numeric HTML entities.
static String entities(dynamic value) {
if (value == null) return '';
var text = value.toString();
for (var pass = 0; pass < 3; pass++) {
var changed = false;
final decoded = text.replaceAllMapped(_entity, (match) {
final token = match.group(1)!;
String? replacement;
if (token.startsWith('#x') || token.startsWith('#X')) {
final code = int.tryParse(token.substring(2), radix: 16);
replacement = _codePoint(code);
} else if (token.startsWith('#')) {
final code = int.tryParse(token.substring(1));
replacement = _codePoint(code);
} else {
replacement = _named[token] ?? _named[token.toLowerCase()];
}
if (replacement == null) return match.group(0)!;
changed = true;
return replacement;
});
text = decoded;
if (!changed) break;
}
return text.replaceAll('\u00A0', ' ');
}
/// Converts basic WordPress HTML to plain text with line breaks.
static String plain(dynamic value) {
if (value == null) return '';
var text = value.toString();
text = text.replaceAll(_br, '\n');
text = text.replaceAll(_blockEnd, '\n');
text = text.replaceAll(_tag, '');
text = entities(text);
text = text.replaceAll(RegExp(r'[ \t]+\n'), '\n');
text = text.replaceAll(RegExp(r'\n[ \t]+'), '\n');
text = text.replaceAll(RegExp(r'\n{3,}'), '\n\n');
return text.trim();
}
/// Decodes HTML entities in URL values.
static String? url(dynamic value) {
if (value == null) return null;
final decoded = entities(value).trim();
return decoded.isEmpty ? null : decoded;
}
static String? nullablePlain(dynamic value) {
if (value == null) return null;
final decoded = plain(value);
return decoded.isEmpty ? null : decoded;
}
static String? _codePoint(int? code) {
if (code == null || code < 0 || code > 0x10FFFF) return null;
if (code >= 0xD800 && code <= 0xDFFF) return null;
try {
return String.fromCharCode(code);
} catch (_) {
return null;
}
}
}

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%