Compare commits

...
10 Commits
Author SHA1 Message Date
Sven d585da11b5 Aufgabenliste in Workflow umbenannt und Nachrichtenbestätigung abgesichert 2026-10-10 10:48:42 +02:00
Sven 5e7babf265 VHS-Karten integriert, Bildzoom ergänzt und Chat-IDs korrigiert
Unterschriftenerfassung dauerhaft auf Deutsch anzeigen, lokale VHS-Buildkonfiguration und Regressionstests ergänzen sowie Version auf 357.0.0+357 anheben.
2026-10-07 10:41:41 +02:00
Sven c0b667c4ee Auftragsnummer im Annahme-Popup auf Server-ID korrigiert 2026-10-06 14:13:28 +02:00
SvenandClaude Fable 5.1 bb7e0b6b82 POST_NOTIFICATIONS-Berechtigung fuer Android 13+ ergaenzt und Version auf 355 angehoben
Seit targetSdk 33 muss POST_NOTIFICATIONS im Manifest deklariert und zur
Laufzeit angefragt werden, sonst bleibt der Benachrichtigungsschalter in
den Systemeinstellungen ausgegraut. MainActivity fragt die Berechtigung
jetzt in onCreate ab API 33 an.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-21 09:47:03 +02:00
Sven CarstensenandClaude Fable 5 4b649c6ed9 Kotlin auf 2.2.20 angehoben
Behebt die Flutter-Warnung, dass Kotlin-Versionen unter 2.2.20 bald nicht
mehr unterstuetzt werden; Debug-Kotlin-Build erfolgreich verifiziert.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 14:33:52 +02:00
Sven CarstensenandClaude Fable 5 ac4266079a Task-Typnummern neu zugeordnet und benutzerdefinierte Typen via effectiveType unterstuetzt
- TourTask: base_type-Feld und effectiveType-Getter ergaenzt; benutzerdefinierte
  Typen (>= 1000) aus dem Workflow-Editor werden auf ihren Basis-Typ abgebildet,
  bei Altdaten ohne base_type wird der Basis-Typ aus den Eigenschaften abgeleitet
- TaskListPage: alle Typ-Switches auf effectiveType und die neuen Typnummern
  umgestellt (u.a. Ankunft 1->3, Fotos 3->4, Bestaetigung 6->12, Listen 12-14->13-15)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 14:29:22 +02:00
Sven CarstensenandClaude Fable 5 aa44311776 Build- und Plugin-Konfiguration aktualisiert: generierte Plugin-Registranten, pubspec.lock, Gradle-Migrator-Flags; hprof-Dumps ignoriert
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 11:41:06 +02:00
Sven CarstensenandClaude Fable 5 b8f321daf9 Lokale Tool-Berechtigungen erweitert und Launch-Konfiguration auf Emulator Pixel_10_Pro umgestellt
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 19:17:41 +02:00
SvenandClaude Fable 5 f58f3f0c59 Pflichtfeld-Kennzeichen (required) aus dem Workflow-Editor auswerten
- TourField/TourEntry um optionales required-Flag erweitert (Altdaten ohne
  Angabe verhalten sich unverändert)
- Weiter-Button frei, wenn alle Pflichtfelder erfüllt sind oder der Task
  optional ist; Listen ohne Pflichtfelder (inkl. leerer Listen) sind frei
- Pflichtfelder und Pflicht-Checkboxen mit rotem * markiert; Titel-* nur
  noch, wenn der Task tatsächlich zwingende Eingaben verlangt

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 08:03:51 +02:00
SvenandClaude Fable 5 d864b9c72a Aufgaben-JSON testweise von Web-URL laden, Ankunft-Task als Button, RadioGroup-Umstellung
- TEMP-Loader: Aufgaben beim Oeffnen der Task-Seite von
  https://appcreation.de/download/task.json laden (Fallback: Tour-Daten)
- Task-Typ 1 (Ankunft melden) als Bestaetigungs-Button statt Checkbox
- Statusauswahl (Typ 11) auf RadioGroup umgestellt (Deprecation behoben)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 10:15:01 +02:00
40 changed files with 1425 additions and 329 deletions

No files matched your search

+10 -1
View File
@@ -8,7 +8,16 @@
"Bash(source ~/.bash_profile)",
"Bash(/Users/svencarstensen/Documents/Entwicklung/flutter/bin/flutter pub:*)",
"Bash(/Users/svencarstensen/Documents/Entwicklung/flutter/bin/flutter analyze:*)",
"Bash(/Users/svencarstensen/Documents/Entwicklung/flutter/bin/flutter:*)"
"Bash(/Users/svencarstensen/Documents/Entwicklung/flutter/bin/flutter:*)",
"Bash(flutter emulators *)",
"Bash(flutter devices *)",
"Read(//Users/svencarstensen/development/flutter/bin/**)",
"Read(//Users/svencarstensen/**)",
"Read(//Users/svencarstensen/Library/Android/sdk/emulator/**)",
"Bash(~/Library/Android/sdk/emulator/emulator -list-avds)",
"Read(//Users/svencarstensen/.android/avd/**)",
"Bash(xcrun simctl *)",
"Read(//opt/homebrew/Caskroom/**)"
]
}
}
+6
View File
@@ -45,3 +45,9 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release
# Java heap dumps
*.hprof
# Lokale Zugangsdaten für die VHS-Karte
/config/vhs.local.json
+18 -6
View File
@@ -6,16 +6,22 @@
"request": "launch",
"type": "dart",
"flutterMode": "debug",
"deviceId": "emulator-5554",
"program": "lib/main.dart"
"emulatorId": "Pixel_10_Pro",
"program": "lib/main.dart",
"toolArgs": [
"--dart-define-from-file=config/vhs.local.json"
]
},
{
"name": "Stadtbote - Android Emulator (Release)",
"request": "launch",
"type": "dart",
"flutterMode": "release",
"deviceId": "emulator-5554",
"program": "lib/main.dart"
"emulatorId": "Pixel_10_Pro",
"program": "lib/main.dart",
"toolArgs": [
"--dart-define-from-file=config/vhs.local.json"
]
},
{
"name": "Stadtbote - Pixel 7",
@@ -23,7 +29,10 @@
"type": "dart",
"flutterMode": "debug",
"deviceId": "2B101FDH2003HB",
"program": "lib/main.dart"
"program": "lib/main.dart",
"toolArgs": [
"--dart-define-from-file=config/vhs.local.json"
]
},
{
"name": "Stadtbote - Pixel 7 (Release)",
@@ -31,7 +40,10 @@
"type": "dart",
"flutterMode": "release",
"deviceId": "2B101FDH2003HB",
"program": "lib/main.dart"
"program": "lib/main.dart",
"toolArgs": [
"--dart-define-from-file=config/vhs.local.json"
]
}
]
}
+3
View File
@@ -0,0 +1,3 @@
{
"java.configuration.updateBuildConfiguration": "automatic"
}
+20
View File
@@ -1 +1,21 @@
dart run build_runner build
## VHS-Karte
Aufträge können im JSON eine optionale `map_url` enthalten. Die Schaltfläche
„Karte“ öffnet diese HTTPS-Adresse innerhalb der App. Ohne `map_url` wird die
bisherige Stationskarte verwendet.
Die HTTP-Basic-Zugangsdaten werden beim Build übergeben. Dazu
`config/vhs.example.json` nach `config/vhs.local.json` kopieren und die beiden
Werte lokal eintragen. Die lokale Datei ist von Git ausgeschlossen.
```bash
flutter run --dart-define-from-file=config/vhs.local.json
flutter build apk --release --dart-define-from-file=config/vhs.local.json
```
Die VS-Code-Startkonfigurationen und `build_release_apk.sh` verwenden diese
Datei ebenfalls. Zugangsdaten werden nur für den Host der ursprünglichen
Kartenadresse verwendet; die Ansicht erlaubt Navigation innerhalb desselben
HTTPS-Ursprungs (Host und Port).
+1
View File
@@ -1,6 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="de.assecutor.stadtbote">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:label="Stadtbote"
android:name="${applicationName}"
@@ -1,5 +1,32 @@
package de.assecutor.stadtbote
import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity()
class MainActivity: FlutterActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestNotificationPermissionIfNeeded()
}
/**
* Ab Android 13 (API 33) ist POST_NOTIFICATIONS eine "dangerous" Permission,
* die zur Laufzeit angefragt werden muss. Ohne sie darf die App keine
* Benachrichtigungen anzeigen und der Schalter in den Systemeinstellungen
* bleibt ausgegraut.
*/
private fun requestNotificationPermissionIfNeeded() {
if (Build.VERSION.SDK_INT >= 33 &&
checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS), REQUEST_CODE_POST_NOTIFICATIONS)
}
}
companion object {
private const val REQUEST_CODE_POST_NOTIFICATIONS = 1001
}
}
+4
View File
@@ -1,3 +1,7 @@
org.gradle.jvmargs=-Xmx4096M -XX:MaxMetaspaceSize=512m
android.useAndroidX=true
android.enableJetifier=true
# This builtInKotlin flag was added automatically by Flutter migrator
android.builtInKotlin=false
# This newDsl flag was added automatically by Flutter migrator
android.newDsl=false
+1 -1
View File
@@ -19,7 +19,7 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.9.1" apply false
id "org.jetbrains.kotlin.android" version "2.1.0" apply false
id "org.jetbrains.kotlin.android" version "2.2.20" apply false
}
include ":app"
+5 -1
View File
@@ -34,7 +34,11 @@ echo "Ziel-APK-Name: $APK_NAME"
echo
# Release-APK bauen
flutter build apk --release
BUILD_ARGS=(--release)
if [[ -f "config/vhs.local.json" ]]; then
BUILD_ARGS+=(--dart-define-from-file=config/vhs.local.json)
fi
flutter build apk "${BUILD_ARGS[@]}"
# Pfad zur erzeugten APK
BUILT_APK="build/app/outputs/flutter-apk/app-release.apk"
+4
View File
@@ -0,0 +1,4 @@
{
"VHS_BASIC_AUTH_USERNAME": "",
"VHS_BASIC_AUTH_PASSWORD": ""
}
+8 -8
View File
@@ -91,13 +91,12 @@ class _MessagesPageState extends State<MessagesPage> with ProducerMixin {
void _handleSendPressed(types.PartialText text) async {
var now = DateTime.now();
var message = sbmessage.ChatMessage();
message.text = text.text;
message.from = _sid;
message.subj = '';
message.time = Util.nowUnixSecondsUtc();
message.read = true;
message.id = await sbmessage.ChatMessage.getNextId();
final message = sbmessage.ChatMessage.outgoing(
externalId: await sbmessage.ChatMessage.getNextId(),
time: Util.nowUnixSecondsUtc(),
from: _sid,
text: text.text,
);
final box = MyApp.objectBoxStore.box<sbmessage.ChatMessage>();
box.put(message);
@@ -205,7 +204,8 @@ class _MessagesPageState extends State<MessagesPage> with ProducerMixin {
if (unreadChatMessages.isNotEmpty) {
List<String> chatMessageIds = [];
for (var message in unreadChatMessages) {
chatMessageIds.add(message.id.toString());
// Der Server erwartet seine Nachrichten-ID, nicht die lokale ObjectBox-ID.
chatMessageIds.add(message.externalId.toString());
}
var chatMessagesShowed = ChatMessagesReceived();
+239 -70
View File
@@ -2,6 +2,8 @@ import 'dart:convert';
import 'package:dart_mq/dart_mq.dart';
import 'package:flutter/material.dart';
// TEMP (nur zum Testen, s. _tempTaskJsonUrl): wieder entfernen.
import 'package:http/http.dart' as http;
import 'package:flutter/services.dart';
import 'package:image/image.dart' as image_lib;
import 'package:intl/intl.dart';
@@ -25,6 +27,44 @@ import '../model/tour_task.dart';
import '../tools/app_localizations.dart';
import '../tools/util.dart';
// =============================================================================
// TEMPORÄR — NUR ZUM TESTEN, WIEDER ENTFERNEN
//
// Lädt das Aufgaben-JSON beim Öffnen der Seite von einer Webseite statt die
// Aufgaben der übergebenen Tour zu verwenden. Die URL muss entweder eine
// JSON-Liste von Aufgaben liefern oder ein Objekt mit einer "tasks"-Liste.
//
// Zum Entfernen: diesen Block, den http-Import oben und die mit
// "TEMP" markierte Zeile in _fetchTaskList() löschen. Alternativ die URL auf
// null setzen, dann greift wieder das normale Verhalten (auch bei Ladefehler).
// =============================================================================
// ignore: unnecessary_nullable_for_final_variable_declarations
const String? _tempTaskJsonUrl = "https://appcreation.de/download/task.json";
Future<List<TourTask>?> _tempLoadTasksFromWeb() async {
final url = _tempTaskJsonUrl;
if (url == null) {
return null;
}
try {
final response = await http.get(Uri.parse(url));
if (response.statusCode != 200) {
debugPrint("TEMP: Aufgaben-JSON laden fehlgeschlagen, HTTP ${response.statusCode}");
return null;
}
final decoded = jsonDecode(utf8.decode(response.bodyBytes));
final list = decoded is List ? decoded : (decoded as Map<String, dynamic>)['tasks'] as List<dynamic>;
return list.map((e) => TourTask.fromJson(e as Map<String, dynamic>)).toList();
} catch (e) {
debugPrint("TEMP: Aufgaben-JSON laden fehlgeschlagen: $e");
return null;
}
}
// ========================================================== ENDE TEMPORÄR ====
/// Seiten-Wizard zur Bearbeitung der Aufgaben einer Tour. Pro Aufgabe wird
/// genau eine Seite angezeigt; mit „Zurück"/„Weiter" navigiert der Bearbeiter
/// zwischen den Aufgaben, die letzte Seite schließt die Tour ab.
@@ -98,7 +138,10 @@ class _TaskListPageState extends State<TaskListPage> with ProducerMixin {
Future<List<TaskInfo>> _fetchTaskList() async {
List<TaskInfo> taskList = [];
for (var task in widget.tour.tasks!) {
// TEMP (nur zum Testen): Aufgaben von Webseite statt aus der Tour laden.
final tasks = await _tempLoadTasksFromWeb() ?? widget.tour.tasks!;
for (var task in tasks) {
final box = MyApp.objectBoxStore.box<TaskInfo>();
final query = box.query(TaskInfo_.sId.equals(task.sId)).build();
var taskInfo = query.findFirst();
@@ -122,6 +165,14 @@ class _TaskListPageState extends State<TaskListPage> with ProducerMixin {
// Aufbau der Seite
// ---------------------------------------------------------------------------
String _taskText(TaskInfo taskInfo, String key) {
// Die Unterschriftenerfassung bleibt unabhängig von der App-Sprache deutsch.
return AppLocalizations.tr(
key,
language: taskInfo.task?.effectiveType == 2 ? AppLanguage.german : null,
);
}
@override
Widget build(BuildContext context) {
if (!_initCompleted) {
@@ -147,13 +198,13 @@ class _TaskListPageState extends State<TaskListPage> with ProducerMixin {
child: Padding(
padding: const EdgeInsets.only(bottom: 6),
child: Text(
'${'commission_number_short'.tr}: $_commisionNumber',
'${_taskText(taskInfo, 'commission_number_short')}: $_commisionNumber',
style: const TextStyle(fontSize: 18),
),
),
),
Text(
'${'task'.tr} ${_index + 1} / ${_taskList.length}',
'${_taskText(taskInfo, 'task')} ${_index + 1} / ${_taskList.length}',
style: const TextStyle(fontSize: 13, color: Colors.grey),
),
const SizedBox(height: 4),
@@ -172,7 +223,7 @@ class _TaskListPageState extends State<TaskListPage> with ProducerMixin {
children: [
Expanded(
child: Text(
(taskInfo.task?.topic ?? '') + ((taskInfo.task?.opt ?? false) ? "" : " *"),
(taskInfo.task?.topic ?? '') + (_isTaskOptional(taskInfo) ? "" : " *"),
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
),
@@ -195,7 +246,7 @@ class _TaskListPageState extends State<TaskListPage> with ProducerMixin {
children: [
Expanded(
child: _navButton(
'back'.tr,
_taskText(taskInfo, 'back'),
_index > 0 ? () => setState(() => _index--) : null,
primary: false,
),
@@ -204,9 +255,9 @@ class _TaskListPageState extends State<TaskListPage> with ProducerMixin {
Expanded(
child: isLast
? (widget.readOnly
? _navButton('close'.tr, () => _popBack())
: _navButton('finish'.tr, canProceed ? () => _onFinish() : null))
: _navButton('next'.tr, canProceed ? () => _onNext() : null),
? _navButton(_taskText(taskInfo, 'close'), () => _popBack())
: _navButton(_taskText(taskInfo, 'finish'), canProceed ? () => _onFinish() : null))
: _navButton(_taskText(taskInfo, 'next'), canProceed ? () => _onNext() : null),
),
],
),
@@ -242,7 +293,11 @@ class _TaskListPageState extends State<TaskListPage> with ProducerMixin {
// ---------------------------------------------------------------------------
Widget _buildReadOnlyBody(TaskInfo taskInfo) {
final summary = (taskInfo.additionalText ?? '').trim();
var summary = (taskInfo.additionalText ?? '').trim();
if (taskInfo.task?.effectiveType == 2 && summary.isNotEmpty) {
// Auch früher in einer anderen Sprache gespeicherte Bestätigungen anzeigen.
summary = _taskText(taskInfo, 'signature_captured');
}
return Container(
width: double.infinity,
padding: const EdgeInsets.all(14),
@@ -254,7 +309,7 @@ class _TaskListPageState extends State<TaskListPage> with ProducerMixin {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('completed'.tr, style: const TextStyle(fontWeight: FontWeight.bold)),
Text(_taskText(taskInfo, 'completed'), style: const TextStyle(fontWeight: FontWeight.bold)),
if (summary.isNotEmpty) ...[
const SizedBox(height: 6),
Text(summary, style: const TextStyle(fontSize: 15)),
@@ -270,19 +325,19 @@ class _TaskListPageState extends State<TaskListPage> with ProducerMixin {
Widget _buildEditableBody(TaskInfo taskInfo) {
final draft = _draftFor(taskInfo);
final type = taskInfo.task?.type ?? 0;
// Benutzerdefinierte Typen aus dem Workflow-Editor (>= 1000) verhalten
// sich wie ihr Basis-Typ, siehe TourTask.effectiveType.
final type = taskInfo.task?.effectiveType ?? 0;
switch (type) {
case 1:
return _buildConfirmBody(draft, 'confirm_arrival'.tr);
case 2:
return _buildSignatureBody(taskInfo, draft);
case 3:
return _buildConfirmButtonBody(draft, 'confirm_arrival'.tr);
case 4:
return _buildPhotosBody(taskInfo, draft);
case 5:
return Text('barcode_disabled'.tr, style: const TextStyle(color: Colors.grey));
case 6:
return _buildConfirmBody(draft, taskInfo.task?.topic ?? 'confirm'.tr);
case 7:
case 8:
case 9:
@@ -290,17 +345,51 @@ class _TaskListPageState extends State<TaskListPage> with ProducerMixin {
case 11:
return _buildStatusBody(taskInfo, draft);
case 12:
return _buildFieldsBody(taskInfo, draft, numeric: false);
return _buildConfirmBody(draft, taskInfo.task?.topic ?? 'confirm'.tr);
case 13:
return _buildFieldsBody(taskInfo, draft, numeric: true);
return _buildFieldsBody(taskInfo, draft, numeric: false);
case 14:
return _buildFieldsBody(taskInfo, draft, numeric: true);
case 15:
return _buildCheckboxListBody(taskInfo, draft);
default:
return Text('task_type_unsupported'.tr, style: const TextStyle(color: Colors.grey));
}
}
/// Bestätigungs-Aufgabe (Ankunft melden = 1, Checkbox = 6).
/// Bestätigung per Button (Ankunft melden = 3). Antippen bestätigt bzw.
/// nimmt die Bestätigung wieder zurück; der Zustand wird über ein
/// Häkchen-Symbol im Button angezeigt.
Widget _buildConfirmButtonBody(_TaskDraft draft, String label) {
final confirmed = draft.confirmed;
return SizedBox(
height: 52,
child: MaterialButton(
color: Constants.colorPrimary,
elevation: 3,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(15))),
onPressed: () => setState(() => draft.confirmed = !confirmed),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (confirmed) ...[
const Icon(Icons.check_circle, color: Colors.white),
const SizedBox(width: 8),
],
Flexible(
child: Text(
label,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 16, color: Colors.white),
),
),
],
),
),
);
}
/// Bestätigungs-Aufgabe als Checkbox (Typ 12).
Widget _buildConfirmBody(_TaskDraft draft, String label) {
return CheckboxListTile(
value: draft.confirmed,
@@ -339,14 +428,20 @@ class _TaskListPageState extends State<TaskListPage> with ProducerMixin {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...options.map((o) => RadioListTile<int>(
value: o.code,
groupValue: draft.selectedCode,
contentPadding: EdgeInsets.zero,
activeColor: Constants.colorPrimary,
title: Text(o.label),
onChanged: (value) => setState(() => draft.selectedCode = value),
)),
RadioGroup<int>(
groupValue: draft.selectedCode,
onChanged: (value) => setState(() => draft.selectedCode = value),
child: Column(
children: options
.map((o) => RadioListTile<int>(
value: o.code,
contentPadding: EdgeInsets.zero,
activeColor: Constants.colorPrimary,
title: Text(o.label),
))
.toList(),
),
),
if (requiresRemark) ...[
const SizedBox(height: 8),
Util.getDefaultTextField(
@@ -359,7 +454,7 @@ class _TaskListPageState extends State<TaskListPage> with ProducerMixin {
);
}
/// Text-/Zahlen-Liste (Typ 12/13): je Feld ein beschriftetes Eingabefeld.
/// Text-/Zahlen-Liste (Typ 13/14): je Feld ein beschriftetes Eingabefeld.
Widget _buildFieldsBody(TaskInfo taskInfo, _TaskDraft draft, {required bool numeric}) {
final fields = taskInfo.task?.fields ?? [];
if (fields.isEmpty) {
@@ -374,7 +469,16 @@ class _TaskListPageState extends State<TaskListPage> with ProducerMixin {
if ((fields[i].title ?? '').isNotEmpty)
Padding(
padding: const EdgeInsets.only(bottom: 4, top: 4),
child: Text(fields[i].title!, style: const TextStyle(fontWeight: FontWeight.w600)),
child: Text.rich(
TextSpan(
text: fields[i].title!,
children: [
if (fields[i].required == true)
const TextSpan(text: ' *', style: TextStyle(color: Colors.red)),
],
),
style: const TextStyle(fontWeight: FontWeight.w600),
),
),
Util.getDefaultTextField(
fields[i].placeholder ?? '',
@@ -390,7 +494,7 @@ class _TaskListPageState extends State<TaskListPage> with ProducerMixin {
);
}
/// Checkbox-Liste (Typ 14): Mehrfachauswahl aus den entries.
/// Checkbox-Liste (Typ 15): Mehrfachauswahl aus den entries.
Widget _buildCheckboxListBody(TaskInfo taskInfo, _TaskDraft draft) {
final entries = taskInfo.task?.entries ?? [];
if (entries.isEmpty) {
@@ -404,7 +508,15 @@ class _TaskListPageState extends State<TaskListPage> with ProducerMixin {
contentPadding: EdgeInsets.zero,
controlAffinity: ListTileControlAffinity.leading,
activeColor: Constants.colorPrimary,
title: Text(e.text),
title: Text.rich(
TextSpan(
text: e.text,
children: [
if (e.required == true)
const TextSpan(text: ' *', style: TextStyle(color: Colors.red)),
],
),
),
onChanged: (value) => setState(() {
if (value ?? false) {
draft.checked.add(e.code);
@@ -448,7 +560,7 @@ class _TaskListPageState extends State<TaskListPage> with ProducerMixin {
alignment: Alignment.centerRight,
child: TextButton.icon(
icon: const Icon(Icons.clear),
label: Text('clear'.tr),
label: Text(_taskText(taskInfo, 'clear')),
onPressed: () => setState(() => draft.signatureController!.clear()),
),
),
@@ -456,7 +568,7 @@ class _TaskListPageState extends State<TaskListPage> with ProducerMixin {
);
}
/// Fotos aufnehmen (Typ 3) – Inline-Aufnahme und Vorschau.
/// Fotos aufnehmen (Typ 4) – Inline-Aufnahme und Vorschau.
Widget _buildPhotosBody(TaskInfo taskInfo, _TaskDraft draft) {
final max = taskInfo.task?.max ?? 999;
final min = taskInfo.task?.min ?? 0;
@@ -535,17 +647,42 @@ class _TaskListPageState extends State<TaskListPage> with ProducerMixin {
return _isValid(taskInfo);
}
/// Ob die Aufgabe insgesamt optional ist (steuert das "*" im Titel):
/// entweder per {@code opt}-Flag oder – bei Listen-Tasks – weil kein
/// einziger Eintrag als Pflichtfeld gekennzeichnet ist. Altdaten ohne
/// Pflicht-Kennzeichen gelten wie bisher als verpflichtend.
bool _isTaskOptional(TaskInfo taskInfo) {
final task = taskInfo.task;
if (task == null) return true;
if (task.opt ?? false) return true;
switch (task.effectiveType) {
case 13:
case 14:
final fields = task.fields ?? [];
if (fields.isEmpty) return true;
if (fields.every((f) => f.required == null)) return false;
return fields.every((f) => f.required != true);
case 15:
final entries = task.entries ?? [];
if (entries.isEmpty) return true;
if (entries.every((e) => e.required == null)) return false;
return entries.every((e) => e.required != true);
default:
return false;
}
}
bool _isValid(TaskInfo taskInfo) {
final draft = _draftFor(taskInfo);
final type = taskInfo.task?.type ?? 0;
final type = taskInfo.task?.effectiveType ?? 0;
switch (type) {
case 1:
case 6:
return draft.confirmed;
case 2:
final sig = draft.signatureController;
return sig != null && sig.points.length > 50;
case 3:
case 12:
return draft.confirmed;
case 4:
final min = (taskInfo.task?.min ?? 0);
final max = (taskInfo.task?.max ?? 999);
final required = min > 0 ? min : 1;
@@ -564,32 +701,64 @@ class _TaskListPageState extends State<TaskListPage> with ProducerMixin {
return (draft.remark?.text.trim().isNotEmpty ?? false);
}
return true;
case 12:
_ensureFieldControllers(draft, taskInfo.task?.fields?.length ?? 0);
return draft.fields.isNotEmpty && draft.fields.every((c) => c.text.trim().isNotEmpty);
case 13:
_ensureFieldControllers(draft, taskInfo.task?.fields?.length ?? 0);
return draft.fields.isNotEmpty && draft.fields.every((c) => _isNumeric(c.text));
return _fieldsValid(taskInfo, draft, numeric: false);
case 14:
return draft.checked.isNotEmpty;
_ensureFieldControllers(draft, taskInfo.task?.fields?.length ?? 0);
return _fieldsValid(taskInfo, draft, numeric: true);
case 15:
final entries = taskInfo.task?.entries ?? [];
// Ohne definierte Einträge gibt es keine Pflichtfelder -> freigeben.
if (entries.isEmpty) return true;
// Altdaten ohne Pflicht-Kennzeichen: wie bisher mindestens ein Häkchen.
if (entries.every((e) => e.required == null)) {
return draft.checked.isNotEmpty;
}
return entries
.where((e) => e.required == true)
.every((e) => draft.checked.contains(e.code));
default:
return true;
}
}
/// Prüft die Eingabefelder einer Text-/Zahlen-Liste (Typ 13/14) gegen ihre
/// Pflicht-Kennzeichen. Altdaten ohne Kennzeichen behalten das bisherige
/// Verhalten (alle Felder müssen gefüllt bzw. numerisch sein). Sonst müssen
/// nur Pflichtfelder gefüllt (bei Zahlenlisten numerisch) sein; optionale
/// Felder dürfen leer bleiben, müssen aber – falls ausgefüllt – bei
/// Zahlenlisten ebenfalls numerisch sein.
bool _fieldsValid(TaskInfo taskInfo, _TaskDraft draft, {required bool numeric}) {
final fields = taskInfo.task?.fields ?? [];
// Ohne definierte Felder gibt es keine Pflichtfelder -> freigeben.
if (fields.isEmpty) return true;
final legacy = fields.every((f) => f.required == null);
for (int i = 0; i < fields.length && i < draft.fields.length; i++) {
final text = draft.fields[i].text.trim();
final mandatory = legacy || fields[i].required == true;
if (mandatory) {
if (numeric ? !_isNumeric(text) : text.isEmpty) return false;
} else if (numeric && text.isNotEmpty && !_isNumeric(text)) {
return false;
}
}
return true;
}
/// Ob der Bearbeiter überhaupt Daten erfasst hat (für optionale Aufgaben:
/// entscheidet, ob eine Antwort geschrieben oder die Aufgabe übersprungen wird).
bool _hasData(TaskInfo taskInfo) {
final draft = _draftFor(taskInfo);
final type = taskInfo.task?.type ?? 0;
final type = taskInfo.task?.effectiveType ?? 0;
switch (type) {
case 1:
case 6:
return draft.confirmed;
case 2:
final sig = draft.signatureController;
return sig != null && sig.points.length > 50;
case 3:
case 12:
return draft.confirmed;
case 4:
return draft.photos.isNotEmpty;
case 7:
case 8:
@@ -597,10 +766,10 @@ class _TaskListPageState extends State<TaskListPage> with ProducerMixin {
return (draft.text?.text.trim().isNotEmpty ?? false);
case 11:
return draft.selectedCode != null;
case 12:
case 13:
return draft.fields.any((c) => c.text.trim().isNotEmpty);
case 14:
return draft.fields.any((c) => c.text.trim().isNotEmpty);
case 15:
return draft.checked.isNotEmpty;
default:
return false;
@@ -645,7 +814,7 @@ class _TaskListPageState extends State<TaskListPage> with ProducerMixin {
Future<void> _commitCurrent(TaskInfo taskInfo) async {
final draft = _draftFor(taskInfo);
final type = taskInfo.task?.type ?? 0;
final type = taskInfo.task?.effectiveType ?? 0;
// Optionale Aufgabe ohne Eingabe: als erledigt markieren, keine Antwort.
if (!_hasData(taskInfo)) {
@@ -657,7 +826,16 @@ class _TaskListPageState extends State<TaskListPage> with ProducerMixin {
}
switch (type) {
case 1: // Ankunft melden
case 2: // Unterschrift
final answer = await _createAnswerHeader(taskInfo);
answer.sign = _convertSignaturePoints(draft.signatureController!.points);
taskInfo.answer = jsonEncode(answer.toJson());
taskInfo.additionalText = _taskText(taskInfo, 'signature_captured');
taskInfo.completed = true;
_persist(taskInfo);
break;
case 3: // Ankunft melden
final answer = await _createAnswerHeader(taskInfo);
taskInfo.answer = jsonEncode(answer.toJson());
taskInfo.additionalText = DateFormat("dd.MM.yyyy HH:mm").format(DateTime.now());
@@ -666,16 +844,7 @@ class _TaskListPageState extends State<TaskListPage> with ProducerMixin {
_sendArrivedMessage(taskInfo);
break;
case 2: // Unterschrift
final answer = await _createAnswerHeader(taskInfo);
answer.sign = _convertSignaturePoints(draft.signatureController!.points);
taskInfo.answer = jsonEncode(answer.toJson());
taskInfo.additionalText = 'signature_captured'.tr;
taskInfo.completed = true;
_persist(taskInfo);
break;
case 3: // Fotos
case 4: // Fotos
_storePhotos(draft.photos);
taskInfo.answer = null; // Bilder werden separat als pic-Nachrichten gesendet.
taskInfo.additionalText = 'photos_taken'.tr;
@@ -683,7 +852,7 @@ class _TaskListPageState extends State<TaskListPage> with ProducerMixin {
_persist(taskInfo);
break;
case 6: // Checkbox / Bestätigung
case 12: // Checkbox / Bestätigung
final answer = await _createAnswerHeader(taskInfo);
answer.val = "1";
taskInfo.answer = jsonEncode(answer.toJson());
@@ -716,8 +885,8 @@ class _TaskListPageState extends State<TaskListPage> with ProducerMixin {
_persist(taskInfo);
break;
case 12: // Text-Liste
case 13: // Zahlen-Liste
case 13: // Text-Liste
case 14: // Zahlen-Liste
final answer = await _createAnswerHeader(taskInfo);
final fields = taskInfo.task?.fields ?? [];
final answers = <FinFieldAnswer>[];
@@ -736,7 +905,7 @@ class _TaskListPageState extends State<TaskListPage> with ProducerMixin {
_persist(taskInfo);
break;
case 14: // Checkbox-Liste
case 15: // Checkbox-Liste
final answer = await _createAnswerHeader(taskInfo);
final checked = draft.checked.toList()..sort();
answer.checked = checked;
@@ -788,9 +957,9 @@ class _TaskListPageState extends State<TaskListPage> with ProducerMixin {
/// erneut bearbeitbaren Aufgaben aus der gespeicherten Antwort vor.
_TaskDraft _buildDraft(TaskInfo taskInfo) {
final draft = _TaskDraft();
final type = taskInfo.task?.type ?? 0;
final type = taskInfo.task?.effectiveType ?? 0;
if (type == 12 || type == 13) {
if (type == 13 || type == 14) {
_ensureFieldControllers(draft, taskInfo.task?.fields?.length ?? 0);
}
@@ -802,8 +971,8 @@ class _TaskListPageState extends State<TaskListPage> with ProducerMixin {
try {
final answer = FinTourTask.fromJson(jsonDecode(taskInfo.answer!) as Map<String, dynamic>);
switch (type) {
case 1:
case 6:
case 3:
case 12:
draft.confirmed = true;
break;
case 7:
@@ -815,14 +984,14 @@ class _TaskListPageState extends State<TaskListPage> with ProducerMixin {
draft.selectedCode = answer.code;
draft.remark = _listenedController(answer.val ?? '');
break;
case 12:
case 13:
case 14:
final values = answer.fields ?? [];
for (int i = 0; i < draft.fields.length && i < values.length; i++) {
draft.fields[i].text = values[i].val ?? '';
}
break;
case 14:
case 15:
draft.checked = (answer.checked ?? []).toSet();
break;
}
@@ -1118,7 +1287,7 @@ class _HeaderWidgetState extends State<_HeaderWidget> with ProducerMixin {
child: const Center(
child: Text(
textAlign: TextAlign.center,
"Liste der Aufgaben",
"Workflow",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18, color: Colors.white),
),
),
+2 -13
View File
@@ -4,10 +4,10 @@ import 'package:stadtbote/content_views/task_list_page.dart';
import 'package:stadtbote/content_views/utils/content_view_widget.dart';
import 'package:stadtbote/messages/pop_content_view_message.dart';
import 'package:stadtbote/model/job_tour.dart';
import 'package:stadtbote/popups/job_map_popup.dart';
import '../model/job.dart';
import '../popups/files_popup.dart';
import '../popups/map_popup.dart';
import '../tools/app_localizations.dart';
import '../tools/constants.dart';
import '../tools/util.dart';
@@ -124,18 +124,7 @@ class _TourDetailsPageState extends State<TourDetailsPage> with ProducerMixin {
const SizedBox(height: 10),
getDefaultButton(
'map'.tr,
() => {
if (widget.tour.pos != null)
{
showMapPopup(
context, widget.tour, widget.job.tours)
}
else
{
showAlertDialog('information'.tr,
'no_position'.tr)
}
}),
() => showJobMapPopup(context, widget.job, currentTour: widget.tour)),
],
),
)),
+5 -4
View File
@@ -4,7 +4,7 @@ import 'package:stadtbote/content_views/tour_details_page.dart';
import 'package:stadtbote/content_views/utils/content_view_widget.dart';
import 'package:stadtbote/messages/pop_content_view_message.dart';
import 'package:stadtbote/model/job_tour.dart';
import 'package:stadtbote/popups/map_popup.dart';
import 'package:stadtbote/popups/job_map_popup.dart';
import '../model/job.dart';
import '../popups/files_popup.dart';
@@ -90,7 +90,8 @@ class _TourListPageState extends State<TourListPage> with ProducerMixin {
SizedBox(
width: 100,
child: Visibility(
visible: widget.job.tours[0].pos != null,
visible: (widget.job.mapUrl?.trim().isNotEmpty ?? false) ||
(_tours.isNotEmpty && _tours.first.pos != null),
child: ElevatedButton(
style: ButtonStyle(
shape: WidgetStateProperty.all<RoundedRectangleBorder>(
@@ -102,7 +103,7 @@ class _TourListPageState extends State<TourListPage> with ProducerMixin {
backgroundColor: WidgetStateProperty.all<Color>(Colors.lightGreen),
foregroundColor: WidgetStateProperty.all<Color>(Colors.black),
),
onPressed: () => {showMapPopup(context, widget.job.tours[0], widget.job.tours)},
onPressed: () => showJobMapPopup(context, widget.job),
child: Text('map'.tr)),
))
],
@@ -305,4 +306,4 @@ class _HeaderWidgetState extends State<_HeaderWidget> with ProducerMixin {
),
);
}
}
}
+246
View File
@@ -0,0 +1,246 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:stadtbote/tools/vhs_map_access.dart';
import 'package:webview_flutter/webview_flutter.dart';
class VhsMapPage extends StatefulWidget {
const VhsMapPage({super.key, required this.uri});
final Uri uri;
@override
State<VhsMapPage> createState() => _VhsMapPageState();
}
class _VhsMapPageState extends State<VhsMapPage> {
static const _username = String.fromEnvironment('VHS_BASIC_AUTH_USERNAME');
static const _password = String.fromEnvironment('VHS_BASIC_AUTH_PASSWORD');
static const _loadError =
'Die VHS-Karte konnte nicht geladen werden. '
'Bitte prüfen Sie Ihre Internetverbindung und versuchen Sie es erneut.';
WebViewController? _controller;
String? _errorMessage;
int _loadId = 0;
int _progress = 0;
bool _isLoading = true;
@override
void initState() {
super.initState();
unawaited(_loadMap());
}
bool _isCurrentLoad(int loadId) => mounted && _loadId == loadId;
void _showError(int loadId, String message) {
if (!_isCurrentLoad(loadId)) return;
setState(() {
_errorMessage ??= message;
_isLoading = false;
});
}
Future<void> _stopLoading(WebViewController? controller) async {
try {
await controller?.runJavaScript('window.stop();');
} catch (_) {
// Beim Schließen kann die native WebView bereits entfernt worden sein.
}
}
Future<void> _loadMap() async {
final loadId = ++_loadId;
final previousController = _controller;
setState(() {
_controller = null;
_errorMessage = null;
_progress = 0;
_isLoading = true;
});
await _stopLoading(previousController);
if (!_isCurrentLoad(loadId)) return;
final access = VhsMapAccess(widget.uri);
if (!access.isValid) {
_showError(
loadId,
'Für diesen Auftrag ist kein gültiger Kartenlink verfügbar.',
);
return;
}
Uri documentUri = widget.uri;
WebViewController? controller;
try {
controller = WebViewController();
await controller.setJavaScriptMode(JavaScriptMode.unrestricted);
if (!_isCurrentLoad(loadId)) return;
await controller.setNavigationDelegate(
NavigationDelegate(
onNavigationRequest: (request) {
if (!_isCurrentLoad(loadId) || _errorMessage != null) {
return NavigationDecision.prevent;
}
final target = Uri.tryParse(request.url);
if (target == null || !access.allowsNavigation(target)) {
if (request.isMainFrame) {
_showError(
loadId,
'Dieser Link kann in der VHS-Karte nicht geöffnet werden.',
);
}
return NavigationDecision.prevent;
}
if (request.isMainFrame) documentUri = target;
return NavigationDecision.navigate;
},
onPageStarted: (url) {
if (!_isCurrentLoad(loadId) || _errorMessage != null) return;
final target = Uri.tryParse(url);
if (target == null || !access.allowsNavigation(target)) {
unawaited(_stopLoading(controller));
_showError(
loadId,
'Dieser Link kann in der VHS-Karte nicht geöffnet werden.',
);
return;
}
documentUri = target;
setState(() {
_progress = 0;
_isLoading = true;
});
},
onProgress: (progress) {
if (!_isCurrentLoad(loadId) || _errorMessage != null) return;
setState(() => _progress = progress);
},
onPageFinished: (_) {
if (!_isCurrentLoad(loadId) || _errorMessage != null) return;
setState(() => _isLoading = false);
},
onWebResourceError: (error) {
// Fehlende Kartenkacheln dürfen die gesamte Karte nicht verdecken.
if (error.isForMainFrame == true ||
(error.isForMainFrame == null &&
error.url != null &&
Uri.tryParse(error.url!) == documentUri)) {
_showError(loadId, _loadError);
}
},
onHttpError: (error) {
// 401 kann vor der erfolgreichen HTTP-Anmeldung gemeldet werden.
if (error.response?.statusCode == 401) return;
// WebKit meldet für Dokumentfehler teilweise keine Request-URI.
if (error.request == null || error.request!.uri == documentUri) {
_showError(loadId, _loadError);
}
},
onHttpAuthRequest: (request) {
if (!_isCurrentLoad(loadId) ||
_errorMessage != null ||
!access.allowsAuthentication(request.host)) {
request.onCancel();
return;
}
if (_username.isEmpty || _password.isEmpty) {
request.onCancel();
_showError(
loadId,
'Der Zugang zur VHS-Karte ist in dieser App-Version nicht eingerichtet.',
);
return;
}
if (!access.tryAuthentication(request.host)) {
request.onCancel();
_showError(
loadId,
'Die Anmeldung an der VHS-Karte ist fehlgeschlagen.',
);
return;
}
request.onProceed(
const WebViewCredential(user: _username, password: _password),
);
},
),
);
if (!_isCurrentLoad(loadId)) return;
setState(() => _controller = controller);
await controller.loadRequest(widget.uri);
} catch (_) {
_showError(loadId, _loadError);
}
}
@override
void dispose() {
_loadId++;
unawaited(_stopLoading(_controller));
super.dispose();
}
@override
Widget build(BuildContext context) {
final controller = _controller;
final errorMessage = _errorMessage;
return Scaffold(
appBar: AppBar(
title: const Text('VHS-Karte'),
leading: IconButton(
tooltip: 'Schließen',
icon: const Icon(Icons.close),
onPressed: () => Navigator.of(context).pop(),
),
actions: [
IconButton(
tooltip: 'Neu laden',
icon: const Icon(Icons.refresh),
onPressed: _isLoading ? null : _loadMap,
),
],
),
body: SafeArea(
child: errorMessage != null
? Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.error_outline, size: 48),
const SizedBox(height: 16),
Text(errorMessage, textAlign: TextAlign.center),
const SizedBox(height: 16),
FilledButton(
onPressed: _loadMap,
child: const Text('Erneut versuchen'),
),
],
),
),
)
: Column(
children: [
if (_isLoading)
LinearProgressIndicator(
value: _progress == 0 ? null : _progress / 100,
semanticsLabel: 'VHS-Karte wird geladen',
),
Expanded(
child: controller == null
? const Center(child: CircularProgressIndicator())
: WebViewWidget(
key: ValueKey(_loadId),
controller: controller,
),
),
],
),
),
);
}
}
+17 -1
View File
@@ -12,7 +12,7 @@ class ChatMessage {
@Id()
int id = 0;
// External ID from server
// Nachrichten-ID für den Server, getrennt von der lokalen ObjectBox-ID.
@Unique()
late int externalId;
@@ -28,6 +28,22 @@ class ChatMessage {
ChatMessage();
factory ChatMessage.outgoing({
required int externalId,
required int time,
required String from,
required String text,
}) {
// ObjectBox vergibt die lokale ID beim ersten Speichern selbst.
return ChatMessage()
..externalId = externalId
..time = time
..from = from
..subj = ''
..text = text
..read = true;
}
factory ChatMessage.fromJson(Map<String, dynamic> json) {
final message = ChatMessage();
message.id = 0; // Let ObjectBox assign the ID
+3 -1
View File
@@ -15,6 +15,7 @@ class Job {
late String ver;
late int state;
String? crFilter;
String? mapUrl;
late int ordertime;
late int offsWarn;
late bool global;
@@ -63,6 +64,7 @@ class Job {
job.ver = json['ver'] as String;
job.state = json['state'] as int;
job.crFilter = json['cr_filter'] as String?;
job.mapUrl = json['map_url'] as String?;
job.ordertime = json['ordertime'] as int;
job.offsWarn = json['offs_warn'] as int;
job.global = json['global'] as bool;
@@ -86,6 +88,7 @@ class Job {
'ver': ver,
'state': state,
'cr_filter': crFilter,
'map_url': mapUrl,
'ordertime': ordertime,
'offs_warn': offsWarn,
'global': global,
@@ -97,4 +100,3 @@ class Job {
};
}
}
+46 -2
View File
@@ -1,6 +1,16 @@
class TourTask {
/// Untergrenze der im Workflow-Editor vergebenen Typnummern für
/// benutzerdefinierte Typen.
static const int customTypeStart = 1000;
late int sId;
late int type;
/// Typnummer des Basis-Typs bei benutzerdefinierten Typen
/// (>= [customTypeStart]) aus dem Workflow-Editor. `null` bei
/// Standard-Typen und bei Altdaten ohne Angabe.
int? baseType;
late String topic;
bool? opt;
bool? reenter;
@@ -17,15 +27,36 @@ class TourTask {
/// Auswahloptionen eines Status-Tasks (Typ 11).
List<TourChoice>? choices;
/// Eingabefelder einer Text-/Zahlen-Liste (Typ 12/13).
/// Eingabefelder einer Text-/Zahlen-Liste (Typ 13/14).
List<TourField>? fields;
TourTask();
/// Der für Darstellung und Validierung maßgebliche Typ: Standard-Typen
/// unverändert; benutzerdefinierte Typen (>= [customTypeStart]) werden auf
/// ihren Basis-Typ abgebildet. Fehlt {@code base_type} (Altdaten), wird der
/// Basis-Typ aus den typprägenden Eigenschaften abgeleitet.
int get effectiveType {
if (type < customTypeStart) {
return type;
}
if (baseType != null) {
return baseType!;
}
if (choices != null && choices!.isNotEmpty) return 11; // Statusauswahl
if (entries != null && entries!.isNotEmpty) return 15; // Checkbox-Liste
if (fields != null && fields!.isNotEmpty) return 13; // Text-Liste
if (min != null || max != null) return 4; // Fotos
if (maxLen != null) return 8; // Texteingabe (Bemerkung)
if (track != null) return 3; // Ankunft melden
return 12; // Bestätigung per Checkbox
}
factory TourTask.fromJson(Map<String, dynamic> json) {
final task = TourTask();
task.sId = json['s_id'] as int;
task.type = json['type'] as int;
task.baseType = json['base_type'] as int?;
task.topic = json['topic'] as String;
task.opt = json['opt'] as bool? ?? false;
task.reenter = json['reenter'] as bool? ?? false;
@@ -59,6 +90,7 @@ class TourTask {
return {
's_id': sId,
'type': type,
if (baseType != null) 'base_type': baseType,
'topic': topic,
'opt': opt,
'reenter': reenter,
@@ -82,6 +114,10 @@ class TourEntry {
late String text;
late bool queryRemark;
/// Pflicht-Kennzeichen aus dem Workflow-Editor (Checkbox-Liste, Typ 15).
/// `null`, wenn das JSON keine Angabe enthält (Altdaten).
bool? required;
TourEntry();
factory TourEntry.fromJson(Map<String, dynamic> json) {
@@ -89,6 +125,7 @@ class TourEntry {
entry.code = json['code'] as int;
entry.text = json['text'] as String;
entry.queryRemark = json['query_remark'] as bool? ?? false;
entry.required = json['required'] as bool?;
return entry;
}
@@ -97,6 +134,7 @@ class TourEntry {
'code': code,
'text': text,
'query_remark': queryRemark,
if (required != null) 'required': required,
};
}
}
@@ -139,17 +177,22 @@ class TourChoice {
bool get requiresRemark => (quest != null && quest!.trim().isNotEmpty);
}
/// Ein Eingabefeld einer Text-/Zahlen-Liste (Typ 12/13).
/// Ein Eingabefeld einer Text-/Zahlen-Liste (Typ 13/14).
class TourField {
String? title;
String? placeholder;
/// Pflicht-Kennzeichen aus dem Workflow-Editor. `null`, wenn das JSON
/// keine Angabe enthält (Altdaten).
bool? required;
TourField();
factory TourField.fromJson(Map<String, dynamic> json) {
final field = TourField();
field.title = json['title'] as String?;
field.placeholder = json['placeholder'] as String?;
field.required = json['required'] as bool?;
return field;
}
@@ -157,6 +200,7 @@ class TourField {
return {
'title': title,
'placeholder': placeholder,
if (required != null) 'required': required,
};
}
}
+6 -1
View File
@@ -34,7 +34,7 @@
},
{
"id": "3:808686831791135454",
"lastPropertyId": "13:1803006975816940878",
"lastPropertyId": "14:4499212061291648440",
"name": "Job",
"properties": [
{
@@ -104,6 +104,11 @@
"id": "13:1803006975816940878",
"name": "filesJson",
"type": 9
},
{
"id": "14:4499212061291648440",
"name": "mapUrl",
"type": 9
}
],
"relations": []
+17 -3
View File
@@ -60,7 +60,7 @@ final _entities = <obx_int.ModelEntity>[
obx_int.ModelEntity(
id: const obx_int.IdUid(3, 808686831791135454),
name: 'Job',
lastPropertyId: const obx_int.IdUid(13, 1803006975816940878),
lastPropertyId: const obx_int.IdUid(14, 4499212061291648440),
flags: 0,
properties: <obx_int.ModelProperty>[
obx_int.ModelProperty(
@@ -128,6 +128,11 @@ final _entities = <obx_int.ModelEntity>[
id: const obx_int.IdUid(13, 1803006975816940878),
name: 'filesJson',
type: 9,
flags: 0),
obx_int.ModelProperty(
id: const obx_int.IdUid(14, 4499212061291648440),
name: 'mapUrl',
type: 9,
flags: 0)
],
relations: <obx_int.ModelRelation>[],
@@ -542,7 +547,9 @@ obx_int.ModelDefinition getObjectBoxModel() {
final remOffset = fbb.writeString(object.rem);
final toursJsonOffset = fbb.writeString(object.toursJson);
final filesJsonOffset = fbb.writeString(object.filesJson);
fbb.startTable(14);
final mapUrlOffset =
object.mapUrl == null ? null : fbb.writeString(object.mapUrl!);
fbb.startTable(15);
fbb.addInt64(0, object.id);
fbb.addOffset(1, verOffset);
fbb.addInt64(2, object.state);
@@ -556,6 +563,7 @@ obx_int.ModelDefinition getObjectBoxModel() {
fbb.addOffset(10, toursJsonOffset);
fbb.addInt64(11, object.externalId);
fbb.addOffset(12, filesJsonOffset);
fbb.addOffset(13, mapUrlOffset);
fbb.finish(fbb.endTable());
return object.id;
},
@@ -587,7 +595,9 @@ obx_int.ModelDefinition getObjectBoxModel() {
..externalId =
const fb.Int64Reader().vTableGet(buffer, rootOffset, 26, 0)
..filesJson = const fb.StringReader(asciiOptimization: true)
.vTableGet(buffer, rootOffset, 28, '');
.vTableGet(buffer, rootOffset, 28, '')
..mapUrl = const fb.StringReader(asciiOptimization: true)
.vTableGetNullable(buffer, rootOffset, 30);
return object;
}),
@@ -965,6 +975,10 @@ class Job_ {
/// See [Job.filesJson].
static final filesJson =
obx.QueryStringProperty<Job>(_entities[1].properties[12]);
/// See [Job.mapUrl].
static final mapUrl =
obx.QueryStringProperty<Job>(_entities[1].properties[13]);
}
/// [LocationStatus] entity fields to define ObjectBox queries.
+2 -2
View File
@@ -192,7 +192,7 @@ Future<List<Widget>> _fetchJobWidgets(StateSetter dialogState, Function() onFini
style: TextStyle(backgroundColor: isJobTooFarInFuture(job) ? Colors.red : Colors.transparent),
),
),
SizedBox(width: double.infinity, child: Text('${'order_number'.tr}: ${job.id}', textAlign: TextAlign.left)),
SizedBox(width: double.infinity, child: Text('${'order_number'.tr}: ${job.externalId}', textAlign: TextAlign.left)),
SizedBox(width: double.infinity, child: Text(job.tours[0].comp, textAlign: TextAlign.left)),
SizedBox(width: double.infinity, child: Text(job.tours[0].street, textAlign: TextAlign.left)),
SizedBox(width: double.infinity, child: Text(job.tours[0].city, textAlign: TextAlign.left)),
@@ -294,7 +294,7 @@ Future<List<Widget>> _fetchJobWidgets(StateSetter dialogState, Function() onFini
style: TextStyle(backgroundColor: isJobTooFarInFuture(job) ? Colors.red : Colors.transparent),
),
),
SizedBox(width: double.infinity, child: Text('Auftragsnummer: ${job.id}', textAlign: TextAlign.left)),
SizedBox(width: double.infinity, child: Text('Auftragsnummer: ${job.externalId}', textAlign: TextAlign.left)),
SizedBox(width: double.infinity, child: Text(job.tours[0].comp, textAlign: TextAlign.left)),
SizedBox(width: double.infinity, child: Text(job.tours[0].street, textAlign: TextAlign.left)),
SizedBox(width: double.infinity, child: Text(job.tours[0].city, textAlign: TextAlign.left)),
+40 -35
View File
@@ -114,14 +114,13 @@ class _FileListItemState extends State<_FileListItem> {
},
);
} else if (widget.file.isImage) {
// Show image in a dialog on top of the files dialog
// Bildanhänge wie PDFs vergrößern und im gezoomten Zustand verschieben.
await showDialog(
context: context,
builder: (BuildContext context) {
return Dialog(
insetPadding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
AppBar(
title: Text(widget.file.name),
@@ -131,41 +130,47 @@ class _FileListItemState extends State<_FileListItem> {
),
automaticallyImplyLeading: false,
),
Flexible(
child: Image.network(
widget.file.url,
fit: BoxFit.contain,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
);
},
errorBuilder: (context, error, stackTrace) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.error_outline,
size: 64,
color: Colors.red,
Expanded(
child: InteractiveViewer(
minScale: 1,
maxScale: 5,
child: SizedBox.expand(
child: Image.network(
widget.file.url,
fit: BoxFit.contain,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
const SizedBox(height: 16),
Text(
'Fehler beim Laden des Bildes:\n$error',
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.red),
);
},
errorBuilder: (context, error, stackTrace) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.error_outline,
size: 64,
color: Colors.red,
),
const SizedBox(height: 16),
Text(
'Fehler beim Laden des Bildes:\n$error',
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.red),
),
],
),
],
),
);
},
);
},
),
),
),
),
],
+34
View File
@@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
import 'package:stadtbote/content_views/vhs_map_page.dart';
import 'package:stadtbote/model/job.dart';
import 'package:stadtbote/model/job_tour.dart';
import 'package:stadtbote/popups/map_popup.dart';
import 'package:stadtbote/tools/app_localizations.dart';
import 'package:stadtbote/tools/util.dart';
Future<void> showJobMapPopup(
BuildContext context,
Job job, {
JobTour? currentTour,
}) async {
final mapUrl = job.mapUrl?.trim();
if (mapUrl != null && mapUrl.isNotEmpty) {
final uri = Uri.tryParse(mapUrl);
if (uri == null) {
await showAlertDialog('error'.tr, 'Die Kartenadresse ist ungültig.');
return;
}
await Navigator.of(
context,
).push<void>(MaterialPageRoute(builder: (_) => VhsMapPage(uri: uri)));
return;
}
final tours = job.tours;
final tour = currentTour ?? (tours.isNotEmpty ? tours.first : null);
if (tour?.pos == null) {
await showAlertDialog('information'.tr, 'no_position'.tr);
return;
}
await showMapPopup(context, tour!, tours);
}
+8 -3
View File
@@ -13,7 +13,7 @@ showSignaturePopup(BuildContext context, TaskInfo taskInfo, Function(String?) on
canPop: false,
onPopInvokedWithResult: (bool didPop, dynamic result) async {
if (!didPop) {
final shouldPop = await taskDialogSecurityException(context);
final shouldPop = await taskDialogSecurityException(context, language: AppLanguage.german);
if (shouldPop) {
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
if (context.mounted) {
@@ -79,13 +79,18 @@ showSignaturePopup(BuildContext context, TaskInfo taskInfo, Function(String?) on
),
),
getDefaultButton(
'apply'.tr,
AppLocalizations.tr('apply', language: AppLanguage.german),
buttonHeight: 50,
() => {
if (controller.points.length > 50)
{Navigator.of(context).pop(), onFinished(convertPoints(controller.points))}
else
{showAlertDialog('error'.tr, 'signature_too_short'.tr)}
{
showAlertDialog(
AppLocalizations.tr('error', language: AppLanguage.german),
AppLocalizations.tr('signature_too_short', language: AppLanguage.german),
)
}
})
],
),
+12 -12
View File
@@ -56,8 +56,8 @@ class AppLocalizations {
}
// Hilfsmethode für einfachen Zugriff
static String tr(String key) {
return _instance._translations[_instance._currentLanguage.code]?[key] ??
static String tr(String key, {AppLanguage? language}) {
return _instance._translations[(language ?? _instance._currentLanguage).code]?[key] ??
_instance._translations['de']?[key] ??
key;
}
@@ -145,7 +145,7 @@ class AppLocalizations {
'unknown': 'Unbekannt',
// Aufgabenliste
'task_list': 'Liste der Aufgaben',
'task_list': 'Workflow',
'commission_number_short': 'Komm-#',
'photos_taken': 'Fotos aufgenommen',
'signature_captured': 'Unterschrift erfasst',
@@ -287,7 +287,7 @@ class AppLocalizations {
'unknown': 'Unknown',
// Task List
'task_list': 'Task List',
'task_list': 'Workflow',
'commission_number_short': 'Comm-#',
'photos_taken': 'Photos taken',
'signature_captured': 'Signature captured',
@@ -418,7 +418,7 @@ class AppLocalizations {
'unknown': 'Desconocido',
// Task List
'task_list': 'Lista de Tareas',
'task_list': 'Workflow',
'commission_number_short': 'Núm. Com.',
'photos_taken': 'Fotos tomadas',
'signature_captured': 'Firma capturada',
@@ -539,7 +539,7 @@ class AppLocalizations {
'unknown': 'Inconnu',
// Task List
'task_list': 'Liste des Tâches',
'task_list': 'Workflow',
'commission_number_short': 'N° Comm.',
'photos_taken': 'Photos prises',
'signature_captured': 'Signature capturée',
@@ -660,7 +660,7 @@ class AppLocalizations {
'unknown': 'Nieznany',
// Task List
'task_list': 'Lista Zadań',
'task_list': 'Workflow',
'commission_number_short': 'Nr Zlec.',
'photos_taken': 'Zrobione zdjęcia',
'signature_captured': 'Podpis zapisany',
@@ -781,7 +781,7 @@ class AppLocalizations {
'unknown': 'Неизвестно',
// Task List
'task_list': 'Список Задач',
'task_list': 'Workflow',
'commission_number_short': '№ Комм.',
'photos_taken': 'Фотографии сделаны',
'signature_captured': 'Подпись сохранена',
@@ -902,7 +902,7 @@ class AppLocalizations {
'unknown': 'Bilinmiyor',
// Task List
'task_list': 'Görev Listesi',
'task_list': 'Workflow',
'commission_number_short': 'Kom. No.',
'photos_taken': 'Fotoğraflar çekildi',
'signature_captured': 'İmza kaydedildi',
@@ -1023,7 +1023,7 @@ class AppLocalizations {
'unknown': 'Teadmata',
// Task List
'task_list': 'Ülesannete Nimekiri',
'task_list': 'Workflow',
'commission_number_short': 'Kom. Nr.',
'photos_taken': 'Fotod tehtud',
'signature_captured': 'Allkiri salvestatud',
@@ -1144,7 +1144,7 @@ class AppLocalizations {
'unknown': 'Nezināms',
// Task List
'task_list': 'Uzdevumu Saraksts',
'task_list': 'Workflow',
'commission_number_short': 'Kom. Nr.',
'photos_taken': 'Fotogrāfijas uzņemtas',
'signature_captured': 'Paraksts saglabāts',
@@ -1265,7 +1265,7 @@ class AppLocalizations {
'unknown': 'Nežinoma',
// Task List
'task_list': 'Užduočių Sąrašas',
'task_list': 'Workflow',
'commission_number_short': 'Kom. Nr.',
'photos_taken': 'Nuotraukos padarytos',
'signature_captured': 'Parašas užfiksuotas',
-1
View File
@@ -16,4 +16,3 @@ class MessageHeader {
Map<String, dynamic> toJson() => _$MessageHeaderToJson(this);
}
+5 -5
View File
@@ -489,22 +489,22 @@ bool isJobTooFarInFuture(Job job) {
return ordertime > dateTimeThreshold;
}
Future<bool> taskDialogSecurityException(BuildContext context) async {
Future<bool> taskDialogSecurityException(BuildContext context, {AppLanguage? language}) async {
final result = await showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (dialogContext) {
return AlertDialog(
title: Text('attention'.tr),
content: Text('close_dialog_warning'.tr),
title: Text(AppLocalizations.tr('attention', language: language)),
content: Text(AppLocalizations.tr('close_dialog_warning', language: language)),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(true),
child: Text('close'.tr),
child: Text(AppLocalizations.tr('close', language: language)),
),
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(false),
child: Text('back'.tr),
child: Text(AppLocalizations.tr('back', language: language)),
),
],
);
+33
View File
@@ -0,0 +1,33 @@
class VhsMapAccess {
VhsMapAccess(this.uri);
final Uri uri;
static const int maxAuthenticationAttempts = 3;
int _authenticationAttempts = 0;
static bool _isValidUri(Uri value) =>
value.scheme == 'https' &&
value.hasAuthority &&
value.host.isNotEmpty &&
value.userInfo.isEmpty &&
value.port > 0 &&
value.port <= 65535;
bool get isValid => _isValidUri(uri);
bool allowsNavigation(Uri target) =>
isValid && _isValidUri(target) && target.origin == uri.origin;
// Die WebView liefert bei Authentifizierungsanfragen nur Host und Realm.
bool allowsAuthentication(String host) =>
isValid && host.toLowerCase() == uri.host.toLowerCase();
bool tryAuthentication(String host) {
if (!allowsAuthentication(host) ||
_authenticationAttempts >= maxAuthenticationAttempts) {
return false;
}
_authenticationAttempts++;
return true;
}
}
@@ -9,6 +9,7 @@
#include <audioplayers_linux/audioplayers_linux_plugin.h>
#include <file_selector_linux/file_selector_plugin.h>
#include <objectbox_flutter_libs/objectbox_flutter_libs_plugin.h>
#include <permission_manager/permission_manager_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
@@ -21,6 +22,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) objectbox_flutter_libs_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "ObjectboxFlutterLibsPlugin");
objectbox_flutter_libs_plugin_register_with_registrar(objectbox_flutter_libs_registrar);
g_autoptr(FlPluginRegistrar) permission_manager_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "PermissionManagerPlugin");
permission_manager_plugin_register_with_registrar(permission_manager_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
+2 -1
View File
@@ -6,11 +6,12 @@ list(APPEND FLUTTER_PLUGIN_LIST
audioplayers_linux
file_selector_linux
objectbox_flutter_libs
permission_manager
url_launcher_linux
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
pdfium_flutter
jni
)
set(PLUGIN_BUNDLED_LIBRARIES)
@@ -11,9 +11,11 @@ import file_selector_macos
import geolocator_apple
import objectbox_flutter_libs
import package_info_plus
import path_provider_foundation
import pdfium_flutter
import permission_manager
import shared_preferences_foundation
import url_launcher_macos
import webview_flutter_wkwebview
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin"))
@@ -22,7 +24,9 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
ObjectboxFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "ObjectboxFlutterLibsPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
PDFiumFlutterPlugin.register(with: registry.registrar(forPlugin: "PDFiumFlutterPlugin"))
PermissionManagerPlugin.register(with: registry.registrar(forPlugin: "PermissionManagerPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin"))
}
+233 -153
View File
@@ -29,10 +29,10 @@ packages:
dependency: "direct main"
description:
name: archive
sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff
url: "https://pub.dev"
source: hosted
version: "4.0.7"
version: "4.0.9"
args:
dependency: transitive
description:
@@ -45,74 +45,74 @@ packages:
dependency: transitive
description:
name: async
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.dev"
source: hosted
version: "2.13.0"
version: "2.13.1"
audioplayers:
dependency: "direct main"
description:
name: audioplayers
sha256: "5441fa0ceb8807a5ad701199806510e56afde2b4913d9d17c2f19f2902cf0ae4"
sha256: "2ba4bb2944baacbdd5372ff8254a8e7feb8c10d7739545e392f5605a8f618745"
url: "https://pub.dev"
source: hosted
version: "6.5.1"
version: "6.8.1"
audioplayers_android:
dependency: transitive
description:
name: audioplayers_android
sha256: "60a6728277228413a85755bd3ffd6fab98f6555608923813ce383b190a360605"
sha256: f5ff5b15620fbab8cb0849e9636c48e2b96c3f0f71723bbbe2ad3c761b205f05
url: "https://pub.dev"
source: hosted
version: "5.2.1"
version: "5.3.0"
audioplayers_darwin:
dependency: transitive
description:
name: audioplayers_darwin
sha256: "0811d6924904ca13f9ef90d19081e4a87f7297ddc19fc3d31f60af1aaafee333"
sha256: "1ca553add991384ecf421b9569da850f3ab2472ffb83f6970b0416365abc51be"
url: "https://pub.dev"
source: hosted
version: "6.3.0"
version: "6.5.0"
audioplayers_linux:
dependency: transitive
description:
name: audioplayers_linux
sha256: f75bce1ce864170ef5e6a2c6a61cd3339e1a17ce11e99a25bae4474ea491d001
sha256: "15178b726b7cdee5364d0463c8d445630c4e0fb7d26612b73c767e7d25de9417"
url: "https://pub.dev"
source: hosted
version: "4.2.1"
version: "4.3.0"
audioplayers_platform_interface:
dependency: transitive
description:
name: audioplayers_platform_interface
sha256: "0e2f6a919ab56d0fec272e801abc07b26ae7f31980f912f24af4748763e5a656"
sha256: "765f6f0e6dca55cb471c9483fc77700564b3484d19198aca4ebb5147c6c85acb"
url: "https://pub.dev"
source: hosted
version: "7.1.1"
version: "7.2.0"
audioplayers_web:
dependency: transitive
description:
name: audioplayers_web
sha256: "1c0f17cec68455556775f1e50ca85c40c05c714a99c5eb1d2d57cc17ba5522d7"
sha256: ae1e0103c865a03e273f6d13d97b93f5595eac09915729cd5e37ef96e2857319
url: "https://pub.dev"
source: hosted
version: "5.1.1"
version: "5.3.0"
audioplayers_windows:
dependency: transitive
description:
name: audioplayers_windows
sha256: "4048797865105b26d47628e6abb49231ea5de84884160229251f37dfcbe52fd7"
sha256: a70ae82bba2dfcb6eb03dd4815d737a2d46d33ea5a96a03f535cfcaac490e413
url: "https://pub.dev"
source: hosted
version: "4.2.1"
version: "4.4.1"
badges:
dependency: "direct main"
description:
name: badges
sha256: a7b6bbd60dce418df0db3058b53f9d083c22cdb5132a052145dc267494df0b84
sha256: cf1c88fb3777df69ccd630b80de5267f54efa4a39381b0404a7c03d56cb7c041
url: "https://pub.dev"
source: hosted
version: "3.1.2"
version: "3.2.0"
boolean_selector:
dependency: transitive
description:
@@ -141,10 +141,10 @@ packages:
dependency: transitive
description:
name: build_daemon
sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957
sha256: fd754058c342243718d5171a95f352cfc9fcf0cba8cfa26df67cb13a5836db78
url: "https://pub.dev"
source: hosted
version: "4.1.1"
version: "4.1.2"
build_resolvers:
dependency: transitive
description:
@@ -181,26 +181,26 @@ packages:
dependency: transitive
description:
name: built_value
sha256: "426cf75afdb23aa74bd4e471704de3f9393f3c7b04c1e2d9c6f1073ae0b8b139"
sha256: "34e4067d30ce212937df995f03b69992eea683539ceeac7f679a1f1eba055b56"
url: "https://pub.dev"
source: hosted
version: "8.12.1"
version: "8.12.6"
carousel_slider:
dependency: "direct main"
description:
name: carousel_slider
sha256: bcc61735345c9ab5cb81073896579e735f81e35fd588907a393143ea986be8ff
sha256: febf4b0163e0242adc13d7a863b04965351f59e7dfea56675c7c2caa7bcd7476
url: "https://pub.dev"
source: hosted
version: "5.1.1"
version: "5.1.2"
characters:
dependency: transitive
description:
name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.0"
version: "1.4.1"
charcode:
dependency: transitive
description:
@@ -233,6 +233,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.2"
code_assets:
dependency: transitive
description:
name: code_assets
sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8
url: "https://pub.dev"
source: hosted
version: "1.2.1"
code_builder:
dependency: transitive
description:
@@ -261,10 +269,10 @@ packages:
dependency: transitive
description:
name: cross_file
sha256: "701dcfc06da0882883a2657c445103380e53e647060ad8d9dfb710c100996608"
sha256: "92c9c43c383bfa1c32079d3bc492d55d6d4318044b7b47edaff8971cbb555c51"
url: "https://pub.dev"
source: hosted
version: "0.3.5+1"
version: "0.3.5+4"
crypto:
dependency: transitive
description:
@@ -285,10 +293,10 @@ packages:
dependency: "direct main"
description:
name: cupertino_icons
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd"
url: "https://pub.dev"
source: hosted
version: "1.0.8"
version: "1.0.9"
dart_earcut:
dependency: transitive
description:
@@ -349,26 +357,26 @@ packages:
dependency: transitive
description:
name: dio
sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9
sha256: ea2bad3c89a27635ce2d85cce4d6b199da49a5a48ec77b03e45b65a3b90922b0
url: "https://pub.dev"
source: hosted
version: "5.9.0"
version: "5.10.0"
dio_web_adapter:
dependency: transitive
description:
name: dio_web_adapter
sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78"
sha256: dd58dc3861eb36edb13b217efc006a1c21e5bbc341de8c229b85634fa5e362e4
url: "https://pub.dev"
source: hosted
version: "2.1.1"
version: "2.2.0"
equatable:
dependency: transitive
description:
name: equatable
sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b"
sha256: "3bce007a596ff8b3119c45d68aaef631272537c03d30e5d4534dd24bf4c5eaa2"
url: "https://pub.dev"
source: hosted
version: "2.0.8"
version: "2.1.0"
event_bus:
dependency: transitive
description:
@@ -397,10 +405,10 @@ packages:
dependency: transitive
description:
name: ffi
sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
version: "2.2.0"
file:
dependency: transitive
description:
@@ -450,7 +458,7 @@ packages:
source: hosted
version: "1.1.1"
flat_buffers:
dependency: transitive
dependency: "direct dev"
description:
name: flat_buffers
sha256: "380bdcba5664a718bfd4ea20a45d39e13684f5318fcd8883066a55e21f37f4c3"
@@ -514,34 +522,34 @@ packages:
dependency: "direct main"
description:
name: flutter_map
sha256: "391e7dc95cc3f5190748210a69d4cfeb5d8f84dcdfa9c3235d0a9d7742ccb3f8"
sha256: ce1debbb29cade61964334b2a19c7c76e5bb2ef7dacf126c6424ee44036232f8
url: "https://pub.dev"
source: hosted
version: "8.2.2"
version: "8.3.1"
flutter_osm_interface:
dependency: transitive
description:
name: flutter_osm_interface
sha256: fe7b02fee205aec861d0f08f643d4d6c49a5b9df06d3b43919fdf6699a275829
sha256: "98cc8855f3d94e1c953b72c31ae594221d54a5ef1b5a6689f8c0df2f613fe356"
url: "https://pub.dev"
source: hosted
version: "1.4.0"
version: "1.5.0"
flutter_osm_plugin:
dependency: "direct main"
description:
name: flutter_osm_plugin
sha256: a35b76082818d457c750eaf071d9dbbbb49ee3a0092be6bf7ddcd0c345396b86
sha256: "2c2be585a23f9e213993e82765b66fe04c1bf1e7f337ee11ff4b8ee5fb5cd2dd"
url: "https://pub.dev"
source: hosted
version: "1.3.8"
version: "1.4.5"
flutter_osm_web:
dependency: transitive
description:
name: flutter_osm_web
sha256: "8542dc76aee29479b75fc79861ac748cd0a58482b71f0dfd116af6306e377300"
sha256: "4b457aed05915a4af820b74e8bd387a8d1b562b538b4cac14f8fadee9fffa80e"
url: "https://pub.dev"
source: hosted
version: "1.3.5"
version: "1.4.4"
flutter_parsed_text:
dependency: transitive
description:
@@ -554,18 +562,18 @@ packages:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1
sha256: "3854fe5e3bff0b113c658f260b90c95dea17c92db0f2addeac2e343dd9969785"
url: "https://pub.dev"
source: hosted
version: "2.0.33"
version: "2.0.35"
flutter_svg:
dependency: transitive
description:
name: flutter_svg
sha256: "87fbd7c534435b6c5d9d98b01e1fd527812b82e68ddd8bd35fc45ed0fa8f0a95"
sha256: "35882981abcbfb8c15b286f0cd690ff25bac12d95eff3e25ee207f37d4c42e7f"
url: "https://pub.dev"
source: hosted
version: "2.2.3"
version: "2.3.0"
flutter_test:
dependency: "direct dev"
description: flutter
@@ -604,26 +612,26 @@ packages:
dependency: transitive
description:
name: geolocator_apple
sha256: dbdd8789d5aaf14cf69f74d4925ad1336b4433a6efdf2fce91e8955dc921bf22
sha256: "853803d6bb1713c094e935b4a5ae5f19c0308acf81da13fa9ff84fb4c70c0b73"
url: "https://pub.dev"
source: hosted
version: "2.3.13"
version: "2.3.14"
geolocator_platform_interface:
dependency: transitive
description:
name: geolocator_platform_interface
sha256: "30cb64f0b9adcc0fb36f628b4ebf4f731a2961a0ebd849f4b56200205056fe67"
sha256: cdb082e4f048b69da244117b7914cc60d2a8897546ffaa4f2529c786ded7aee2
url: "https://pub.dev"
source: hosted
version: "4.2.6"
version: "4.2.8"
geolocator_web:
dependency: transitive
description:
name: geolocator_web
sha256: b1ae9bdfd90f861fde8fd4f209c37b953d65e92823cb73c7dee1fa021b06f172
sha256: "19e485a0f8d6a88abcf9c53cba3a4105e14b7435ed8ac1c108c067b938fe8429"
url: "https://pub.dev"
source: hosted
version: "4.1.3"
version: "4.1.4"
geolocator_windows:
dependency: transitive
description:
@@ -656,6 +664,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.2"
hooks:
dependency: transitive
description:
name: hooks
sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
html:
dependency: transitive
description:
@@ -692,26 +708,26 @@ packages:
dependency: "direct main"
description:
name: image
sha256: "492bd52f6c4fbb6ee41f781ff27765ce5f627910e1e0cbecfa3d9add5562604c"
sha256: "6300175e00616bbc832e2fc91bfa4d776af5402c81c7151bee6905bb08473c52"
url: "https://pub.dev"
source: hosted
version: "4.7.2"
version: "4.9.1"
image_picker:
dependency: "direct main"
description:
name: image_picker
sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320"
sha256: d8402284df184bc05f4a2210c6c23983b0720f4cd87cbd05c5390a78af602667
url: "https://pub.dev"
source: hosted
version: "1.2.1"
version: "1.2.3"
image_picker_android:
dependency: transitive
description:
name: image_picker_android
sha256: "5e9bf126c37c117cf8094215373c6d561117a3cfb50ebc5add1a61dc6e224677"
sha256: "6f3a1995eafb000333174fae92202622033b0ee7fd917a6cd3730295264df84a"
url: "https://pub.dev"
source: hosted
version: "0.8.13+10"
version: "0.8.13+19"
image_picker_for_web:
dependency: transitive
description:
@@ -724,10 +740,10 @@ packages:
dependency: transitive
description:
name: image_picker_ios
sha256: "956c16a42c0c708f914021666ffcd8265dde36e673c9fa68c81f7d085d9774ad"
sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588
url: "https://pub.dev"
source: hosted
version: "0.8.13+3"
version: "0.8.13+6"
image_picker_linux:
dependency: transitive
description:
@@ -764,10 +780,10 @@ packages:
dependency: "direct main"
description:
name: intl
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
sha256: "1ca20c894b1717686a2319b8548763d812bc0aabdac580420a44c5178c57a867"
url: "https://pub.dev"
source: hosted
version: "0.20.2"
version: "0.20.3"
io:
dependency: transitive
description:
@@ -776,6 +792,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.5"
jni:
dependency: transitive
description:
name: jni
sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f
url: "https://pub.dev"
source: hosted
version: "1.0.0"
jni_flutter:
dependency: transitive
description:
name: jni_flutter
sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
js:
dependency: transitive
description:
@@ -848,22 +880,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.1.1"
lists:
dependency: transitive
description:
name: lists
sha256: "4ca5c19ae4350de036a7e996cdd1ee39c93ac0a2b840f4915459b7d0a7d4ab27"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
logger:
dependency: transitive
description:
name: logger
sha256: a7967e31b703831a893bbc3c3dd11db08126fe5f369b5c648a36f821979f5be3
url: "https://pub.dev"
source: hosted
version: "2.6.2"
logging:
dependency: transitive
description:
@@ -876,34 +892,34 @@ packages:
dependency: transitive
description:
name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev"
source: hosted
version: "0.12.17"
version: "0.12.19"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.11.1"
version: "0.13.0"
meta:
dependency: transitive
description:
name: meta
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
url: "https://pub.dev"
source: hosted
version: "1.17.0"
version: "1.18.0"
mgrs_dart:
dependency: transitive
description:
name: mgrs_dart
sha256: fb89ae62f05fa0bb90f70c31fc870bcbcfd516c843fb554452ab3396f78586f7
sha256: "385e7168ecc77eb545220223c49eef8ab249da7bf57f22781c40a04d23fb196f"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
version: "3.0.0"
mime:
dependency: "direct main"
description:
@@ -916,10 +932,10 @@ packages:
dependency: "direct main"
description:
name: mqtt5_client
sha256: "3cbb546019d455330494357c922b5c7758c622f0e6f3955146dd2712fa8f7f7a"
sha256: a716e6bddc4715cd585ac7147383080dffafb6b7d55f370dc596b38c85e8c557
url: "https://pub.dev"
source: hosted
version: "4.15.2"
version: "4.17.1"
objectbox:
dependency: "direct main"
description:
@@ -944,6 +960,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.0"
objective_c:
dependency: transitive
description:
name: objective_c
sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed"
url: "https://pub.dev"
source: hosted
version: "9.4.1"
package_config:
dependency: transitive
description:
@@ -988,42 +1012,42 @@ packages:
dependency: "direct main"
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825
url: "https://pub.dev"
source: hosted
version: "2.1.5"
version: "2.1.6"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e
sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd"
url: "https://pub.dev"
source: hosted
version: "2.2.22"
version: "2.3.1"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4"
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
url: "https://pub.dev"
source: hosted
version: "2.5.1"
version: "2.6.0"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16"
url: "https://pub.dev"
source: hosted
version: "2.2.1"
version: "2.2.2"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
version: "2.1.3"
path_provider_windows:
dependency: transitive
description:
@@ -1036,34 +1060,34 @@ packages:
dependency: transitive
description:
name: pdfium_dart
sha256: f1683b9070ddc5c9189c6ee008c285791da66328ce1b882c7162d3393f3a4a74
sha256: "86e95c66b09f3245b95c4924f2edce8d4f7c9786876e5c5ee8e36b104a94bbb0"
url: "https://pub.dev"
source: hosted
version: "0.1.3"
version: "0.2.5"
pdfium_flutter:
dependency: transitive
description:
name: pdfium_flutter
sha256: "0c8b7d5d11d20a1486eade599648e907067568955bd14a1b06de076a968b60a1"
sha256: "0b115c0917aef9cf6bb9c4d47d0efc61d97f44844cd76287c921f997c26cf15d"
url: "https://pub.dev"
source: hosted
version: "0.1.9"
version: "0.2.3"
pdfrx:
dependency: "direct main"
description:
name: pdfrx
sha256: "82e719f7e7994cb78344a1d3c0e204ffdf86ddf9364ff495368acdae6e3f489f"
sha256: acce61944c31bd36f0c3031b1bbf41f59d0b0d4e5e27614fbdc74ff093d55464
url: "https://pub.dev"
source: hosted
version: "2.2.20"
version: "2.4.7"
pdfrx_engine:
dependency: transitive
description:
name: pdfrx_engine
sha256: a28e9d4436dcb9a659b50f71ca1eed72f30b5c45340a9b68299fa904e735128c
sha256: ef8f8cfa64255bfc91a559dd2d32d7cfe7a0f8003bac297bbf9a0aed1182fae5
url: "https://pub.dev"
source: hosted
version: "0.3.7"
version: "0.4.6"
permission_handler:
dependency: "direct main"
description:
@@ -1084,10 +1108,10 @@ packages:
dependency: transitive
description:
name: permission_handler_apple
sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023
sha256: "79dfa1df734798aa3cfdad166d3a3698c206d8813de13516ea1071b5d7e2f420"
url: "https://pub.dev"
source: hosted
version: "9.4.7"
version: "9.4.10"
permission_handler_html:
dependency: transitive
description:
@@ -1112,14 +1136,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.2.1"
permission_manager:
dependency: transitive
description:
name: permission_manager
sha256: c3ad9951dd23fa713559a6dde3974421273b53cf62fe0088d2c860260fa03623
url: "https://pub.dev"
source: hosted
version: "2.0.9"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1"
sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
url: "https://pub.dev"
source: hosted
version: "7.0.1"
version: "7.0.2"
photo_view:
dependency: transitive
description:
@@ -1164,18 +1196,18 @@ packages:
dependency: transitive
description:
name: posix
sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61"
sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07"
url: "https://pub.dev"
source: hosted
version: "6.0.3"
version: "6.5.0"
proj4dart:
dependency: transitive
description:
name: proj4dart
sha256: c8a659ac9b6864aa47c171e78d41bbe6f5e1d7bd790a5814249e6b68bc44324e
sha256: ddcedc1f7876e62717de43ab3491e2829bdad0b028261805f94aa080967e5859
url: "https://pub.dev"
source: hosted
version: "2.1.0"
version: "3.0.0"
pub_semver:
dependency: transitive
description:
@@ -1192,6 +1224,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.5.0"
record_use:
dependency: transitive
description:
name: record_use
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
url: "https://pub.dev"
source: hosted
version: "0.6.0"
routing_client_dart:
dependency: transitive
description:
@@ -1228,18 +1268,18 @@ packages:
dependency: "direct main"
description:
name: shared_preferences
sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64"
sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf
url: "https://pub.dev"
source: hosted
version: "2.5.4"
version: "2.5.5"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "83af5c682796c0f7719c2bbf74792d113e40ae97981b8f266fa84574573556bc"
sha256: "0634e64bd719f89c012f392938e173521f535d3ecaf66558fa94a056d22b5cc7"
url: "https://pub.dev"
source: hosted
version: "2.4.18"
version: "2.4.27"
shared_preferences_foundation:
dependency: transitive
description:
@@ -1260,10 +1300,10 @@ packages:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
version: "2.4.2"
shared_preferences_web:
dependency: transitive
description:
@@ -1304,6 +1344,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.3.0"
simple_sparse_list:
dependency: transitive
description:
name: simple_sparse_list
sha256: aa648fd240fa39b49dcd11c19c266990006006de6699a412de485695910fbc1f
url: "https://pub.dev"
source: hosted
version: "0.1.4"
sky_engine:
dependency: transitive
description: flutter
@@ -1329,10 +1377,10 @@ packages:
dependency: transitive
description:
name: source_span
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev"
source: hosted
version: "1.10.1"
version: "1.10.2"
stack_trace:
dependency: transitive
description:
@@ -1369,10 +1417,10 @@ packages:
dependency: "direct main"
description:
name: synchronized
sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0
sha256: "61894a1956de6b4fc1aefd0892e109514a1a706cbece3ac59decd90ff5a7a423"
url: "https://pub.dev"
source: hosted
version: "3.4.0"
version: "3.4.1+1"
term_glyph:
dependency: transitive
description:
@@ -1385,10 +1433,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
url: "https://pub.dev"
source: hosted
version: "0.7.7"
version: "0.7.11"
timing:
dependency: transitive
description:
@@ -1409,10 +1457,10 @@ packages:
dependency: transitive
description:
name: unicode
sha256: "0f69e46593d65245774d4f17125c6084d2c20b4e473a983f6e21b7d7762218f1"
sha256: a6f7bcfc8ea1d5ce1f6c0b1c39117a9919f4953edd9fd7a64090a9796c499b57
url: "https://pub.dev"
source: hosted
version: "0.3.1"
version: "1.1.9"
universal_html:
dependency: transitive
description:
@@ -1441,18 +1489,18 @@ packages:
dependency: transitive
description:
name: url_launcher_android
sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611"
sha256: b413d49b73867ac08dd2f9890efd3cc11f2a0e577618d50843440a1fb3776c32
url: "https://pub.dev"
source: hosted
version: "6.3.28"
version: "6.3.32"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: cfde38aa257dae62ffe79c87fab20165dfdf6988c1d31b58ebf59b9106062aad
sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0"
url: "https://pub.dev"
source: hosted
version: "6.3.6"
version: "6.4.1"
url_launcher_linux:
dependency: transitive
description:
@@ -1481,10 +1529,10 @@ packages:
dependency: transitive
description:
name: url_launcher_web
sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2"
sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
version: "2.4.3"
url_launcher_windows:
dependency: transitive
description:
@@ -1497,18 +1545,18 @@ packages:
dependency: "direct main"
description:
name: uuid
sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8
sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd"
url: "https://pub.dev"
source: hosted
version: "4.5.2"
version: "4.6.0"
vector_graphics:
dependency: transitive
description:
name: vector_graphics
sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6
sha256: "2306c03da2ba81724afeb589c351ebbc0aa7d86005925be8f8735856dbe5e42d"
url: "https://pub.dev"
source: hosted
version: "1.1.19"
version: "1.2.2"
vector_graphics_codec:
dependency: transitive
description:
@@ -1521,10 +1569,10 @@ packages:
dependency: transitive
description:
name: vector_graphics_compiler
sha256: d354a7ec6931e6047785f4db12a1f61ec3d43b207fc0790f863818543f8ff0dc
sha256: "142a9146f447d15b10bdc00e21d5f4d83e5b32bb5f8f8f5a04c75311344923a3"
url: "https://pub.dev"
source: hosted
version: "1.1.19"
version: "1.2.6"
vector_math:
dependency: transitive
description:
@@ -1545,10 +1593,10 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
url: "https://pub.dev"
source: hosted
version: "15.0.2"
version: "15.2.0"
watcher:
dependency: transitive
description:
@@ -1581,6 +1629,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.3"
webview_flutter:
dependency: "direct main"
description:
name: webview_flutter
sha256: d53e1ccf5516f25017e3c9d44c39034db352d20fa34fe200674270242c2c5111
url: "https://pub.dev"
source: hosted
version: "4.14.1"
webview_flutter_android:
dependency: transitive
description:
name: webview_flutter_android
sha256: "4de8b3d1ff4ebe1bdb42e68a5e4f809194a3cb0117a8f495f590004f00da3964"
url: "https://pub.dev"
source: hosted
version: "4.14.1"
webview_flutter_platform_interface:
dependency: transitive
description:
name: webview_flutter_platform_interface
sha256: "1221c1b12f5278791042f2ec2841743784cf25c5a644e23d6680e5d718824f04"
url: "https://pub.dev"
source: hosted
version: "2.15.1"
webview_flutter_wkwebview:
dependency: transitive
description:
name: webview_flutter_wkwebview
sha256: "0f784be1b080e5b918ef7c2c070c1f4616899fd23f1d001feb74b4978f92bdbc"
url: "https://pub.dev"
source: hosted
version: "3.26.2"
win32:
dependency: transitive
description:
@@ -1617,10 +1697,10 @@ packages:
dependency: transitive
description:
name: xml
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
sha256: "67f0aff7be013d107995e9b75bf4e7f2c3ef2dfdb2c8e68024bba0a7fd5756a4"
url: "https://pub.dev"
source: hosted
version: "6.6.1"
version: "7.0.1"
yaml:
dependency: transitive
description:
@@ -1630,5 +1710,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.9.0 <4.0.0"
flutter: ">=3.35.1"
dart: ">=3.12.0 <4.0.0"
flutter: ">=3.44.0"
+3 -1
View File
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 354.0.0+354
version: 357.0.0+357
environment:
sdk: '>=3.9.0 <4.0.0'
@@ -56,6 +56,7 @@ dependencies:
geolocator: ^13.0.2
expandable: ^5.0.1
url_launcher: ^6.2.4
webview_flutter: ^4.13.0
audioplayers: ^6.2.0
flutter_launcher_icons: ^0.14.3
package_info_plus: ^8.0.2
@@ -72,6 +73,7 @@ dependencies:
dev_dependencies:
flutter_test:
sdk: flutter
flat_buffers: ^23.5.26
build_runner: ^2.4.13
objectbox_generator: ^4.0.3
json_serializable: ^6.6.2
+86
View File
@@ -0,0 +1,86 @@
import 'package:flat_buffers/flat_buffers.dart' as fb;
import 'package:flutter_test/flutter_test.dart';
import 'package:objectbox/internal.dart' show EntityDefinition;
import 'package:stadtbote/model/chat_message.dart';
import 'package:stadtbote/objectbox.g.dart';
void main() {
group('ChatMessage IDs', () {
test('initialisiert ausgehende Nachrichten vollständig', () {
final message = _outgoingMessage();
expect(message.id, 0);
expect(message.toJson(), {
'id': 42,
'time': 1791366832,
'from': 'courier-123',
'subj': '',
'text': 'Auftrag erledigt',
'read': true,
});
});
test('serialisiert ausgehende Nachrichten für ObjectBox ohne Fehler', () {
final message = _outgoingMessage();
final definition = getObjectBoxModel().bindings[ChatMessage]!
as EntityDefinition<ChatMessage>;
final builder = fb.Builder();
expect(definition.objectToFB(message, builder), 0);
final buffer = fb.BufferContext.fromBytes(builder.buffer);
final root = buffer.derefObject(0);
const intReader = fb.Int64Reader();
const stringReader = fb.StringReader();
expect(intReader.vTableGet(buffer, root, 4, -1), 0);
expect(intReader.vTableGet(buffer, root, 6, -1), 42);
expect(intReader.vTableGet(buffer, root, 8, -1), 1791366832);
expect(stringReader.vTableGet(buffer, root, 10, ''), 'courier-123');
expect(stringReader.vTableGet(buffer, root, 12, 'missing'), '');
expect(stringReader.vTableGet(buffer, root, 14, ''), 'Auftrag erledigt');
expect(const fb.BoolReader().vTableGet(buffer, root, 16, false), isTrue);
});
test('verwendet nach lokaler ID-Vergabe weiterhin die Nachrichten-ID', () {
final message = _outgoingMessage();
final definition = getObjectBoxModel().bindings[ChatMessage]!
as EntityDefinition<ChatMessage>;
definition.setId(message, 99);
expect(message.id, 99);
expect(message.externalId, 42);
expect(message.toJson()['id'], 42);
});
test('übernimmt empfangene Server-IDs getrennt von der lokalen ID', () {
final message = ChatMessage.fromJson({
'id': 21891302,
'time': 1791366832,
'from': 'Zentrale',
'subj': 'Rückfrage',
'text': 'Bitte melden',
});
expect(message.id, 0);
expect(message.externalId, 21891302);
expect(message.read, isFalse);
expect(message.toJson(), {
'id': 21891302,
'time': 1791366832,
'from': 'Zentrale',
'subj': 'Rückfrage',
'text': 'Bitte melden',
'read': false,
});
});
});
}
ChatMessage _outgoingMessage() => ChatMessage.outgoing(
externalId: 42,
time: 1791366832,
from: 'courier-123',
text: 'Auftrag erledigt',
);
+51
View File
@@ -0,0 +1,51 @@
import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:stadtbote/model/job.dart';
void main() {
group('Job map_url', () {
test('erhält die Server-URL einschließlich Query und Fragment', () {
const mapUrl = 'https://vhs.example.invalid/map?job=21891302#courier';
final payload = _jobPayload()..['map_url'] = mapUrl;
final job = Job.fromJson(payload);
final serialized =
jsonDecode(jsonEncode(job.toJson())) as Map<String, dynamic>;
final restored = Job.fromJson(serialized);
expect(job.mapUrl, mapUrl);
expect(serialized['map_url'], mapUrl);
expect(restored.mapUrl, mapUrl);
expect(restored.externalId, 21891302);
});
test('liest ältere Aufträge ohne map_url weiterhin ein', () {
final job = Job.fromJson(_jobPayload());
final restored = Job.fromJson(job.toJson());
expect(job.mapUrl, isNull);
expect(restored.mapUrl, isNull);
expect(restored.externalId, 21891302);
});
test('akzeptiert eine explizit fehlende map_url', () {
final payload = _jobPayload()..['map_url'] = null;
expect(Job.fromJson(payload).mapUrl, isNull);
});
});
}
Map<String, dynamic> _jobPayload() => {
'id': 21891302,
'ver': '0',
'state': 0,
'ordertime': 1791285073,
'offs_warn': 0,
'global': false,
'vht_id': 2,
'rem': '',
'delayOrderTime': 0,
'tours': <Map<String, dynamic>>[],
};
+82
View File
@@ -0,0 +1,82 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:stadtbote/tools/vhs_map_access.dart';
void main() {
group('VHS-Kartenlink', () {
test('erlaubt HTTPS inklusive Kartenparameter und Fragment', () {
final access = VhsMapAccess(
Uri.parse('https://vhs.example/map?job=42#courier'),
);
expect(access.isValid, isTrue);
});
test('weist unsichere und unvollständige Links zurück', () {
for (final url in [
'http://vhs.example/map',
'/map?job=42',
'//vhs.example/map',
'javascript:alert(1)',
'https:///map',
'https://user:password@vhs.example/map',
'https://vhs.example:0/map',
'https://vhs.example:65536/map',
]) {
expect(VhsMapAccess(Uri.parse(url)).isValid, isFalse, reason: url);
}
});
test('erlaubt Navigation nur innerhalb derselben HTTPS-Origin', () {
final access = VhsMapAccess(Uri.parse('https://vhs.example/map?job=42'));
expect(
access.allowsNavigation(Uri.parse('https://vhs.example:443/other')),
isTrue,
);
for (final url in [
'http://vhs.example/map',
'https://other.example/map',
'https://vhs.example.other.example/map',
'https://vhs.example:8443/map',
'https://user@vhs.example/map',
'file:///map',
]) {
expect(access.allowsNavigation(Uri.parse(url)), isFalse, reason: url);
}
});
});
group('VHS-Anmeldung', () {
test('sendet Zugangsdaten nur an den exakten Kartenhost', () {
final access = VhsMapAccess(Uri.parse('https://vhs.example/map'));
expect(access.allowsAuthentication('VHS.EXAMPLE'), isTrue);
for (final host in [
'other.example',
'vhs.example.other.example',
'sub.vhs.example',
'',
]) {
expect(access.allowsAuthentication(host), isFalse, reason: host);
}
expect(
VhsMapAccess(
Uri.parse('http://vhs.example'),
).allowsAuthentication('vhs.example'),
isFalse,
);
});
test('begrenzt wiederholte Authentifizierungsanfragen', () {
final access = VhsMapAccess(Uri.parse('https://vhs.example/map'));
expect(access.tryAuthentication('other.example'), isFalse);
for (
var attempt = 0;
attempt < VhsMapAccess.maxAuthenticationAttempts;
attempt++
) {
expect(access.tryAuthentication('vhs.example'), isTrue);
}
expect(access.tryAuthentication('vhs.example'), isFalse);
expect(access.tryAuthentication('vhs.example'), isFalse);
expect(VhsMapAccess(access.uri).tryAuthentication('vhs.example'), isTrue);
});
});
}
+130
View File
@@ -0,0 +1,130 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:dart_mq/dart_mq.dart';
import 'package:flat_buffers/flat_buffers.dart' as fb;
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:objectbox/internal.dart' show EntityDefinition;
import 'package:stadtbote/content_views/messages_page.dart';
import 'package:stadtbote/main.dart';
import 'package:stadtbote/model/chat_message.dart';
import 'package:stadtbote/model/session_status.dart';
import 'package:stadtbote/objectbox.g.dart';
import 'package:stadtbote/tools/constants.dart';
import 'package:stadtbote/tools/messages/publish_payload_message.dart';
void main() {
setUp(() {
MQClient.initialize();
MQClient.instance.declareQueue(Constants.messageTopicPublishPayload);
});
tearDown(() {
MQClient.instance.close();
});
testWidgets('bestätigt die Server-ID auch nach der lokalen ID-Vergabe', (
tester,
) async {
final message = ChatMessage.fromJson({
'id': 6094830,
'time': 1791536566,
'from': 'NL',
'subj': 'Test1',
'text': 'Test2',
});
final store = _MessageStore();
final definition =
getObjectBoxModel().bindings[ChatMessage]!
as EntityDefinition<ChatMessage>;
// Die nativen Datenbankzugriffe werden ersetzt; die generierte
// ObjectBox-Serialisierung und der Nachrichtenversand laufen unverändert.
definition.setId(message, 2);
final builder = fb.Builder();
definition.objectToFB(message, builder);
final restored = definition.objectFromFB(
store,
ByteData.sublistView(builder.buffer),
);
expect(restored.id, 2);
expect(restored.externalId, 6094830);
expect(restored.read, isFalse);
store.messages = _MessageBox(restored);
MyApp.objectBoxStore = store;
await tester.pumpWidget(
const MaterialApp(home: Scaffold(body: MessagesPage())),
);
await tester.pumpAndSettle();
final published =
MQClient.instance
.getLatestMessage(Constants.messageTopicPublishPayload)!
.payload
as PublishPayloadMessage;
expect(published.topic, 'mssg_received');
expect(published.acknowledgeRequired, isFalse);
expect(jsonDecode(jsonEncode(published.message)), {
'ids': ['6094830'],
'time': isA<int>(),
});
expect(restored.read, isTrue);
expect(find.text('Test1\nTest2', findRichText: true), findsOneWidget);
});
}
class _MessageStore extends Fake implements Store {
late final _MessageBox messages;
@override
Box<T> box<T>() {
if (T == ChatMessage) return messages as Box<T>;
if (T == SessionStatus) return _SessionBox() as Box<T>;
throw StateError('Unerwarteter Entity-Typ: $T');
}
}
class _SessionBox extends Fake implements Box<SessionStatus> {
@override
List<SessionStatus> getAll() => [];
}
class _MessageBox extends Fake implements Box<ChatMessage> {
final ChatMessage message;
_MessageBox(this.message);
@override
List<ChatMessage> getAll() => [message];
@override
QueryBuilder<ChatMessage> query([Condition<ChatMessage>? qc]) =>
_UnreadQueryBuilder(message);
@override
int put(ChatMessage object, {PutMode mode = PutMode.put}) => object.id;
}
class _UnreadQueryBuilder extends Fake implements QueryBuilder<ChatMessage> {
final ChatMessage message;
_UnreadQueryBuilder(this.message);
@override
Query<ChatMessage> build() => _UnreadQuery(message);
}
class _UnreadQuery extends Fake implements Query<ChatMessage> {
final ChatMessage message;
_UnreadQuery(this.message);
@override
List<ChatMessage> find() => [if (!message.read) message];
@override
void close() {}
}
@@ -11,6 +11,7 @@
#include <geolocator_windows/geolocator_windows.h>
#include <objectbox_flutter_libs/objectbox_flutter_libs_plugin.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <permission_manager/permission_manager_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
@@ -24,6 +25,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("ObjectboxFlutterLibsPlugin"));
PermissionHandlerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
PermissionManagerPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionManagerPlugin"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
}
+2 -1
View File
@@ -8,11 +8,12 @@ list(APPEND FLUTTER_PLUGIN_LIST
geolocator_windows
objectbox_flutter_libs
permission_handler_windows
permission_manager
url_launcher_windows
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
pdfium_flutter
jni
)
set(PLUGIN_BUNDLED_LIBRARIES)