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/map/map_surface.dartDart
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import '../../core/config/app_config.dart';
import '../../core/theme/app_theme.dart';
import '../../l10n/app_localizations.dart';
import '../../models/content_models.dart';
import '../media/public_media_image.dart';
import '../navigation/external_navigation_adapter.dart';
import 'google_map_surface.dart';
import 'google_maps_runtime.dart';
import 'map_policy.dart';
const _bundledUserMarkerAsset = 'assets/ico/user_location_marker.png';
/// Shared interactive map adapter for Website/API-aligned discovery.
///
/// The adapter always renders a real map surface. If WordPress has not
/// supplied an internal tile source, it keeps the geographic markers and
/// offers the official Google Maps URL fallback instead of showing a dead-end
/// configuration error.
class MapSurface extends StatefulWidget {
const MapSurface({
super.key,
required this.items,
required this.meta,
required this.geoConfig,
required this.onItemTap,
this.onRetry,
this.onOpenGoogleMaps,
this.initialSelectedItem,
this.searchLatitude,
this.searchLongitude,
this.userLatitude,
this.userLongitude,
this.radiusKm = 10,
this.compact = false,
this.showSearchCenterMarker = true,
this.visible = true,
});
final List<DiscoveryItem> items;
final DiscoveryMeta? meta;
final GeoConfig? geoConfig;
final ValueChanged<DiscoveryItem> onItemTap;
final VoidCallback? onRetry;
final Future<bool> Function(NavigationTarget target)? onOpenGoogleMaps;
final DiscoveryItem? initialSelectedItem;
final double? searchLatitude;
final double? searchLongitude;
final double? userLatitude;
final double? userLongitude;
final double radiusKm;
final bool compact;
/// Discovery maps show the address origin as a separate marker. Embedded
/// detail maps must opt out because their only origin is the selected item.
final bool showSearchCenterMarker;
final bool visible;
@override
State<MapSurface> createState() => _MapSurfaceState();
}
class _MapSurfaceState extends State<MapSurface> {
final _controller = MapController();
DiscoveryItem? _selectedItem;
late final StreamController<void> _tileReset;
String _style = 'road';
bool _mapReady = false;
bool _tileError = false;
bool _cameraRebuildScheduled = false;
String _lastViewportSignature = '';
Timer? _clusterRefreshTimer;
@override
void initState() {
super.initState();
_tileReset = StreamController<void>.broadcast();
_selectedItem = widget.initialSelectedItem;
}
@override
void didUpdateWidget(covariant MapSurface oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.initialSelectedItem?.id != widget.initialSelectedItem?.id) {
_selectedItem = widget.initialSelectedItem;
}
final oldGeo = oldWidget.geoConfig ?? GeoConfig.fallback();
final newGeo = widget.geoConfig ?? GeoConfig.fallback();
final oldTileUrl = MapPolicy.tileUrlFor(
_style,
oldGeo.tileUrlTemplate,
oldGeo.tileStyles,
);
final newTileUrl = MapPolicy.tileUrlFor(
_style,
newGeo.tileUrlTemplate,
newGeo.tileStyles,
);
if (oldTileUrl != newTileUrl) {
_tileError = false;
_tileReset.add(null);
}
if (!oldWidget.visible && widget.visible && _mapReady) {
WidgetsBinding.instance.addPostFrameCallback(
(_) => _fitViewport(force: true),
);
}
if (_shouldRefitAfterUpdate(oldWidget, widget) && _mapReady) {
WidgetsBinding.instance.addPostFrameCallback(
(_) => _fitViewport(force: true),
);
}
}
@override
void dispose() {
_clusterRefreshTimer?.cancel();
_tileReset.close();
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (googleMapsEnabled) {
return GoogleMapSurface(
items: widget.items,
meta: widget.meta,
geoConfig: widget.geoConfig,
onItemTap: widget.onItemTap,
onRetry: widget.onRetry,
onOpenGoogleMaps: widget.onOpenGoogleMaps,
initialSelectedItem: widget.initialSelectedItem,
searchLatitude: widget.searchLatitude,
searchLongitude: widget.searchLongitude,
userLatitude: widget.userLatitude,
userLongitude: widget.userLongitude,
radiusKm: widget.radiusKm,
compact: widget.compact,
showSearchCenterMarker: widget.showSearchCenterMarker,
);
}
final l10n = AppLocalizations.of(context);
final scheme = Theme.of(context).colorScheme;
final geo = widget.geoConfig ?? GeoConfig.fallback();
final mapHeight = widget.compact ? 220.0 : 330.0;
final center = _priorityCenter(geo);
final tileUrl = MapPolicy.tileUrlFor(
_style,
geo.tileUrlTemplate,
geo.tileStyles,
);
final tileConfigured = geo.mapConfigured && tileUrl.isNotEmpty;
final mapError = _tileError;
final mapStatusText = _tileError
? l10n.mapTileLoadFailed
: l10n.mapGoogleFallback;
final mapBackground = tileConfigured
? scheme.surfaceContainerHighest
: scheme.surface;
return Semantics(
container: true,
label: l10n.mapView,
child: Container(
height: mapHeight,
margin: EdgeInsets.only(bottom: widget.compact ? 0 : 16),
decoration: BoxDecoration(
color: mapBackground,
borderRadius: BorderRadius.circular(widget.compact ? 14 : 20),
),
clipBehavior: Clip.antiAlias,
child: Stack(
children: [
Positioned.fill(
child: FlutterMap(
mapController: _controller,
options: MapOptions(
initialCenter: center,
initialZoom: _initialZoom(geo),
minZoom: MapPolicy.minZoom,
maxZoom: MapPolicy.maxZoom,
keepAlive: true,
backgroundColor: scheme.surfaceContainerHighest,
onMapReady: _handleMapReady,
// Do not rebuild the complete marker/cluster tree for every
// camera tick. flutter_map already reprojects geographic
// markers during the gesture; rebuilding here caused the
// map to fight the gesture and made swipes feel sticky.
// Clusters are refreshed after a movement ends instead.
onMapEvent: _handleMapEvent,
),
children: [
if (tileConfigured)
TileLayer(
key: ValueKey<String>('tiles:$tileUrl'),
urlTemplate: tileUrl,
// flutter_map expects an application/package identifier
// here, not the operator's e-mail address. The latter
// remains part of the centrally supplied provider
// contract but must not be sent as a package name.
userAgentPackageName: MapPolicy.tileUserAgentPackageName(
geo.userAgentContact,
),
errorTileCallback: _handleTileError,
reset: _tileReset.stream,
),
// Keep the marker tree available for late tile
// configuration and testable geographic state, but do not
// show floating pins on a basemap-less fallback surface.
IgnorePointer(
ignoring: !tileConfigured,
child: Opacity(
opacity: tileConfigured ? 1 : 0,
child: MarkerLayer(markers: _markers(context)),
),
),
if (geo.attribution.isNotEmpty && tileConfigured)
RichAttributionWidget(
attributions: [
TextSourceAttribution(
geo.attribution,
prependCopyright: false,
),
],
),
],
),
),
Positioned(
top: 12,
left: 12,
right: 12,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (mapError)
Expanded(
child: _MapStatus(
text: mapStatusText,
error: true,
retryLabel: l10n.retry,
onRetry: (widget.onRetry != null || _tileError)
? _retryMap
: null,
),
)
else
const Spacer(),
if (tileConfigured)
_MapControls(
controller: _controller,
styles: geo.tileStyles,
selectedStyle: _style,
onStyleChanged: (style) => setState(() => _style = style),
styleLabel: _styleLabel,
zoomInLabel: l10n.mapZoomIn,
zoomOutLabel: l10n.mapZoomOut,
),
],
),
),
if (!tileConfigured && !_tileError)
Positioned.fill(
child: Center(
child: _GoogleMapsFallbackCard(
text: l10n.mapGoogleFallback,
actionLabel: l10n.openGoogleMaps,
onOpen: () => _openGoogleMaps(geo),
compact: widget.compact,
),
),
),
if (widget.items.every((item) => !_isMappable(item)))
Positioned(
left: 16,
right: 16,
bottom: _selectedItem == null ? 20 : 84,
child: _MapMessage(
text: l10n.mapNoPublicLocations,
icon: Icons.place_outlined,
),
),
if (_selectedItem != null && !widget.compact)
Positioned(
left: 16,
right: 16,
bottom: 16,
child: _MapPreviewCard(
item: _selectedItem!,
onClose: () => setState(() => _selectedItem = null),
onOpen: () => widget.onItemTap(_selectedItem!),
),
),
],
),
),
);
}
List<Marker> _markers(BuildContext context) {
final mappable = widget.items.where(_isMappable).toList(growable: false);
final clusters = _clusters(mappable);
final markers = <Marker>[
for (final cluster in clusters)
Marker(
point: LatLng(cluster.latitude, cluster.longitude),
width: cluster.items.length > 1 ? 52 : 48,
height: cluster.items.length > 1 ? 52 : 58,
alignment: cluster.items.length > 1
? Alignment.center
: Alignment.bottomCenter,
child: cluster.items.length > 1
? _ClusterPin(
count: cluster.items.length,
onTap: () => _focusItems(cluster.items),
)
: _ItemPin(
item: cluster.items.single,
onTap: () => _selectItem(cluster.items.single),
),
),
];
final search = _searchCenter;
if (search != null && !_hasContentMarkerAt(search)) {
markers.add(
Marker(
point: search,
width: 44,
height: 44,
child: _SearchCenterPin(
label: AppLocalizations.of(context).mapSearchCenter,
),
),
);
}
if (_showUserLocation) {
markers.add(
Marker(
point: LatLng(widget.userLatitude!, widget.userLongitude!),
width: 42,
height: 42,
child: _UserPin(
label: AppLocalizations.of(context).mapCurrentLocation,
imageUrl: _userMarkerImageUrl,
),
),
);
}
return markers;
}
String get _userMarkerImageUrl {
final marker = widget.geoConfig?.userMarker;
if (marker == null) return '';
final appUrl = marker['app_url'];
if (appUrl is String && appUrl.trim().isNotEmpty) return appUrl.trim();
final imageUrl = marker['image_url'];
return imageUrl is String ? imageUrl.trim() : '';
}
bool _hasContentMarkerAt(LatLng point) {
for (final item in widget.items) {
if (!_isMappable(item)) continue;
final latitudeDelta = (item.geo.latitude! - point.latitude).abs();
final longitudeDelta = (item.geo.longitude! - point.longitude).abs();
// At MeinWeimar's scale this conservative degree check covers the
// small physical collision radius without moving or re-projecting any
// marker. The actual item marker remains at its API Lat/Lon.
final latitudeMeters = latitudeDelta * 111320;
final longitudeMeters =
longitudeDelta * 111320 * _longitudeScale(point.latitude);
final squaredDistanceMeters =
(latitudeMeters * latitudeMeters + longitudeMeters * longitudeMeters)
.toDouble();
final distance = math.sqrt(squaredDistanceMeters);
if (distance <= MapPolicy.searchCenterCollisionMeters) {
return true;
}
}
return false;
}
double _longitudeScale(double latitude) {
// Convert longitude degrees to metres at the marker's latitude while
// keeping the marker itself at the API-provided geographic coordinate.
return math.cos(latitude * math.pi / 180).abs().clamp(0.01, 1.0).toDouble();
}
List<_MapCluster> _clusters(List<DiscoveryItem> items) {
if (!_mapReady || items.length < 2) {
return [
for (final item in items)
_MapCluster(
items: [item],
latitude: item.geo.latitude!,
longitude: item.geo.longitude!,
),
];
}
// Cluster in the camera's current screen space, not in a latitude/
// longitude grid. A degree grid changes its cell boundaries continuously
// during zoom and makes markers jump between unrelated centroids. The
// actual Marker points below remain geographic Lat/Lon values.
final clusters = <_MapCluster>[];
final sorted = [...items]..sort((a, b) => a.id.compareTo(b.id));
for (final item in sorted) {
final lat = item.geo.latitude!;
final lng = item.geo.longitude!;
final screenPoint = _controller.camera.latLngToScreenOffset(
LatLng(lat, lng),
);
_MapCluster? existing;
for (final cluster in clusters) {
if ((screenPoint - cluster.screenPoint).distance <=
MapPolicy.clusterRadiusPixels) {
existing = cluster;
break;
}
}
if (existing == null) {
clusters.add(
_MapCluster(
items: [item],
latitude: lat,
longitude: lng,
screenPoint: screenPoint,
),
);
} else {
// Keep the first, deterministically sorted item as the geographic
// anchor. Replacing it with the arithmetic centroid on every rebuild
// makes the visible cluster marker drift while the camera is zoomed
// or panned. The anchor is still a real content Lat/Lon and therefore
// never depends on a screen coordinate.
existing.items.add(item);
}
}
return clusters;
}
void _scheduleCameraRebuild() {
if (!mounted) return;
if (_cameraRebuildScheduled) return;
_cameraRebuildScheduled = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
setState(() {
_cameraRebuildScheduled = false;
});
});
}
void _handleMapEvent(MapEvent event) {
if (event is MapEventMoveEnd ||
event is MapEventFlingAnimationEnd ||
event is MapEventDoubleTapZoomEnd ||
event is MapEventRotateEnd) {
_scheduleCameraRebuild();
return;
}
// Desktop/web wheel zoom does not have a separate end event. Debounce its
// cluster refresh so tiles and geographic markers remain fluid while the
// wheel is moving, then settle the cluster layout once the gesture stops.
if (event is MapEventScrollWheelZoom) {
_clusterRefreshTimer?.cancel();
_clusterRefreshTimer = Timer(
const Duration(milliseconds: 140),
_scheduleCameraRebuild,
);
}
}
void _handleTileError(TileImage tile, Object error, StackTrace? stackTrace) {
if (!mounted || _tileError) return;
setState(() => _tileError = true);
}
void _retryMap() {
if (mounted) setState(() => _tileError = false);
_tileReset.add(null);
widget.onRetry?.call();
}
Future<void> _openGoogleMaps(GeoConfig geo) async {
final target = _fallbackTarget(geo);
final opened = widget.onOpenGoogleMaps != null
? await widget.onOpenGoogleMaps!(target)
: await ExternalNavigationAdapter.system().openMap(
target,
zoom: geo.defaultMapZoom,
);
if (!opened && mounted) {
final l10n = AppLocalizations.of(context);
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(SnackBar(content: Text(l10n.linkOpenFailed)));
}
}
NavigationTarget _fallbackTarget(GeoConfig geo) {
final search = _searchCenter;
if (search != null) {
return NavigationTarget(
latitude: search.latitude,
longitude: search.longitude,
);
}
if (widget.userLatitude != null && widget.userLongitude != null) {
return NavigationTarget(
latitude: widget.userLatitude!,
longitude: widget.userLongitude!,
);
}
final detail = widget.initialSelectedItem;
if (detail != null && _isMappable(detail)) {
return NavigationTarget(
latitude: detail.geo.latitude!,
longitude: detail.geo.longitude!,
address: detail.address.formatted,
);
}
for (final item in widget.items) {
if (_isMappable(item)) {
return NavigationTarget(
latitude: item.geo.latitude!,
longitude: item.geo.longitude!,
address: item.address.formatted,
);
}
}
return NavigationTarget(
latitude: geo.defaultLatitude,
longitude: geo.defaultLongitude,
);
}
void _handleMapReady() {
_mapReady = true;
// Wait until Flutter has committed the map's real viewport dimensions.
// Calling fitCamera synchronously from onMapReady can run before the first
// layout on web; the map then paints only after a user pan/zoom causes a
// second camera calculation.
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_fitViewport(force: true);
});
}
LatLng? get _searchCenter {
// A compact map embedded in a detail page represents the selected object,
// not a discovery search. Even if a parent retains stale search state,
// never paint the search-loupe marker over the object's own marker.
if (!widget.showSearchCenterMarker) return null;
if (_isEmbeddedDetailMap) return null;
final lat = widget.searchLatitude;
final lng = widget.searchLongitude;
return lat != null && lng != null ? LatLng(lat, lng) : null;
}
bool get _isEmbeddedDetailMap {
final selected = widget.initialSelectedItem;
if (selected == null) return false;
if (widget.compact) return true;
// Keep the invariant robust even if a detail caller forgets the compact
// flag: a single map item that is the selected detail is not a discovery
// result set and must never receive a second search-origin pin.
return widget.items.length == 1 && widget.items.single.id == selected.id;
}
/// A manually selected address is the active discovery context. Do not
/// expose the user's GPS position as a second marker while that context is
/// active; the address must remain the only search origin shown on the map.
bool get _showUserLocation =>
_searchCenter == null &&
widget.userLatitude != null &&
widget.userLongitude != null;
LatLng _priorityCenter(GeoConfig geo) {
final search = _searchCenter;
if (search != null) return search;
final detail = widget.initialSelectedItem;
if (_isEmbeddedDetailMap && detail != null && _isMappable(detail)) {
// A detail map is an object context, not a discovery viewport. Keep
// the selected object under the camera even when a personal GPS point
// is available; the personal marker is still rendered separately.
return LatLng(detail.geo.latitude!, detail.geo.longitude!);
}
if (widget.userLatitude != null && widget.userLongitude != null) {
return LatLng(widget.userLatitude!, widget.userLongitude!);
}
if (detail != null && _isMappable(detail)) {
return LatLng(detail.geo.latitude!, detail.geo.longitude!);
}
return LatLng(geo.defaultLatitude, geo.defaultLongitude);
}
double _initialZoom(GeoConfig geo) => MapPolicy.zoomForRadius(
widget.radiusKm,
fallback: geo.defaultMapZoom.toDouble(),
);
List<LatLng> _viewportPoints(GeoConfig geo) {
final detail = widget.initialSelectedItem;
if (_isEmbeddedDetailMap && detail != null && _isMappable(detail)) {
// Do not fit a detail card between the object and the user's location.
// The object remains the camera target; the GPS pin is an additional
// context marker and does not alter the detail viewport.
return [LatLng(detail.geo.latitude!, detail.geo.longitude!)];
}
final points = <LatLng>[
for (final item in widget.items)
if (_isMappable(item)) LatLng(item.geo.latitude!, item.geo.longitude!),
];
final search = _searchCenter;
if (search != null) points.add(search);
if (_showUserLocation) {
points.add(LatLng(widget.userLatitude!, widget.userLongitude!));
}
if (points.isEmpty) points.add(_priorityCenter(geo));
return points;
}
void _fitViewport({bool force = false}) {
if (!_mapReady || !mounted) return;
final geo = widget.geoConfig ?? GeoConfig.fallback();
final points = _viewportPoints(geo);
final signature = _viewportContextSignature(widget);
if (!force && signature == _lastViewportSignature) return;
_lastViewportSignature = signature;
if (points.length == 1) {
_controller.move(
points.single,
MapPolicy.zoomForRadius(
widget.radiusKm,
fallback: geo.defaultMapZoom.toDouble(),
).clamp(MapPolicy.minZoom, MapPolicy.maxZoom),
);
return;
}
_controller.fitCamera(
CameraFit.coordinates(
coordinates: points,
padding: const EdgeInsets.all(MapPolicy.fitPadding),
minZoom: MapPolicy.minZoom,
maxZoom: MapPolicy.maxZoom,
),
);
}
void _focusItems(List<DiscoveryItem> items) {
final points = [
for (final item in items)
if (_isMappable(item)) LatLng(item.geo.latitude!, item.geo.longitude!),
];
if (points.isNotEmpty) {
_controller.fitCamera(
CameraFit.coordinates(
coordinates: points,
padding: const EdgeInsets.all(MapPolicy.fitPadding),
minZoom: MapPolicy.minZoom,
maxZoom: MapPolicy.maxZoom,
),
);
}
}
void _selectItem(DiscoveryItem item) => setState(() => _selectedItem = item);
bool _isMappable(DiscoveryItem item) =>
item.geo.available &&
item.geo.privacy != 'private' &&
item.geo.latitude != null &&
item.geo.longitude != null;
bool _shouldRefitAfterUpdate(MapSurface oldValue, MapSurface newValue) {
if (!_sameViewportContext(oldValue, newValue)) return true;
if (oldValue.initialSelectedItem?.id != newValue.initialSelectedItem?.id) {
return true;
}
// Loading another page must not undo a user's pan or zoom. A replacement
// result set (new search/filter) still reframes the map because the old
// mappable IDs are no longer all present. An empty-to-populated result
// set also needs one initial fit.
final oldIds = oldValue.items
.where(_isMappable)
.map((item) => item.id)
.toSet();
final newIds = newValue.items
.where(_isMappable)
.map((item) => item.id)
.toSet();
if (oldIds.isEmpty) return newIds.isNotEmpty;
return !newIds.containsAll(oldIds);
}
bool _sameViewportContext(MapSurface oldValue, MapSurface newValue) =>
_viewportContextSignature(oldValue) ==
_viewportContextSignature(newValue);
String _viewportContextSignature(MapSurface value) {
final detail = value.initialSelectedItem;
final embeddedDetail = _isEmbeddedDetailMapFor(value);
final searchActive =
value.showSearchCenterMarker &&
!embeddedDetail &&
value.searchLatitude != null &&
value.searchLongitude != null;
final userActive =
!searchActive &&
!embeddedDetail &&
value.userLatitude != null &&
value.userLongitude != null;
final selectedPoint =
detail != null &&
detail.geo.available &&
detail.geo.privacy != 'private' &&
detail.geo.latitude != null &&
detail.geo.longitude != null
? '${detail.id}:${detail.geo.latitude}:${detail.geo.longitude}'
: '';
final geo = value.geoConfig ?? GeoConfig.fallback();
return [
embeddedDetail,
searchActive ? value.searchLatitude : null,
searchActive ? value.searchLongitude : null,
userActive ? value.userLatitude : null,
userActive ? value.userLongitude : null,
value.radiusKm,
selectedPoint,
geo.defaultLatitude,
geo.defaultLongitude,
geo.defaultMapZoom,
].join('|');
}
bool _isEmbeddedDetailMapFor(MapSurface value) {
final selected = value.initialSelectedItem;
if (selected == null) return false;
if (value.compact) return true;
return value.items.length == 1 && value.items.single.id == selected.id;
}
String _styleLabel(BuildContext context, String style) {
final l10n = AppLocalizations.of(context);
return switch (style) {
'light' => l10n.mapStyleLight,
'dark' => l10n.mapStyleDark,
'satellite' => l10n.mapStyleSatellite,
_ => l10n.mapStyleRoad,
};
}
}
class _MapCluster {
_MapCluster({
required this.items,
required this.latitude,
required this.longitude,
this.screenPoint = Offset.zero,
});
final List<DiscoveryItem> items;
double latitude;
double longitude;
Offset screenPoint;
}
class _MapControls extends StatelessWidget {
const _MapControls({
required this.controller,
required this.styles,
required this.selectedStyle,
required this.onStyleChanged,
required this.styleLabel,
required this.zoomInLabel,
required this.zoomOutLabel,
});
final MapController controller;
final Map<String, String> styles;
final String selectedStyle;
final ValueChanged<String> onStyleChanged;
final String Function(BuildContext, String) styleLabel;
final String zoomInLabel;
final String zoomOutLabel;
@override
Widget build(BuildContext context) {
final options = <String>{
'road',
...styles.keys.where((key) => key != 'road'),
}.toList();
return Card(
margin: EdgeInsets.zero,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: () => controller.move(
controller.camera.center,
controller.camera.zoom + 1,
),
tooltip: zoomInLabel,
icon: const Icon(Icons.add),
visualDensity: VisualDensity.compact,
),
IconButton(
onPressed: () => controller.move(
controller.camera.center,
controller.camera.zoom - 1,
),
tooltip: zoomOutLabel,
icon: const Icon(Icons.remove),
visualDensity: VisualDensity.compact,
),
if (options.length > 1)
PopupMenuButton<String>(
tooltip: styleLabel(context, selectedStyle),
icon: const Icon(Icons.layers_outlined),
onSelected: onStyleChanged,
itemBuilder: (context) => [
for (final option in options)
PopupMenuItem(
value: option,
child: Text(styleLabel(context, option)),
),
],
),
],
),
);
}
}
class _MapStatus extends StatelessWidget {
const _MapStatus({
required this.text,
required this.error,
this.retryLabel,
this.onRetry,
});
final String text;
final bool error;
final String? retryLabel;
final VoidCallback? onRetry;
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
return Card(
margin: EdgeInsets.zero,
color: error
? scheme.errorContainer
: scheme.surface.withValues(alpha: .94),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.map_outlined,
size: 18,
color: error
? scheme.onErrorContainer
: AppTheme.palette(context).muted,
),
const SizedBox(width: 8),
Flexible(
child: Text(text, maxLines: 2, overflow: TextOverflow.ellipsis),
),
if (onRetry != null) ...[
const SizedBox(width: 4),
TextButton(
onPressed: onRetry,
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 8),
minimumSize: const Size(0, 36),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: Text(retryLabel ?? ''),
),
],
],
),
),
);
}
}
class _GoogleMapsFallbackCard extends StatelessWidget {
const _GoogleMapsFallbackCard({
required this.text,
required this.actionLabel,
required this.onOpen,
required this.compact,
});
final String text;
final String actionLabel;
final VoidCallback onOpen;
final bool compact;
@override
Widget build(BuildContext context) {
final palette = AppTheme.palette(context);
final iconSize = compact ? 26.0 : 34.0;
final spacing = compact ? 6.0 : 10.0;
return Card(
margin: EdgeInsets.all(compact ? 12 : 20),
child: Padding(
padding: EdgeInsets.fromLTRB(
16,
compact ? 12 : 18,
16,
compact ? 10 : 16,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.map_outlined, size: iconSize, color: palette.brand),
SizedBox(height: spacing),
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 280),
child: Text(text, textAlign: TextAlign.center),
),
SizedBox(height: compact ? 8 : 12),
FilledButton.icon(
onPressed: onOpen,
style: compact
? FilledButton.styleFrom(
minimumSize: const Size(0, 36),
padding: const EdgeInsets.symmetric(horizontal: 12),
)
: null,
icon: const Icon(Icons.open_in_new),
label: Text(actionLabel),
),
],
),
),
);
}
}
class _MapMessage extends StatelessWidget {
const _MapMessage({required this.text, required this.icon});
final String text;
final IconData icon;
@override
Widget build(BuildContext context) => Card(
margin: EdgeInsets.zero,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 18),
const SizedBox(width: 8),
Flexible(child: Text(text)),
],
),
),
);
}
class _ItemPin extends StatelessWidget {
const _ItemPin({required this.item, required this.onTap});
final DiscoveryItem item;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final marker = item.marker;
final child = PublicMediaImage(
url: marker.imageUrl,
alt: marker.alt,
width: 34,
height: 42,
fit: BoxFit.contain,
emptyIcon: Icons.location_on,
errorIcon: Icons.location_on,
iconSize: 36,
iconColor: AppTheme.palette(context).brand,
backgroundColor: Colors.transparent,
borderColor: Colors.transparent,
includeSemantics: false,
);
return Semantics(
button: true,
label: AppLocalizations.of(context).markerLabel(item.title),
child: InkResponse(onTap: onTap, child: child),
);
}
}
class _ClusterPin extends StatelessWidget {
const _ClusterPin({required this.count, required this.onTap});
final int count;
final VoidCallback onTap;
@override
Widget build(BuildContext context) => Semantics(
button: true,
label: AppLocalizations.of(context).mapClusterLabel(count),
child: InkResponse(
onTap: onTap,
child: Container(
width: 46,
height: 46,
alignment: Alignment.center,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.secondary,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 3),
),
child: Text(
'$count',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w700,
),
),
),
),
);
}
class _SearchCenterPin extends StatelessWidget {
const _SearchCenterPin({required this.label});
final String label;
@override
Widget build(BuildContext context) => Semantics(
label: label,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
border: Border.all(
color: Theme.of(context).colorScheme.secondary,
width: 3,
),
),
child: Icon(
Icons.search,
size: 20,
color: Theme.of(context).colorScheme.secondary,
),
),
);
}
class _UserPin extends StatelessWidget {
const _UserPin({required this.label, this.imageUrl = ''});
final String label;
final String imageUrl;
@override
Widget build(BuildContext context) => Semantics(
label: label,
child: SizedBox(
width: 38,
height: 44,
child: Center(
child: imageUrl.isNotEmpty
? PublicMediaImage(
url: imageUrl,
alt: label,
width: 38,
height: 44,
fit: BoxFit.contain,
includeSemantics: false,
fallbackBuilder: (_) => _bundledImage(context, label),
)
: Image.asset(
_bundledUserMarkerAsset,
width: 38,
height: 44,
fit: BoxFit.contain,
errorBuilder: (context, _, _) => _defaultDot(context),
),
),
),
);
Widget _defaultDot(BuildContext context) => DecoratedBox(
decoration: BoxDecoration(
color: AppTheme.palette(context).brand,
shape: BoxShape.circle,
),
child: const SizedBox(width: 10, height: 10),
);
Widget _bundledImage(BuildContext context, String label) => Image.asset(
_bundledUserMarkerAsset,
width: 38,
height: 44,
fit: BoxFit.contain,
semanticLabel: label,
errorBuilder: (context, _, _) => _defaultDot(context),
);
}
class _MapPreviewCard extends StatelessWidget {
const _MapPreviewCard({
required this.item,
required this.onClose,
required this.onOpen,
});
final DiscoveryItem item;
final VoidCallback onClose;
final VoidCallback onOpen;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return Card(
margin: EdgeInsets.zero,
elevation: 4,
child: Padding(
padding: const EdgeInsets.fromLTRB(14, 10, 8, 10),
child: Row(
children: [
Icon(Icons.place_outlined, color: AppTheme.palette(context).brand),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w700),
),
if (item.address.formatted.isNotEmpty)
Text(
item.address.formatted,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: AppTheme.palette(context).muted,
fontSize: 12,
),
),
],
),
),
TextButton(onPressed: onOpen, child: Text(l10n.openDetails)),
IconButton(
onPressed: onClose,
tooltip: l10n.close,
icon: const Icon(Icons.close),
),
],
),
),
);
}
}

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%