From d4e3060653929c461ff9d764d5ccf6b7e4ef012c Mon Sep 17 00:00:00 2001 From: Horuse <39675195+Horuse@users.noreply.github.com> Date: Fri, 11 Sep 2026 01:57:48 +0300 Subject: [PATCH 1/2] feat: add beta updates channel setting --- .github/workflows/release.yml | 6 ++-- src-tauri/Cargo.lock | 1 + src-tauri/Cargo.toml | 1 + src-tauri/src/commands.rs | 24 +++++++++---- src/lib/modules/settings/stores.svelte.ts | 5 +++ src/lib/modules/updater/methods.ts | 34 ++++++++++++++++--- .../modules/updater/ui/update_banner.svelte | 13 +++++-- src/routes/settings/+page.svelte | 7 ++++ 8 files changed, 74 insertions(+), 17 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cfe2050e..ad555c0c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -52,7 +52,7 @@ jobs: tagName: ${{ github.ref_name }} releaseName: Splitwave ${{ github.ref_name }} releaseDraft: true - prerelease: false + prerelease: ${{ contains(github.ref_name, '-rc') || contains(github.ref_name, '-beta') || contains(github.ref_name, '-alpha') }} args: ${{ matrix.args }} linux: @@ -106,7 +106,7 @@ jobs: tagName: ${{ github.ref_name }} releaseName: Splitwave ${{ github.ref_name }} releaseDraft: true - prerelease: false + prerelease: ${{ contains(github.ref_name, '-rc') || contains(github.ref_name, '-beta') || contains(github.ref_name, '-alpha') }} args: --bundles appimage,deb,rpm # linuxdeploy bundles libwayland/libpipewire/libspa; on newer Mesa they @@ -211,5 +211,5 @@ jobs: tagName: ${{ github.ref_name }} releaseName: Splitwave ${{ github.ref_name }} releaseDraft: true - prerelease: false + prerelease: ${{ contains(github.ref_name, '-rc') || contains(github.ref_name, '-beta') || contains(github.ref_name, '-alpha') }} args: --bundles nsis diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 9c38f698..b1a01d75 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -6284,6 +6284,7 @@ dependencies = [ "tracing", "tracing-subscriber", "ts-rs", + "url", "vst3", "webpki-roots 0.26.11", "webrtc", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 46e124a4..8a237552 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -70,6 +70,7 @@ tauri-plugin-os = "2" tauri-plugin-autostart = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" +url = "2" thiserror = "1" cpal = "0.15" rtrb = "0.3" diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 2739d6ec..c0c289bd 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -533,8 +533,8 @@ pub async fn stop_pipeline(state: State<'_, AppState>, app: AppHandle) -> AppRes // Updater errors serialize Display-only, hiding reqwest's cause; unwind source()+Debug. #[tauri::command] -pub async fn diagnose_update_error(app: AppHandle) -> String { - let report = match configured_updater(&app) { +pub async fn diagnose_update_error(app: AppHandle, endpoint: Option) -> String { + let report = match configured_updater(&app, endpoint.as_deref()) { Ok(updater) => match updater.check().await { Ok(Some(u)) => format!("check succeeded; update {} is available", u.version), Ok(None) => "check succeeded; no update available".to_string(), @@ -563,14 +563,21 @@ fn updater_tls_config() -> rustls::ClientConfig { .with_no_client_auth() } -fn configured_updater(app: &AppHandle) -> Result { +fn configured_updater( + app: &AppHandle, + endpoint: Option<&str>, +) -> Result { use tauri_plugin_updater::UpdaterExt; #[cfg(target_os = "linux")] - let builder = app + let mut builder = app .updater_builder() .configure_client(|b| b.tls_backend_preconfigured(updater_tls_config())); #[cfg(not(target_os = "linux"))] - let builder = app.updater_builder(); + let mut builder = app.updater_builder(); + if let Some(ep) = endpoint { + let url = url::Url::parse(ep).map_err(|e| e.to_string())?; + builder = builder.endpoints(vec![url]).map_err(|e| e.to_string())?; + } builder.build().map_err(|e| e.to_string()) } @@ -588,9 +595,12 @@ pub struct UpdateMetadata { /// Mirrors `plugin:updater|check` but with bundled roots wired into the HTTP /// client; the plugin's own `check` command can't be configured. #[tauri::command] -pub async fn check_for_updates(app: AppHandle) -> Result, String> { +pub async fn check_for_updates( + app: AppHandle, + endpoint: Option, +) -> Result, String> { use tauri::Manager; - let updater = configured_updater(&app)?; + let updater = configured_updater(&app, endpoint.as_deref())?; let Some(update) = updater.check().await.map_err(|e| e.to_string())? else { return Ok(None); }; diff --git a/src/lib/modules/settings/stores.svelte.ts b/src/lib/modules/settings/stores.svelte.ts index ed397b29..b67f9558 100644 --- a/src/lib/modules/settings/stores.svelte.ts +++ b/src/lib/modules/settings/stores.svelte.ts @@ -5,6 +5,7 @@ const KEY = 'app:settings'; interface Stored { checkUpdatesOnLaunch: boolean; + includePreReleases: boolean; maxSnapshots: number; snapToGrid: boolean; gridSize: number; @@ -16,6 +17,7 @@ interface Stored { const DEFAULTS: Stored = { checkUpdatesOnLaunch: true, + includePreReleases: false, maxSnapshots: 20, snapToGrid: false, gridSize: 20, @@ -41,6 +43,7 @@ function load(): Stored { class AppSettings { #initial = load(); checkUpdatesOnLaunch = $state(this.#initial.checkUpdatesOnLaunch); + includePreReleases = $state(this.#initial.includePreReleases ?? false); maxSnapshots = $state(this.#initial.maxSnapshots); snapToGrid = $state(this.#initial.snapToGrid); gridSize = $state(this.#initial.gridSize); @@ -53,6 +56,7 @@ class AppSettings { if (!browser) return; const { checkUpdatesOnLaunch, + includePreReleases, maxSnapshots, snapToGrid, gridSize, @@ -65,6 +69,7 @@ class AppSettings { KEY, JSON.stringify({ checkUpdatesOnLaunch, + includePreReleases, maxSnapshots, snapToGrid, gridSize, diff --git a/src/lib/modules/updater/methods.ts b/src/lib/modules/updater/methods.ts index 1521805e..1b0e44fd 100644 --- a/src/lib/modules/updater/methods.ts +++ b/src/lib/modules/updater/methods.ts @@ -4,6 +4,7 @@ import { LazyStore } from '@tauri-apps/plugin-store'; import { invoke } from '@tauri-apps/api/core'; import { arch, type as osType } from '@tauri-apps/plugin-os'; import { updaterStore } from './stores.svelte'; +import { appSettings } from '$lib/modules/settings/stores.svelte'; // Shape returned by the `check_for_updates` command; mirrors the plugin's // `check` metadata so `new Update(...)` can wrap it unchanged. @@ -53,10 +54,35 @@ function noBuildMessage(): string { return `Unfortunately, the latest version has no build for ${arch()} on ${osLabel(osType())}. It looks like you built it yourself, or this platform is no longer supported.`; } +async function resolveUpdateEndpoint(): Promise { + if (!appSettings.includePreReleases) return null; + try { + const res = await fetch(RELEASES_API, { + headers: { Accept: 'application/vnd.github+json' } + }); + if (!res.ok) return null; + const releases = await res.json(); + if (!Array.isArray(releases)) return null; + const candidate = releases.find( + (r: { draft?: boolean; assets?: Array<{ name: string; browser_download_url: string }> }) => + !r.draft && Array.isArray(r.assets) && r.assets.some((a) => a.name === 'latest.json') + ); + if (candidate) { + const asset = candidate.assets.find((a: { name: string }) => a.name === 'latest.json'); + return asset?.browser_download_url ?? null; + } + } catch { + // Fall back to default endpoint + } + return null; +} + export async function checkForUpdates(silent = false): Promise { updaterStore.state = { phase: 'checking' }; + let endpoint: string | null = null; try { - const metadata = await invoke('check_for_updates'); + endpoint = await resolveUpdateEndpoint(); + const metadata = await invoke('check_for_updates', { endpoint }); const update = metadata ? new Update(metadata) : null; if (!update) { updaterStore.state = silent ? { phase: 'idle' } : { phase: 'up_to_date' }; @@ -77,7 +103,7 @@ export async function checkForUpdates(silent = false): Promise { updaterStore.state = silent ? { phase: 'idle' } : { phase: 'unsupported', message: noBuildMessage() }; return; } - const message = await diagnoseError(e); + const message = await diagnoseError(e, endpoint); updaterStore.state = silent ? { phase: 'idle' } : { phase: 'error', message }; } } @@ -115,10 +141,10 @@ export function latestRelease(): Promise { return fetchRelease('/latest'); } -async function diagnoseError(e: unknown): Promise { +async function diagnoseError(e: unknown, endpoint?: string | null): Promise { const base = e instanceof Error ? e.message : String(e); try { - const detail = await invoke('diagnose_update_error'); + const detail = await invoke('diagnose_update_error', { endpoint: endpoint ?? null }); return detail ? `${base}\n\n${detail}` : base; } catch { return base; diff --git a/src/lib/modules/updater/ui/update_banner.svelte b/src/lib/modules/updater/ui/update_banner.svelte index 03b54194..3e012356 100644 --- a/src/lib/modules/updater/ui/update_banner.svelte +++ b/src/lib/modules/updater/ui/update_banner.svelte @@ -42,9 +42,16 @@ {#snippet badge()} {#if s.phase === 'available' || s.phase === 'downloading'} - - v{s.update.version} - +
+ + v{s.update.version} + + {#if /-(rc|beta|alpha)/i.test(s.update.version)} + + BETA + + {/if} +
{/if} {/snippet} diff --git a/src/routes/settings/+page.svelte b/src/routes/settings/+page.svelte index 92ab44c1..73c35de5 100644 --- a/src/routes/settings/+page.svelte +++ b/src/routes/settings/+page.svelte @@ -46,6 +46,7 @@ function setApp< K extends | 'checkUpdatesOnLaunch' + | 'includePreReleases' | 'maxSnapshots' | 'snapToGrid' | 'gridSize' @@ -209,6 +210,12 @@ label="Check on launch" hint="Looks for a new version each time the app starts." onChange={() => setApp('checkUpdatesOnLaunch', !appSettings.checkUpdatesOnLaunch)} /> + + setApp('includePreReleases', !appSettings.includePreReleases)} />
From b820f5de50040471f6eed30f1f53ffa21fa6ab63 Mon Sep 17 00:00:00 2001 From: Horuse <39675195+Horuse@users.noreply.github.com> Date: Fri, 11 Sep 2026 02:25:59 +0300 Subject: [PATCH 2/2] feat: safe mode banner, pre-release badge and dev triggers --- src-tauri/src/lib.rs | 11 ++++++ src/lib/components/layout/header.svelte | 19 ++++++++++ src/lib/modules/audio/stores.svelte.ts | 19 ++++++++++ src/lib/modules/debug/ui/debug_panel.svelte | 38 ++++++++++++++++++- src/lib/modules/error/init.ts | 27 +++++++------ src/lib/modules/error/stores.svelte.ts | 4 ++ src/lib/modules/error/ui/error_modal.svelte | 22 +++++++++-- src/lib/modules/pipeline/methods.ts | 13 ++++++- .../migrations/v2_file_recording_mode.ts | 2 +- .../modules/updater/ui/update_banner.svelte | 2 +- src/routes/+layout.svelte | 14 +++---- src/routes/+page.svelte | 2 +- src/routes/pipelines/[id]/+page.svelte | 2 +- src/routes/settings/+page.svelte | 2 +- src/routes/virtual-devices/+page.svelte | 2 +- src/routes/wiki/+page.svelte | 2 +- 16 files changed, 148 insertions(+), 33 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 318e46a3..e7ceb043 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -259,6 +259,17 @@ pub fn run() { } } + if let Ok(dir) = handle.path().app_data_dir() { + let current = dir.join("pipelines.json"); + let backup = dir.join("pipelines.backup-v1.json"); + if current.exists() && !backup.exists() { + if let Ok(bytes) = std::fs::read(¤t) { + let _ = std::fs::write(&backup, bytes); + info!(path = %backup.display(), "created automatic pre-1.2.0 pipeline backup"); + } + } + } + #[cfg(target_os = "linux")] if let Err(error) = audio::virtual_device::restore(&handle) { tracing::error!(%error, "failed to restore PipeWire virtual devices"); diff --git a/src/lib/components/layout/header.svelte b/src/lib/components/layout/header.svelte index 666db648..ac8b7710 100644 --- a/src/lib/components/layout/header.svelte +++ b/src/lib/components/layout/header.svelte @@ -1,5 +1,6 @@