Files
stadtbote-app/lib/main.dart
T
2024-11-14 10:56:36 +01:00

471 lines
16 KiB
Dart

import 'dart:async';
import 'package:dart_mq/dart_mq.dart';
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:isar/isar.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/enqueued_message.dart';
import 'package:stadtbote/model/log_entry.dart';
import 'package:stadtbote/model/chat_message.dart';
import 'package:stadtbote/model/pictures.dart';
import 'package:stadtbote/model/rank.dart';
import 'package:stadtbote/model/location_status.dart';
import 'package:stadtbote/model/task_info.dart';
import 'package:stadtbote/popups/accept_jobs_popup.dart';
import 'package:stadtbote/tools/colored_safe_area.dart';
import 'package:stadtbote/model/session_status.dart';
import 'package:stadtbote/tools/connection_status.dart';
import 'package:stadtbote/tools/message_service.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/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 'tools/constants.dart';
import 'tools/util.dart';
Future<void> main() async {
runZonedGuarded(() async {
WidgetsFlutterBinding.ensureInitialized();
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);
final dir = await getApplicationDocumentsDirectory();
MyApp.isarInstance = await Isar.open([
SessionStatusSchema,
LocationStatusSchema,
JobSchema,
TaskInfoSchema,
ChatMessageSchema,
EnqueuedMessageSchema,
LogEntrySchema,
RankingSchema,
PictureSchema
], directory: dir.path, name: "database_v1");
await _clearLog();
runApp(const MyApp());
}, (error, stackTrace) {
_traceUncaughtExceptions(error, stackTrace);
});
}
Future<void> _clearLog() async {
var dateTime = DateTime.now().add(const Duration(days: -7));
await MyApp.isarInstance.writeTxn(() async {
await MyApp.isarInstance.logEntrys.where().filter().timeLessThan(dateTime).deleteAll();
});
}
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<NavigatorState>();
static late Isar isarInstance;
static Position? currentPosition;
static bool suppressAcceptDialog = false;
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> 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) {
MyApp.currentPosition = position;
});
}
}
Future<bool> _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<Main> createState() => _MainState();
}
class _MainState extends State<Main> with SingleTickerProviderStateMixin, ProducerMixin, ConsumerMixin {
late ContentViewWidget _currentContent;
late WidgetStack<ContentViewWidget> contentStack = WidgetStack();
late Widget _currentTitleWidget;
bool _initCompleted = false;
final ValueNotifier<bool> _connectionLostValueNotifier = ValueNotifier<bool>(true);
late AnimationController _animationController;
late Animation<double> _animation;
Timer? _checkForNewState0JobsJobsTimer;
bool _dialogOpened = false;
late SharedMap<int, Job>? _jobMap;
@override
void initState() {
super.initState();
_animationController = AnimationController(vsync: this, duration: const Duration(milliseconds: 1000));
_animationController.repeat(reverse: true);
_animation = Tween<double>(begin: 0.5, end: 1.0).animate(_animationController);
WidgetsBinding.instance.addPostFrameCallback((_) async {
var store1 = SharedStore('jobs');
_jobMap = await store1.getSharedMap<int, Job>('m1');
_initMessages();
_startCheckForNewState0JobsJobsTimer();
_currentContent = const MainPage();
_currentTitleWidget = await _currentContent.getHeaderWidget(MyApp.appNavigatorKey.currentContext!);
_initCompleted = true;
setState(() {});
});
}
@override
Widget build(BuildContext context) {
if (!_initCompleted) {
return Util.getWaitWidget(context);
}
return ColoredSafeArea(
key: GlobalKey(),
child: WillPopScope(
onWillPop: () async {
if (contentStack.isNotEmpty) {
_popContentView(1);
return false;
} else {
return true;
}
},
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<bool>(
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);
},
);
/*MessageService().subscribe<ContentViewWidget>(Constants.messageTopicPushContentView, this, (ContentViewWidget widget) {
_pushContentView(widget);
});*/
subscribe(
queueId: Constants.messageTopicPopContentView,
callback: (message) {
var popContentViewMessage = message.payload as PopContentViewMessage;
_popContentView(popContentViewMessage.count);
},
);
/*MessageService().subscribe<PopContentViewMessage>(Constants.messageTopicPopContentView, this, (PopContentViewMessage message) {
_popContentView(message.count);
});*/
subscribe(
queueId: Constants.messageTopicReplaceContentView,
callback: (message) {
var widget = message.payload as ContentViewWidget;
_replaceContentView(widget);
},
);
/*MessageService().subscribe<ContentViewWidget>(Constants.messageTopicReplaceContentView, this, (ContentViewWidget widget) {
_replaceContentView(widget);
});*/
subscribe(
queueId: Constants.messageTopicConnectionStatusChanged,
callback: (message) {
var connectionStatus = message.payload as ConnectionStatus;
_connectionStatusChanged(connectionStatus);
},
);
/*MessageService().subscribe<ConnectionStatus>(Constants.messageTopicConnectionStatusChanged, this, (ConnectionStatus connectionStatus) {
_connectionStatusChanged(connectionStatus);
});*/
subscribe(
queueId: Constants.messageTopicRemoveJob,
callback: (message) async {
var removeJobMessage = message.payload as RemoveJobMessage;
await MyApp.isarInstance.writeTxn(() async {
await MyApp.isarInstance.jobs.where().filter().idEqualTo(removeJobMessage.id).deleteAll();
});
// TODO MESSAGING
sendMessage(
routingKey: Constants.messageTopicJobDeleted,
payload: JobDeletedMessage(id: removeJobMessage.id),
);
//await MessageService().publish<JobDeletedMessage>(Constants.messageTopicJobDeleted, arguments: JobDeletedMessage(id: job.id));
// TODO MESSAGING
sendMessage(
routingKey: Constants.messageTopicUpdateBadges,
payload: RefreshBadgesMessage,
);
//await MessageService().publish<RefreshBadgesMessage>(Constants.messageTopicUpdateBadges, arguments: RefreshBadgesMessage());
},
);
/*MessageService().subscribe<RemoveJobMessage>(Constants.messageTopicRemoveJob, this, (RemoveJobMessage job) async {
await MyApp.isarInstance.writeTxn(() async {
await MyApp.isarInstance.jobs.where().filter().idEqualTo(job.id).deleteAll();
});
await MessageService().publish<JobDeletedMessage>(Constants.messageTopicJobDeleted, arguments: JobDeletedMessage(id: job.id));
await MessageService().publish<RefreshBadgesMessage>(Constants.messageTopicUpdateBadges, arguments: 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);
}
},
);
/*MessageService().subscribe<JobReceivedMessage>(Constants.messageTopicJobReceived, this, (JobReceivedMessage jobReceivedMessage) async {
if (jobReceivedMessage.job.state == 0) {
await _jobMap!.put(jobReceivedMessage.job.id, jobReceivedMessage.job);
}
});*/
subscribe(
queueId: Constants.messageTopicJobDeleted,
callback: (message) async {
var jobDeleted = message.payload as JobDeletedMessage;
await _jobMap!.remove(jobDeleted.id);
},
);
/*MessageService().subscribe<JobDeletedMessage>(Constants.messageTopicJobDeleted, this, (JobDeletedMessage jobDeletedMessage) async {
await _jobMap!.remove(jobDeletedMessage.id);
});*/
}
void _replaceContentView(ContentViewWidget widget) async {
Future.delayed(const Duration(seconds: 0), () async {
contentStack.clear();
setState(() {
_currentContent = widget;
});
_currentTitleWidget = await _currentContent.getHeaderWidget(context);
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;
_currentTitleWidget = await _currentContent.getHeaderWidget(context);
setState(() {});
});
}
void _pushContentView(ContentViewWidget widget) {
Future.delayed(const Duration(seconds: 0), () async {
contentStack.push(_currentContent);
_currentContent = widget;
_currentTitleWidget = await _currentContent.getHeaderWidget(context);
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;
});
}
});
}
}