diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 56aca60..063240c 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -5,6 +5,7 @@
+
+
+
+
+
+
+
+
+
diff --git a/lib/app_version.dart b/lib/app_version.dart
index b4f3348..70c271a 100644
--- a/lib/app_version.dart
+++ b/lib/app_version.dart
@@ -1,3 +1,3 @@
/// Manuell mit pubspec.yaml synchron halten (siehe timemaster-mobile,
/// gleiche Konvention – kein package_info_plus während aktiver Feldtests).
-const String kAppVersion = '0.1.4+5';
+const String kAppVersion = '0.1.5+6';
diff --git a/lib/screens/login_screen.dart b/lib/screens/login_screen.dart
index 0357edc..d0a1e35 100644
--- a/lib/screens/login_screen.dart
+++ b/lib/screens/login_screen.dart
@@ -7,6 +7,7 @@ import '../app_version.dart';
import '../models/kiosk_session.dart';
import '../services/api_client.dart';
import '../services/kiosk_service.dart';
+import '../services/update_check_service.dart';
import 'enrollment_screen.dart';
import 'stamp_screen.dart';
@@ -32,12 +33,49 @@ class _LoginScreenState extends State {
String? _scannedUid;
List? _employees;
+ final _updateService = UpdateCheckService();
+ String? _newerVersion;
+ bool _updating = false;
+ double? _updateProgress;
+
+ @override
+ void initState() {
+ super.initState();
+ _updateService.checkForNewerVersion().then((v) {
+ if (mounted && v != null) setState(() => _newerVersion = v);
+ });
+ }
+
@override
void dispose() {
if (_nfcScanning) NfcManager.instance.stopSession();
super.dispose();
}
+ Future _installUpdate() async {
+ setState(() {
+ _updating = true;
+ _updateProgress = null;
+ _error = null;
+ });
+ try {
+ await _updateService.downloadAndInstall(
+ onProgress: (p) {
+ if (mounted) setState(() => _updateProgress = p);
+ },
+ );
+ } catch (e) {
+ if (mounted) setState(() => _error = 'Update fehlgeschlagen: $e');
+ } finally {
+ if (mounted) {
+ setState(() {
+ _updating = false;
+ _updateProgress = null;
+ });
+ }
+ }
+ }
+
String _dioMessage(DioException e) {
final detail = e.response?.data is Map ? e.response?.data['detail'] : null;
if (detail is String) return detail;
@@ -223,6 +261,44 @@ class _LoginScreenState extends State {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
+ if (_newerVersion != null) ...[
+ Card(
+ color: Colors.blue[50],
+ child: Padding(
+ padding: const EdgeInsets.all(12),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ const Icon(Icons.system_update, color: Colors.blue),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Text(
+ 'Neue Version $_newerVersion verfügbar (installiert: $kAppVersion).',
+ style: const TextStyle(color: Colors.blue),
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 8),
+ if (_updating)
+ LinearProgressIndicator(value: _updateProgress)
+ else
+ SizedBox(
+ width: double.infinity,
+ child: OutlinedButton.icon(
+ onPressed: _installUpdate,
+ icon: const Icon(Icons.download),
+ label: const Text('Jetzt aktualisieren'),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ const SizedBox(height: 16),
+ ],
SegmentedButton<_LoginMode>(
segments: const [
ButtonSegment(value: _LoginMode.pin, label: Text('PIN'), icon: Icon(Icons.pin)),
diff --git a/lib/services/update_check_service.dart b/lib/services/update_check_service.dart
new file mode 100644
index 0000000..72168e8
--- /dev/null
+++ b/lib/services/update_check_service.dart
@@ -0,0 +1,56 @@
+import 'package:dio/dio.dart';
+import 'package:open_filex/open_filex.dart';
+import 'package:path_provider/path_provider.dart';
+import '../app_version.dart';
+import 'api_client.dart';
+
+/// Prüft `/static/downloads/kiosk-version.txt` auf dem konfigurierten Server
+/// gegen die eigene kAppVersion und kann das neue APK direkt herunterladen +
+/// den System-Installer öffnen (REQUEST_INSTALL_PACKAGES + FileProvider,
+/// siehe AndroidManifest.xml). Analog timemaster-mobile, eigene Dateinamen
+/// (kiosk-version.txt / timemaster-kiosk.apk – geteiltes Downloads-
+/// Verzeichnis mit der Employee-App auf dem Server, andere Dateien).
+class UpdateCheckService {
+ String get _origin {
+ final apiBase = ApiClient.instance.baseUrl; // endet auf .../api/v1
+ return apiBase.replaceFirst(RegExp(r'/api/v1/?$'), '');
+ }
+
+ Future checkForNewerVersion() async {
+ try {
+ final response = await Dio(BaseOptions(
+ baseUrl: _origin,
+ connectTimeout: const Duration(seconds: 5),
+ receiveTimeout: const Duration(seconds: 5),
+ )).get('/static/downloads/kiosk-version.txt');
+
+ final remote = (response.data ?? '').trim();
+ if (remote.isEmpty || remote == kAppVersion) return null;
+ return remote;
+ } on DioException {
+ return null;
+ } catch (_) {
+ return null;
+ }
+ }
+
+ /// Lädt die APK herunter und öffnet den Android-Paketinstaller.
+ /// [onProgress] liefert 0.0–1.0, falls die Antwort ein Content-Length hat.
+ Future downloadAndInstall({void Function(double)? onProgress}) async {
+ final dir = await getExternalStorageDirectory() ?? await getTemporaryDirectory();
+ final filePath = '${dir.path}/timemaster-kiosk-update.apk';
+
+ await Dio(BaseOptions(baseUrl: _origin)).download(
+ '/static/downloads/timemaster-kiosk.apk',
+ filePath,
+ onReceiveProgress: (received, total) {
+ if (total > 0 && onProgress != null) onProgress(received / total);
+ },
+ );
+
+ final result = await OpenFilex.open(filePath, type: 'application/vnd.android.package-archive');
+ if (result.type != ResultType.done) {
+ throw Exception('Installer konnte nicht geöffnet werden: ${result.message}');
+ }
+ }
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 3575e28..36f8bd0 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,7 +1,7 @@
name: timemaster_kiosk
description: TimeMaster Kiosk – Stempeluhr für Android-Tablets hinter VPN
publish_to: 'none'
-version: 0.1.4+5
+version: 0.1.5+6
environment:
sdk: '>=3.3.0 <4.0.0'
@@ -15,6 +15,8 @@ dependencies:
nfc_manager: ^4.2.1
wakelock_plus: ^1.2.8
mobile_scanner: ^6.0.2
+ open_filex: ^4.5.0
+ path_provider: ^2.1.4
dev_dependencies:
flutter_test: