Feature: Auto-Update System für AppImage
- Backend (update.rs): Forgejo-API Check, Download mit Progress-Events, AppImage-Replace + Restart - Frontend (UpdateDialog.svelte): Modal mit Version, Release-Notes, Fortschrittsbalken - Automatischer Update-Check 3s nach App-Start - reqwest mit stream-Feature für Download-Progress Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
adcb8ac3b5
commit
a519a7cdd2
7 changed files with 661 additions and 2 deletions
|
|
@ -43,7 +43,23 @@
|
||||||
**Status:** Sub-Agents laufen auf Opus (inherit vom Main). Custom `agents`-Option in SDK scheint ignoriert zu werden bzw. spawnt Agents ohne Tools (halluziniert).
|
**Status:** Sub-Agents laufen auf Opus (inherit vom Main). Custom `agents`-Option in SDK scheint ignoriert zu werden bzw. spawnt Agents ohne Tools (halluziniert).
|
||||||
**Nächster Ansatz:** Im Orchestrator-Prompt Claude explizit vorgeben `model: "haiku"` in Task-Calls zu setzen. Ob das SDK das respektiert, ist offen.
|
**Nächster Ansatz:** Im Orchestrator-Prompt Claude explizit vorgeben `model: "haiku"` in Task-Calls zu setzen. Ob das SDK das respektiert, ist offen.
|
||||||
|
|
||||||
|
## Neue Features (15.04.2026)
|
||||||
|
|
||||||
|
### Auto-Update System
|
||||||
|
- **Backend** ([update.rs](src-tauri/src/update.rs)):
|
||||||
|
- `check_for_update()` — Prüft Forgejo `/repos/data/claude-desktop/releases`
|
||||||
|
- `download_update()` — Lädt AppImage mit Progress-Events herunter
|
||||||
|
- `apply_update()` — Backup, Replace, Restart via `app.restart()`
|
||||||
|
- **Frontend** ([UpdateDialog.svelte](src/lib/components/UpdateDialog.svelte)):
|
||||||
|
- Automatischer Check 3s nach App-Start
|
||||||
|
- Dialog mit Versions-Vergleich (v0.1.0 → v1.0.0)
|
||||||
|
- Release-Notes Anzeige
|
||||||
|
- Download-Fortschrittsbalken
|
||||||
|
- "Später" / "Jetzt aktualisieren" / "Jetzt installieren & neustarten"
|
||||||
|
- **Status**: Implementiert, wartet auf erstes Forgejo Release mit `.AppImage` Asset
|
||||||
|
|
||||||
## Letzte Commits
|
## Letzte Commits
|
||||||
|
- Feature: Auto-Update System für AppImage
|
||||||
- `f191cd0` Feature: Kontext-Auslastung im Footer (X% ctx)
|
- `f191cd0` Feature: Kontext-Auslastung im Footer (X% ctx)
|
||||||
- `48fd61f` Fix: Auto-Session erscheint sofort in Session-Liste
|
- `48fd61f` Fix: Auto-Session erscheint sofort in Session-Liste
|
||||||
- `a203589` Fix: Date-Panic in Wissensbasis (chrono::NaiveDateTime)
|
- `a203589` Fix: Date-Panic in Wissensbasis (chrono::NaiveDateTime)
|
||||||
|
|
|
||||||
17
src-tauri/Cargo.lock
generated
17
src-tauri/Cargo.lock
generated
|
|
@ -3571,12 +3571,14 @@ dependencies = [
|
||||||
"sync_wrapper",
|
"sync_wrapper",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-native-tls",
|
"tokio-native-tls",
|
||||||
|
"tokio-util",
|
||||||
"tower",
|
"tower",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"url",
|
"url",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
|
"wasm-streams 0.4.2",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -3610,7 +3612,7 @@ dependencies = [
|
||||||
"url",
|
"url",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
"wasm-streams",
|
"wasm-streams 0.5.0",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -5489,6 +5491,19 @@ dependencies = [
|
||||||
"wasmparser",
|
"wasmparser",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-streams"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
|
||||||
|
dependencies = [
|
||||||
|
"futures-util",
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-streams"
|
name = "wasm-streams"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ chrono = { version = "0.4", features = ["serde"] }
|
||||||
uuid = { version = "1", features = ["v4", "serde"] }
|
uuid = { version = "1", features = ["v4", "serde"] }
|
||||||
rusqlite = { version = "0.31", features = ["bundled"] }
|
rusqlite = { version = "0.31", features = ["bundled"] }
|
||||||
mysql_async = { version = "0.34", features = ["chrono"] }
|
mysql_async = { version = "0.34", features = ["chrono"] }
|
||||||
reqwest = { version = "0.12", features = ["json", "multipart"] }
|
reqwest = { version = "0.12", features = ["json", "multipart", "stream"] }
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
tokio-tungstenite = "0.23"
|
tokio-tungstenite = "0.23"
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ mod memory;
|
||||||
mod programs;
|
mod programs;
|
||||||
mod session;
|
mod session;
|
||||||
mod teaching;
|
mod teaching;
|
||||||
|
mod update;
|
||||||
mod voice;
|
mod voice;
|
||||||
|
|
||||||
/// Initialisiert die App
|
/// Initialisiert die App
|
||||||
|
|
@ -130,6 +131,11 @@ pub fn run() {
|
||||||
teaching::presentation_close,
|
teaching::presentation_close,
|
||||||
teaching::presentation_send_slide,
|
teaching::presentation_send_slide,
|
||||||
teaching::presentation_clear,
|
teaching::presentation_clear,
|
||||||
|
// Auto-Update
|
||||||
|
update::check_for_update,
|
||||||
|
update::download_update,
|
||||||
|
update::apply_update,
|
||||||
|
update::get_current_version,
|
||||||
])
|
])
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
let handle = app.handle().clone();
|
let handle = app.handle().clone();
|
||||||
|
|
|
||||||
268
src-tauri/src/update.rs
Normal file
268
src-tauri/src/update.rs
Normal file
|
|
@ -0,0 +1,268 @@
|
||||||
|
// Claude Desktop — Auto-Update System
|
||||||
|
// Prüft Forgejo-Releases und aktualisiert das AppImage
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use tauri::{AppHandle, Manager, Emitter};
|
||||||
|
|
||||||
|
/// Forgejo API Konfiguration
|
||||||
|
const FORGEJO_URL: &str = "https://git.data-it-solution.de";
|
||||||
|
const REPO_OWNER: &str = "data";
|
||||||
|
const REPO_NAME: &str = "claude-desktop";
|
||||||
|
|
||||||
|
/// Aktuelle App-Version (aus Cargo.toml)
|
||||||
|
const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
|
/// Release-Info von Forgejo
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ReleaseInfo {
|
||||||
|
pub id: i64,
|
||||||
|
pub tag_name: String,
|
||||||
|
pub name: String,
|
||||||
|
pub body: Option<String>,
|
||||||
|
pub draft: bool,
|
||||||
|
pub prerelease: bool,
|
||||||
|
pub created_at: String,
|
||||||
|
pub published_at: Option<String>,
|
||||||
|
pub assets: Vec<ReleaseAsset>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Release-Asset (z.B. AppImage)
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ReleaseAsset {
|
||||||
|
pub id: i64,
|
||||||
|
pub name: String,
|
||||||
|
pub size: i64,
|
||||||
|
pub download_count: i64,
|
||||||
|
pub browser_download_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update-Status für Frontend
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct UpdateStatus {
|
||||||
|
pub available: bool,
|
||||||
|
pub current_version: String,
|
||||||
|
pub latest_version: Option<String>,
|
||||||
|
pub release_notes: Option<String>,
|
||||||
|
pub download_url: Option<String>,
|
||||||
|
pub download_size: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prüft ob ein Update verfügbar ist
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn check_for_update() -> Result<UpdateStatus, String> {
|
||||||
|
let url = format!(
|
||||||
|
"{}/api/v1/repos/{}/{}/releases?limit=1",
|
||||||
|
FORGEJO_URL, REPO_OWNER, REPO_NAME
|
||||||
|
);
|
||||||
|
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let response = client
|
||||||
|
.get(&url)
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Netzwerkfehler: {}", e))?;
|
||||||
|
|
||||||
|
if !response.status().is_success() {
|
||||||
|
return Ok(UpdateStatus {
|
||||||
|
available: false,
|
||||||
|
current_version: CURRENT_VERSION.to_string(),
|
||||||
|
latest_version: None,
|
||||||
|
release_notes: None,
|
||||||
|
download_url: None,
|
||||||
|
download_size: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let releases: Vec<ReleaseInfo> = response
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("JSON-Fehler: {}", e))?;
|
||||||
|
|
||||||
|
let latest = match releases.into_iter().find(|r| !r.draft && !r.prerelease) {
|
||||||
|
Some(r) => r,
|
||||||
|
None => {
|
||||||
|
return Ok(UpdateStatus {
|
||||||
|
available: false,
|
||||||
|
current_version: CURRENT_VERSION.to_string(),
|
||||||
|
latest_version: None,
|
||||||
|
release_notes: None,
|
||||||
|
download_url: None,
|
||||||
|
download_size: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Version vergleichen (tag_name ohne 'v' Prefix)
|
||||||
|
let latest_version = latest.tag_name.trim_start_matches('v').to_string();
|
||||||
|
let is_newer = version_compare(&latest_version, CURRENT_VERSION);
|
||||||
|
|
||||||
|
// AppImage-Asset finden
|
||||||
|
let appimage = latest.assets.iter().find(|a| a.name.ends_with(".AppImage"));
|
||||||
|
|
||||||
|
Ok(UpdateStatus {
|
||||||
|
available: is_newer,
|
||||||
|
current_version: CURRENT_VERSION.to_string(),
|
||||||
|
latest_version: Some(latest_version),
|
||||||
|
release_notes: latest.body,
|
||||||
|
download_url: appimage.map(|a| a.browser_download_url.clone()),
|
||||||
|
download_size: appimage.map(|a| a.size),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Download-Fortschritt
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct DownloadProgress {
|
||||||
|
pub downloaded: u64,
|
||||||
|
pub total: u64,
|
||||||
|
pub percent: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lädt das Update herunter
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn download_update(
|
||||||
|
app: AppHandle,
|
||||||
|
download_url: String,
|
||||||
|
) -> Result<PathBuf, String> {
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let response = client
|
||||||
|
.get(&download_url)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Download-Fehler: {}", e))?;
|
||||||
|
|
||||||
|
let total_size = response.content_length().unwrap_or(0);
|
||||||
|
|
||||||
|
// Temp-Verzeichnis für Download
|
||||||
|
let cache_dir = app.path().app_cache_dir()
|
||||||
|
.map_err(|e| format!("Cache-Verzeichnis nicht gefunden: {}", e))?;
|
||||||
|
std::fs::create_dir_all(&cache_dir).ok();
|
||||||
|
|
||||||
|
let file_name = download_url.split('/').last().unwrap_or("update.AppImage");
|
||||||
|
let download_path = cache_dir.join(file_name);
|
||||||
|
|
||||||
|
let mut file = std::fs::File::create(&download_path)
|
||||||
|
.map_err(|e| format!("Datei erstellen fehlgeschlagen: {}", e))?;
|
||||||
|
|
||||||
|
let mut downloaded: u64 = 0;
|
||||||
|
let mut stream = response.bytes_stream();
|
||||||
|
|
||||||
|
use futures_util::StreamExt;
|
||||||
|
while let Some(chunk) = stream.next().await {
|
||||||
|
let chunk = chunk.map_err(|e| format!("Download-Chunk-Fehler: {}", e))?;
|
||||||
|
file.write_all(&chunk)
|
||||||
|
.map_err(|e| format!("Schreibfehler: {}", e))?;
|
||||||
|
|
||||||
|
downloaded += chunk.len() as u64;
|
||||||
|
|
||||||
|
// Fortschritt an Frontend senden
|
||||||
|
let progress = DownloadProgress {
|
||||||
|
downloaded,
|
||||||
|
total: total_size,
|
||||||
|
percent: if total_size > 0 {
|
||||||
|
(downloaded as f32 / total_size as f32) * 100.0
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
},
|
||||||
|
};
|
||||||
|
app.emit("update-progress", &progress).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ausführbar machen
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
let mut perms = std::fs::metadata(&download_path)
|
||||||
|
.map_err(|e| format!("Metadata-Fehler: {}", e))?
|
||||||
|
.permissions();
|
||||||
|
perms.set_mode(0o755);
|
||||||
|
std::fs::set_permissions(&download_path, perms)
|
||||||
|
.map_err(|e| format!("Permissions-Fehler: {}", e))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("✅ Update heruntergeladen: {:?}", download_path);
|
||||||
|
Ok(download_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wendet das Update an (ersetzt AppImage und startet neu)
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn apply_update(app: AppHandle, update_path: PathBuf) -> Result<(), String> {
|
||||||
|
// Prüfen ob wir in einem AppImage laufen
|
||||||
|
let appimage_path = std::env::var("APPIMAGE").ok();
|
||||||
|
|
||||||
|
if let Some(appimage) = appimage_path {
|
||||||
|
let appimage_path = PathBuf::from(&appimage);
|
||||||
|
|
||||||
|
// Backup erstellen
|
||||||
|
let backup_path = appimage_path.with_extension("AppImage.backup");
|
||||||
|
std::fs::rename(&appimage_path, &backup_path)
|
||||||
|
.map_err(|e| format!("Backup fehlgeschlagen: {}", e))?;
|
||||||
|
|
||||||
|
// Neues AppImage verschieben
|
||||||
|
std::fs::rename(&update_path, &appimage_path)
|
||||||
|
.map_err(|e| {
|
||||||
|
// Backup wiederherstellen bei Fehler
|
||||||
|
std::fs::rename(&backup_path, &appimage_path).ok();
|
||||||
|
format!("Update-Installation fehlgeschlagen: {}", e)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Backup löschen
|
||||||
|
std::fs::remove_file(&backup_path).ok();
|
||||||
|
|
||||||
|
println!("✅ Update installiert, starte neu...");
|
||||||
|
|
||||||
|
// App neustarten (kehrt nicht zurück)
|
||||||
|
app.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nicht in AppImage — Entwicklungsmodus
|
||||||
|
Err("Update nur für AppImage-Builds verfügbar. In Entwicklung: manuell `git pull` und neu bauen.".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gibt aktuelle Version zurück
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_current_version() -> String {
|
||||||
|
CURRENT_VERSION.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vergleicht zwei Versionen (semver-ähnlich)
|
||||||
|
fn version_compare(new: &str, current: &str) -> bool {
|
||||||
|
let parse = |v: &str| -> Vec<u32> {
|
||||||
|
v.split('.')
|
||||||
|
.filter_map(|s| s.parse().ok())
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
let new_parts = parse(new);
|
||||||
|
let current_parts = parse(current);
|
||||||
|
|
||||||
|
for i in 0..3 {
|
||||||
|
let n = new_parts.get(i).copied().unwrap_or(0);
|
||||||
|
let c = current_parts.get(i).copied().unwrap_or(0);
|
||||||
|
if n > c {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if n < c {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_version_compare() {
|
||||||
|
assert!(version_compare("1.0.1", "1.0.0"));
|
||||||
|
assert!(version_compare("1.1.0", "1.0.9"));
|
||||||
|
assert!(version_compare("2.0.0", "1.9.9"));
|
||||||
|
assert!(!version_compare("1.0.0", "1.0.0"));
|
||||||
|
assert!(!version_compare("1.0.0", "1.0.1"));
|
||||||
|
assert!(!version_compare("0.9.0", "1.0.0"));
|
||||||
|
}
|
||||||
|
}
|
||||||
350
src/lib/components/UpdateDialog.svelte
Normal file
350
src/lib/components/UpdateDialog.svelte
Normal file
|
|
@ -0,0 +1,350 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { onMount, onDestroy } from 'svelte';
|
||||||
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
|
||||||
|
|
||||||
|
interface UpdateStatus {
|
||||||
|
available: boolean;
|
||||||
|
current_version: string;
|
||||||
|
latest_version: string | null;
|
||||||
|
release_notes: string | null;
|
||||||
|
download_url: string | null;
|
||||||
|
download_size: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DownloadProgress {
|
||||||
|
downloaded: number;
|
||||||
|
total: number;
|
||||||
|
percent: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
let showDialog = false;
|
||||||
|
let updateInfo: UpdateStatus | null = null;
|
||||||
|
let downloading = false;
|
||||||
|
let progress: DownloadProgress | null = null;
|
||||||
|
let error: string | null = null;
|
||||||
|
let downloadedPath: string | null = null;
|
||||||
|
|
||||||
|
let progressListener: UnlistenFn | null = null;
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
// Update-Check beim Start (mit kurzer Verzögerung)
|
||||||
|
setTimeout(checkForUpdate, 3000);
|
||||||
|
|
||||||
|
// Progress-Events vom Backend
|
||||||
|
progressListener = await listen<DownloadProgress>('update-progress', (event) => {
|
||||||
|
progress = event.payload;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
progressListener?.();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function checkForUpdate() {
|
||||||
|
try {
|
||||||
|
const status: UpdateStatus = await invoke('check_for_update');
|
||||||
|
if (status.available) {
|
||||||
|
updateInfo = status;
|
||||||
|
showDialog = true;
|
||||||
|
console.log('🔄 Update verfügbar:', status.latest_version);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.debug('Update-Check fehlgeschlagen:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startDownload() {
|
||||||
|
if (!updateInfo?.download_url) return;
|
||||||
|
|
||||||
|
downloading = true;
|
||||||
|
error = null;
|
||||||
|
progress = { downloaded: 0, total: updateInfo.download_size || 0, percent: 0 };
|
||||||
|
|
||||||
|
try {
|
||||||
|
downloadedPath = await invoke('download_update', {
|
||||||
|
downloadUrl: updateInfo.download_url,
|
||||||
|
});
|
||||||
|
console.log('✅ Download abgeschlossen:', downloadedPath);
|
||||||
|
} catch (err) {
|
||||||
|
error = String(err);
|
||||||
|
downloading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyUpdate() {
|
||||||
|
if (!downloadedPath) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await invoke('apply_update', { updatePath: downloadedPath });
|
||||||
|
// App wird neugestartet, kein weiterer Code erreicht
|
||||||
|
} catch (err) {
|
||||||
|
error = String(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDialog() {
|
||||||
|
showDialog = false;
|
||||||
|
downloading = false;
|
||||||
|
progress = null;
|
||||||
|
error = null;
|
||||||
|
downloadedPath = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSize(bytes: number): string {
|
||||||
|
if (bytes < 1024) return `${bytes} B`;
|
||||||
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||||
|
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if showDialog && updateInfo}
|
||||||
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
<div class="modal-overlay" on:click={closeDialog}>
|
||||||
|
<div class="modal" on:click|stopPropagation>
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2>🔄 Update verfügbar</h2>
|
||||||
|
<button class="close-btn" on:click={closeDialog}>✕</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="version-info">
|
||||||
|
<span class="current">v{updateInfo.current_version}</span>
|
||||||
|
<span class="arrow">→</span>
|
||||||
|
<span class="new">v{updateInfo.latest_version}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if updateInfo.release_notes}
|
||||||
|
<div class="release-notes">
|
||||||
|
<h3>Änderungen:</h3>
|
||||||
|
<div class="notes-content">
|
||||||
|
{@html updateInfo.release_notes.replace(/\n/g, '<br>')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if updateInfo.download_size}
|
||||||
|
<p class="download-size">
|
||||||
|
Download: {formatSize(updateInfo.download_size)}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if downloading && progress}
|
||||||
|
<div class="progress-container">
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="progress-fill" style="width: {progress.percent}%"></div>
|
||||||
|
</div>
|
||||||
|
<span class="progress-text">
|
||||||
|
{formatSize(progress.downloaded)} / {formatSize(progress.total)}
|
||||||
|
({progress.percent.toFixed(0)}%)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if error}
|
||||||
|
<div class="error">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
{#if downloadedPath}
|
||||||
|
<button class="btn btn-primary" on:click={applyUpdate}>
|
||||||
|
Jetzt installieren & neustarten
|
||||||
|
</button>
|
||||||
|
{:else if downloading}
|
||||||
|
<button class="btn btn-disabled" disabled>
|
||||||
|
Wird heruntergeladen...
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<button class="btn btn-secondary" on:click={closeDialog}>
|
||||||
|
Später
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary" on:click={startDownload}>
|
||||||
|
Jetzt aktualisieren
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
width: 90%;
|
||||||
|
max-width: 450px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header h2 {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
margin-bottom: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-info .current {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-info .arrow {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-info .new {
|
||||||
|
color: var(--success);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.release-notes {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: var(--spacing-sm);
|
||||||
|
margin-bottom: var(--spacing-md);
|
||||||
|
max-height: 150px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.release-notes h3 {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin: 0 0 var(--spacing-xs) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notes-content {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-size {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-container {
|
||||||
|
margin: var(--spacing-md) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
height: 8px;
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: var(--accent);
|
||||||
|
transition: width 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-text {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-top: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
background: rgba(239, 68, 68, 0.1);
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||||
|
color: #ef4444;
|
||||||
|
padding: var(--spacing-sm);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-top: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: var(--spacing-sm) var(--spacing-md);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--accent);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: var(--accent-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-disabled {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
border: none;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import { isProcessing, agentCount, currentModel, sessionStats, contextPercent, contextUsage, initEventListeners, cleanupEventListeners, currentSessionId, setMessagesFromDb, stickyContextInfo, agentMode, type DbMessage, type StickyContextInfo, type AgentMode } from '$lib/stores';
|
import { isProcessing, agentCount, currentModel, sessionStats, contextPercent, contextUsage, initEventListeners, cleanupEventListeners, currentSessionId, setMessagesFromDb, stickyContextInfo, agentMode, type DbMessage, type StickyContextInfo, type AgentMode } from '$lib/stores';
|
||||||
import StopButton from '$lib/components/StopButton.svelte';
|
import StopButton from '$lib/components/StopButton.svelte';
|
||||||
|
import UpdateDialog from '$lib/components/UpdateDialog.svelte';
|
||||||
|
|
||||||
// Session-Typ vom Backend
|
// Session-Typ vom Backend
|
||||||
interface Session {
|
interface Session {
|
||||||
|
|
@ -206,6 +207,9 @@
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Auto-Update Dialog -->
|
||||||
|
<UpdateDialog />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.app-container {
|
.app-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue