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/google_map_surface.dartDart
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' as services;
import 'package:google_maps_flutter/google_maps_flutter.dart' as google;
import 'package:http/http.dart' as http;
import 'package:latlong2/latlong.dart' as latlong;
import '../../core/config/app_config.dart';
import '../../l10n/app_localizations.dart';
import '../../models/content_models.dart';
import '../navigation/external_navigation_adapter.dart';
import 'google_maps_runtime.dart';
import 'map_policy.dart';
const _bundledUserMarkerAsset = 'assets/ico/user_location_marker.png';
/// The product map implementation for builds with Google Maps Platform
/// enabled. WordPress still supplies the geographic content and viewport
/// policy; the Google SDK owns projection, tiles, camera and gestures.
class GoogleMapSurface extends StatefulWidget {
const GoogleMapSurface({
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,
});
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;
final bool showSearchCenterMarker;
@override
State<GoogleMapSurface> createState() => _GoogleMapSurfaceState();
}
class _GoogleMapSurfaceState extends State<GoogleMapSurface> {
google.GoogleMapController? _controller;
late Future<bool> _mapsReady;
bool _hasFittedViewport = false;
bool _cameraMoveRequested = false;
final _markerIconLoads = <String, Future<google.BitmapDescriptor?>>{};
final _markerIcons = <String, google.BitmapDescriptor>{};
Future<google.BitmapDescriptor?>? _bundledUserMarkerLoad;
google.BitmapDescriptor? _userMarkerIcon;
late Future<void> _markerIconsReady;
@override
void initState() {
super.initState();
_mapsReady = ensureGoogleMapsWebReady();
_markerIconsReady = _prepareMarkerIcons(widget.items, widget.geoConfig);
}
@override
void didUpdateWidget(covariant GoogleMapSurface oldWidget) {
super.didUpdateWidget(oldWidget);
if (_markerSignature(oldWidget.items, oldWidget.geoConfig) !=
_markerSignature(widget.items, widget.geoConfig)) {
_markerIconsReady = _prepareMarkerIcons(widget.items, widget.geoConfig);
}
if (_viewportSignature(oldWidget) != _viewportSignature(widget)) {
_hasFittedViewport = false;
_scheduleFit();
}
}
@override
Widget build(BuildContext context) {
final height = widget.compact ? 220.0 : 330.0;
return Container(
height: height,
margin: EdgeInsets.only(bottom: widget.compact ? 0 : 16),
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(widget.compact ? 14 : 20),
),
child: FutureBuilder<bool>(
future: _mapsReady,
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.data != true) {
return _GoogleMapSetupState(
compact: widget.compact,
onOpen: () => _openExternalMap(context),
);
}
return FutureBuilder<void>(
future: _markerIconsReady,
builder: (context, markerSnapshot) {
if (markerSnapshot.connectionState != ConnectionState.done) {
return const Center(child: CircularProgressIndicator());
}
return _buildMap(context);
},
);
},
),
);
}
Widget _buildMap(BuildContext context) {
final geo = widget.geoConfig ?? GeoConfig.fallback();
final center = _priorityCenter(geo);
final l10n = AppLocalizations.of(context);
final clusterManager = const google.ClusterManager(
clusterManagerId: google.ClusterManagerId('meinweimar-content'),
);
final useNativeClustering = !kIsWeb;
return Stack(
children: [
Positioned.fill(
child: google.GoogleMap(
initialCameraPosition: google.CameraPosition(
target: google.LatLng(center.latitude, center.longitude),
zoom: _initialZoom(geo),
),
mapType: google.MapType.normal,
compassEnabled: !kIsWeb,
mapToolbarEnabled: false,
mapTypeControlEnabled: false,
fullscreenControlEnabled: false,
streetViewControlEnabled: false,
myLocationEnabled: false,
myLocationButtonEnabled: false,
zoomControlsEnabled: !kIsWeb,
webGestureHandling: kIsWeb
? google.WebGestureHandling.greedy
: null,
minMaxZoomPreference: const google.MinMaxZoomPreference(
MapPolicy.minZoom,
MapPolicy.maxZoom,
),
clusterManagers: useNativeClustering ? {clusterManager} : const {},
markers: _markers(
l10n,
useNativeClustering ? clusterManager.clusterManagerId : null,
),
onMapCreated: _onMapCreated,
onCameraIdle: _scheduleFit,
),
),
if (widget.items.every((item) => !_isMappable(item)))
Positioned(
left: 16,
right: 16,
bottom: 16,
child: _MapMessage(
text: l10n.mapNoPublicLocations,
icon: Icons.place_outlined,
),
),
if (widget.initialSelectedItem != null && !widget.compact)
Positioned(
left: 16,
right: 16,
bottom: 16,
child: _MapPreviewCard(
item: widget.initialSelectedItem!,
onOpen: () => widget.onItemTap(widget.initialSelectedItem!),
),
),
],
);
}
Set<google.Marker> _markers(
AppLocalizations l10n,
google.ClusterManagerId? clusterManagerId,
) {
final markers = <google.Marker>{};
for (final item in widget.items.where(_isMappable)) {
final id = 'content-${item.resource}-${item.id}';
final markerImageUrl = item.marker.imageUrl.trim();
markers.add(
google.Marker(
markerId: google.MarkerId(id),
position: google.LatLng(item.geo.latitude!, item.geo.longitude!),
clusterManagerId: clusterManagerId,
infoWindow: google.InfoWindow(title: item.title),
onTap: () => widget.onItemTap(item),
icon:
_markerIcons[markerImageUrl] ??
google.BitmapDescriptor.defaultMarkerWithHue(
google.BitmapDescriptor.hueRose,
),
),
);
}
final search = _searchCenter;
if (widget.showSearchCenterMarker &&
search != null &&
!_hasContentMarkerAt(search)) {
markers.add(
google.Marker(
markerId: const google.MarkerId('search-center'),
position: google.LatLng(search.latitude, search.longitude),
infoWindow: google.InfoWindow(title: l10n.mapSearchCenter),
icon: google.BitmapDescriptor.defaultMarkerWithHue(
google.BitmapDescriptor.hueAzure,
),
),
);
}
if (_showUserLocation) {
markers.add(
google.Marker(
markerId: const google.MarkerId('user-location'),
position: google.LatLng(widget.userLatitude!, widget.userLongitude!),
infoWindow: google.InfoWindow(title: l10n.mapCurrentLocation),
icon:
_userMarkerIcon ??
google.BitmapDescriptor.defaultMarkerWithHue(
google.BitmapDescriptor.hueViolet,
),
),
);
}
return markers;
}
Future<void> _prepareMarkerIcons(
Iterable<DiscoveryItem> items,
GeoConfig? geoConfig,
) async {
final urls = items
.where(_isMappable)
.map((item) => item.marker.imageUrl.trim())
.where((url) => url.isNotEmpty)
.toSet();
await Future.wait(urls.map(_loadMarkerIcon));
final userUrl = _userMarkerImageUrl(geoConfig);
_userMarkerIcon = userUrl.isNotEmpty
? await _loadMarkerIcon(userUrl) ?? await _loadBundledUserMarker()
: await _loadBundledUserMarker();
}
Future<google.BitmapDescriptor?> _loadMarkerIcon(String url) {
final cached = _markerIcons[url];
if (cached != null) return Future.value(cached);
final existing = _markerIconLoads[url];
if (existing != null) return existing;
final load = _fetchMarkerIcon(url).then((icon) {
if (icon != null) _markerIcons[url] = icon;
return icon;
});
_markerIconLoads[url] = load;
return load;
}
Future<google.BitmapDescriptor?> _fetchMarkerIcon(String url) async {
// WordPress exposes approved marker media as image bytes through the
// public media route. BitmapDescriptor.bytes is supported by Android,
// iOS and the Google Maps JavaScript implementation. SVG media is left
// on the deterministic hue fallback because the native SDKs require PNG
// bytes for bitmap markers.
if (url.toLowerCase().split('?').first.endsWith('.svg')) return null;
try {
final response = await http
.get(Uri.parse(url))
.timeout(const Duration(seconds: 4));
if (response.statusCode != 200 || response.bodyBytes.isEmpty) {
return null;
}
return google.BitmapDescriptor.bytes(
response.bodyBytes,
// Set only the height. google_maps_flutter preserves the source
// aspect ratio when exactly one dimension is supplied; passing both
// dimensions would behave like BoxFit.fill and stretch markers such
// as the 224x340 MeinWeimar pin horizontally.
height: 52,
);
} on Object {
return null;
}
}
Future<google.BitmapDescriptor?> _loadBundledUserMarker() {
final existing = _bundledUserMarkerLoad;
if (existing != null) return existing;
final load = _readBundledUserMarker();
_bundledUserMarkerLoad = load;
return load;
}
Future<google.BitmapDescriptor?> _readBundledUserMarker() async {
try {
final data = await services.rootBundle.load(_bundledUserMarkerAsset);
return google.BitmapDescriptor.bytes(
data.buffer.asUint8List(),
height: 52,
);
} on Object {
return null;
}
}
String _userMarkerImageUrl(GeoConfig? geoConfig) {
final marker = 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() : '';
}
String _markerSignature(Iterable<DiscoveryItem> items, GeoConfig? geoConfig) {
final urls =
items
.where(_isMappable)
.map((item) => item.marker.imageUrl.trim())
.where((url) => url.isNotEmpty)
.toSet()
.toList()
..sort();
return '${_userMarkerImageUrl(geoConfig)}\u0000${urls.join('\u0000')}';
}
void _onMapCreated(google.GoogleMapController controller) {
_controller = controller;
_scheduleFit();
}
void _scheduleFit() {
if (_cameraMoveRequested || _controller == null || _hasFittedViewport) {
return;
}
_cameraMoveRequested = true;
WidgetsBinding.instance.addPostFrameCallback((_) async {
_cameraMoveRequested = false;
if (!mounted || _controller == null || _hasFittedViewport) return;
await _fitViewport();
});
}
Future<void> _fitViewport() async {
final controller = _controller;
if (controller == null) return;
final points = <google.LatLng>[];
final center = _searchCenter;
if (center != null) {
points.add(google.LatLng(center.latitude, center.longitude));
}
for (final item in widget.items.where(_isMappable)) {
points.add(google.LatLng(item.geo.latitude!, item.geo.longitude!));
}
if (points.length > 1 && widget.initialSelectedItem == null) {
final bounds = _boundsFor(points);
try {
await controller.animateCamera(
google.CameraUpdate.newLatLngBounds(bounds, widget.compact ? 28 : 56),
);
_hasFittedViewport = true;
return;
} on Object {
// The native/web map may not have committed its dimensions yet. The
// next idle callback will retry without changing marker coordinates.
return;
}
}
final target = widget.initialSelectedItem != null
? google.LatLng(
widget.initialSelectedItem!.geo.latitude!,
widget.initialSelectedItem!.geo.longitude!,
)
: points.length == 1
? points.single
: google.LatLng(
_priorityCenter(widget.geoConfig ?? GeoConfig.fallback()).latitude,
_priorityCenter(widget.geoConfig ?? GeoConfig.fallback()).longitude,
);
await controller.animateCamera(
google.CameraUpdate.newCameraPosition(
google.CameraPosition(
target: target,
zoom: widget.initialSelectedItem != null
? MapPolicy.detailZoom
: points.length == 1
? MapPolicy.detailZoom
: _initialZoom(widget.geoConfig ?? GeoConfig.fallback()),
),
),
);
_hasFittedViewport = true;
}
google.LatLngBounds _boundsFor(List<google.LatLng> points) {
var minLat = points.first.latitude;
var maxLat = points.first.latitude;
var minLng = points.first.longitude;
var maxLng = points.first.longitude;
for (final point in points.skip(1)) {
minLat = math.min(minLat, point.latitude);
maxLat = math.max(maxLat, point.latitude);
minLng = math.min(minLng, point.longitude);
maxLng = math.max(maxLng, point.longitude);
}
return google.LatLngBounds(
southwest: google.LatLng(minLat, minLng),
northeast: google.LatLng(maxLat, maxLng),
);
}
Future<void> _openExternalMap(BuildContext context) async {
final geo = widget.geoConfig ?? GeoConfig.fallback();
final center = _priorityCenter(geo);
final target = NavigationTarget(
latitude: center.latitude,
longitude: center.longitude,
);
final opened = widget.onOpenGoogleMaps != null
? await widget.onOpenGoogleMaps!(target)
: await ExternalNavigationAdapter.system().openMap(
target,
zoom: geo.defaultMapZoom,
);
if (!opened && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(AppLocalizations.of(context).linkOpenFailed)),
);
}
}
latlong.LatLng _priorityCenter(GeoConfig geo) {
if (widget.searchLatitude != null && widget.searchLongitude != null) {
return latlong.LatLng(widget.searchLatitude!, widget.searchLongitude!);
}
if (widget.userLatitude != null && widget.userLongitude != null) {
return latlong.LatLng(widget.userLatitude!, widget.userLongitude!);
}
final detail = widget.initialSelectedItem;
if (detail != null && _isMappable(detail)) {
return latlong.LatLng(detail.geo.latitude!, detail.geo.longitude!);
}
return latlong.LatLng(geo.defaultLatitude, geo.defaultLongitude);
}
latlong.LatLng? get _searchCenter {
if (widget.searchLatitude == null || widget.searchLongitude == null) {
return null;
}
return latlong.LatLng(widget.searchLatitude!, widget.searchLongitude!);
}
bool get _showUserLocation =>
_searchCenter == null &&
widget.userLatitude != null &&
widget.userLongitude != null;
bool _isMappable(DiscoveryItem item) =>
item.geo.available &&
item.geo.latitude != null &&
item.geo.longitude != null;
bool _hasContentMarkerAt(latlong.LatLng point) {
for (final item in widget.items.where(_isMappable)) {
final latDelta = (item.geo.latitude! - point.latitude).abs() * 111320;
final lngDelta =
(item.geo.longitude! - point.longitude).abs() *
111320 *
math.cos(point.latitude * math.pi / 180).abs().clamp(0.01, 1.0);
if (math.sqrt(latDelta * latDelta + lngDelta * lngDelta) <=
MapPolicy.searchCenterCollisionMeters) {
return true;
}
}
return false;
}
double _initialZoom(GeoConfig geo) {
if (widget.initialSelectedItem != null) return MapPolicy.detailZoom;
return MapPolicy.zoomForRadius(
widget.radiusKm,
fallback: geo.defaultMapZoom.toDouble(),
);
}
String _viewportSignature(GoogleMapSurface value) => [
value.initialSelectedItem?.id,
value.searchLatitude,
value.searchLongitude,
value.userLatitude,
value.userLongitude,
value.radiusKm,
value.items
.map((item) => '${item.id}:${item.geo.latitude}:${item.geo.longitude}')
.join(','),
].join('|');
}
class _GoogleMapSetupState extends StatelessWidget {
const _GoogleMapSetupState({required this.compact, required this.onOpen});
final bool compact;
final VoidCallback onOpen;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final scheme = Theme.of(context).colorScheme;
return Center(
child: Padding(
padding: EdgeInsets.all(compact ? 12 : 20),
child: DecoratedBox(
decoration: BoxDecoration(
color: scheme.surface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: scheme.outlineVariant),
),
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: compact ? 12 : 18,
vertical: compact ? 10 : 14,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.map_outlined, color: scheme.primary),
const SizedBox(width: 10),
Flexible(
child: Text(
l10n.mapGoogleFallback,
textAlign: TextAlign.center,
),
),
const SizedBox(width: 10),
TextButton(onPressed: onOpen, child: Text(l10n.openGoogleMaps)),
],
),
),
),
),
);
}
}
class _MapMessage extends StatelessWidget {
const _MapMessage({required this.text, required this.icon});
final String text;
final IconData icon;
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
return Material(
color: scheme.surface.withValues(alpha: .94),
borderRadius: BorderRadius.circular(14),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, color: scheme.primary),
const SizedBox(width: 8),
Flexible(child: Text(text)),
],
),
),
);
}
}
class _MapPreviewCard extends StatelessWidget {
const _MapPreviewCard({required this.item, required this.onOpen});
final DiscoveryItem item;
final VoidCallback onOpen;
@override
Widget build(BuildContext context) {
return Material(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(14),
child: InkWell(
onTap: onOpen,
borderRadius: BorderRadius.circular(14),
child: Padding(
padding: const EdgeInsets.all(10),
child: Row(
children: [
Icon(
Icons.place_outlined,
size: 34,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 10),
Expanded(
child: Text(
item.title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleSmall,
),
),
const Icon(Icons.chevron_right),
],
),
),
),
);
}
}

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%