461 lines
14 KiB
Dart
461 lines
14 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/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 'tools/constants.dart';
|
|
import 'tools/util.dart';
|
|
|
|
Future<void> 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);
|
|
|
|
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 = null;
|
|
late Animation<double> _animation;
|
|
|
|
Timer? _checkForNewState0JobsJobsTimer;
|
|
|
|
bool _dialogOpened = false;
|
|
|
|
late SharedMap<int, Job>? _jobMap;
|
|
|
|
@override
|
|
void dispose() {
|
|
_animationController?.dispose();
|
|
|
|
super.dispose();
|
|
}
|
|
|
|
@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);
|
|
},
|
|
);
|
|
|
|
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;
|
|
|
|
var job = await MyApp.isarInstance.jobs.where().filter().idEqualTo(removeJobMessage.id).findFirst();
|
|
|
|
if (job != null) {
|
|
await MyApp.isarInstance.writeTxn(() async {
|
|
await MyApp.isarInstance.jobs.where().filter().idEqualTo(removeJobMessage.id).deleteAll();
|
|
});
|
|
|
|
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();
|
|
|
|
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;
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|