Files
stadtbote-app/lib/tools/util.dart
T
SvenandClaude Opus 4.8 e36fc69edc Feature "Wunschziel für heute" entfernen und BWV-Seite ergänzen
Entfernt den Button und die komplette Funktionalität "Wunschziel für
heute" (desired_zip): Seite, Netzwerk-Nachricht, Preference-Konstante,
Übermittlung beim Session-Start sowie alle Übersetzungen. Ergänzt die
neue BWV-Seite samt SetBwvLocationMessage und preferencesRemove-Helfer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 08:11:54 +02:00

516 lines
13 KiB
Dart

import 'dart:io';
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:image_picker/image_picker.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:stadtbote/main.dart';
import 'package:stadtbote/model/job.dart';
import 'package:stadtbote/model/log_entry.dart';
import 'package:stadtbote/objectbox.g.dart';
import 'package:stadtbote/tools/app_localizations.dart';
import 'package:stadtbote/tools/constants.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:uuid/uuid.dart';
import '../Tools/colored_safe_area.dart';
class Util {
static late SharedPreferences prefs;
static int nowUnixSecondsUtc() => DateTime.now().toUtc().millisecondsSinceEpoch ~/ 1000;
static int toUnixSecondsUtc(DateTime dt) => dt.toUtc().millisecondsSinceEpoch ~/ 1000;
static init() async {
prefs = await SharedPreferences.getInstance();
}
static bool preferencesContainsKey(String key) {
return prefs.containsKey(key);
}
static void preferencesSaveString(String key, String value) async {
await prefs.setString(key, value);
}
static String? preferencesGetString(String key, String? defaultValue) {
if (preferencesContainsKey(key)) {
return prefs.getString(key);
}
return defaultValue;
}
static void preferencesSaveInt(String key, int value) async {
await prefs.setInt(key, value);
}
static int? preferencesGetInt(String key, int? defaultValue) {
if (preferencesContainsKey(key)) {
return prefs.getInt(key);
}
return defaultValue;
}
static void preferencesSaveBool(String key, bool value) async {
await prefs.setBool(key, value);
}
static bool preferencesGetBool(String key, bool defaultValue) {
if (preferencesContainsKey(key)) {
return prefs.getBool(key) as bool;
}
return defaultValue;
}
static void preferencesRemove(String key) async {
await prefs.remove(key);
}
static String getAppUuid() {
var currentUuid = preferencesGetString('uuid', null);
if (currentUuid == null) {
currentUuid ??= const Uuid().v4();
preferencesSaveString("uuid", currentUuid);
}
return currentUuid;
}
static Future<int> getAppVersion() async {
PackageInfo packageInfo = await PackageInfo.fromPlatform();
return int.parse(packageInfo.version.replaceAll(".0.0", ""));
}
static _printLongText(String text) {
final pattern = RegExp('.{1,800}'); // 800 is the size of each chunk
pattern.allMatches(text).forEach((match) => debugPrint(match.group(0)));
}
static void _persistLogMessage(String text, LogEntrySeverityEnum severity) async {
var logEntry = LogEntry();
logEntry.time = DateTime.now();
logEntry.text = text;
logEntry.severityEnum = severity;
final box = MyApp.objectBoxStore.box<LogEntry>();
box.put(logEntry);
// Cleanup old logs (delete entries older than 3 days)
_cleanupOldLogs();
}
static void _cleanupOldLogs() async {
try {
final box = MyApp.objectBoxStore.box<LogEntry>();
// Delete logs older than 3 days
final threeDaysAgo = DateTime.now().subtract(const Duration(days: 3));
final oldLogsQuery = box.query(LogEntry_.time.lessThan(threeDaysAgo.millisecondsSinceEpoch)).build();
final oldLogs = oldLogsQuery.find();
oldLogsQuery.close();
if (oldLogs.isNotEmpty) {
box.removeMany(oldLogs.map((e) => e.id).toList());
}
} catch (e) {
// Silent fail for cleanup - don't log to avoid infinite loop
debugPrint('Log cleanup failed: $e');
}
}
static logInfo(String message) {
_printLongText(message);
_persistLogMessage(message, LogEntrySeverityEnum.info);
}
static logDebug(String message) {
_printLongText(message);
_persistLogMessage(message, LogEntrySeverityEnum.debug);
}
static logException(String message) {
_printLongText(message);
_persistLogMessage(message, LogEntrySeverityEnum.error);
}
static int fastHash(String string) {
var hash = 0xcbf29ce484222325;
var i = 0;
while (i < string.length) {
final codeUnit = string.codeUnitAt(i++);
hash ^= codeUnit >> 8;
hash *= 0x100000001b3;
hash ^= codeUnit & 0xFF;
hash *= 0x100000001b3;
}
return hash;
}
static Widget getWaitWidget(context) {
return ColoredSafeArea(
key: const Key('value'),
child: Container(
width: 10,
height: 10,
color: Colors.white,
),
);
}
static Future<Widget> fetchFutureIcon(icon) {
return Future.delayed(const Duration(seconds: 0), () async {
return icon;
});
}
static TextField getDefaultTextField(String hint,
{TextEditingController? controller,
FocusNode? focusNode,
IconButton? iconButton,
int? maxLines = 1,
int? maxLength = 100,
TextInputType? textInputType,
List<TextInputFormatter>? formatter,
Function()? onTap,
bool? autofocus}) {
iconButton ??= IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
if (controller != null) {
controller.text = "";
}
},
);
return TextField(
controller: controller,
focusNode: focusNode,
maxLines: maxLines,
onTap: onTap,
keyboardType: textInputType,
inputFormatters: formatter,
maxLength: maxLength,
autofocus: (autofocus != null) ? autofocus : false,
decoration: InputDecoration(
hintText: hint,
contentPadding: const EdgeInsets.all(10),
border: Util.getDefaultBorder(),
errorBorder: Util.getDefaultBorder(),
disabledBorder: Util.getDefaultBorder(),
focusedBorder: Util.getDefaultFocusedBorder(),
focusedErrorBorder: Util.getDefaultBorder(),
suffixIcon: iconButton,
labelText: hint),
);
}
static OutlineInputBorder getDefaultBorder() {
return const OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(20.0)));
}
static OutlineInputBorder getDefaultFocusedBorder() {
return const OutlineInputBorder(
borderSide: BorderSide(color: Constants.colorPrimary),
borderRadius: BorderRadius.all(Radius.circular(20.0)),
);
}
static Future<File?> capturePhoto() async {
final imagePicker = ImagePicker();
final pickedFile = await imagePicker.pickImage(source: ImageSource.camera);
if (pickedFile != null) {
return File(pickedFile.path);
}
return null;
}
static Future<void> startBrowser(String websiteUrl) async {
if (!await launchUrl(Uri.parse(websiteUrl))) {
throw Exception('Could not launch $websiteUrl');
}
}
static Future<void> playMp3(String name) async {
final player = AudioPlayer();
await player.play(AssetSource('sounds/$name'), volume: 1);
}
}
Widget getDefaultButton(String text, Function() onPressed, {Image? image, double? buttonHeight, bool enabledTimerActive = false, int enabledTimerDelay = 0}) {
bool enabledState = !enabledTimerActive;
return StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
if (!enabledState) {
Future.delayed(Duration(seconds: enabledTimerDelay), () {
enabledState = true;
if (context.mounted) {
setState(() {});
}
});
}
return SizedBox(
height: buttonHeight ?? 50,
child: MaterialButton(
color: const Color.fromARGB(255, 220, 220, 220),
disabledColor: const Color.fromARGB(255, 220, 220, 220),
disabledElevation: 5,
elevation: 5,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(15),
),
),
onPressed: () {
if (enabledState) {
Util.logInfo("Button \"$text\" clicked");
onPressed();
}
},
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Visibility(
visible: image != null,
child: CircleAvatar(
backgroundColor: Colors.transparent,
child: image,
),
),
Expanded(
child: Opacity(
opacity: enabledState ? 1.0 : 0.3,
child: Text(
text,
style: const TextStyle(color: Colors.black, fontSize: 14.0),
textAlign: TextAlign.center,
),
),
),
],
),
),
);
});
}
String mapVehicleTypeToImageName(int vehType)
{
switch (vehType) {
case 1:
return "fahrrad.png";
case 2:
return "pkw.png";
case 3:
return "kombi.png";
case 4:
return "kasten.png";
case 5:
return "bus.png";
case 6:
return "grbus.png";
case 7:
return "lkw.png";
case 8:
return "lkw_12.png";
case 10:
return "lkw_8.png";
case 11:
return "lkw_10.png";
case 12:
return "lkw_15.png";
case 13:
return "lkw_20.png";
case 999:
return "service.png";
}
return "unbekannt.png";
}
String mapVehicleTypeToText(int vehType) {
switch (vehType) {
case 1:
return 'bicycle'.tr;
case 2:
return 'car'.tr;
case 3:
return 'station_wagon'.tr;
case 4:
return 'van'.tr;
case 5:
return 'bus'.tr;
case 6:
return 'large_bus'.tr;
case 7:
case 8:
case 10:
case 11:
case 12:
case 13:
case 14:
return 'truck'.tr;
case 999:
return 'service'.tr;
}
return "!?????!";
}
String mapTourMode(String mode) {
var text = "";
switch (mode) {
case "pu":
text += "/ ${'pickup'.tr}";
break;
case "del":
text += "/ ${'delivery'.tr}";
break;
case "pu/del":
text += "/ ${'pickup_delivery'.tr}";
break;
}
return text;
}
Future<void> showQuestionDialog(BuildContext context, String title, String message, List<Widget> actions) async {
await showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text(title),
content: Text(message),
actions: actions,
);
}
);
}
Future<void> showAlertDialog(String title, String message) async {
await showDialog(
context: MyApp.appNavigatorKey.currentContext!,
builder: (context) {
return AlertDialog(
title: Text(title),
content: Text(message),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
],
);
}
);
}
Future<void> showMessageDialog(BuildContext context, String title, String message) async {
await showDialog(
barrierDismissible: false,
context: context,
builder: (context) {
return AlertDialog(
title: Text(title),
content: Text(message),
);
}
);
}
class DecimalTextInputFormatter extends TextInputFormatter {
int maxLength;
int decimalPlaces;
DecimalTextInputFormatter({required this.maxLength, required this.decimalPlaces});
@override
TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
var text = newValue.text;
String? filteredText = "";
if (text != "") {
RegExp regExp;
if (decimalPlaces > 0) {
regExp = RegExp(r'[0-9]+[,]{0,1}[0-9]{0,${decimalPlaces}');
} else {
regExp = RegExp(r'[0-9]{0,${maxLength}}');
}
filteredText = regExp.stringMatch(text);
}
return TextEditingValue(text: filteredText ?? oldValue.text);
}
}
bool isJobTooFarInFuture(Job job) {
var ordertime = job.ordertime * 1000; // convert seconds to milliseconds
var dateTimeThreshold = DateTime.now().add(Duration(minutes: job.offsWarn)).toUtc().millisecondsSinceEpoch;
return ordertime > dateTimeThreshold;
}
Future<bool> taskDialogSecurityException(BuildContext context) async {
final result = await showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (dialogContext) {
return AlertDialog(
title: Text('attention'.tr),
content: Text('close_dialog_warning'.tr),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(true),
child: Text('close'.tr),
),
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(false),
child: Text('back'.tr),
),
],
);
},
);
return result ?? false;
}