feat: in-app update downloader with progress, Gitea auto-push hook
- update_checker: Gitea at git.steggi-matrix.work, Windows + Android support, asset URL detection - update_download_dialog: animated Pyramid logo, progress bar, platform install (exe/apk) - update_banner: Herunterladen button triggers dialog, dismiss option - Android: REQUEST_INSTALL_PACKAGES permission + FileProvider for APK install - post-commit hook: auto-push to Gitea on every commit - chat_input: larger send button (60px desktop), more padding, bigger icon - rooms_panel: space menu visible on mobile (leave space fix) - docs: feature-checklist.md added Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,24 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30"/>
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/>
|
||||
|
||||
<application
|
||||
android:label="pyramid"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<service
|
||||
android:name="com.cloudwebrtc.webrtc.FlutterForegroundService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="mediaProjection" />
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
@@ -25,6 +41,16 @@
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- FileProvider for APK installs (update downloader) -->
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths"/>
|
||||
</provider>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<cache-path name="pyramid_downloads" path="downloads/"/>
|
||||
<external-cache-path name="pyramid_ext_downloads" path="downloads/"/>
|
||||
</paths>
|
||||
@@ -0,0 +1,354 @@
|
||||
# Matrix / Element Feature Checklist für Pyramid
|
||||
|
||||
*Stand: April 2026 — Pyramid v0.1.x (Early Development)*
|
||||
|
||||
---
|
||||
|
||||
## Nachrichten (Messaging)
|
||||
|
||||
### Kern-Funktionen
|
||||
- [x] Textnachrichten senden
|
||||
- [x] Nachrichten empfangen & anzeigen
|
||||
- [x] Zeitstempel (formatiert, relativ)
|
||||
- [x] Nachrichtengruppierung (gleicher Sender, <5 Min.)
|
||||
- [x] Datumstrennlinien zwischen Gruppen
|
||||
- [x] Verschlüsselte Nachrichten anzeigen (Entschlüsselungsstatus)
|
||||
- [x] Nachrichtenverlauf paginieren (ältere Nachrichten beim Scrollen laden)
|
||||
- [x] Ungelesene-Nachrichten-Tracking
|
||||
|
||||
### Nachrichtenaktionen
|
||||
- [x] Nachrichtentext kopieren
|
||||
- [x] Eigene Nachrichten löschen (redact)
|
||||
- [x] Nachrichten anpinnen / Pinboard anzeigen
|
||||
- [~] Auf Nachrichten antworten (in-reply-to vorhanden, kein visuelles Zitatfeld)
|
||||
- [~] Nachrichten bearbeiten (m.edit im SDK erkannt, kein vollständiges UI)
|
||||
- [ ] Nachrichten weiterleiten
|
||||
- [ ] Zitat-Styling (visuelle Blockquote-Anzeige)
|
||||
|
||||
### Textformatierung
|
||||
- [ ] Fett (**text**)
|
||||
- [ ] Kursiv (*text*)
|
||||
- [ ] Durchgestrichen (~~text~~)
|
||||
- [ ] Inline-Code (`code`)
|
||||
- [ ] Code-Block (```code```)
|
||||
- [ ] Aufzählungslisten
|
||||
- [ ] Klickbare Links
|
||||
- [ ] @Erwähnungen (User)
|
||||
- [ ] #Kanal-Erwähnungen
|
||||
- [ ] Spoiler-Tags (||text||)
|
||||
|
||||
### Medien & Anhänge
|
||||
- [x] Dateianhänge senden (mit MIME-Erkennung)
|
||||
- [x] Bilder senden & inline anzeigen
|
||||
- [x] Videos senden & inline abspielen
|
||||
- [x] Audio senden & inline abspielen
|
||||
- [x] Sticker anzeigen (m.sticker)
|
||||
- [x] Dateianhang mit Untertitel
|
||||
- [x] Dateigröße berechnen & anzeigen
|
||||
- [x] Verschlüsselte Medien entschlüsseln & herunterladen
|
||||
- [x] Thumbnail-Generierung für Bilder/Videos
|
||||
- [~] Sprachnachrichten (Struktur vorhanden, Wiedergabe-UI fehlt)
|
||||
- [ ] Animierte GIFs
|
||||
- [ ] Link-Vorschau (URL-Unfurling)
|
||||
- [ ] Dokument-Vorschau (PDF, Office)
|
||||
|
||||
### Reaktionen
|
||||
- [x] Emoji-Reaktionen senden
|
||||
- [x] Reaktionszähler anzeigen
|
||||
- [x] 6 häufigste Reaktionen als Schnellauswahl
|
||||
- [x] Vollständiger Emoji-Picker mit Suche
|
||||
- [x] Eigene Reaktion entfernen
|
||||
- [x] Anzeigen wer reagiert hat
|
||||
- [x] Mehrere Reaktionen pro Nachricht
|
||||
- [x] Kategoriebasierte Emoji-Organisation
|
||||
|
||||
### Threads
|
||||
- [ ] Thread-Antworten (m.in_reply_to vorhanden, kein Thread-View)
|
||||
- [ ] Thread aufklappen / eigene Ansicht
|
||||
- [ ] Thread-Benachrichtigungen
|
||||
- [ ] Thread als gelöst markieren
|
||||
|
||||
---
|
||||
|
||||
## Raumverwaltung
|
||||
|
||||
### Grundfunktionen
|
||||
- [x] Raumliste anzeigen (sortiert nach letzter Aktivität)
|
||||
- [x] Räume beitreten
|
||||
- [x] Räume verlassen
|
||||
- [x] Raumname & Avatar anzeigen
|
||||
- [x] Verschlüsselungsstatus (Schloss-Icon)
|
||||
- [x] Einladungen annehmen / ablehnen
|
||||
- [ ] Neuen Raum erstellen
|
||||
- [ ] Raumname bearbeiten
|
||||
- [ ] Raumthema/Beschreibung bearbeiten
|
||||
- [ ] Raum-Avatar ändern
|
||||
- [ ] Raumvisibilität (öffentlich / privat)
|
||||
- [ ] Zugriffsregeln (invite/knock/public)
|
||||
- [ ] Verlaufsvisibilität konfigurieren
|
||||
- [ ] Raum-Aliases verwalten
|
||||
|
||||
### Direktnachrichten (DMs)
|
||||
- [x] 1:1 DM-Räume anzeigen & öffnen
|
||||
- [x] Presence-Status des Gegenübers anzeigen
|
||||
- [x] Echtzeit-Presence-Updates
|
||||
- [x] Sprachanruf aus DM starten
|
||||
- [x] Videoanruf aus DM starten
|
||||
- [ ] DM starten (neuen Chat beginnen, nur über Suchfeld)
|
||||
|
||||
### Mitglieder & Rollen
|
||||
- [x] Mitgliederliste anzeigen
|
||||
- [x] Mitgliederanzahl anzeigen
|
||||
- [x] Avatar & Anzeigename der Mitglieder
|
||||
- [x] Nutzer in Raum einladen (Raum-Kontextmenü)
|
||||
- [ ] Nutzer aus Raum entfernen (Kick)
|
||||
- [ ] Nutzer sperren (Ban)
|
||||
- [ ] Power Levels bearbeiten
|
||||
- [ ] Nutzer zum Moderator / Admin befördern
|
||||
- [ ] Bans aufheben
|
||||
|
||||
### Raumeinstellungen
|
||||
- [x] Raumeinstellungen-Dialog (Name, Thema, Moderationsregeln)
|
||||
- [ ] Raumverschlüsselung aktivieren/deaktivieren
|
||||
- [ ] Erweiterte Raumeinstellungen (Fluffychat-Level)
|
||||
- [ ] Widget-/App-Integration in Räume
|
||||
|
||||
---
|
||||
|
||||
## Spaces & Gemeinschaften
|
||||
|
||||
### Spaces
|
||||
- [x] Spaces in der Seitenleiste anzeigen
|
||||
- [x] Zwischen Spaces navigieren
|
||||
- [x] Space-Avatar anzeigen
|
||||
- [x] Space-Banner hochladen & anzeigen (Discord-Style)
|
||||
- [x] Räume nach Space filtern
|
||||
- [x] Virtuelle "Rooms"- und "DMs"-Spaces
|
||||
- [x] Unterkategorien (Sub-Spaces) als Kategorien in der Kanalleiste
|
||||
- [x] Drag-and-drop Kanalreihenfolge für Admins
|
||||
- [x] Space-Verwaltungsdialog (Name, Avatar, Banner, Einladungen, Mitglieder, Power Levels)
|
||||
- [ ] Neuen Space erstellen
|
||||
- [ ] Räume zum Space hinzufügen / entfernen
|
||||
- [ ] Space-Regeln / Beschreibung
|
||||
- [ ] Space-Entdeckung (öffentliche Spaces durchsuchen)
|
||||
|
||||
### Kanalorganisation
|
||||
- [x] Kanäle nach letzter Aktivität sortieren
|
||||
- [x] Ungelesene-Badge auf Kanälen
|
||||
- [x] Sprachkanal-Teilnehmerliste (Discord-Style, mit Echtzeit-Updates)
|
||||
- [x] Kollabierbare Kategorien
|
||||
- [ ] Favoriten / angeheftete Räume
|
||||
- [ ] Räume stummschalten
|
||||
- [ ] Räume ausblenden / archivieren
|
||||
- [ ] Kanalreihenfolge für reguläre Nutzer
|
||||
|
||||
---
|
||||
|
||||
## Sprach- & Videoanrufe
|
||||
|
||||
### Sprachanrufe (LiveKit)
|
||||
- [x] Sprachkanal betreten / verlassen (Toggle)
|
||||
- [x] Teilnehmerliste im Sprachkanal
|
||||
- [x] Mikrofon stummschalten
|
||||
- [x] Audio-Ausgabe deaktivieren (Deafen)
|
||||
- [x] Anruf-UI im Profil-Panel (aktiver Anruf)
|
||||
- [x] Anruf-Ansicht öffnen (Vollansicht)
|
||||
- [x] Mini-Call-Widget bei VOIP-Anrufen
|
||||
- [ ] Klingelton bei eingehenden Anrufen
|
||||
- [ ] Anruf annehmen / ablehnen (UI)
|
||||
- [ ] Anrufdauer-Timer
|
||||
- [ ] Anrufverlauf
|
||||
|
||||
### Videoanrufe
|
||||
- [x] Videoanruf starten (aus DM)
|
||||
- [x] Kamera-Toggle
|
||||
- [x] Videoteilnehmer-Anzeige
|
||||
- [ ] Bildschirmfreigabe (Toggle-UI vorhanden, Implementierung unklar)
|
||||
- [ ] Picture-in-Picture-Modus
|
||||
- [ ] Virtueller Hintergrund
|
||||
|
||||
### Gruppenanrufe
|
||||
- [x] Mehrere Teilnehmer im Sprachkanal (LiveKit)
|
||||
- [ ] Gruppen-Videogitter
|
||||
- [ ] Spotlight-/Galerie-Ansicht
|
||||
|
||||
---
|
||||
|
||||
## Benachrichtigungen & Presence
|
||||
|
||||
### Push-Benachrichtigungen
|
||||
- [ ] Desktop-Benachrichtigungen (Windows/macOS/Linux)
|
||||
- [ ] Mobile Push-Benachrichtigungen (Android/iOS)
|
||||
- [ ] Benachrichtigungseinstellungen pro Raum
|
||||
- [ ] Nicht-Stören-Modus
|
||||
- [ ] Benachrichtigungssound konfigurieren
|
||||
|
||||
### Presence & Status
|
||||
- [x] Nutzer-Presence abrufen (online/offline/abwesend)
|
||||
- [x] Presence-Indikator anzeigen
|
||||
- [x] Letzte-Aktivität-Zeitstempel
|
||||
- [x] Echtzeit-Presence-Updates
|
||||
- [ ] Eigenen Status setzen (online/abwesend/unsichtbar)
|
||||
- [ ] Benutzerdefinierte Statusmeldung
|
||||
- [ ] Aktivitätsbasierte Status-Änderung
|
||||
|
||||
### Tipp-Indikatoren
|
||||
- [ ] "X tippt gerade"-Anzeige empfangen
|
||||
- [ ] Eigene Tipp-Benachrichtigung senden
|
||||
- [ ] Timeout nach Tipp-Ende
|
||||
|
||||
---
|
||||
|
||||
## Nutzereinstellungen & Profil
|
||||
|
||||
### Profilverwaltung
|
||||
- [x] Eigene User-ID anzeigen
|
||||
- [x] Eigenen Avatar anzeigen (Initialen-Fallback)
|
||||
- [x] Anzeigenamen anzeigen
|
||||
- [ ] Anzeigenamen bearbeiten
|
||||
- [ ] Eigenen Avatar hochladen / entfernen
|
||||
- [ ] Status-Nachricht setzen
|
||||
|
||||
### Kontosicherheit
|
||||
- [ ] Passwort ändern
|
||||
- [ ] E-Mail-Adresse verwalten
|
||||
- [ ] Sitzungsverwaltung (alle Sitzungen anzeigen)
|
||||
- [ ] Andere Sitzungen abmelden
|
||||
- [ ] Konto deaktivieren / löschen
|
||||
- [ ] Zwei-Faktor-Authentifizierung (2FA)
|
||||
|
||||
### App-Einstellungen
|
||||
- [x] Hell / Dunkel-Theme
|
||||
- [x] Akzentfarbe (6 Voreinstellungen)
|
||||
- [x] Eckradius anpassen
|
||||
- [x] UI-Dichte / Kompaktheit
|
||||
- [x] Animationssteuerung
|
||||
- [ ] Schriftgröße anpassen
|
||||
- [ ] Sprachauswahl (i18n)
|
||||
- [ ] Kompaktansicht (kein Avatar in Nachrichten)
|
||||
|
||||
---
|
||||
|
||||
## Datenschutz & Sicherheit
|
||||
|
||||
### Ende-zu-Ende-Verschlüsselung (E2EE)
|
||||
- [x] E2EE-Unterstützung (Olm/Megolm via vodozemac)
|
||||
- [x] E2EE-Schlüssel beim ersten Login einrichten (Bootstrap)
|
||||
- [x] Verschlüsselte Nachrichten-Indikatoren
|
||||
- [x] Entschlüsselungsstatus anzeigen
|
||||
- [x] SSSS (Symmetric Secret Storage)
|
||||
- [x] Recovery-Key erzeugen & anzeigen
|
||||
- [x] Recovery-Key aus bestehender Einrichtung importieren
|
||||
- [x] Schlüssel-Caching (lokal speichern)
|
||||
- [x] Session-Keys von anderen Geräten anfordern
|
||||
- [ ] Schlüssel-Backup (Server-seitig)
|
||||
- [ ] Schlüssel-Backup verifizieren
|
||||
|
||||
### Geräte-Verifizierung
|
||||
- [x] Cross-Signing (Master/Self-Signing/User-Signing Keys)
|
||||
- [x] Gerät per Emoji verifizieren
|
||||
- [x] Gerät per Zahlencodes verifizieren
|
||||
- [x] Geräte als vertrauenswürdig markieren
|
||||
- [ ] Gerätename / Modell anzeigen
|
||||
- [ ] Sitzungsverwaltung (alle Geräte dieses Accounts)
|
||||
- [ ] Fremde Geräte abmelden
|
||||
|
||||
### Vertrauen & Sicherheit
|
||||
- [ ] Geräte-Vertrauensstatus-Indikator in Nachrichten
|
||||
- [ ] Nutzer blockieren / entsperren
|
||||
- [ ] Blockierte Nutzer-Liste
|
||||
- [ ] Verifizierter-Nutzer-Badge
|
||||
|
||||
---
|
||||
|
||||
## Suche
|
||||
|
||||
### Nachrichtensuche
|
||||
- [x] Suchdialog öffnen (UI vorhanden)
|
||||
- [ ] Volltextsuche in Nachrichteninhalten
|
||||
- [ ] Filter nach Absender
|
||||
- [ ] Filter nach Datum
|
||||
- [ ] Filter nach Raum
|
||||
- [ ] Filter nach Medientyp
|
||||
- [ ] Suchergebnisse hervorheben
|
||||
- [ ] Suchverlauf
|
||||
|
||||
### Nutzer- / Raumsuche
|
||||
- [ ] Nutzer nach Anzeigenamen suchen
|
||||
- [ ] Nutzer nach User-ID suchen
|
||||
- [ ] Öffentliche Räume entdecken (Directory)
|
||||
- [ ] Eigene Räume filtern
|
||||
- [ ] Räume nach Name / Thema suchen
|
||||
|
||||
---
|
||||
|
||||
## Auto-Update
|
||||
|
||||
- [x] GitHub-Releases als Update-Quelle
|
||||
- [x] Versions-Vergleich (SemVer)
|
||||
- [x] Rate-Limiting (max. alle 4 Stunden prüfen)
|
||||
- [x] Update-Banner im App-Header (Windows)
|
||||
- [x] "Diese Version überspringen"-Option
|
||||
- [x] Manueller Update-Check in den Einstellungen
|
||||
- [ ] Automatischer Download & Installation (Squirrel/NSIS)
|
||||
- [ ] Delta-Updates (nur geänderte Dateien)
|
||||
- [ ] Update-Kanal wählen (Stable/Beta/Nightly)
|
||||
|
||||
---
|
||||
|
||||
## Plattform-spezifisch
|
||||
|
||||
### Desktop (Windows / macOS / Linux)
|
||||
- [x] Native Desktop-App (Flutter Desktop)
|
||||
- [x] Tastenkürzel (Enter = Senden, Shift+Enter = Zeile)
|
||||
- [x] Drag-and-Drop für Dateianhänge
|
||||
- [ ] System-Tray-Integration
|
||||
- [ ] Desktop-Benachrichtigungen
|
||||
- [ ] Minimieren in System-Tray
|
||||
- [ ] Fenster-State speichern (Größe, Position)
|
||||
- [ ] MSIX / Installer-Paketierung
|
||||
|
||||
### Mobile (Android / iOS)
|
||||
- [x] Mobile App (Flutter)
|
||||
- [x] Drawer-Navigation (Hamburger-Menü)
|
||||
- [ ] Touch-optimiertes UI (derzeit Desktop-first)
|
||||
- [ ] Mobile Push-Benachrichtigungen
|
||||
- [ ] Zurück-Geste Navigation
|
||||
- [ ] Bottom-Sheet-Modals
|
||||
- [ ] Share-Sheet-Integration
|
||||
- [ ] Haptisches Feedback
|
||||
|
||||
---
|
||||
|
||||
## Statistik
|
||||
|
||||
| Bereich | Implementiert | Teilweise | Fehlend |
|
||||
|---|---|---|---|
|
||||
| Messaging | 14 | 2 | 14 |
|
||||
| Raumverwaltung | 8 | 0 | 14 |
|
||||
| Spaces | 10 | 0 | 6 |
|
||||
| Anrufe | 8 | 1 | 10 |
|
||||
| Benachrichtigungen | 4 | 0 | 8 |
|
||||
| Einstellungen/Profil | 5 | 0 | 11 |
|
||||
| Sicherheit/E2EE | 9 | 0 | 7 |
|
||||
| Suche | 1 | 0 | 10 |
|
||||
| Auto-Update | 6 | 0 | 3 |
|
||||
| Plattform | 5 | 0 | 10 |
|
||||
| **Gesamt** | **~70** | **~3** | **~93** |
|
||||
|
||||
**Implementierungsgrad: ~42 %** (inkl. Teilimplementierungen)
|
||||
|
||||
---
|
||||
|
||||
## Empfohlene Feature-Prioritäten
|
||||
|
||||
1. **Textformatierung** — häufig genutzt, von Nutzern erwartet
|
||||
2. **Push-Benachrichtigungen** — Desktop-Benachrichtigungen für Windows
|
||||
3. **Tipp-Indikatoren** — verbessert Gesprächsgefühl deutlich
|
||||
4. **Nachrichtenbearbeitung** — SDK-Grundlage vorhanden, nur UI fehlt
|
||||
5. **Profilbearbeitung** — Avatar & Anzeigename ändern
|
||||
6. **Raum erstellen** — Basis-Workflow, derzeit nicht möglich
|
||||
7. **Sprachnachricht abspielen** — Struktur vorhanden
|
||||
8. **Suche** — Dialog vorhanden, Backend-Abfrage fehlt
|
||||
9. **Nutzer blockieren** — Sicherheits-Grundfunktion
|
||||
10. **System-Tray** — Desktop-Erlebnis verbessern
|
||||
@@ -0,0 +1,156 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
// ─── Configuration ────────────────────────────────────────────────────────────
|
||||
|
||||
const _kGiteaBase = 'https://git.steggi-matrix.work';
|
||||
const _kGiteaRepo = 'steggi/pyramid';
|
||||
const _kCheckIntervalHours = 4;
|
||||
const _kPrefLastCheck = 'update_last_check_ms';
|
||||
const _kPrefSkippedTag = 'update_skipped_tag';
|
||||
|
||||
// ─── Data model ───────────────────────────────────────────────────────────────
|
||||
|
||||
class UpdateInfo {
|
||||
final String tagName;
|
||||
final String releaseName;
|
||||
final String body;
|
||||
final String htmlUrl;
|
||||
final String? downloadUrl;
|
||||
final (int, int, int) remoteVersion;
|
||||
|
||||
const UpdateInfo({
|
||||
required this.tagName,
|
||||
required this.releaseName,
|
||||
required this.body,
|
||||
required this.htmlUrl,
|
||||
required this.remoteVersion,
|
||||
this.downloadUrl,
|
||||
});
|
||||
|
||||
bool get canDownload => downloadUrl != null;
|
||||
}
|
||||
|
||||
// ─── Version helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
(int, int, int)? _parseVersion(String raw) {
|
||||
final clean = raw.trim().replaceFirst(RegExp(r'^v'), '').split('+').first;
|
||||
final parts = clean.split('.');
|
||||
if (parts.length < 3) return null;
|
||||
try {
|
||||
return (int.parse(parts[0]), int.parse(parts[1]), int.parse(parts[2]));
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
bool _isNewer((int, int, int) remote, (int, int, int) local) {
|
||||
if (remote.$1 != local.$1) return remote.$1 > local.$1;
|
||||
if (remote.$2 != local.$2) return remote.$2 > local.$2;
|
||||
return remote.$3 > local.$3;
|
||||
}
|
||||
|
||||
String? _findDownloadUrl(List<dynamic> assets) {
|
||||
if (assets.isEmpty) return null;
|
||||
final list = assets.cast<Map<String, dynamic>>();
|
||||
|
||||
String? pick(bool Function(String name) test) {
|
||||
for (final a in list) {
|
||||
final name = (a['name'] as String? ?? '').toLowerCase();
|
||||
if (test(name)) return a['browser_download_url'] as String?;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Platform.isWindows) {
|
||||
return pick((n) => n.endsWith('.exe')) ?? pick((n) => n.endsWith('.msix'));
|
||||
}
|
||||
if (Platform.isAndroid) {
|
||||
return pick((n) => n.endsWith('.apk'));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ─── Fetch ────────────────────────────────────────────────────────────────────
|
||||
|
||||
Future<UpdateInfo?> _fetchLatestRelease() async {
|
||||
try {
|
||||
final uri = Uri.parse(
|
||||
'$_kGiteaBase/api/v1/repos/$_kGiteaRepo/releases?limit=1',
|
||||
);
|
||||
final response = await http
|
||||
.get(uri, headers: {'Accept': 'application/json'})
|
||||
.timeout(const Duration(seconds: 10));
|
||||
|
||||
if (response.statusCode != 200) return null;
|
||||
|
||||
final list = json.decode(response.body) as List<dynamic>;
|
||||
if (list.isEmpty) return null;
|
||||
final data = list.first as Map<String, dynamic>;
|
||||
|
||||
final tagName = data['tag_name'] as String? ?? '';
|
||||
if (tagName.isEmpty) return null;
|
||||
|
||||
final remoteVer = _parseVersion(tagName);
|
||||
if (remoteVer == null) return null;
|
||||
|
||||
final assets = (data['assets'] as List<dynamic>?) ?? [];
|
||||
final downloadUrl = _findDownloadUrl(assets);
|
||||
|
||||
return UpdateInfo(
|
||||
tagName: tagName,
|
||||
releaseName: data['name'] as String? ?? tagName,
|
||||
body: data['body'] as String? ?? '',
|
||||
htmlUrl: '$_kGiteaBase/$_kGiteaRepo/releases/tag/$tagName',
|
||||
remoteVersion: remoteVer,
|
||||
downloadUrl: downloadUrl,
|
||||
);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Provider ─────────────────────────────────────────────────────────────────
|
||||
|
||||
final updateInfoProvider = FutureProvider<UpdateInfo?>((ref) async {
|
||||
// iOS: can't sideload, skip
|
||||
if (Platform.isIOS || Platform.isMacOS || Platform.isLinux) return null;
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final lastCheckMs = prefs.getInt(_kPrefLastCheck) ?? 0;
|
||||
final elapsed = DateTime.now().millisecondsSinceEpoch - lastCheckMs;
|
||||
if (elapsed < _kCheckIntervalHours * 3600 * 1000) return null;
|
||||
|
||||
await prefs.setInt(_kPrefLastCheck, DateTime.now().millisecondsSinceEpoch);
|
||||
|
||||
final remote = await _fetchLatestRelease();
|
||||
if (remote == null) return null;
|
||||
|
||||
final info = await PackageInfo.fromPlatform();
|
||||
final localVer = _parseVersion(info.version);
|
||||
if (localVer == null) return null;
|
||||
|
||||
if (!_isNewer(remote.remoteVersion, localVer)) return null;
|
||||
|
||||
final skipped = prefs.getString(_kPrefSkippedTag);
|
||||
if (skipped == remote.tagName) return null;
|
||||
|
||||
return remote;
|
||||
});
|
||||
|
||||
// ─── Actions ──────────────────────────────────────────────────────────────────
|
||||
|
||||
Future<void> skipUpdateVersion(String tagName) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_kPrefSkippedTag, tagName);
|
||||
}
|
||||
|
||||
Future<void> resetUpdateCheckTimer() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_kPrefLastCheck);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
@@ -53,6 +54,10 @@ class _ChatInputState extends State<ChatInput> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final isDesktop = !kIsWeb &&
|
||||
(defaultTargetPlatform == TargetPlatform.windows ||
|
||||
defaultTargetPlatform == TargetPlatform.linux ||
|
||||
defaultTargetPlatform == TargetPlatform.macOS);
|
||||
|
||||
return SafeArea(
|
||||
child: Column(
|
||||
@@ -63,7 +68,12 @@ class _ChatInputState extends State<ChatInput> {
|
||||
onClear: widget.onClearReply ?? () {},
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8, 4, 8, 8),
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
isDesktop ? 12 : 8,
|
||||
isDesktop ? 10 : 4,
|
||||
isDesktop ? 12 : 8,
|
||||
isDesktop ? 14 : 8,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
@@ -73,14 +83,19 @@ class _ChatInputState extends State<ChatInput> {
|
||||
minLines: 1,
|
||||
maxLines: 6,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
style: TextStyle(fontSize: isDesktop ? 16 : 14),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Nachricht...',
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 10,
|
||||
hintStyle: TextStyle(
|
||||
fontSize: isDesktop ? 16 : 14,
|
||||
color: theme.colorScheme.onSurface.withAlpha(100),
|
||||
),
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: isDesktop ? 22 : 16,
|
||||
vertical: isDesktop ? 20 : 10,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
borderRadius: BorderRadius.circular(isDesktop ? 16 : 24),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
filled: true,
|
||||
@@ -89,19 +104,30 @@ class _ChatInputState extends State<ChatInput> {
|
||||
onSubmitted: (_) => _send(),
|
||||
keyboardType: TextInputType.multiline,
|
||||
inputFormatters: [
|
||||
// Shift+Enter = newline, Enter = send
|
||||
_SendOnEnterFormatter(onSend: _send),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
AnimatedScale(
|
||||
scale: _canSend ? 1.0 : 0.8,
|
||||
SizedBox(width: isDesktop ? 10 : 8),
|
||||
GestureDetector(
|
||||
onTap: _canSend ? _send : null,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
child: FloatingActionButton.small(
|
||||
onPressed: _canSend ? _send : null,
|
||||
elevation: _canSend ? 2 : 0,
|
||||
child: const Icon(Icons.send_rounded),
|
||||
width: isDesktop ? 60 : 44,
|
||||
height: isDesktop ? 60 : 44,
|
||||
decoration: BoxDecoration(
|
||||
color: _canSend
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.primary.withAlpha(70),
|
||||
borderRadius: BorderRadius.circular(isDesktop ? 16 : 14),
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.send_rounded,
|
||||
size: isDesktop ? 26 : 20,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,144 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:pyramid/core/theme.dart';
|
||||
import 'package:pyramid/core/update_checker.dart';
|
||||
import 'package:pyramid/widgets/update_download_dialog.dart';
|
||||
|
||||
/// Slim top banner shown when a new Pyramid version is available.
|
||||
class UpdateBanner extends ConsumerWidget {
|
||||
const UpdateBanner({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return ref.watch(updateInfoProvider).when(
|
||||
loading: () => const SizedBox.shrink(),
|
||||
error: (e, _) => const SizedBox.shrink(),
|
||||
data: (info) =>
|
||||
info == null ? const SizedBox.shrink() : _Banner(info: info),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Banner extends ConsumerWidget {
|
||||
final UpdateInfo info;
|
||||
const _Banner({required this.info});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final pt = PyramidTheme.of(context);
|
||||
final canDownload = info.canDownload;
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: pt.accent.withAlpha(28),
|
||||
border: Border(
|
||||
bottom: BorderSide(color: pt.accent.withAlpha(80)),
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.system_update_alt_rounded, size: 15, color: pt.accent),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Pyramid ${info.tagName} ist verfügbar',
|
||||
style: TextStyle(
|
||||
color: pt.fg, fontSize: 12.5, fontWeight: FontWeight.w500),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_BannerBtn(
|
||||
label: 'Was ist neu',
|
||||
icon: Icons.open_in_new_rounded,
|
||||
color: pt.bg3,
|
||||
fgColor: pt.fg,
|
||||
onTap: () => _openUrl(info.htmlUrl),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
_BannerBtn(
|
||||
label: canDownload ? 'Herunterladen' : 'Öffnen',
|
||||
icon: canDownload
|
||||
? Icons.download_rounded
|
||||
: Icons.open_in_new_rounded,
|
||||
color: pt.accent,
|
||||
fgColor: pt.accentFg,
|
||||
onTap: () {
|
||||
if (canDownload) {
|
||||
showUpdateDownloadDialog(context, info);
|
||||
} else {
|
||||
_openUrl(info.htmlUrl);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Tooltip(
|
||||
message: 'Für diese Version ausblenden',
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
onTap: () async {
|
||||
await skipUpdateVersion(info.tagName);
|
||||
ref.invalidate(updateInfoProvider);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Icon(Icons.close_rounded, size: 14, color: pt.fgDim),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _openUrl(String url) async {
|
||||
final uri = Uri.parse(url);
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _BannerBtn extends StatelessWidget {
|
||||
final String label;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final Color fgColor;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _BannerBtn({
|
||||
required this.label,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
required this.fgColor,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration:
|
||||
BoxDecoration(color: color, borderRadius: BorderRadius.circular(6)),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 12, color: fgColor),
|
||||
const SizedBox(width: 4),
|
||||
Text(label,
|
||||
style: TextStyle(
|
||||
color: fgColor,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:open_file_plus/open_file_plus.dart';
|
||||
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_logo.dart';
|
||||
|
||||
// ─── Entry point ──────────────────────────────────────────────────────────────
|
||||
|
||||
Future<void> 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>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _spinCtrl;
|
||||
|
||||
_DownloadState _state = _DownloadState.idle;
|
||||
double _progress = 0.0;
|
||||
int _received = 0;
|
||||
int _total = 0;
|
||||
String _errorMsg = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_spinCtrl = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(seconds: 3),
|
||||
)..repeat();
|
||||
_startDownload();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_spinCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// ─── Download ───────────────────────────────────────────────────────────────
|
||||
|
||||
Future<void> _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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _install(File file) async {
|
||||
setState(() => _state = _DownloadState.installing);
|
||||
|
||||
if (Platform.isWindows) {
|
||||
await Process.start(file.path, [], runInShell: false);
|
||||
if (mounted) Navigator.of(context).pop();
|
||||
exit(0);
|
||||
} else if (Platform.isAndroid) {
|
||||
final result = await OpenFile.open(
|
||||
file.path,
|
||||
type: 'application/vnd.android.package-archive',
|
||||
);
|
||||
if (mounted) {
|
||||
if (result.type == ResultType.done) {
|
||||
setState(() => _state = _DownloadState.done);
|
||||
} else {
|
||||
setState(() {
|
||||
_state = _DownloadState.error;
|
||||
_errorMsg = result.message;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 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: [
|
||||
// ── Animated Pyramid ──
|
||||
_AnimatedPyramid(ctrl: _spinCtrl, pt: pt),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// ── Title ──
|
||||
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)}';
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Animated pyramid ─────────────────────────────────────────────────────────
|
||||
|
||||
class _AnimatedPyramid extends StatelessWidget {
|
||||
final AnimationController ctrl;
|
||||
final PyramidTheme pt;
|
||||
const _AnimatedPyramid({required this.ctrl, required this.pt});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: ctrl,
|
||||
builder: (context, _) {
|
||||
final pulse = 0.9 + 0.1 * (0.5 - (ctrl.value - 0.5).abs()) * 2;
|
||||
return Transform.scale(
|
||||
scale: pulse,
|
||||
child: PyramidLogo(size: 72, color: pt.accent),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+371
-3
@@ -89,6 +89,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
checked_yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: checked_yaml
|
||||
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.4"
|
||||
cli_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -97,6 +105,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
cli_util:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cli_util
|
||||
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.2"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -153,8 +169,16 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.15.0"
|
||||
crypto:
|
||||
cross_file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cross_file
|
||||
sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.5+2"
|
||||
crypto:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: crypto
|
||||
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||
@@ -193,6 +217,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.12"
|
||||
desktop_drop:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: desktop_drop
|
||||
sha256: d55a010fe46c8e8fcff4ea4b451a9ff84a162217bdb3b2a0aa1479776205e15d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.4"
|
||||
device_info_plus:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -241,6 +273,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
file_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: file_picker
|
||||
sha256: ab13ae8ef5580a411c458d6207b6774a6c237d77ac37011b13994879f68a8810
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.3.7"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -254,6 +294,14 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_launcher_icons:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_launcher_icons
|
||||
sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.13.1"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@@ -267,6 +315,14 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_plugin_android_lifecycle:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_plugin_android_lifecycle
|
||||
sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.34"
|
||||
flutter_riverpod:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -331,6 +387,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.0"
|
||||
flutter_svg:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_svg
|
||||
sha256: "1ded017b39c8e15c8948ea855070a5ff8ff8b3d5e83f3446e02d6bb12add7ad9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.4"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
@@ -541,6 +605,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
lottie:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: lottie
|
||||
sha256: "8b6359a7422167014aa73ce763fa133fb832065dcc0ac4d1dec1f603a5cef7d0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.3"
|
||||
markdown:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -573,6 +645,70 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.2.0"
|
||||
media_kit:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: media_kit
|
||||
sha256: ae9e79597500c7ad6083a3c7b7b7544ddabfceacce7ae5c9709b0ec16a5d6643
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.6"
|
||||
media_kit_libs_android_video:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: media_kit_libs_android_video
|
||||
sha256: "3f6274e5ab2de512c286a25c327288601ee445ed8ac319e0ef0b66148bd8f76c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.8"
|
||||
media_kit_libs_ios_video:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: media_kit_libs_ios_video
|
||||
sha256: b5382994eb37a4564c368386c154ad70ba0cc78dacdd3fb0cd9f30db6d837991
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.4"
|
||||
media_kit_libs_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: media_kit_libs_linux
|
||||
sha256: "2b473399a49ec94452c4d4ae51cfc0f6585074398d74216092bf3d54aac37ecf"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
media_kit_libs_macos_video:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: media_kit_libs_macos_video
|
||||
sha256: f26aa1452b665df288e360393758f84b911f70ffb3878032e1aabba23aa1032d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.4"
|
||||
media_kit_libs_video:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: media_kit_libs_video
|
||||
sha256: "2b235b5dac79c6020e01eef5022c6cc85fedc0df1738aadc6ea489daa12a92a9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.7"
|
||||
media_kit_libs_windows_video:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: media_kit_libs_windows_video
|
||||
sha256: dff76da2778729ab650229e6b4ec6ec111eb5151431002cbd7ea304ff1f112ab
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.11"
|
||||
media_kit_video:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: media_kit_video
|
||||
sha256: "813858c3fe84eb46679eb698695f60665e2bfbef757766fac4d2e683f926e15a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -621,6 +757,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.3.0"
|
||||
open_file_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: open_file_plus
|
||||
sha256: "01ad7011d46d253aa22e5f0bc7345ca49cfe652c50ae9b0af945491c1728a8c3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.4.1+1"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -629,6 +773,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
package_info_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: package_info_plus
|
||||
sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.3.1"
|
||||
package_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus_platform_interface
|
||||
sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
path:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -637,6 +797,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
path_parsing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_parsing
|
||||
sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
path_provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -710,7 +878,7 @@ packages:
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
pointycastle:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: pointycastle
|
||||
sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5"
|
||||
@@ -733,6 +901,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.5.0"
|
||||
pretty_qr_code:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: pretty_qr_code
|
||||
sha256: "474f8a4512113fba06f14a6ec9bbf42353b4e651d7a520e3096f2a9b6bbe7a8a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.6.0"
|
||||
protobuf:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -749,6 +925,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
qr:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: qr
|
||||
sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
random_string:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -773,6 +957,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.6.1"
|
||||
safe_local_storage:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: safe_local_storage
|
||||
sha256: "287ea1f667c0b93cdc127dccc707158e2d81ee59fba0459c31a0c7da4d09c755"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
screen_brightness_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: screen_brightness_android
|
||||
sha256: d34f5321abd03bc3474f4c381f53d189117eba0b039eac1916aa92cca5fd0a96
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
screen_brightness_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: screen_brightness_platform_interface
|
||||
sha256: "737bd47b57746bc4291cab1b8a5843ee881af499514881b0247ec77447ee769c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
sdp_transform:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -874,6 +1082,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.2"
|
||||
sqflite:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sqflite
|
||||
sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
sqflite_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_android
|
||||
sha256: "881e28efdcc9950fd8e9bb42713dcf1103e62a2e7168f23c9338d82db13dec40"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2+3"
|
||||
sqflite_common:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -890,6 +1114,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.7+1"
|
||||
sqflite_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_darwin
|
||||
sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
sqflite_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_platform_interface
|
||||
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
sqlcipher_flutter_libs:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -978,6 +1218,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
universal_platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: universal_platform
|
||||
sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
unorm_dart:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -986,6 +1234,78 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.2"
|
||||
uri_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: uri_parser
|
||||
sha256: "051c62e5f693de98ca9f130ee707f8916e2266945565926be3ff20659f7853ce"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
url_launcher:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher
|
||||
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.2"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: "3bb000251e55d4a209aa0e2e563309dc9bb2befea2295fd0cec1f51760aac572"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.29"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.4.1"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_linux
|
||||
sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.2"
|
||||
url_launcher_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_macos
|
||||
sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.5"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_platform_interface
|
||||
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
url_launcher_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_windows
|
||||
sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.5"
|
||||
uuid:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -994,6 +1314,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.3"
|
||||
vector_graphics:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics
|
||||
sha256: "81da85e9ca8885ade47f9685b953cb098970d11be4821ac765580a6607ea4373"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.21"
|
||||
vector_graphics_codec:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics_codec
|
||||
sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.13"
|
||||
vector_graphics_compiler:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics_compiler
|
||||
sha256: "5a88dd14c0954a5398af544651c7fb51b457a2a556949bfb25369b210ef73a74"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1018,6 +1362,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.0"
|
||||
volume_controller:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: volume_controller
|
||||
sha256: ce25d4e02dd6f0b6d5d02dc1daa8de11b2d42d0f5afab8b1eda6aa0ebab3937a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.4.4"
|
||||
wakelock_plus:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: wakelock_plus
|
||||
sha256: "61713aa82b7f85c21c9f4cd0a148abd75f38a74ec645fcb1e446f882c82fd09b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.3"
|
||||
wakelock_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: wakelock_plus_platform_interface
|
||||
sha256: "14b2e5b9e35c2631e656913c47adecdd71633ae92896a27a64c8f1fcfabc21cc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.0"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1093,4 +1461,4 @@ packages:
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.11.1 <4.0.0"
|
||||
flutter: ">=3.38.4"
|
||||
flutter: ">=3.41.0"
|
||||
|
||||
@@ -16,6 +16,7 @@ dependencies:
|
||||
matrix: ^6.2.0
|
||||
flutter_vodozemac: ^0.5.0
|
||||
flutter_secure_storage: ^10.0.0
|
||||
sqflite: ^2.4.2
|
||||
sqflite_common_ffi: ^2.3.7+1
|
||||
sqlcipher_flutter_libs: ^0.6.8
|
||||
|
||||
@@ -33,6 +34,14 @@ dependencies:
|
||||
# UI
|
||||
dynamic_color: ^1.8.1
|
||||
google_fonts: ^6.2.1
|
||||
flutter_svg: ^2.0.17
|
||||
lottie: ^3.3.2
|
||||
pretty_qr_code: ^3.6.0
|
||||
qr: ^3.0.2
|
||||
|
||||
# Cryptography (for E2EE key export/import)
|
||||
crypto: ^3.0.3
|
||||
pointycastle: ^4.0.0
|
||||
|
||||
# Utilities
|
||||
shared_preferences: ^2.5.5
|
||||
@@ -42,11 +51,24 @@ dependencies:
|
||||
http: ^1.6.0
|
||||
mime: ^2.0.0
|
||||
collection: ^1.18.0
|
||||
url_launcher: ^6.3.0
|
||||
package_info_plus: ^8.1.0
|
||||
|
||||
# File handling
|
||||
file_picker: ^8.1.7
|
||||
desktop_drop: ^0.4.4
|
||||
open_file_plus: ^3.4.1
|
||||
|
||||
# Media playback
|
||||
media_kit: ^1.1.11
|
||||
media_kit_video: ^1.2.5
|
||||
media_kit_libs_video: ^1.0.5
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^6.0.0
|
||||
flutter_launcher_icons: ^0.13.1
|
||||
|
||||
dependency_overrides:
|
||||
webcrypto:
|
||||
|
||||
Reference in New Issue
Block a user