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).
|
||||
**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
|
||||
- Feature: Auto-Update System für AppImage
|
||||
- `f191cd0` Feature: Kontext-Auslastung im Footer (X% ctx)
|
||||
- `48fd61f` Fix: Auto-Session erscheint sofort in Session-Liste
|
||||
- `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",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-streams 0.4.2",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
|
|
@ -3610,7 +3612,7 @@ dependencies = [
|
|||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-streams",
|
||||
"wasm-streams 0.5.0",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
|
|
@ -5489,6 +5491,19 @@ dependencies = [
|
|||
"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]]
|
||||
name = "wasm-streams"
|
||||
version = "0.5.0"
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ chrono = { version = "0.4", features = ["serde"] }
|
|||
uuid = { version = "1", features = ["v4", "serde"] }
|
||||
rusqlite = { version = "0.31", features = ["bundled"] }
|
||||
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"
|
||||
tokio-tungstenite = "0.23"
|
||||
futures-util = "0.3"
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ mod memory;
|
|||
mod programs;
|
||||
mod session;
|
||||
mod teaching;
|
||||
mod update;
|
||||
mod voice;
|
||||
|
||||
/// Initialisiert die App
|
||||
|
|
@ -130,6 +131,11 @@ pub fn run() {
|
|||
teaching::presentation_close,
|
||||
teaching::presentation_send_slide,
|
||||
teaching::presentation_clear,
|
||||
// Auto-Update
|
||||
update::check_for_update,
|
||||
update::download_update,
|
||||
update::apply_update,
|
||||
update::get_current_version,
|
||||
])
|
||||
.setup(|app| {
|
||||
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 { 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 UpdateDialog from '$lib/components/UpdateDialog.svelte';
|
||||
|
||||
// Session-Typ vom Backend
|
||||
interface Session {
|
||||
|
|
@ -206,6 +207,9 @@
|
|||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- Auto-Update Dialog -->
|
||||
<UpdateDialog />
|
||||
|
||||
<style>
|
||||
.app-container {
|
||||
display: flex;
|
||||
|
|
|
|||
Loading…
Reference in a new issue