import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:http/http.dart' as http; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; import 'package:pyramid/core/theme.dart'; import 'package:pyramid/core/update_checker.dart'; import 'package:pyramid/widgets/pyramid_loader.dart'; // ─── Entry point ────────────────────────────────────────────────────────────── Future showUpdateDownloadDialog(BuildContext context, UpdateInfo info) { return showDialog( context: context, barrierDismissible: false, barrierColor: Colors.black.withAlpha(160), builder: (_) => _UpdateDownloadDialog(info: info), ); } // ─── States ─────────────────────────────────────────────────────────────────── enum _DownloadState { idle, downloading, installing, done, error } // ─── Dialog widget ──────────────────────────────────────────────────────────── class _UpdateDownloadDialog extends ConsumerStatefulWidget { final UpdateInfo info; const _UpdateDownloadDialog({required this.info}); @override ConsumerState<_UpdateDownloadDialog> createState() => _UpdateDownloadDialogState(); } class _UpdateDownloadDialogState extends ConsumerState<_UpdateDownloadDialog> { _DownloadState _state = _DownloadState.idle; double _progress = 0.0; int _received = 0; int _total = 0; String _errorMsg = ''; @override void initState() { super.initState(); _startDownload(); } // ─── Download ─────────────────────────────────────────────────────────────── Future _startDownload() async { setState(() { _state = _DownloadState.downloading; _progress = 0; _received = 0; _total = 0; }); try { final dir = await getTemporaryDirectory(); final downloadDir = Directory(p.join(dir.path, 'downloads')); await downloadDir.create(recursive: true); final ext = Platform.isWindows ? '.exe' : '.apk'; final fileName = 'pyramid-update-${widget.info.tagName}$ext'; final file = File(p.join(downloadDir.path, fileName)); final request = http.Request('GET', Uri.parse(widget.info.downloadUrl!)); final streamedResponse = await http.Client().send(request).timeout(const Duration(minutes: 5)); _total = streamedResponse.contentLength ?? 0; final sink = file.openWrite(); await for (final chunk in streamedResponse.stream) { sink.add(chunk); _received += chunk.length; if (mounted) { setState(() { _progress = _total > 0 ? _received / _total : 0; }); } } await sink.flush(); await sink.close(); if (!mounted) return; await _install(file); } catch (e) { if (mounted) { setState(() { _state = _DownloadState.error; _errorMsg = e.toString().split('\n').first; }); } } } static const _installChannel = MethodChannel('chat.pyramid.pyramid/install'); Future _install(File file) async { setState(() => _state = _DownloadState.installing); try { if (Platform.isWindows) { await Process.start(file.path, [], runInShell: false); if (mounted) Navigator.of(context).pop(); exit(0); } else if (Platform.isAndroid) { await _installChannel .invokeMethod('installApk', {'path': file.path}); if (mounted) setState(() => _state = _DownloadState.done); } } on PlatformException catch (e) { if (e.code == 'PERMISSION_REQUIRED') { if (mounted) { setState(() { _state = _DownloadState.error; _errorMsg = 'Bitte "Unbekannte Apps installieren" für Pyramid erlauben (Einstellungen geöffnet) — dann erneut versuchen.'; }); } } else { if (mounted) { setState(() { _state = _DownloadState.error; _errorMsg = e.message ?? e.toString(); }); } } } catch (e) { if (mounted) { setState(() { _state = _DownloadState.error; _errorMsg = e.toString().split('\n').first; }); } } } // ─── Helpers ──────────────────────────────────────────────────────────────── String _formatBytes(int bytes) { if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB'; return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB'; } // ─── UI ───────────────────────────────────────────────────────────────────── @override Widget build(BuildContext context) { final pt = PyramidTheme.of(context); return Dialog( backgroundColor: Colors.transparent, elevation: 0, child: Container( width: 320, padding: const EdgeInsets.fromLTRB(32, 36, 32, 28), decoration: BoxDecoration( color: pt.bg1, borderRadius: BorderRadius.circular(20), border: Border.all(color: pt.border), boxShadow: [ BoxShadow( color: Colors.black.withAlpha(120), blurRadius: 40, spreadRadius: 4, ), ], ), child: Column( mainAxisSize: MainAxisSize.min, children: [ // ── Loading animation ── const PyramidLoader(size: 72), const SizedBox(height: 24), // ── Title (hidden while actively downloading) ── if (_state != _DownloadState.downloading && _state != _DownloadState.installing) ...[ Text( _titleText(), style: TextStyle( color: pt.fg, fontSize: 16, fontWeight: FontWeight.w700, ), textAlign: TextAlign.center, ), const SizedBox(height: 6), ], Text( widget.info.tagName, style: TextStyle(color: pt.accent, fontSize: 13), ), const SizedBox(height: 24), // ── Progress / Status ── if (_state == _DownloadState.downloading) ...[ _ProgressBar(progress: _progress, pt: pt), const SizedBox(height: 10), Text( _progressLabel(), style: TextStyle(color: pt.fgMuted, fontSize: 12), ), ] else if (_state == _DownloadState.installing) ...[ _ProgressBar(progress: 1.0, pt: pt), const SizedBox(height: 10), Text( 'Wird installiert…', style: TextStyle(color: pt.fgMuted, fontSize: 12), ), ] else if (_state == _DownloadState.done) ...[ Icon(Icons.check_circle_rounded, color: pt.accent, size: 32), const SizedBox(height: 8), Text( 'Update installiert', style: TextStyle(color: pt.fgMuted, fontSize: 12), ), ] else if (_state == _DownloadState.error) ...[ Icon(Icons.error_outline_rounded, color: pt.danger, size: 32), const SizedBox(height: 8), Text( _errorMsg, style: TextStyle(color: pt.danger, fontSize: 11), textAlign: TextAlign.center, maxLines: 3, overflow: TextOverflow.ellipsis, ), ], const SizedBox(height: 24), // ── Action buttons ── if (_state == _DownloadState.error) Row( children: [ Expanded( child: _DialogBtn( label: 'Abbrechen', pt: pt, onTap: () => Navigator.of(context).pop(), ), ), const SizedBox(width: 10), Expanded( child: _DialogBtn( label: 'Erneut', pt: pt, primary: true, onTap: _startDownload, ), ), ], ) else if (_state == _DownloadState.done) _DialogBtn( label: 'Schließen', pt: pt, onTap: () => Navigator.of(context).pop(), ) else if (_state == _DownloadState.downloading) _DialogBtn( label: 'Abbrechen', pt: pt, onTap: () => Navigator.of(context).pop(), ), ], ), ), ); } String _titleText() => switch (_state) { _DownloadState.idle => 'Vorbereitung…', _DownloadState.downloading => 'Update wird geladen', _DownloadState.installing => 'Wird installiert', _DownloadState.done => 'Fertig!', _DownloadState.error => 'Fehler beim Download', }; String _progressLabel() { final pct = (_progress * 100).toStringAsFixed(0); if (_total > 0) { return '$pct% · ${_formatBytes(_received)} / ${_formatBytes(_total)}'; } return '$pct% · ${_formatBytes(_received)}'; } } // ─── Progress bar ───────────────────────────────────────────────────────────── class _ProgressBar extends StatelessWidget { final double progress; final PyramidTheme pt; const _ProgressBar({required this.progress, required this.pt}); @override Widget build(BuildContext context) { return Container( height: 6, decoration: BoxDecoration( color: pt.bg3, borderRadius: BorderRadius.circular(3), ), child: FractionallySizedBox( alignment: Alignment.centerLeft, widthFactor: progress.clamp(0.0, 1.0), child: AnimatedContainer( duration: const Duration(milliseconds: 200), decoration: BoxDecoration( color: pt.accent, borderRadius: BorderRadius.circular(3), boxShadow: [ BoxShadow( color: pt.accent.withAlpha(100), blurRadius: 6, spreadRadius: 1, ), ], ), ), ), ); } } // ─── Dialog button ──────────────────────────────────────────────────────────── class _DialogBtn extends StatelessWidget { final String label; final PyramidTheme pt; final bool primary; final VoidCallback onTap; const _DialogBtn({ required this.label, required this.pt, required this.onTap, this.primary = false, }); @override Widget build(BuildContext context) { return GestureDetector( onTap: onTap, child: Container( height: 36, decoration: BoxDecoration( color: primary ? pt.accent : pt.bg3, borderRadius: BorderRadius.circular(8), ), child: Center( child: Text( label, style: TextStyle( color: primary ? pt.accentFg : pt.fgMuted, fontSize: 13, fontWeight: FontWeight.w600, ), ), ), ), ); } }