import 'dart:async'; import 'package:dart_mq/dart_mq.dart'; import 'package:synchronized/synchronized.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:geolocator/geolocator.dart'; import 'package:path_provider/path_provider.dart'; import 'package:shared_map/shared_map.dart'; import 'package:stadtbote/messages/pop_content_view_message.dart'; import 'package:stadtbote/model/log_entry.dart'; import 'package:stadtbote/popups/accept_jobs_popup.dart'; import 'package:stadtbote/tools/colored_safe_area.dart'; import 'package:stadtbote/tools/connection_status.dart'; import 'package:stadtbote/tools/messages/job_deleted_message.dart'; import 'package:stadtbote/tools/messages/job_received_message.dart'; import 'package:stadtbote/tools/messages/refresh_badges_message.dart'; import 'package:stadtbote/tools/messages/remove_job_message.dart'; import 'package:stadtbote/tools/messages/remove_state_0_job_from_map.dart'; import 'package:stadtbote/tools/mqtt_core.dart'; import 'content_views/main_page.dart'; import 'content_views/utils/content_view_widget.dart'; import 'content_views/utils/widget_stack.dart'; import 'model/job.dart'; import 'objectbox.g.dart'; import 'tools/constants.dart'; import 'tools/util.dart'; Future main() async { runZonedGuarded(() async { WidgetsFlutterBinding.ensureInitialized(); await Util.init(); MQClient.initialize(); MQClient.instance.declareQueue(Constants.messageTopicRemoveJob); MQClient.instance.declareQueue(Constants.messageTopicPublishPayload); MQClient.instance.declareQueue(Constants.messageTopicDisconnectClient); MQClient.instance.declareQueue(Constants.messageTopicConnectClient); MQClient.instance.declareQueue(Constants.messageTopicRequestConnectionStatus); MQClient.instance.declareQueue(Constants.messageTopicConnectionStatusChanged); MQClient.instance.declareQueue(Constants.messageTopicJobDeleted); MQClient.instance.declareQueue(Constants.messageTopicRankingUpdated); MQClient.instance.declareQueue(Constants.messageTopicSessionInfoUpdate); MQClient.instance.declareQueue(Constants.messageTopicSessionStatusInfoUpdate); MQClient.instance.declareQueue(Constants.messageTopicLocationStatusUpdated); MQClient.instance.declareQueue(Constants.messageTopicPushContentView); MQClient.instance.declareQueue(Constants.messageTopicPopContentView); MQClient.instance.declareQueue(Constants.messageTopicReplaceContentView); MQClient.instance.declareQueue(Constants.messageTopicJobReceived); MQClient.instance.declareQueue(Constants.messageTopicMessageReceived); MQClient.instance.declareQueue(Constants.messageTopicUpdateBadges); MQClient.instance.declareQueue(Constants.messageTopicRemoveState0JobFromMap); MQClient.instance.declareQueue(Constants.messageTopicBWVEnabled); final dir = await getApplicationDocumentsDirectory(); MyApp.objectBoxStore = await openStore(directory: dir.path); await _clearLog(); runApp(const MyApp()); }, (error, stackTrace) { _traceUncaughtExceptions(error, stackTrace); }); } Future _clearLog() async { var dateTime = DateTime.now().add(const Duration(days: -7)); final box = MyApp.objectBoxStore.box(); final query = box.query(LogEntry_.time.lessThan(dateTime.millisecondsSinceEpoch)).build(); final oldEntries = query.find(); query.close(); box.removeMany(oldEntries.map((e) => e.id).toList()); } void _traceUncaughtExceptions(Object error, StackTrace stackTrace) { Util.logException('Ein Fehler ist aufgetreten: $error'); Util.logException('Stack-Trace: $stackTrace'); } class MyApp extends StatefulWidget { const MyApp({super.key}); static final appNavigatorKey = GlobalKey(); static late Store objectBoxStore; static Position? _currentPosition; static final Lock _positionLock = Lock(); static bool suppressAcceptDialog = false; /// Thread-safe getter for current position static Future getCurrentPosition() async { return await _positionLock.synchronized(() { return _currentPosition; }); } /// Thread-safe setter for current position static Future setCurrentPosition(Position? position) async { await _positionLock.synchronized(() { _currentPosition = position; }); } @override State createState() => _MyAppState(); } class _MyAppState extends State with ConsumerMixin { final MqttCore mqttCore = MqttCore(); @override void initState() { super.initState(); _startLocationService(); } @override Widget build(BuildContext context) { return MaterialApp( navigatorKey: MyApp.appNavigatorKey, title: 'Stadtbote', theme: ThemeData( primarySwatch: Colors.blue, ), home: const Main(), ); } _startLocationService() async { var result = await _handleLocationPermissions(); if (result) { LocationSettings locationSettings = const LocationSettings( accuracy: LocationAccuracy.high, distanceFilter: 0, ); Geolocator.getPositionStream(locationSettings: locationSettings).listen((Position? position) async { await MyApp.setCurrentPosition(position); }); } } Future _handleLocationPermissions() async { // Test if location services are enabled. var serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) { // Location services are not enabled don't continue // accessing the position and request users of the // App to enable the location services. return false; //return Future.error('Location services are disabled.'); } var permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); if (permission == LocationPermission.denied) { // Permissions are denied, next time you could try // requesting permissions again (this is also where // Android's shouldShowRequestPermissionRationale // returned true. According to Android guidelines // your App should show an explanatory UI now. //return Future.error('Location permissions are denied'); return false; } } if (permission == LocationPermission.deniedForever) { //return Future.error('Location permissions are permanently denied, we cannot request permissions.'); return false; } return true; } } class Main extends StatefulWidget { const Main({super.key}); @override State
createState() => _MainState(); } class _MainState extends State
with SingleTickerProviderStateMixin, ProducerMixin, ConsumerMixin { DateTime? _lastBackPressed; late ContentViewWidget _currentContent; late WidgetStack contentStack = WidgetStack(); late Widget _currentTitleWidget; bool _initCompleted = false; final ValueNotifier _connectionLostValueNotifier = ValueNotifier(true); late AnimationController? _animationController; late Animation _animation; Timer? _checkForNewState0JobsJobsTimer; bool _dialogOpened = false; late SharedMap? _jobMap; @override void dispose() { _animationController?.dispose(); // Unsubscribe from all message topics unsubscribe(queueId: Constants.messageTopicPushContentView); unsubscribe(queueId: Constants.messageTopicPopContentView); unsubscribe(queueId: Constants.messageTopicReplaceContentView); unsubscribe(queueId: Constants.messageTopicConnectionStatusChanged); unsubscribe(queueId: Constants.messageTopicRemoveJob); unsubscribe(queueId: Constants.messageTopicJobReceived); unsubscribe(queueId: Constants.messageTopicJobDeleted); unsubscribe(queueId: Constants.messageTopicRemoveState0JobFromMap); super.dispose(); } @override void initState() { super.initState(); _animationController = AnimationController(vsync: this, duration: const Duration(milliseconds: 1000)); _animationController!.repeat(reverse: true); _animation = Tween(begin: 0.5, end: 1.0).animate(_animationController!); WidgetsBinding.instance.addPostFrameCallback((_) async { var store1 = SharedStore('jobs'); _jobMap = await store1.getSharedMap('m1'); _initMessages(); _startCheckForNewState0JobsJobsTimer(); _currentContent = const MainPage(); _currentTitleWidget = await _currentContent.getHeaderWidget(MyApp.appNavigatorKey.currentContext!); _initCompleted = true; if (mounted) { setState(() {}); } }); } @override Widget build(BuildContext context) { if (!_initCompleted) { return Util.getWaitWidget(context); } return ColoredSafeArea( key: GlobalKey(), child: PopScope( canPop: false, onPopInvokedWithResult: (didPop, result) async { if (didPop) return; if (contentStack.isNotEmpty) { _popContentView(1); // alten Inhalt entfernen } else { final now = DateTime.now(); if (_lastBackPressed == null || now.difference(_lastBackPressed!) > const Duration(seconds: 2)) { _lastBackPressed = now; if (mounted) { ScaffoldMessenger.of(context).hideCurrentSnackBar(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Zum Beenden erneut Zurück drücken'), duration: Duration(seconds: 2)), ); } } else { // Zweiter Zurück-Klick innerhalb des Zeitfensters: App beenden // ignore: use_build_context_synchronously SystemNavigator.pop(); } } }, child: Scaffold( appBar: PreferredSize( preferredSize: const Size.fromHeight(100), child: ColoredSafeArea( key: const Key("appBar"), child: SizedBox( height: 100, child: Padding( padding: const EdgeInsets.all(0), child: Stack( alignment: Alignment.center, children: [ Center(heightFactor: 1, child: _currentTitleWidget), ], ), ), )), ), body: Column( children: [ ValueListenableBuilder( valueListenable: _connectionLostValueNotifier, builder: (context, value, child) { return Visibility( visible: value, child: Column( children: [ const SizedBox(height: 1), Container( width: MediaQuery.of(context).size.width, height: 40, color: Constants.colorPrimary, child: Center( child: FadeTransition( opacity: _animation, child: const Text( "*** OFFLINE ***", style: TextStyle( color: Colors.white, fontSize: 25.0, ), ), )), ), ], ), ); }, ), Expanded(child: _currentContent), ], ), ), ), ); } void _initMessages() { subscribe( queueId: Constants.messageTopicPushContentView, callback: (message) { var widget = message.payload as ContentViewWidget; _pushContentView(widget); }, ); subscribe( queueId: Constants.messageTopicPopContentView, callback: (message) { var popContentViewMessage = message.payload as PopContentViewMessage; _popContentView(popContentViewMessage.count); }, ); subscribe( queueId: Constants.messageTopicReplaceContentView, callback: (message) { var widget = message.payload as ContentViewWidget; _replaceContentView(widget); }, ); subscribe( queueId: Constants.messageTopicConnectionStatusChanged, callback: (message) { var connectionStatus = message.payload as ConnectionStatus; _connectionStatusChanged(connectionStatus); }, ); subscribe( queueId: Constants.messageTopicRemoveJob, callback: (message) async { var removeJobMessage = message.payload as RemoveJobMessage; final box = MyApp.objectBoxStore.box(); var job = box.get(removeJobMessage.id); if (job != null) { box.remove(removeJobMessage.id); sendMessage( routingKey: Constants.messageTopicJobDeleted, payload: JobDeletedMessage(job: job), ); sendMessage( routingKey: Constants.messageTopicUpdateBadges, payload: RefreshBadgesMessage, ); } }, ); subscribe( queueId: Constants.messageTopicJobReceived, callback: (message) async { var jobReceived = message.payload as JobReceivedMessage; if (jobReceived.job.state == 0) { await _jobMap!.put(jobReceived.job.id, jobReceived.job); } }, ); subscribe( queueId: Constants.messageTopicJobDeleted, callback: (message) async { var jobDeleted = message.payload as JobDeletedMessage; await _jobMap!.remove(jobDeleted.job.id); }, ); subscribe( queueId: Constants.messageTopicRemoveState0JobFromMap, callback: (message) async { var removeState0JobFromMap = message.payload as RemoveState0JobFromMapMessage; await _jobMap!.remove(removeState0JobFromMap.jobId); }, ); } void _replaceContentView(ContentViewWidget widget) async { Future.delayed(const Duration(seconds: 0), () async { contentStack.clear(); if (mounted) { setState(() { _currentContent = widget; }); } if (!context.mounted) return; _currentTitleWidget = await _currentContent.getHeaderWidget(MyApp.appNavigatorKey.currentContext!); if (mounted) { setState(() {}); } }); } void _popContentView(int count) { Future.delayed(const Duration(seconds: 0), () async { for (int i = 0; i < count - 1; i++) { contentStack.pop(); } var previousContentView = contentStack.pop(); _currentContent = previousContentView; if (!context.mounted) return; _currentTitleWidget = await _currentContent.getHeaderWidget(MyApp.appNavigatorKey.currentContext!); if (mounted) { setState(() {}); } }); } void _pushContentView(ContentViewWidget widget) { Future.delayed(const Duration(seconds: 0), () async { contentStack.push(_currentContent); _currentContent = widget; if (!context.mounted) return; _currentTitleWidget = await _currentContent.getHeaderWidget(MyApp.appNavigatorKey.currentContext!); if (mounted) { setState(() {}); } }); } void _connectionStatusChanged(ConnectionStatus newConnectionStatus) { var currentConnectionStatus = newConnectionStatus.status; Util.logInfo("_connectionStatusChanged called: $currentConnectionStatus"); _connectionLostValueNotifier.value = newConnectionStatus.status == ConnectionStatusEnum.notConnected; } void _startCheckForNewState0JobsJobsTimer() { _checkForNewState0JobsJobsTimer ??= Timer.periodic(const Duration(seconds: 5), (Timer t) async { if (await _jobMap!.length() > 0 && !_dialogOpened) { _dialogOpened = true; await showAcceptJobsPopup(_jobMap!.sharedReference(), () async { _dialogOpened = false; }); } }); } }