glanzlicht / MeinWeimar_APP

Official MeinWeimar App

Verified secure showcase
204 Files 2 MB Public size 14 Redacted Dart · PHP · XML · GITIGNORE · YAML · PLIST

Project overview

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

README

MeinWeimar — Flutter App & WordPress Plugin

> Working in Progress · Private portfolio repository · Active development

MeinWeimar is a location-based local platform for residents, students and visitors. This repository intentionally contains only two product parts:

  1. the Flutter consumer app;
  2. the meinweimar-core WordPress plugin that provides the editorial/admin
  3. layer and the REST contract consumed by the app.

The complete public website, WordPress core, databases, private infrastructure and unrelated platform modules are not part of this repository.

MeinWeimar consumer app preview

Why this project is interesting

MeinWeimar combines a mobile/web Flutter client with WordPress used as a structured content and administration backend. The goal is a maintainable product boundary: content, branding, navigation, categories, media and configuration are managed in WordPress and delivered through a versioned REST contract instead of being hardcoded in the Flutter UI.

The project demonstrates work across:

  • Flutter/Dart application architecture for iOS, Android and Web;
  • WordPress plugin architecture with custom post types, taxonomies, admin
  • settings and REST endpoints;

  • localized German/English UI and backend-controlled labels;
  • discovery/list/detail flows for places, services, events, classifieds, jobs
  • and real estate;

  • map, GPS, geocoding, marker and navigation contracts;
  • caching, ETag/config-version handling and offline-safe fallbacks;
  • role-aware account, claim, favorites, moderation and messaging boundaries;
  • automated Flutter tests, PHP/REST contract checks and release builds.

Repository structure

flutter-app/       Flutter consumer application
wordpress-plugin/  meinweimar-core WordPress plugin
screenshots/       Curated portfolio preview only

Current status

This is an active Working in Progress project. The core architecture and many end-to-end flows are implemented and continuously tested. Native device polish, production content, final legal/privacy review and further product modules are still being developed. The repository does not claim to be a finished production release.

Local development

Flutter app

cd flutter-app
flutter pub get
flutter analyze
flutter test
flutter run --dart-define=MW_API_BASE_URL=https://example.test/wp-json/meinweimar/v1

The API base URL and other environment-specific values are supplied at build or run time. No credentials are stored in the repository.

WordPress plugin

Copy wordpress-plugin into a WordPress installation's wp-content/plugins/meinweimar-core directory and activate it in the admin area. The plugin owns the MeinWeimar content types, editorial settings, permissions, REST responses, media/configuration contracts and frontend integration points.

Privacy and repository boundary

This repository is private and is prepared for controlled technical review. Secrets, production database exports, user data, WordPress uploads, private keys and infrastructure configuration must remain outside version control. The public-facing portfolio page uses a separate, redacted presentation layer and does not expose the private repository directly.

For technical reviewers

The most relevant entry points are:

  • flutter-app/lib/core — shared configuration, networking, localization,
  • theme and location contracts;

  • flutter-app/lib/features — consumer-facing product surfaces;
  • flutter-app/lib/repositories — API boundary and data access;
  • wordpress-plugin/includes — plugin services, REST, admin and contracts;
  • wordpress-plugin/assets — admin/frontend integration assets.

MeinWeimar is deliberately presented as an evolving product rather than a finished demo: the repository shows the architectural decisions, the working implementation and the remaining engineering work transparently.

flutter-app/lib/shared/app_shell/app_shell.dartDart
import 'dart:async';
import 'package:flutter/material.dart';
import '../../core/config/app_config.dart';
import '../../core/content/navigation_labels.dart';
import '../../core/location/location_controller.dart';
import '../../core/location/location_service.dart';
import '../../core/navigation/canonical_module_routes.dart';
import '../../core/networking/api_client.dart';
import '../../core/theme/app_theme.dart';
import '../../features/detail/content_detail_page.dart';
import '../../features/discovery/discovery_page.dart';
import '../../features/discovery/resource_browser_page.dart';
import '../../features/home/home_page.dart';
import '../../features/more/more_page.dart';
import '../../features/community/community_page.dart';
import '../../features/saved/saved_page.dart';
import '../../l10n/app_localizations.dart';
import '../../l10n/locale_controller.dart';
import '../../models/content_models.dart';
import '../../repositories/discovery_repository.dart';
import '../../repositories/account_repository.dart';
import '../../repositories/claim_repository.dart';
import '../../repositories/geo_repository.dart';
import '../../repositories/image_suggestion_repository.dart';
import '../../repositories/notifications_repository.dart';
import '../navigation/app_navigation.dart';
class AppShell extends StatefulWidget {
const AppShell({
super.key,
required this.snapshot,
required this.localeController,
required this.apiClient,
required this.repository,
required this.accountRepository,
required this.claimRepository,
required this.geoRepository,
required this.imageSuggestionRepository,
required this.notificationsRepository,
this.locationController,
this.initialPath,
this.onRetryBootstrap,
});
final BootstrapSnapshot snapshot;
final LocaleController localeController;
final ApiClient apiClient;
final DiscoveryRepository repository;
final AccountRepository accountRepository;
final ClaimRepository claimRepository;
final GeoRepository geoRepository;
final ImageSuggestionRepository imageSuggestionRepository;
final NotificationsRepository notificationsRepository;
final LocationController? locationController;
final String? initialPath;
final VoidCallback? onRetryBootstrap;
@override
State<AppShell> createState() => _AppShellState();
}
class _AppShellState extends State<AppShell> {
late final LocationController _locationController =
widget.locationController ?? LocationController();
int _selectedIndex = 0;
bool _showHome = true;
bool _initialPathHandled = false;
String? _activeModuleId;
final Map<String, String> _initialCategories = <String, String>{};
bool _showLocationPrompt = false;
bool _locationPromptBusy = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_openInitialCanonicalRoute();
if (widget.locationController != null) {
unawaited(_maybePromptForLocation());
}
});
}
@override
void dispose() {
if (widget.locationController == null) {
_locationController.dispose();
}
super.dispose();
}
Future<void> _maybePromptForLocation() async {
await _locationController.restore();
if (!mounted) return;
if (_locationController.startupPromptCompleted) {
// Permission has already been explained and accepted before. Refresh
// the locally remembered coordinate silently on app start so nearby
// results do not become stale while the user still keeps control over
// the native permission.
if (_locationController.access == LocationAccess.granted) {
// A background refresh is best effort. A transient WebView/browser
// geolocation failure must not erase the coordinate already restored
// from local storage or make every following module forget GPS.
unawaited(
_locationController.requestLocation(preserveOnUnavailable: true),
);
}
return;
}
if (!mounted) return;
// Keep the first-run explanation in the app shell instead of opening a
// modal route during the initial frame. A native location prompt or a
// slow bootstrap must never leave the whole app behind an undismissable
// gray barrier when the platform cannot paint the dialog immediately.
setState(() => _showLocationPrompt = true);
}
Future<void> _handleLocationPrompt(bool allow) async {
if (_locationPromptBusy) return;
setState(() {
_locationPromptBusy = true;
_showLocationPrompt = false;
});
try {
if (allow) {
// Permission/location lookup continues in the shared controller, but
// the shell must remain usable if a browser/WebView promise stalls.
unawaited(_locationController.requestLocation());
}
unawaited(_locationController.markStartupPromptCompleted());
} finally {
if (mounted) setState(() => _locationPromptBusy = false);
}
}
Future<void> _openInitialCanonicalRoute() async {
if (!mounted || _initialPathHandled) return;
_initialPathHandled = true;
final moduleId =
CanonicalModuleRoutes.idForPath(widget.initialPath) ??
_resourceIdForPath(widget.initialPath);
if (moduleId == null) return;
if (CanonicalModuleRoutes.contains(moduleId)) {
await _pushCanonicalModule(moduleId);
} else if (_resourceFor(moduleId) != null) {
await Navigator.of(context).push<void>(
MaterialPageRoute<void>(
settings: RouteSettings(name: widget.initialPath),
builder: (_) => _canonicalModulePage(moduleId),
),
);
}
}
String? _resourceIdForPath(String? rawPath) {
if (rawPath == null || rawPath.trim().isEmpty) return null;
final uri = Uri.tryParse(rawPath.trim());
var path = uri?.path ?? rawPath.trim();
if (path == '/' && uri != null && uri.fragment.isNotEmpty) {
path = uri.fragment;
}
path = '/${path.replaceAll(RegExp(r'^/+|/+$'), '')}/';
final navigationRoutes = <String, String>{
for (final item in _navigation)
'/${item.route.replaceAll(RegExp(r'^/+|/+$'), '')}/': item.id,
};
final navigationMatch = navigationRoutes[path];
if (navigationMatch != null && _resourceFor(navigationMatch) != null) {
return navigationMatch;
}
// Deep links for secondary resources such as News and Guides must remain
// resolvable even when the backend intentionally keeps them out of the
// configured bottom/fullscreen navigation. Their resource definitions are
// still the canonical API contract; absence from navigation must not turn
// a valid route into the Home screen.
for (final resource in widget.snapshot.resources) {
final resourcePath =
CanonicalModuleRoutes.pathFor(resource.id) ??
switch (resource.id) {
'news' => '/news/',
'guides' => '/guides/',
_ => '/${resource.id}/',
};
if (resourcePath == path) return resource.id;
}
return null;
}
List<NavigationItem> get _navigation {
final rawItems =
widget.snapshot.config.appNavigationConfigured &&
widget.snapshot.config.bottomNavigation.isNotEmpty
? widget.snapshot.config.bottomNavigation
: _generalNavigation;
return _uniqueNavigation(rawItems);
}
List<NavigationItem> get _generalNavigation {
final configured = widget.snapshot.config.navigation;
return _uniqueNavigation(
configured.isEmpty ? NavigationItem.defaults : configured,
);
}
List<NavigationItem> _uniqueNavigation(Iterable<NavigationItem> items) {
final seen = <String>{};
final result = <NavigationItem>[];
for (final item in items) {
if (!item.enabled) continue;
if (seen.add(_navigationIdentity(item))) {
result.add(item);
}
}
return result;
}
String _navigationIdentity(NavigationItem item) {
final moduleId = _canonicalModuleId(item);
if (moduleId != null) return 'module:$moduleId';
final id = item.id.trim().toLowerCase();
final route = item.route.trim().toLowerCase();
return 'surface:$id:$route';
}
String? _canonicalModuleId(NavigationItem item) {
final canonicalRoute = CanonicalModuleRoutes.idForPath(item.route);
if (canonicalRoute != null) return canonicalRoute;
final id = item.id.trim().toLowerCase();
return CanonicalModuleRoutes.contains(id) ? id : null;
}
List<NavigationItem> get _configuredFullscreenNavigation {
if (widget.snapshot.config.appNavigationConfigured &&
widget.snapshot.config.fullscreenNavigation.isNotEmpty) {
return _uniqueNavigation(widget.snapshot.config.fullscreenNavigation);
}
return const [];
}
List<NavigationItem> get _primaryNavigation {
if (widget.snapshot.config.appNavigationConfigured &&
widget.snapshot.config.bottomNavigation.isNotEmpty) {
return _navigation
.where((item) => item.enabled)
.take(6)
.toList(growable: false);
}
final configured = _navigation.where((item) => item.enabled).toList();
final configuredById = <String, NavigationItem>{};
for (final item in configured) {
final moduleId = _canonicalModuleId(item);
if (moduleId != null) {
configuredById[moduleId] = item;
}
}
final availableResources = widget.snapshot.resources
.map((resource) => resource.id)
.toSet();
if (availableResources.isEmpty) {
availableResources.addAll(const {
'places',
'events',
'map',
'classifieds',
'jobs',
'real-estate',
});
}
const preferredIds = CanonicalModuleRoutes.fallbackPrimaryIds;
final primary = <NavigationItem>[];
for (final id in preferredIds) {
final configuredItem = configuredById[id];
if (configuredItem != null) {
primary.add(configuredItem);
} else if (availableResources.contains(id)) {
primary.add(_defaultNavigationItem(id));
}
}
if (primary.length >= 4) return primary.take(6).toList(growable: false);
return configured.where((item) => item.primary).take(6).toList();
}
List<NavigationItem> get _menuNavigation {
final fullscreen = _configuredFullscreenNavigation;
// The fullscreen surface remains backend-ordered, but the six public
// content routes are mandatory reachability targets. Older/sparse saved
// configs often mirror the bottom bar into fullscreen; that must not make
// a Home quick-access module disappear from the global menu.
final configured = _uniqueNavigation(
(fullscreen.isNotEmpty ? fullscreen : _generalNavigation).where(
(item) => item.id != 'more',
),
);
final knownIds = configured.map(_navigationIdentity).toSet();
for (final id in CanonicalModuleRoutes.ids) {
if (knownIds.contains('module:$id')) continue;
NavigationItem? configuredItem;
for (final item in _generalNavigation) {
if (_navigationIdentity(item) == 'module:$id') {
configuredItem = item;
break;
}
}
configured.add(configuredItem ?? _defaultNavigationItem(id));
knownIds.add('module:$id');
}
for (final resource in widget.snapshot.resources) {
final resourceIdentity = CanonicalModuleRoutes.contains(resource.id)
? 'module:${resource.id}'
: 'surface:${resource.id}';
if (knownIds.contains(resourceIdentity)) {
continue;
}
configured.add(
NavigationItem(
id: resource.id,
route:
CanonicalModuleRoutes.pathFor(resource.id) ?? '/${resource.id}/',
enabled: true,
label: resource.label,
labelKey: _resourceLabelKey(resource.id),
icon: _resourceIconKey(resource.id),
iconUrl: resource.iconUrl,
iconAlt: resource.iconAlt,
),
);
knownIds.add(resourceIdentity);
}
return _uniqueNavigation(configured);
}
NavigationItem _defaultNavigationItem(String id) {
ResourceDefinition? resource;
for (final candidate in widget.snapshot.resources) {
if (candidate.id == id) {
resource = candidate;
break;
}
}
return NavigationItem(
id: id,
route: CanonicalModuleRoutes.pathFor(id) ?? '/$id/',
enabled: true,
labelKey: switch (id) {
'places' => 'navPlaces',
'events' => 'navEvents',
'map' => 'navMap',
'classifieds' => 'moduleClassifieds',
'jobs' => 'moduleJobs',
'real-estate' => 'moduleRealEstate',
_ => 'navMore',
},
icon: switch (id) {
'places' => 'place',
'events' => 'event',
'map' => 'map',
'classifieds' => 'sell',
'jobs' => 'work',
'real-estate' => 'home_work',
_ => 'grid_view',
},
iconUrl: resource?.iconUrl ?? '',
iconAlt: resource?.iconAlt ?? '',
primary: true,
);
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final primary = _primaryNavigation;
final visibleNavigation = _activeModuleId == null
? primary
: [...primary, _defaultNavigationItem(_activeModuleId!)];
final destinations = visibleNavigation
.map(_destinationForNavigationItem)
.toList();
final selected = _activeModuleId != null
? visibleNavigation.length - 1
: destinations.isEmpty
? 0
: _selectedIndex.clamp(0, destinations.length - 1);
final selectedItem = visibleNavigation.isEmpty
? null
: visibleNavigation[selected];
final selectedModuleId = selectedItem == null
? null
: _canonicalModuleId(selectedItem);
final activeDestination = _showHome || destinations.isEmpty
? const _Destination.start()
: destinations[selected];
return Theme(
data: AppTheme.light(colors: widget.snapshot.config.colors),
child: Scaffold(
backgroundColor: Color(widget.snapshot.config.colors.surface),
appBar: _showHome || selectedItem == null
? AppBar(
centerTitle: true,
title: _BrandHeader(
brand: widget.snapshot.config.brand,
fallback: l10n.brandFallback,
),
actions: _offlineActions(l10n),
)
: AppBar(
leading: BackButton(
onPressed: () => setState(() {
_activeModuleId = null;
_showHome = true;
}),
),
title: Text(_labelFor(selectedItem)),
actions: [
if (selectedModuleId != null &&
CanonicalModuleRoutes.isGeoResource(selectedModuleId))
IconButton(
tooltip: l10n.mapView,
icon: const Icon(Icons.map_outlined),
onPressed: () => _openMapForModule(selectedModuleId),
),
..._offlineActions(l10n),
],
),
body: Column(
children: [
if (widget.snapshot.isOffline && widget.onRetryBootstrap != null)
_BootstrapOfflineBanner(onRetry: widget.onRetryBootstrap!),
if (_showLocationPrompt)
_LocationStartupBanner(
onAllow: () => _handleLocationPrompt(true),
onLater: () => _handleLocationPrompt(false),
),
Expanded(
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 1200),
// Keep one active page in the shell. The previous
// conditional IndexedStack removed Home and re-used its
// element slots when a bottom item was selected. In the
// release web renderer that left every non-Home surface as
// an unpainted gray body although the shell chrome stayed
// visible. The active destination is already derived from
// the same canonical navigation list, so a direct page is
// deterministic and keeps all routes on one shell path.
child: SizedBox.expand(child: _pageFor(activeDestination)),
),
),
),
],
),
bottomNavigationBar: AppNavigationBar(
items: primary,
selectedIndex: _showHome || _activeModuleId != null ? -1 : selected,
onSelected: (index) => setState(() {
_activeModuleId = null;
_selectedIndex = index;
_showHome = false;
}),
onOpenMenu: _openMenu,
brand: widget.snapshot.config.brand,
labelBuilder: _labelFor,
showLabels: _showHome,
),
),
);
}
List<Widget> _offlineActions(AppLocalizations l10n) => [
if (widget.snapshot.isOffline)
Padding(
padding: const EdgeInsets.only(right: 16),
child: Icon(Icons.cloud_off_outlined, semanticLabel: l10n.offlineTitle),
),
];
Future<void> _openMenu() async {
final selected = await Navigator.of(context).push<NavigationItem>(
MaterialPageRoute<NavigationItem>(
builder: (_) => FullscreenModuleMenuPage(
items: _menuNavigation,
brand: widget.snapshot.config.brand,
labelBuilder: _labelFor,
),
),
);
if (!mounted || selected == null) return;
if (selected.id == 'start') {
setState(() {
_activeModuleId = null;
_showHome = true;
});
return;
}
final selectedModuleId = _canonicalModuleId(selected);
if (selectedModuleId != null) {
await _pushCanonicalModule(selectedModuleId);
return;
}
final primaryIndex = _primaryNavigation.indexWhere(
(item) => _navigationIdentity(item) == _navigationIdentity(selected),
);
if (primaryIndex >= 0) {
setState(() {
_selectedIndex = primaryIndex;
_showHome = false;
});
return;
}
if (selected.id == 'community') {
await Navigator.of(context).push<void>(
MaterialPageRoute<void>(
builder: (_) => Scaffold(
appBar: AppBar(title: Text(_labelFor(selected))),
body: _pageFor(const _Destination.community()),
),
),
);
return;
}
if (selected.id == 'map' ||
selected.id == 'saved' ||
selected.id == 'more') {
final destination = _destinationForNavigationItem(selected);
await Navigator.of(context).push<void>(
MaterialPageRoute<void>(
builder: (_) => Scaffold(
appBar: AppBar(title: Text(_labelFor(selected))),
body: _pageFor(destination),
),
),
);
return;
}
if (_resourceFor(selected.id) != null) {
await _pushResource(selected.id, route: selected.route);
}
}
void _openDiscoveryFromHome() {
_pushCanonicalModule('places');
}
Future<void> _openResourceFromHome(String id) async {
await _pushResource(id);
}
Future<void> _openQuickItemFromHome(HomeQuickAccessItem item) async {
final route = item.route.trim();
if (route.isEmpty) return;
await _pushResource(route, initialCategory: item.category.trim());
}
ResourceDefinition? _resourceFor(String id) {
for (final resource in widget.snapshot.resources) {
if (resource.id == id) return resource;
}
return null;
}
Widget _canonicalModulePage(
String id, {
bool showRouteAppBar = false,
String initialCategory = '',
}) {
final resource =
_resourceFor(id) ??
ResourceDefinition(
id: id,
label: '',
endpoint: '',
geo: CanonicalModuleRoutes.isGeoResource(id),
);
return ResourceBrowserPage(
resource: resource,
repository: widget.repository,
geoRepository: widget.geoRepository,
geoConfig: widget.snapshot.config.geo,
showRouteAppBar: showRouteAppBar,
initialCategory: initialCategory,
locationController: _locationController,
fallbackHero: widget.snapshot.config.hero,
fallbackHeroImageUrl: widget.snapshot.config.hero.imageUrl,
imageSuggestionRepository: widget.imageSuggestionRepository,
accountRepository: widget.accountRepository,
categorySchema: widget.snapshot.config.contentCategories[id],
fieldSchemas: widget.snapshot.config.contentSchemas[id] ?? const {},
fieldIcons: widget.snapshot.config.fieldIcons,
surfaceHero: widget.snapshot.config.surfaceHeroes[id],
);
}
Future<void> _pushCanonicalModule(
String id, {
String initialCategory = '',
}) async {
if (!mounted || !CanonicalModuleRoutes.contains(id)) return;
_initialCategories[id] = initialCategory;
final index = _primaryNavigation.indexWhere(
(item) => _canonicalModuleId(item) == id,
);
setState(() {
_activeModuleId = index >= 0 ? null : id;
if (index >= 0) _selectedIndex = index;
_showHome = false;
});
}
Future<void> _pushResource(
String id, {
String? route,
String initialCategory = '',
}) async {
if (!mounted) return;
if (CanonicalModuleRoutes.contains(id)) {
await _pushCanonicalModule(id, initialCategory: initialCategory);
return;
}
if (_resourceFor(id) == null) return;
await Navigator.of(context).push<void>(
MaterialPageRoute<void>(
settings: RouteSettings(name: route ?? '/$id/'),
builder: (_) =>
_canonicalModulePage(id, initialCategory: initialCategory),
),
);
}
Future<void> _openHomeItem(DiscoveryItem item) async {
await Navigator.of(context).push<void>(
MaterialPageRoute<void>(
builder: (_) => ContentDetailPage(
item: item,
repository: widget.repository,
accountRepository: widget.accountRepository,
imageSuggestionRepository: widget.imageSuggestionRepository,
geoConfig: widget.snapshot.config.geo,
userLatitude: _locationController.coordinate?.latitude,
userLongitude: _locationController.coordinate?.longitude,
onOpenMap: () => _openMapForItem(item),
fieldSchemas:
widget.snapshot.config.contentSchemas[item.resource] ?? const {},
fieldIcons: widget.snapshot.config.fieldIcons,
),
),
);
}
Future<void> _openMapForItem(DiscoveryItem item) async {
if (!CanonicalModuleRoutes.isGeoResource(item.resource)) return;
await Navigator.of(context).push<void>(
MaterialPageRoute<void>(
builder: (_) => Scaffold(
appBar: AppBar(title: Text(AppLocalizations.of(context).mapView)),
body: DiscoveryPage(
repository: widget.repository,
types: [item.resource],
geoRepository: widget.geoRepository,
geoConfig: widget.snapshot.config.geo,
accountRepository: widget.accountRepository,
imageSuggestionRepository: widget.imageSuggestionRepository,
locationController: _locationController,
showMapInitially: true,
showTitle: false,
initialFocusItem: item,
categorySchema:
widget.snapshot.config.contentCategories[item.resource],
fieldSchemas:
widget.snapshot.config.contentSchemas[item.resource] ??
const {},
fieldIcons: widget.snapshot.config.fieldIcons,
titleBuilder: (l10n) => l10n.mapView,
),
),
),
);
}
Future<void> _openMapForModule(String moduleId) async {
if (!mounted || !CanonicalModuleRoutes.isGeoResource(moduleId)) return;
await Navigator.of(context).push<void>(
MaterialPageRoute<void>(
builder: (_) => Scaffold(
appBar: AppBar(title: Text(AppLocalizations.of(context).mapView)),
body: DiscoveryPage(
repository: widget.repository,
types: [moduleId],
geoRepository: widget.geoRepository,
geoConfig: widget.snapshot.config.geo,
accountRepository: widget.accountRepository,
imageSuggestionRepository: widget.imageSuggestionRepository,
locationController: _locationController,
showMapInitially: true,
showTitle: false,
categorySchema: widget.snapshot.config.contentCategories[moduleId],
fieldSchemas:
widget.snapshot.config.contentSchemas[moduleId] ?? const {},
fieldIcons: widget.snapshot.config.fieldIcons,
titleBuilder: (l10n) => l10n.mapView,
),
),
),
);
}
Future<void> _openCommunityFromHome() async {
await Navigator.of(context).push<void>(
MaterialPageRoute<void>(
builder: (_) => Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context).navCommunity),
),
body: _pageFor(const _Destination.community()),
),
),
);
}
String _labelFor(NavigationItem item) {
final l10n = AppLocalizations.of(context);
final canonicalId = _canonicalModuleId(item) ?? item.id;
final canonicalLabel = switch (canonicalId) {
'places' => l10n.navPlaces,
'services' => l10n.moduleServices,
'events' => l10n.navEvents,
'classifieds' => l10n.moduleClassifieds,
'jobs' => l10n.moduleJobs,
'real-estate' => l10n.moduleRealEstate,
'news' => l10n.moduleNews,
'guides' => l10n.moduleGuides,
_ => null,
};
if (canonicalLabel != null) return canonicalLabel;
return switch (item.labelKey) {
'navStart' => l10n.navStart,
'navPlaces' => l10n.navPlaces,
'navMap' => l10n.navMap,
'navEvents' => l10n.navEvents,
'navCommunity' => l10n.navCommunity,
'navSaved' => l10n.navSaved,
'navMore' => l10n.navMore,
'moduleServices' => l10n.moduleServices,
'moduleClassifieds' => l10n.moduleClassifieds,
'moduleJobs' => l10n.moduleJobs,
'moduleRealEstate' => l10n.moduleRealEstate,
'moduleNews' => l10n.moduleNews,
'moduleGuides' => l10n.moduleGuides,
_ =>
isTechnicalContentLabel(item.label, item.id)
? l10n.contentTypeOther
: item.label,
};
}
String _resourceLabelKey(String id) => switch (id) {
'places' => 'navPlaces',
'events' => 'navEvents',
'services' => 'moduleServices',
'classifieds' => 'moduleClassifieds',
'jobs' => 'moduleJobs',
'real-estate' => 'moduleRealEstate',
'news' => 'moduleNews',
'guides' => 'moduleGuides',
_ => '',
};
String _resourceIconKey(String id) => switch (id) {
'places' => 'place',
'events' => 'event',
'services' => 'account_balance',
'classifieds' => 'sell',
'jobs' => 'work',
'real-estate' => 'home_work',
'news' => 'article',
'guides' => 'menu_book',
_ => 'grid_view',
};
_Destination _destinationFor(String id) {
if (CanonicalModuleRoutes.contains(id) || _resourceFor(id) != null) {
return _Destination.resource(id);
}
switch (id) {
case 'map':
return const _Destination.map();
case 'community':
return const _Destination.community();
case 'saved':
return const _Destination.saved();
case 'more':
return const _Destination.more();
case 'start':
default:
return const _Destination.start();
}
}
_Destination _destinationForNavigationItem(NavigationItem item) {
final moduleId = _canonicalModuleId(item);
if (moduleId != null) return _Destination.resource(moduleId);
return _destinationFor(item.id);
}
Widget _pageFor(_Destination destination) {
switch (destination.kind) {
case _DestinationKind.start:
return HomePage(
repository: widget.repository,
brand: widget.snapshot.config.brand,
hero: widget.snapshot.config.hero,
resources: widget.snapshot.resources,
locationController: _locationController,
homeSections: widget.snapshot.config.homeSections,
onOpenDiscovery: _openDiscoveryFromHome,
onOpenResource: _openResourceFromHome,
onOpenQuickItem: _openQuickItemFromHome,
onOpenItem: _openHomeItem,
onOpenCommunity: _openCommunityFromHome,
);
case _DestinationKind.resource:
final resourceId = destination.resourceId!;
return _canonicalModulePage(
resourceId,
initialCategory: _initialCategories[resourceId] ?? '',
);
case _DestinationKind.map:
return DiscoveryPage(
repository: widget.repository,
// The global map is the shared geo-discovery surface. Dedicated
// module pages still pass exactly one route and therefore retain
// their own category and typed-filter contracts.
types: CanonicalModuleRoutes.ids,
geoRepository: widget.geoRepository,
geoConfig: widget.snapshot.config.geo,
imageSuggestionRepository: widget.imageSuggestionRepository,
locationController: _locationController,
accountRepository: widget.accountRepository,
fieldSchemas: const {},
showMapInitially: true,
titleBuilder: (l10n) => l10n.mapView,
);
case _DestinationKind.community:
return const CommunityPage();
case _DestinationKind.saved:
return SavedPage(
accountRepository: widget.accountRepository,
discoveryRepository: widget.repository,
geoRepository: widget.geoRepository,
geoConfig: widget.snapshot.config.geo,
imageSuggestionRepository: widget.imageSuggestionRepository,
locationController: _locationController,
contentSchemas: widget.snapshot.config.contentSchemas,
contentCategories: widget.snapshot.config.contentCategories,
fieldIcons: widget.snapshot.config.fieldIcons,
);
case _DestinationKind.more:
return MorePage(
localeController: widget.localeController,
snapshot: widget.snapshot,
accountRepository: widget.accountRepository,
claimRepository: widget.claimRepository,
discoveryRepository: widget.repository,
geoRepository: widget.geoRepository,
imageSuggestionRepository: widget.imageSuggestionRepository,
notificationsRepository: widget.notificationsRepository,
// The More page also exposes secondary resources such as News and
// Guides. They are intentionally not part of the primary canonical
// module navigation, but they still need the same resource opener
// so their rows do not become inert.
onOpenModule: _pushResource,
);
}
}
}
class _BrandHeader extends StatelessWidget {
const _BrandHeader({required this.brand, required this.fallback});
final BrandConfig brand;
final String fallback;
@override
Widget build(BuildContext context) {
final imageUrl = brand.logoLightUrl.isNotEmpty
? brand.logoLightUrl
: brand.logoUrl;
if (imageUrl.isEmpty) {
return Text(
brand.wordmark.isEmpty ? fallback : brand.wordmark,
overflow: TextOverflow.ellipsis,
);
}
return SizedBox(
height: 34,
width: 140,
child: Image.network(
imageUrl,
fit: BoxFit.contain,
semanticLabel: brand.logoLightAlt.isNotEmpty
? brand.logoLightAlt
: brand.logoAlt,
errorBuilder: (_, _, _) => Text(
brand.wordmark.isEmpty ? fallback : brand.wordmark,
overflow: TextOverflow.ellipsis,
),
),
);
}
}
class _BootstrapOfflineBanner extends StatelessWidget {
const _BootstrapOfflineBanner({required this.onRetry});
final VoidCallback onRetry;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final colors = Theme.of(context).colorScheme;
return Material(
color: colors.errorContainer,
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 10, 8, 10),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(Icons.cloud_off_outlined, color: colors.onErrorContainer),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.offlineTitle,
style: Theme.of(context).textTheme.labelLarge?.copyWith(
color: colors.onErrorContainer,
),
),
Text(
l10n.offlineBody,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: colors.onErrorContainer,
),
),
],
),
),
TextButton(onPressed: onRetry, child: Text(l10n.retry)),
],
),
),
);
}
}
class _LocationStartupBanner extends StatelessWidget {
const _LocationStartupBanner({required this.onAllow, required this.onLater});
final VoidCallback onAllow;
final VoidCallback onLater;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final scheme = Theme.of(context).colorScheme;
return Material(
color: scheme.surfaceContainerLowest,
elevation: 2,
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 14, 12, 14),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.my_location_outlined, color: scheme.primary),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.locationStartupTitle,
style: Theme.of(context).textTheme.titleSmall,
),
const SizedBox(height: 4),
Text(l10n.locationStartupBody),
const SizedBox(height: 10),
Wrap(
spacing: 8,
runSpacing: 4,
children: [
TextButton(onPressed: onLater, child: Text(l10n.later)),
FilledButton(
onPressed: onAllow,
child: Text(l10n.allowLocation),
),
],
),
],
),
),
],
),
),
);
}
}
enum _DestinationKind { start, map, resource, community, saved, more }
class _Destination {
const _Destination(this.kind, {this.resourceId});
const _Destination.start() : this(_DestinationKind.start);
const _Destination.map() : this(_DestinationKind.map);
const _Destination.resource(String id)
: this(_DestinationKind.resource, resourceId: id);
const _Destination.community() : this(_DestinationKind.community);
const _Destination.saved() : this(_DestinationKind.saved);
const _Destination.more() : this(_DestinationKind.more);
final _DestinationKind kind;
final String? resourceId;
}

Architecture

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

flutter-appmodule
wordpress-pluginmodule

Technology stack

DartPHPJavaScriptARBLOCKPBXPROJFlutter / Dart
Dart56.1%
PHP29.4%
JavaScript4.0%
ARB2.9%
LOCK1.7%
PBXPROJ1.7%
CSS1.5%
Markdown0.4%