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:
Bernd Steckmeister
2026-04-28 15:35:59 +02:00
parent c9af913468
commit cdf48c4d45
10 changed files with 3426 additions and 17 deletions
+26
View File
@@ -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>
+354
View File
@@ -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
+156
View File
@@ -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);
}
+39 -13
View File
@@ -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
+144
View File
@@ -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)),
],
),
),
);
}
}
+384
View File
@@ -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
View File
@@ -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"
+22
View File
@@ -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: