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/features/settings/legal_document_page.dartDart
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:url_launcher/url_launcher.dart';
import '../../core/api/api_config.dart';
import '../../core/text/wp_text.dart';
import '../../core/localization/app_localizations.dart';
import '../../services/analytics_service.dart';
import '../../widgets/analytics_view.dart';
enum LegalDocumentType { privacy, imprint }
class LegalDocumentPage extends StatefulWidget {
const LegalDocumentPage({super.key, required this.type});
final LegalDocumentType type;
@override
State<LegalDocumentPage> createState() => _LegalDocumentPageState();
}
class _LegalDocumentPageState extends State<LegalDocumentPage> {
late Future<_LegalDocument> _document;
@override
void initState() {
super.initState();
_document = _load();
}
String get _slug =>
widget.type == LegalDocumentType.privacy ? 'privacy' : 'imprint';
Future<_LegalDocument> _load() async {
final uri = Uri.parse('${ApiConfig.baseUrl}/legal/$_slug');
final response = await http.get(uri).timeout(ApiConfig.requestTimeout);
if (response.statusCode < 200 || response.statusCode >= 300) {
throw Exception('HTTP ${response.statusCode}');
}
final decoded = jsonDecode(utf8.decode(response.bodyBytes));
if (decoded is! Map<String, dynamic>) {
throw const FormatException('Invalid legal payload');
}
return _LegalDocument.fromJson(decoded);
}
void _retry() => setState(() => _document = _load());
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final title = widget.type == LegalDocumentType.privacy
? l10n.t('legalPrivacyTitle')
: l10n.t('legalImprintTitle');
return AnalyticsView(
event: widget.type == LegalDocumentType.privacy
? 'privacy_view'
: 'imprint_view',
child: Scaffold(
appBar: AppBar(
title: Text(title,
style: const TextStyle(fontWeight: FontWeight.w800))),
body: FutureBuilder<_LegalDocument>(
future: _document,
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError || !snapshot.hasData) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.cloud_off_outlined, size: 42),
const SizedBox(height: 12),
Text(l10n.t('legalLoadError'),
textAlign: TextAlign.center),
const SizedBox(height: 16),
FilledButton.icon(
onPressed: _retry,
icon: const Icon(Icons.refresh_rounded),
label: Text(l10n.t('retry')),
),
],
),
),
);
}
final doc = snapshot.data!;
return ListView(
padding: const EdgeInsets.fromLTRB(20, 18, 20, 40),
children: [
...doc.blocks.map((block) => _LegalBlockWidget(block: block)),
if (doc.links.isNotEmpty) ...[
const SizedBox(height: 24),
const Divider(),
const SizedBox(height: 8),
...doc.links.map((link) => ListTile(
contentPadding: EdgeInsets.zero,
dense: true,
leading:
const Icon(Icons.open_in_new_rounded, size: 20),
title: Text(link.label),
onTap: () async {
final uri = Uri.tryParse(link.url);
if (uri == null) return;
AnalyticsService.instance
.track('legal_link_click', properties: {
'document': _slug,
'url': link.url,
});
await launchUrl(uri,
mode: LaunchMode.externalApplication);
},
)),
],
const SizedBox(height: 18),
Text(
l10n.t('legalSourceNote'),
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(color: Colors.black54),
),
],
);
},
),
),
);
}
}
class _LegalBlockWidget extends StatelessWidget {
const _LegalBlockWidget({required this.block});
final _LegalBlock block;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
if (block.type == 'heading') {
final style = switch (block.level) {
1 => theme.textTheme.headlineMedium,
2 => theme.textTheme.headlineSmall,
_ => theme.textTheme.titleLarge,
};
return Padding(
padding: const EdgeInsets.only(top: 18, bottom: 8),
child: SelectableText(block.text,
style: style?.copyWith(fontWeight: FontWeight.w800)),
);
}
if (block.type == 'list_item') {
return Padding(
padding: const EdgeInsets.only(bottom: 7),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(padding: EdgeInsets.only(top: 1), child: Text('• ')),
Expanded(
child: SelectableText(block.text,
style: theme.textTheme.bodyMedium?.copyWith(height: 1.5))),
],
),
);
}
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: SelectableText(block.text,
style: theme.textTheme.bodyMedium?.copyWith(height: 1.55)),
);
}
}
class _LegalDocument {
const _LegalDocument({required this.blocks, required this.links});
final List<_LegalBlock> blocks;
final List<_LegalLink> links;
factory _LegalDocument.fromJson(Map<String, dynamic> json) {
final rawBlocks = json['blocks'];
final rawLinks = json['links'];
return _LegalDocument(
blocks: rawBlocks is List
? rawBlocks
.whereType<Map>()
.map((e) => _LegalBlock.fromJson(Map<String, dynamic>.from(e)))
.toList()
: const [],
links: rawLinks is List
? rawLinks
.whereType<Map>()
.map((e) => _LegalLink.fromJson(Map<String, dynamic>.from(e)))
.toList()
: const [],
);
}
}
class _LegalBlock {
const _LegalBlock({required this.type, required this.text, this.level = 0});
final String type;
final String text;
final int level;
factory _LegalBlock.fromJson(Map<String, dynamic> json) => _LegalBlock(
type: json['type']?.toString() ?? 'paragraph',
text: WpText.plain(json['text']),
level: int.tryParse(json['level']?.toString() ?? '') ?? 0,
);
}
class _LegalLink {
const _LegalLink({required this.label, required this.url});
final String label;
final String url;
factory _LegalLink.fromJson(Map<String, dynamic> json) => _LegalLink(
label: WpText.plain(json['label']),
url: WpText.url(json['url']) ?? '',
);
}

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%