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: