Files
vikingowl 284299b946 feat: implement full Flutter MVP app
Complete Flutter app (Android + iOS) mirroring the web frontend:

- Core: Riverpod state, Dio networking with auth interceptor + auto-refresh,
  go_router navigation, flutter_secure_storage, light/dark theme with
  MedievalSharp/Crimson Pro fonts, German l10n
- Market: search with text/GPS/radius/date/sort filters, list + map views
  (flutter_map + OSM), detail screen with opening hours, admission prices,
  single-marker map, pagination
- Auth: login (password + magic link tabs), register, OAuth button placeholders,
  2FA code prompt on 401, sealed auth state provider
- User: profile view/edit/delete with confirm dialog, 2FA setup/disable
  on security screen
- GPS: geolocator with IP-based fallback (geojs.io) matching web behavior
- Platform: Android internet + location permissions, iOS NSLocation description
- Tests: date/currency/distance formatter unit tests (13 passing)
- Zero analysis issues, debug APK builds successfully
2026-02-21 07:10:30 +01:00

71 lines
1.9 KiB
Dart

import 'package:flutter/material.dart';
enum AppButtonVariant { primary, outline, text, danger }
class AppButton extends StatelessWidget {
final String label;
final VoidCallback? onPressed;
final AppButtonVariant variant;
final bool isLoading;
final IconData? icon;
final bool fullWidth;
const AppButton({
super.key,
required this.label,
this.onPressed,
this.variant = AppButtonVariant.primary,
this.isLoading = false,
this.icon,
this.fullWidth = false,
});
@override
Widget build(BuildContext context) {
final child = isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
)
: icon != null
? Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 18),
const SizedBox(width: 8),
Text(label),
],
)
: Text(label);
final button = switch (variant) {
AppButtonVariant.primary => ElevatedButton(
onPressed: isLoading ? null : onPressed,
child: child,
),
AppButtonVariant.outline => OutlinedButton(
onPressed: isLoading ? null : onPressed,
child: child,
),
AppButtonVariant.text => TextButton(
onPressed: isLoading ? null : onPressed,
child: child,
),
AppButtonVariant.danger => ElevatedButton(
onPressed: isLoading ? null : onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.error,
foregroundColor: Theme.of(context).colorScheme.onError,
),
child: child,
),
};
if (fullWidth) {
return SizedBox(width: double.infinity, child: button);
}
return button;
}
}