From 285ba5b3afb6fe65a14c7ff4d542a37f7a9ebd70 Mon Sep 17 00:00:00 2001 From: whitlocktech Date: Fri, 12 Jun 2026 21:21:45 -0500 Subject: [PATCH] added updater and fixed fonts --- .gitea/workflows/release.yml | 109 ++++++++++ src-tauri/Cargo.toml | 2 + src-tauri/src/commands/mod.rs | 1 + src-tauri/src/commands/update_commands.rs | 192 ++++++++++++++++++ src-tauri/src/lib.rs | 3 + src/components/asset/ItemPreview.module.css | 8 +- src/components/asset/StaticBrowser.module.css | 8 +- src/components/config/ConfigScreen.module.css | 24 +-- src/components/flow/FakeDataInputs.module.css | 8 +- src/components/flow/FlowViewer.module.css | 12 +- src/components/gump/GumpBrowser.module.css | 16 +- src/components/gump/GumpPreview.module.css | 12 +- .../gump/ScriptGumpDetail.module.css | 14 +- src/components/layout/AppShell.module.css | 4 +- src/components/layout/AppShell.tsx | 2 + src/components/layout/CenterPanel.module.css | 2 +- src/components/layout/LeftPanel.module.css | 6 +- src/components/layout/RightPanel.module.css | 10 +- src/components/script/ClassDetail.module.css | 22 +- src/components/script/ScriptTree.module.css | 16 +- .../update/UpdateChecker.module.css | 147 ++++++++++++++ src/components/update/UpdateChecker.tsx | 173 ++++++++++++++++ src/index.css | 2 + src/store/appStore.ts | 51 ++++- 24 files changed, 760 insertions(+), 84 deletions(-) create mode 100644 .gitea/workflows/release.yml create mode 100644 src-tauri/src/commands/update_commands.rs create mode 100644 src/components/update/UpdateChecker.module.css create mode 100644 src/components/update/UpdateChecker.tsx diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..56d4953 --- /dev/null +++ b/.gitea/workflows/release.yml @@ -0,0 +1,109 @@ +name: Build and Release + +on: + push: + tags: + - 'v*' + +jobs: + build: + runs-on: windows-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '20' + + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '8.0' + + - name: Stamp version from tag + shell: powershell + run: | + $tag = "${{ github.ref_name }}" + $version = $tag.TrimStart('v') + Write-Host "Stamping version: $version" + + # Update Cargo.toml (first occurrence of version = "x.y.z" in [package]) + $cargo = Get-Content src-tauri\Cargo.toml -Raw + $cargo = $cargo -replace '(?m)^version = "\d+\.\d+\.\d+"', "version = `"$version`"" + Set-Content src-tauri\Cargo.toml $cargo + + # Update tauri.conf.json + $conf = Get-Content src-tauri\tauri.conf.json -Raw | ConvertFrom-Json + $conf.version = $version + $conf | ConvertTo-Json -Depth 10 | Set-Content src-tauri\tauri.conf.json + + - name: Install npm dependencies + shell: powershell + run: npm install + + - name: Build C# sidecar + shell: powershell + run: | + cd sidecar + dotnet publish -c Release -r win-x64 --self-contained + + - name: Copy sidecar to Tauri binaries + shell: powershell + run: | + New-Item -ItemType Directory -Force -Path src-tauri\binaries + $src = "sidecar\bin\Release\net8.0\win-x64\publish\asw-sidecar.exe" + $dst = "src-tauri\binaries\asw-sidecar-x86_64-pc-windows-msvc.exe" + Copy-Item $src $dst + Write-Host "Copied sidecar: $dst" + + - name: Build Tauri app + shell: powershell + run: npm run tauri build + + - name: Create Gitea Release + id: create_release + shell: powershell + run: | + $tag = "${{ github.ref_name }}" + $body = @{ + tag_name = $tag + name = "Artificer's Scrollwork $tag" + body = "Automated release $tag" + draft = $false + prerelease = $false + } | ConvertTo-Json + + $response = Invoke-RestMethod ` + -Uri "https://gitea.whitlocktech.com/api/v1/repos/whitlocktech/Artificers-Scrollwork/releases" ` + -Method Post ` + -Headers @{ + Authorization = "token ${{ secrets.RELEASE_TOKEN }}" + "Content-Type" = "application/json" + } ` + -Body $body + + $releaseId = $response.id + Write-Host "Created release ID: $releaseId" + Add-Content -Path $env:GITHUB_OUTPUT -Value "release_id=$releaseId" + + - name: Upload MSI + shell: powershell + run: | + $releaseId = "${{ steps.create_release.outputs.release_id }}" + $msi = Get-ChildItem -Path "src-tauri\target\release\bundle\msi\*.msi" | Select-Object -First 1 + + Write-Host "MSI: $($msi.FullName)" + + if (!(Test-Path $msi.FullName)) { + Write-Error "MSI not found in src-tauri\target\release\bundle\msi\" + exit 1 + } + + $uri = "https://gitea.whitlocktech.com/api/v1/repos/whitlocktech/Artificers-Scrollwork/releases/$releaseId/assets?name=$($msi.Name)" + + & curl.exe -X POST ` + -H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" ` + -F "attachment=@$($msi.FullName)" ` + "$uri" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 50d76ab..43194ea 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -25,6 +25,8 @@ image = { version = "0.25", features = ["dds"] } once_cell = "1" tokio = { version = "1", features = ["full"] } walkdir = "2" +reqwest = { version = "0.12", features = ["json", "stream"] } +futures-util = "0.3" [features] custom-protocol = ["tauri/custom-protocol"] diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index b813be6..b8b060e 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -2,3 +2,4 @@ pub mod asset_commands; pub mod config_commands; pub mod gump_commands; pub mod script_commands; +pub mod update_commands; diff --git a/src-tauri/src/commands/update_commands.rs b/src-tauri/src/commands/update_commands.rs new file mode 100644 index 0000000..6c58b16 --- /dev/null +++ b/src-tauri/src/commands/update_commands.rs @@ -0,0 +1,192 @@ +use futures_util::StreamExt; +use serde::{Deserialize, Serialize}; +use std::io::Write; +use tauri::{AppHandle, Emitter}; + +const GITEA_API: &str = + "https://gitea.whitlocktech.com/api/v1/repos/whitlocktech/Artificers-Scrollwork/releases"; + +const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +// ── Gitea API types ─────────────────────────────────────────────────────────── + +#[derive(Debug, Deserialize)] +struct GiteaRelease { + tag_name: String, + body: String, + assets: Vec, + #[serde(default)] + prerelease: bool, + #[serde(default)] + draft: bool, +} + +#[derive(Debug, Deserialize)] +struct GiteaAsset { + name: String, + size: u64, + browser_download_url: String, +} + +// ── Public types returned to the frontend ──────────────────────────────────── + +#[derive(Debug, Serialize)] +pub struct UpdateInfo { + pub available: bool, + pub latest_version: String, + pub current_version: String, + pub release_notes: String, + pub download_url: String, + pub asset_name: String, + /// File size in bytes (0 if unknown) + pub asset_size: u64, +} + +// ── Version helpers ─────────────────────────────────────────────────────────── + +fn parse_semver(v: &str) -> (u32, u32, u32) { + let v = v.trim_start_matches('v'); + let mut parts = v.split('.').filter_map(|p| p.parse::().ok()); + ( + parts.next().unwrap_or(0), + parts.next().unwrap_or(0), + parts.next().unwrap_or(0), + ) +} + +fn is_newer(tag: &str, current: &str) -> bool { + parse_semver(tag) > parse_semver(current) +} + +// ── Commands ───────────────────────────────────────────────────────────────── + +/// Check the Gitea releases API for a newer version. +/// Returns `UpdateInfo` with `available: true` when a newer MSI release exists. +#[tauri::command] +pub async fn check_for_update() -> Result { + let client = reqwest::Client::builder() + .user_agent(format!( + "ArtificersScrollwork/{CURRENT_VERSION} (updater)" + )) + .timeout(std::time::Duration::from_secs(15)) + .build() + .map_err(|e| e.to_string())?; + + let releases: Vec = client + .get(format!("{GITEA_API}?limit=10&page=1")) + .send() + .await + .map_err(|e| format!("Network error: {e}"))? + .json() + .await + .map_err(|e| format!("Failed to parse release data: {e}"))?; + + // Pick the latest non-draft, non-prerelease that ships an MSI + let release = releases + .iter() + .find(|r| !r.draft && !r.prerelease && r.assets.iter().any(|a| a.name.ends_with(".msi"))); + + let Some(release) = release else { + return Ok(UpdateInfo { + available: false, + latest_version: CURRENT_VERSION.to_string(), + current_version: CURRENT_VERSION.to_string(), + release_notes: String::new(), + download_url: String::new(), + asset_name: String::new(), + asset_size: 0, + }); + }; + + let msi = release + .assets + .iter() + .find(|a| a.name.ends_with(".msi")) + .unwrap(); // safe: filtered above + + Ok(UpdateInfo { + available: is_newer(&release.tag_name, CURRENT_VERSION), + latest_version: release.tag_name.trim_start_matches('v').to_string(), + current_version: CURRENT_VERSION.to_string(), + release_notes: release.body.clone(), + download_url: msi.browser_download_url.clone(), + asset_name: msi.name.clone(), + asset_size: msi.size, + }) +} + +/// Download the MSI to the system temp directory, emitting `update-progress` +/// events as chunks arrive, then launch the installer via msiexec. +/// +/// The frontend should listen for `update-progress` events: +/// `{ downloaded: number, total: number }` +/// +/// After this returns Ok(()), the installer will be running and the user +/// can follow the wizard. The app does NOT auto-close; the installer handles it. +#[tauri::command] +pub async fn download_and_install_update( + app: AppHandle, + download_url: String, + asset_name: String, +) -> Result<(), String> { + let client = reqwest::Client::builder() + .user_agent(format!( + "ArtificersScrollwork/{CURRENT_VERSION} (updater)" + )) + .timeout(std::time::Duration::from_secs(300)) + .build() + .map_err(|e| e.to_string())?; + + let response = client + .get(&download_url) + .send() + .await + .map_err(|e| format!("Download request failed: {e}"))?; + + if !response.status().is_success() { + return Err(format!( + "Download failed with status {}", + response.status() + )); + } + + let total = response.content_length().unwrap_or(0); + + // Write to temp dir + let installer_path = std::env::temp_dir().join(&asset_name); + let mut file = + std::fs::File::create(&installer_path).map_err(|e| format!("Cannot create temp file: {e}"))?; + + let mut downloaded = 0u64; + let mut stream = response.bytes_stream(); + + while let Some(chunk) = stream.next().await { + let chunk = chunk.map_err(|e| format!("Download interrupted: {e}"))?; + file.write_all(&chunk) + .map_err(|e| format!("Write error: {e}"))?; + downloaded += chunk.len() as u64; + let _ = app.emit( + "update-progress", + serde_json::json!({ "downloaded": downloaded, "total": total }), + ); + } + + drop(file); // ensure file handle is closed before launching + + // Launch installer — Windows opens .msi files with msiexec automatically + #[cfg(target_os = "windows")] + { + std::process::Command::new("msiexec") + .args(["/i", &installer_path.to_string_lossy().to_string()]) + .spawn() + .map_err(|e| format!("Failed to launch installer: {e}"))?; + } + + #[cfg(not(target_os = "windows"))] + { + // Fallback for non-Windows (future proofing) + opener::open(&installer_path).map_err(|e| format!("Failed to open installer: {e}"))?; + } + + Ok(()) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 6bf49ca..b241898 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -19,6 +19,9 @@ pub fn run() { Ok(()) }) .invoke_handler(tauri::generate_handler![ + // Updater + commands::update_commands::check_for_update, + commands::update_commands::download_and_install_update, // Config commands::config_commands::get_config, commands::config_commands::set_config, diff --git a/src/components/asset/ItemPreview.module.css b/src/components/asset/ItemPreview.module.css index 87d2ee0..310a559 100644 --- a/src/components/asset/ItemPreview.module.css +++ b/src/components/asset/ItemPreview.module.css @@ -14,14 +14,14 @@ } .name { - font-size: 16px; + font-size: 1em; color: var(--accent-gold); letter-spacing: 0.04em; } .id { font-family: 'JetBrains Mono', monospace; - font-size: 12px; + font-size: 0.75em; color: var(--text-muted); } @@ -42,7 +42,7 @@ .loading, .error { - font-size: 13px; + font-size: 0.8125em; color: var(--text-secondary); font-style: italic; } @@ -57,7 +57,7 @@ align-items: center; gap: 10px; color: var(--text-muted); - font-size: 12px; + font-size: 0.75em; letter-spacing: 0.05em; text-transform: uppercase; } diff --git a/src/components/asset/StaticBrowser.module.css b/src/components/asset/StaticBrowser.module.css index 139142e..d9068bd 100644 --- a/src/components/asset/StaticBrowser.module.css +++ b/src/components/asset/StaticBrowser.module.css @@ -22,13 +22,13 @@ .id { font-family: 'JetBrains Mono', monospace; - font-size: 11px; + font-size: 0.6875em; color: var(--text-muted); min-width: 48px; } .name { - font-size: 13px; + font-size: 0.8125em; color: var(--text-primary); overflow: hidden; text-overflow: ellipsis; @@ -41,7 +41,7 @@ border: none; border-top: 1px solid var(--border); color: var(--accent-gold); - font-size: 13px; + font-size: 0.8125em; cursor: pointer; } @@ -52,7 +52,7 @@ .empty { padding: 20px; color: var(--text-muted); - font-size: 13px; + font-size: 0.8125em; font-style: italic; line-height: 1.6; } diff --git a/src/components/config/ConfigScreen.module.css b/src/components/config/ConfigScreen.module.css index 575040c..fc7fa1c 100644 --- a/src/components/config/ConfigScreen.module.css +++ b/src/components/config/ConfigScreen.module.css @@ -19,13 +19,13 @@ } .title { - font-size: 22px; + font-size: 1.375em; color: var(--accent-gold); letter-spacing: 0.06em; } .subtitle { - font-size: 14px; + font-size: 0.875em; color: var(--text-secondary); margin-top: -16px; } @@ -38,7 +38,7 @@ .label { font-family: 'Cinzel', serif; - font-size: 12px; + font-size: 0.75em; color: var(--text-secondary); letter-spacing: 0.06em; text-transform: uppercase; @@ -52,7 +52,7 @@ .pathInput { flex: 1; font-family: 'JetBrains Mono', monospace; - font-size: 12px; + font-size: 0.75em; } .btn { @@ -60,7 +60,7 @@ background: var(--bg-elevated); border: 1px solid var(--border); color: var(--text-primary); - font-size: 13px; + font-size: 0.8125em; transition: border-color 0.15s, color 0.15s; white-space: nowrap; } @@ -104,7 +104,7 @@ display: flex; flex-direction: column; gap: 3px; - font-size: 12px; + font-size: 0.75em; font-family: 'JetBrains Mono', monospace; } @@ -126,7 +126,7 @@ background: var(--accent-red); border: 1px solid #b04040; padding: 10px; - font-size: 13px; + font-size: 0.8125em; color: #ffd0d0; } @@ -149,7 +149,7 @@ .sectionLabel { font-family: 'Cinzel', serif; - font-size: 11px; + font-size: 0.6875em; letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-muted); @@ -163,7 +163,7 @@ border: 1px solid var(--border); color: var(--text-primary); font-family: 'EB Garamond', Georgia, serif; - font-size: 14px; + font-size: 0.875em; border-radius: 3px; cursor: pointer; } @@ -185,7 +185,7 @@ display: flex; align-items: center; justify-content: center; - font-size: 16px; + font-size: 1em; font-weight: 600; padding: 0; flex-shrink: 0; @@ -231,7 +231,7 @@ .fontSizeLabel { margin-left: auto; font-family: 'JetBrains Mono', monospace; - font-size: 12px; + font-size: 0.75em; color: var(--text-secondary); min-width: 36px; text-align: right; @@ -260,7 +260,7 @@ .colorHex { font-family: 'JetBrains Mono', monospace; - font-size: 13px; + font-size: 0.8125em; color: var(--text-secondary); min-width: 64px; } diff --git a/src/components/flow/FakeDataInputs.module.css b/src/components/flow/FakeDataInputs.module.css index 5a8f4cc..819b5ed 100644 --- a/src/components/flow/FakeDataInputs.module.css +++ b/src/components/flow/FakeDataInputs.module.css @@ -4,7 +4,7 @@ } .title { - font-size: 10px; + font-size: 0.625em; color: var(--accent-gold); letter-spacing: 0.08em; text-transform: uppercase; @@ -12,7 +12,7 @@ } .hint { - font-size: 11px; + font-size: 0.6875em; color: var(--text-muted); font-style: italic; margin-bottom: 10px; @@ -27,12 +27,12 @@ .label { font-family: 'JetBrains Mono', monospace; - font-size: 11px; + font-size: 0.6875em; color: var(--text-secondary); } .input { width: 100%; - font-size: 12px; + font-size: 0.75em; font-family: 'JetBrains Mono', monospace; } diff --git a/src/components/flow/FlowViewer.module.css b/src/components/flow/FlowViewer.module.css index 095afd4..f4d25f4 100644 --- a/src/components/flow/FlowViewer.module.css +++ b/src/components/flow/FlowViewer.module.css @@ -17,7 +17,7 @@ .methodLabel { flex: 1; - font-size: 13px; + font-size: 0.8125em; color: var(--accent-gold); letter-spacing: 0.04em; overflow: hidden; @@ -30,7 +30,7 @@ background: none; border: 1px solid var(--border); color: var(--text-secondary); - font-size: 12px; + font-size: 0.75em; cursor: pointer; transition: border-color 0.15s, color 0.15s; } @@ -58,7 +58,7 @@ align-items: center; justify-content: center; color: var(--text-muted); - font-size: 14px; + font-size: 0.875em; font-style: italic; padding: 40px; text-align: center; @@ -77,12 +77,12 @@ .errorTitle { font-family: 'Cinzel', serif; color: #cc6666; - font-size: 16px; + font-size: 1em; } .errorMsg { font-family: 'JetBrains Mono', monospace; - font-size: 12px; + font-size: 0.75em; color: var(--text-secondary); max-width: 500px; text-align: center; @@ -94,6 +94,6 @@ border: 1px solid var(--border-accent); color: var(--accent-gold); font-family: 'Cinzel', serif; - font-size: 13px; + font-size: 0.8125em; cursor: pointer; } diff --git a/src/components/gump/GumpBrowser.module.css b/src/components/gump/GumpBrowser.module.css index 0ba62ff..e5fa1b7 100644 --- a/src/components/gump/GumpBrowser.module.css +++ b/src/components/gump/GumpBrowser.module.css @@ -19,7 +19,7 @@ border-right: 1px solid var(--border); color: var(--text-secondary); font-family: 'Cinzel', serif; - font-size: 11px; + font-size: 0.6875em; cursor: pointer; transition: color 0.15s, background 0.15s; } @@ -52,7 +52,7 @@ background: var(--bg-elevated); border: 1px solid var(--border); color: var(--text-secondary); - font-size: 10px; + font-size: 0.625em; padding: 2px 7px; border-radius: 3px; cursor: pointer; @@ -73,7 +73,7 @@ } .count { - font-size: 10px; + font-size: 0.625em; color: var(--text-muted); padding: 4px 10px; flex-shrink: 0; @@ -110,7 +110,7 @@ .itemId { font-family: 'JetBrains Mono', 'Fira Code', monospace; - font-size: 10px; + font-size: 0.625em; color: var(--accent-gold); flex-shrink: 0; min-width: 48px; @@ -118,7 +118,7 @@ .itemName { font-family: 'EB Garamond', serif; - font-size: 13px; + font-size: 0.8125em; color: var(--text-primary); overflow: hidden; text-overflow: ellipsis; @@ -127,7 +127,7 @@ .itemPath { font-family: 'JetBrains Mono', 'Fira Code', monospace; - font-size: 9px; + font-size: 0.5625em; color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; @@ -139,12 +139,12 @@ padding: 16px; color: var(--text-secondary); font-family: 'Cinzel', serif; - font-size: 12px; + font-size: 0.75em; } .error { padding: 16px; color: var(--accent-red); font-family: 'JetBrains Mono', monospace; - font-size: 11px; + font-size: 0.6875em; } diff --git a/src/components/gump/GumpPreview.module.css b/src/components/gump/GumpPreview.module.css index d59b710..e2aa2c4 100644 --- a/src/components/gump/GumpPreview.module.css +++ b/src/components/gump/GumpPreview.module.css @@ -17,13 +17,13 @@ .name { font-family: 'Cinzel', serif; - font-size: 16px; + font-size: 1em; color: var(--text-primary); } .id { font-family: 'JetBrains Mono', 'Fira Code', monospace; - font-size: 12px; + font-size: 0.75em; color: var(--accent-gold); } @@ -35,7 +35,7 @@ .tag { font-family: 'Cinzel', serif; - font-size: 10px; + font-size: 0.625em; color: var(--text-secondary); background: var(--bg-elevated); border: 1px solid var(--border); @@ -57,7 +57,7 @@ .loading { color: var(--text-secondary); font-family: 'Cinzel', serif; - font-size: 12px; + font-size: 0.75em; padding: 8px; } @@ -68,7 +68,7 @@ gap: 6px; padding: 16px; color: var(--text-muted); - font-size: 12px; + font-size: 0.75em; font-family: 'Cinzel', serif; } @@ -79,6 +79,6 @@ .meta { font-family: 'JetBrains Mono', 'Fira Code', monospace; - font-size: 11px; + font-size: 0.6875em; color: var(--text-muted); } diff --git a/src/components/gump/ScriptGumpDetail.module.css b/src/components/gump/ScriptGumpDetail.module.css index 4da0c34..cd60330 100644 --- a/src/components/gump/ScriptGumpDetail.module.css +++ b/src/components/gump/ScriptGumpDetail.module.css @@ -14,7 +14,7 @@ .className { display: block; font-family: 'Cinzel', serif; - font-size: 20px; + font-size: 1.25em; color: var(--accent-gold-bright); letter-spacing: 0.04em; margin-bottom: 4px; @@ -23,7 +23,7 @@ .friendlyName { display: block; font-family: 'EB Garamond', serif; - font-size: 14px; + font-size: 0.875em; color: var(--text-secondary); font-style: italic; } @@ -45,7 +45,7 @@ .label { font-family: 'Cinzel', serif; - font-size: 10px; + font-size: 0.625em; color: var(--text-muted); letter-spacing: 0.1em; text-transform: uppercase; @@ -53,7 +53,7 @@ .filepath { font-family: 'JetBrains Mono', 'Fira Code', monospace; - font-size: 12px; + font-size: 0.75em; color: var(--text-secondary); background: var(--bg-elevated); border: 1px solid var(--border); @@ -73,14 +73,14 @@ border: 1px solid var(--border-accent); color: var(--accent-gold); font-family: 'Cinzel', serif; - font-size: 10px; + font-size: 0.625em; padding: 3px 9px; border-radius: 3px; } .notes { font-family: 'EB Garamond', serif; - font-size: 14px; + font-size: 0.875em; color: var(--text-secondary); line-height: 1.6; } @@ -92,5 +92,5 @@ height: 100%; color: var(--text-muted); font-family: 'Cinzel', serif; - font-size: 13px; + font-size: 0.8125em; } diff --git a/src/components/layout/AppShell.module.css b/src/components/layout/AppShell.module.css index 11c07a3..274ce0f 100644 --- a/src/components/layout/AppShell.module.css +++ b/src/components/layout/AppShell.module.css @@ -17,7 +17,7 @@ } .title { - font-size: 15px; + font-size: 0.9375em; color: var(--accent-gold); letter-spacing: 0.05em; } @@ -32,7 +32,7 @@ border: 1px solid var(--border); color: var(--text-secondary); padding: 4px 10px; - font-size: 13px; + font-size: 0.8125em; font-family: 'Cinzel', serif; transition: border-color 0.15s, color 0.15s; } diff --git a/src/components/layout/AppShell.tsx b/src/components/layout/AppShell.tsx index 9286d89..b347138 100644 --- a/src/components/layout/AppShell.tsx +++ b/src/components/layout/AppShell.tsx @@ -5,6 +5,7 @@ import LeftPanel from './LeftPanel'; import CenterPanel from './CenterPanel'; import RightPanel from './RightPanel'; import ConfigScreen from '../config/ConfigScreen'; +import UpdateChecker from '../update/UpdateChecker'; import styles from './AppShell.module.css'; export default function AppShell() { @@ -46,6 +47,7 @@ export default function AppShell() {
Artificer's Scrollwork
+ + ); + } + + if (state.kind === 'checking') { + return Checking…; + } + + if (state.kind === 'up_to_date') { + return ✓ Up to date; + } + + if (state.kind === 'error') { + return ( + setState({ kind: 'idle' })} + > + ✗ Update error — click to dismiss + + ); + } + + if (state.kind === 'launching') { + return ( + + ✓ Installer launched — complete the wizard to finish + + ); + } + + if (state.kind === 'downloading') { + const pct = state.total > 0 ? Math.round((state.downloaded / state.total) * 100) : 0; + return ( +
+ + Downloading… {fmtBytes(state.downloaded)} + {state.total > 0 ? ` / ${fmtBytes(state.total)}` : ''} + +
+
+
+
+ ); + } + + // state.kind === 'available' + const { info } = state; + return ( +
+
+ + ✦ v{info.latest_version} available + + + + +
+ {expanded && info.release_notes && ( +
+
{info.release_notes}
+
+ )} +
+ ); +} diff --git a/src/index.css b/src/index.css index efdefd3..febd472 100644 --- a/src/index.css +++ b/src/index.css @@ -70,11 +70,13 @@ body { button { cursor: pointer; font-family: inherit; + font-size: inherit; } /* Input */ input, textarea, select { font-family: inherit; + font-size: inherit; background: var(--bg-elevated); color: var(--text-primary); border: 1px solid var(--border); diff --git a/src/store/appStore.ts b/src/store/appStore.ts index 2581cc2..b9caa8f 100644 --- a/src/store/appStore.ts +++ b/src/store/appStore.ts @@ -25,11 +25,56 @@ export const DEFAULT_FONT_FAMILY = FONT_FAMILY_OPTIONS[0].value; export const DEFAULT_TEXT_COLOR = '#d4c49a'; export const DEFAULT_FONT_SIZE_INDEX_REAL = 1; // index 1 = 16px +// ── Color utilities ─────────────────────────────────────────────────────────── + +function hexToRgb(hex: string): [number, number, number] { + const n = parseInt(hex.replace('#', ''), 16); + return [(n >> 16) & 255, (n >> 8) & 255, n & 255]; +} + +function rgbToHex(r: number, g: number, b: number): string { + return '#' + [r, g, b] + .map((v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, '0')) + .join(''); +} + +/** Scale each channel by `factor` (0–2). Values above 255 are clamped. */ +function scaleColor(hex: string, factor: number): string { + const [r, g, b] = hexToRgb(hex); + return rgbToHex(r * factor, g * factor, b * factor); +} + +/** + * Derive the full text-color palette from a single primary color. + * + * Relationships mirror the default palette ratios: + * primary (#d4c49a) → 1.0× + * accent-gold-bright → ~1.08× (slightly brighter/warmer) + * accent-gold → ~0.88× + * text-secondary → ~0.54× + * text-muted → ~0.29× + */ +function deriveColorPalette(primary: string) { + return { + '--text-primary': primary, + '--accent-gold-bright': scaleColor(primary, 1.08), + '--accent-gold': scaleColor(primary, 0.88), + '--text-secondary': scaleColor(primary, 0.54), + '--text-muted': scaleColor(primary, 0.29), + }; +} + export function applyTypographyVars(fontFamily: string, fontSizeIndex: number, textColor: string) { const size = FONT_SIZE_VALUES[fontSizeIndex] ?? 16; - document.documentElement.style.setProperty('--font-family-body', fontFamily); - document.documentElement.style.setProperty('--font-size-base', `${size}px`); - document.documentElement.style.setProperty('--text-primary', textColor); + const el = document.documentElement; + + el.style.setProperty('--font-family-body', fontFamily); + el.style.setProperty('--font-size-base', `${size}px`); + + const palette = deriveColorPalette(textColor); + for (const [prop, val] of Object.entries(palette)) { + el.style.setProperty(prop, val); + } } export type LeftPanelTab = 'scripts' | 'statics' | 'mobiles' | 'gumps';