diff --git a/CHANGELOG.md b/CHANGELOG.md index d08a547b..ae724858 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.3.0-rc.1] - 2026-09-11 + +### Fixed + +- Fixed in-app updater throwing invalid resource ID when installing updates by registering update handles into webview resource tables. +- Fixed version display falling back to "v?" in updater banner and about modal by making cached app info reactive. +- Prevented closed plugin editor windows from reopening during pipeline reconcile when connecting or disconnecting edges. +- Disabled browser context menu and web inspector on right-click across all platforms. +- Prohibited accidental text selection across app buttons and canvas elements while keeping text selectable in modals, error dialogs, and inputs. + ## [1.2.0] - 2026-09-11 ### Added @@ -19,7 +29,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### Changed -- Network audio nodes (Net Sender and Net Receiver) use a leaner v2 wire protocol with dynamic format detection and configurable sample rates by @Horuse in (#39). +- Network audio nodes (Net Sender and Net Receiver) use a leaner v2 wire protocol with dynamic format detection and configurable sample rates by @Horuse in (#39). - Audio node specifications use unified styling and a reusable numeric stepper across controls by @Horuse in (#31, #39). - Real-time speaker pacing and prefill budgets are bounded across all platforms to prevent drift and latency creep by @Horuse in (#41). @@ -165,6 +175,7 @@ Initial release. - Node graph routing with hot reconcile: edit the graph while the pipeline runs without interrupting streams. - Undo/redo, node copy/paste, pipeline snapshot history, auto-update and the virtual audio driver. +[1.3.0-rc.1]: https://github.com/Horuse/Splitwave/compare/v1.2.0...v1.3.0-rc.1 [1.2.0]: https://github.com/Horuse/Splitwave/compare/v1.1.0...v1.2.0 [1.1.0]: https://github.com/Horuse/Splitwave/compare/v1.0.0...v1.1.0 [1.0.0]: https://github.com/Horuse/Splitwave/compare/v0.5.0...v1.0.0 diff --git a/announcements.json b/announcements.json new file mode 100644 index 00000000..d5011415 --- /dev/null +++ b/announcements.json @@ -0,0 +1,42 @@ +{ + "announcements": [ + { + "id": "notice-updater-resource-id-workaround", + "type": "both", + "severity": "critical", + "priority": 100, + "badge": "NOTICE", + "title": "Manual Update Required for v1.1.0", + "message": "In-app automatic updates are broken in v1.1.0. Please download the latest version manually once from splitwave.app or GitHub.", + "markdown": "### In-App Update Notice\n\nDue to an updater issue in version `1.1.0`, automatic updates fail with `The resource id is invalid`.\n\nTo update to the latest version, please download and install Splitwave manually once:\n- [Official Website](https://splitwave.app)\n- [GitHub Releases](https://github.com/Horuse/Splitwave/releases)\n\nYour existing pipelines, presets, and audio configuration will be preserved automatically.", + "action": { + "label": "Download Latest", + "url": "https://splitwave.app" + }, + "dismissible": true, + "filters": { + "versions": "1.1.0", + "platforms": ["macos", "windows", "linux"], + "channels": ["stable", "rc", "beta"] + } + }, + { + "id": "notice-testing-v130-rc", + "type": "banner", + "severity": "info", + "priority": 50, + "badge": "RC TEST", + "title": "Testing Splitwave 1.3.0 Release Candidate", + "message": "You are running Splitwave 1.3.0-rc.1. Please report any audio or UI feedback on GitHub.", + "action": { + "label": "Report Issue", + "url": "https://github.com/Horuse/Splitwave/issues" + }, + "dismissible": true, + "filters": { + "versions": "^1.3.0-rc.0", + "channels": ["rc"] + } + } + ] +} diff --git a/bun.lock b/bun.lock index ea396014..71e2e757 100644 --- a/bun.lock +++ b/bun.lock @@ -20,6 +20,7 @@ "@xyflow/svelte": "^1.5.2", "prettier-plugin-svelte": "^3.5.2", "prettier-plugin-tailwindcss": "^0.8.0", + "semver": "^7.8.5", "svelte-floating-ui": "^1.6.2", "svelte-french-toast": "^1.2.0", "svelte-portal": "^2.2.1", @@ -30,6 +31,8 @@ "@sveltejs/kit": "^2.9.0", "@sveltejs/vite-plugin-svelte": "^7.1.2", "@tauri-apps/cli": "^2", + "@types/semver": "^7.8.0", + "bun-types": "^1.4.2", "prettier": "3.8.3", "svelte": "^5.0.0", "svelte-check": "^4.0.0", @@ -265,6 +268,10 @@ "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + "@types/node": ["@types/node@22.20.2", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-xlvWf4Vs9n1PEVYwP1n4vvG07M6y8WgvJ2t0vbrWTmijsIHp1cS+uJ2kMIRdY3nHZK0nCYKrPeD171+SzF4/zw=="], + + "@types/semver": ["@types/semver@7.8.0", "", {}, "sha512-1mAINjtQCXXeLkJ9ehXkwOcBpqtLxiVtKhpUf83DdRNdQKV0iXZpaHYqRr7nj+wvxuJzoAmAwXI+sCNMv1CzLQ=="], + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], "@xyflow/svelte": ["@xyflow/svelte@1.5.2", "", { "dependencies": { "@svelte-put/shortcut": "^4.1.0", "@xyflow/system": "0.0.76" }, "peerDependencies": { "svelte": "^5.25.0" } }, "sha512-jPLc2cwRmrkXSv/jvsNDQqTGCsNyTWURKtBl28UCH3BdOAA3CIc0/oQ0EvIcjPdzr0NwvWMgq9TmLdjOIccCEQ=="], @@ -279,6 +286,8 @@ "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], + "bun-types": ["bun-types@1.4.2", "", { "dependencies": { "@types/node": "*" } }, "sha512-bxV1FgK7yBIzjRe5zBozIM4Bem11ZJcCXSrjWRG3YWLt8yFDePu4cLjpebO8OvPeIE9trbyPF4fuj3Cia4Fj3w=="], + "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], @@ -385,6 +394,8 @@ "sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="], + "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + "set-cookie-parser": ["set-cookie-parser@3.1.0", "", {}, "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw=="], "sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="], @@ -413,6 +424,8 @@ "typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="], + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "vite": ["vite@6.4.2", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ=="], "vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="], diff --git a/package.json b/package.json index 7e511095..57413d48 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "splitwave", - "version": "1.2.0", + "version": "1.3.0-rc.1", "description": "", "type": "module", "scripts": { @@ -30,6 +30,7 @@ "@xyflow/svelte": "^1.5.2", "prettier-plugin-svelte": "^3.5.2", "prettier-plugin-tailwindcss": "^0.8.0", + "semver": "^7.8.5", "svelte-floating-ui": "^1.6.2", "svelte-french-toast": "^1.2.0", "svelte-portal": "^2.2.1", @@ -40,6 +41,8 @@ "@sveltejs/kit": "^2.9.0", "@sveltejs/vite-plugin-svelte": "^7.1.2", "@tauri-apps/cli": "^2", + "@types/semver": "^7.8.0", + "bun-types": "^1.4.2", "prettier": "3.8.3", "svelte": "^5.0.0", "svelte-check": "^4.0.0", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 20620c6e..a28c9c46 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -6224,7 +6224,7 @@ dependencies = [ [[package]] name = "splitwave" -version = "1.2.0" +version = "1.3.0-rc.1" dependencies = [ "audio_thread_priority", "base64 0.22.1", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 2f2fb1a7..00e6e5a6 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "splitwave" -version = "1.2.0" +version = "1.3.0-rc.1" description = "Audio routing app for macOS, Linux and Windows. Build a node graph of inputs, effects, and outputs" license = "MIT" edition = "2021" diff --git a/src-tauri/src/audio/plugins/editor.rs b/src-tauri/src/audio/plugins/editor.rs index 4151b184..325df1e9 100644 --- a/src-tauri/src/audio/plugins/editor.rs +++ b/src-tauri/src/audio/plugins/editor.rs @@ -26,6 +26,16 @@ pub fn window_for(node_id: &str) -> Option { windows().lock().unwrap().get(node_id).cloned() } +/// Returns whether this node's editor window currently exists and is visible. +pub fn is_open(node_id: &str) -> bool { + windows() + .lock() + .unwrap() + .get(node_id) + .and_then(|w| w.is_visible().ok()) + .unwrap_or(false) +} + /// Closes a node's editor window if one is open. Shared with the format hosts, /// which have to take the window down alongside the instance it belongs to. pub fn close_window(node_id: &str) { diff --git a/src-tauri/src/audio/plugins/vst3_registry.rs b/src-tauri/src/audio/plugins/vst3_registry.rs index 393743f0..8e5fe5ca 100644 --- a/src-tauri/src/audio/plugins/vst3_registry.rs +++ b/src-tauri/src/audio/plugins/vst3_registry.rs @@ -119,8 +119,8 @@ fn activate_on_main( let same_plugin = old.path == path && old.plugin_id == plugin_id; GRAVEYARD.with(|g| g.borrow_mut().bury(old.instance, old.alive)); - if same_plugin { - // Same plugin, pipeline rebuilt: re-attach to existing window! + if same_plugin && editor::is_open(&node_id) { + // Same plugin, pipeline rebuilt: re-attach to existing open window! if let Some(window) = editor::window_for(&node_id) { // IMPORTANT: Drop the old editor view first so its removed() and // peer teardown happen BEFORE the new view calls attached()! @@ -157,8 +157,8 @@ fn activate_on_main( } } } else { - // Different plugin chosen on this node: close the previous editor - // window so the new one opens cleanly with its own UI and geometry. + // Different plugin chosen or editor was closed: close the previous editor + // window so it doesn't reopen unexpectedly during pipeline reconcile. old.editor = None; editor::close_window(&node_id); if let Some(app) = crate::app_handle() { diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 8c11605b..099bdfa2 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Splitwave", - "version": "1.2.0", + "version": "1.3.0-rc.1", "identifier": "com.horuse.splitwave", "build": { "beforeDevCommand": "bun run dev", diff --git a/src-tauri/tauri.linux.conf.json b/src-tauri/tauri.linux.conf.json index 0fa86a54..cd03ddb0 100644 --- a/src-tauri/tauri.linux.conf.json +++ b/src-tauri/tauri.linux.conf.json @@ -7,7 +7,8 @@ "width": 1200, "height": 800, "dragDropEnabled": false, - "decorations": false + "decorations": false, + "devtools": false } ] } diff --git a/src-tauri/tauri.macos.conf.json b/src-tauri/tauri.macos.conf.json index 697dc6e0..3adc8b1d 100644 --- a/src-tauri/tauri.macos.conf.json +++ b/src-tauri/tauri.macos.conf.json @@ -10,7 +10,8 @@ "decorations": true, "titleBarStyle": "Overlay", "hiddenTitle": true, - "trafficLightPosition": { "x": 14, "y": 18 } + "trafficLightPosition": { "x": 14, "y": 18 }, + "devtools": false } ] }, diff --git a/src-tauri/tauri.windows.conf.json b/src-tauri/tauri.windows.conf.json index 0fa86a54..cd03ddb0 100644 --- a/src-tauri/tauri.windows.conf.json +++ b/src-tauri/tauri.windows.conf.json @@ -7,7 +7,8 @@ "width": 1200, "height": 800, "dragDropEnabled": false, - "decorations": false + "decorations": false, + "devtools": false } ] } diff --git a/src/lib/components/layout/header.svelte b/src/lib/components/layout/header.svelte index 1f160413..cb0f157d 100644 --- a/src/lib/components/layout/header.svelte +++ b/src/lib/components/layout/header.svelte @@ -10,6 +10,7 @@ import { pipelineStore } from '$lib/modules/pipeline/stores.svelte'; import { audioStore } from '$lib/modules/audio/stores.svelte'; import { Popover } from '$lib/modules/overlay/ui'; + import { AnnouncementBanner } from '$lib/modules/announcements'; interface Props { left?: Snippet; @@ -112,3 +113,5 @@ {/if} + + diff --git a/src/lib/components/waveform_scope.svelte b/src/lib/components/waveform_scope.svelte index 39d310fa..2e0ef7fe 100644 --- a/src/lib/components/waveform_scope.svelte +++ b/src/lib/components/waveform_scope.svelte @@ -19,7 +19,8 @@ // Encoder writes PCM (WAV/AIFF) — the disk-peak source; `null` falls // back to the path-extension heuristic. pcm = null, - maxChannels = null + maxChannels = null, + showBufferLimit = false }: { nodeId: string; height?: number; @@ -29,6 +30,7 @@ pcm?: boolean | null; // Caps displayed lanes; phantom multi lanes without a cable are dropped. maxChannels?: number | null; + showBufferLimit?: boolean; } = $props(); const SEG_FRAMES = 64; @@ -894,7 +896,7 @@ c.stroke(); c.restore(); - if (totalSegs > 0) { + if (showBufferLimit && totalSegs > 0) { c.save(); c.font = '6.5px monospace'; const line1 = 'Live view · Buffer limit'; diff --git a/src/lib/modules/about/ui/about_modal.svelte b/src/lib/modules/about/ui/about_modal.svelte index d103ee85..06f4e910 100644 --- a/src/lib/modules/about/ui/about_modal.svelte +++ b/src/lib/modules/about/ui/about_modal.svelte @@ -1,12 +1,18 @@ + +{#if active} +
+
+ + {badgeText} + + + {#if count > 1} +
+ + {currentIndex + 1}/{count} + +
+ {/if} + +
+ {active.title}: + {active.message} +
+
+ +
+ {#if active.type === 'both' || active.markdown} + + {/if} + + {#if active.action} + + {/if} + + {#if active.dismissible !== false} + + {/if} +
+
+{/if} diff --git a/src/lib/modules/announcements/ui/announcement_modal.svelte b/src/lib/modules/announcements/ui/announcement_modal.svelte new file mode 100644 index 00000000..08aea8c1 --- /dev/null +++ b/src/lib/modules/announcements/ui/announcement_modal.svelte @@ -0,0 +1,68 @@ + + +
+
+ {#if announcement.badge || announcement.severity !== 'info'} + + {announcement.badge ?? announcement.severity} + + {/if} +

{announcement.message}

+ + {#if announcement.markdown} + + {/if} +
+ +
+ + + {#if announcement.action} + + {/if} +
+
diff --git a/src/lib/modules/announcements/ui/index.ts b/src/lib/modules/announcements/ui/index.ts new file mode 100644 index 00000000..b6079970 --- /dev/null +++ b/src/lib/modules/announcements/ui/index.ts @@ -0,0 +1,2 @@ +export { default as AnnouncementBanner } from './announcement_banner.svelte'; +export { default as AnnouncementModal } from './announcement_modal.svelte'; diff --git a/src/lib/modules/app_info/index.ts b/src/lib/modules/app_info/index.ts index ce01bbcb..45b41b26 100644 --- a/src/lib/modules/app_info/index.ts +++ b/src/lib/modules/app_info/index.ts @@ -1,28 +1 @@ -import { arch, platform, version } from '@tauri-apps/plugin-os'; -import { getVersion, getTauriVersion } from '@tauri-apps/api/app'; - -export interface AppInfo { - appVersion: string; - tauriVersion: string; - platform: string; - osVersion: string; - arch: string; -} - -let cached: AppInfo | null = null; - -export async function loadAppInfo(): Promise { - if (cached) return cached; - const [appVersion, tauriVersion, plat, ver, ar] = await Promise.all([getVersion(), getTauriVersion(), platform(), version(), arch()]); - cached = { appVersion, tauriVersion, platform: plat, osVersion: ver, arch: ar }; - return cached; -} - -export function getCachedAppInfo(): AppInfo | null { - return cached; -} - -export function formatAppInfo(info: AppInfo): string { - const osLabel = info.platform === 'macos' ? 'macOS' : info.platform === 'linux' ? 'Linux' : info.platform; - return `${osLabel} ${info.osVersion} (${info.arch})`; -} +export * from './stores.svelte'; diff --git a/src/lib/modules/app_info/stores.svelte.ts b/src/lib/modules/app_info/stores.svelte.ts new file mode 100644 index 00000000..04ce4dd7 --- /dev/null +++ b/src/lib/modules/app_info/stores.svelte.ts @@ -0,0 +1,28 @@ +import { arch, platform, version } from '@tauri-apps/plugin-os'; +import { getVersion, getTauriVersion } from '@tauri-apps/api/app'; + +export interface AppInfo { + appVersion: string; + tauriVersion: string; + platform: string; + osVersion: string; + arch: string; +} + +let cached = $state(null); + +export async function loadAppInfo(): Promise { + if (cached) return cached; + const [appVersion, tauriVersion, plat, ver, ar] = await Promise.all([getVersion(), getTauriVersion(), platform(), version(), arch()]); + cached = { appVersion, tauriVersion, platform: plat, osVersion: ver, arch: ar }; + return cached; +} + +export function getCachedAppInfo(): AppInfo | null { + return cached; +} + +export function formatAppInfo(info: AppInfo): string { + const osLabel = info.platform === 'macos' ? 'macOS' : info.platform === 'linux' ? 'Linux' : info.platform; + return `${osLabel} ${info.osVersion} (${info.arch})`; +} diff --git a/src/lib/modules/debug/ui/debug_panel.svelte b/src/lib/modules/debug/ui/debug_panel.svelte index f8a79efb..912615db 100644 --- a/src/lib/modules/debug/ui/debug_panel.svelte +++ b/src/lib/modules/debug/ui/debug_panel.svelte @@ -7,6 +7,7 @@ import { updaterStore, latestRelease } from '$lib/modules/updater'; import { getCachedAppInfo } from '$lib/modules/app_info'; import { Menu, MenuItem, MenuSection, MenuSeparator } from '$lib/modules/overlay/ui'; + import { announcementStore, runModalQueue } from '$lib/modules/announcements'; let open = $state(false); @@ -118,9 +119,119 @@ updaterStore.state = { phase: 'error', message: 'signature verification failed' }; } + function fakeRcBanner() { + announcementStore.bannerQueue = [ + { + id: 'notice-testing-v130-rc', + type: 'banner', + severity: 'info', + priority: 50, + badge: 'RC TEST', + title: 'Testing Splitwave 1.3.0 Release Candidate', + message: 'You are running Splitwave 1.3.0-rc.1. Please report any audio or UI feedback on GitHub.', + action: { + label: 'Report Issue', + url: 'https://github.com/Horuse/Splitwave/issues' + }, + dismissible: true, + filters: { + channels: ['rc'] + } + }, + ...announcementStore.bannerQueue.filter((b) => b.id !== 'notice-testing-v130-rc') + ]; + announcementStore.currentBannerIndex = 0; + } + + function fakeModalNotice() { + announcementStore.modalQueue = [ + { + id: 'notice-updater-resource-id-workaround', + type: 'modal', + severity: 'critical', + priority: 100, + badge: 'NOTICE', + title: 'Manual Update Required for v1.1.0', + message: 'In-app automatic updates are broken in v1.1.0. Please download the latest version manually once from splitwave.app or GitHub.', + markdown: + '### In-App Update Notice\n\nDue to an updater issue in version `1.1.0`, automatic updates fail with `The resource id is invalid`.\n\nTo update to the latest version, please download and install Splitwave manually once:\n- [Official Website](https://splitwave.app)\n- [GitHub Releases](https://github.com/Horuse/Splitwave/releases)\n\nYour existing pipelines, presets, and audio configuration will be preserved automatically.', + action: { + label: 'Download Latest', + url: 'https://splitwave.app' + }, + dismissible: true, + filters: { + versions: '1.1.0', + channels: ['stable', 'rc', 'beta'] + } + } + ]; + runModalQueue().catch(() => {}); + } + + function fakeQueueMultiple() { + announcementStore.bannerQueue = [ + { + id: 'banner-test-1', + type: 'banner', + severity: 'critical', + priority: 100, + badge: 'CRITICAL', + title: 'Buffer Underrun Detected', + message: 'Audio output buffer underflowed by 128 frames at 96kHz.', + dismissible: true + }, + { + id: 'banner-test-2', + type: 'banner', + severity: 'info', + priority: 50, + badge: 'RC TEST', + title: 'Testing Splitwave 1.3.0 Release Candidate', + message: 'Please report any edge connection or VST3 editor issues on GitHub.', + dismissible: true + } + ]; + announcementStore.currentBannerIndex = 0; + + announcementStore.modalQueue = [ + { + id: 'modal-test-1', + type: 'modal', + severity: 'warning', + title: 'Audio Buffer Optimization Notice', + message: 'Recommended buffer size was adjusted to minimize playback latency.', + markdown: + '### Notice #1\n\nThis tests the **sequential modal queue**. When you click Dismiss or Next, modal #2 opens automatically without overlapping popups.', + dismissible: true + }, + { + id: 'modal-test-2', + type: 'modal', + severity: 'info', + title: 'New Virtual Devices Available', + message: 'Splitwave virtual audio loopback drivers are ready for testing.', + markdown: '### Notice #2\n\nSequential queue verified! All modal announcements processed.', + dismissible: true + } + ]; + runModalQueue().catch(() => {}); + } + + function resetDismissedAnnouncements() { + if (typeof window !== 'undefined') { + window.localStorage.removeItem('announcements:dismissed'); + } + announcementStore.dismissed = {}; + announcementStore.bannerQueue = []; + announcementStore.modalQueue = []; + } + function clearAll() { errorStore.dismiss(); updaterStore.state = { phase: 'idle' }; + announcementStore.bannerQueue = []; + announcementStore.modalQueue = []; } @@ -141,6 +252,11 @@ + + + + + diff --git a/src/lib/modules/flow/ui/effect/waveform.svelte b/src/lib/modules/flow/ui/effect/waveform.svelte index 39371860..09067ce9 100644 --- a/src/lib/modules/flow/ui/effect/waveform.svelte +++ b/src/lib/modules/flow/ui/effect/waveform.svelte @@ -1,9 +1,9 @@ -
+
{#if !isPreview} - + {/if}
@@ -28,13 +57,13 @@
{#if !isPreview} - + {/if} -
- +
+
{#if !isPreview} - + {/if}
diff --git a/src/lib/modules/flow/ui/output/file_recording.svelte b/src/lib/modules/flow/ui/output/file_recording.svelte index d3379b54..249d1413 100644 --- a/src/lib/modules/flow/ui/output/file_recording.svelte +++ b/src/lib/modules/flow/ui/output/file_recording.svelte @@ -833,7 +833,8 @@ nodeId={id} filePath={data.filePath} pcm={data.format.kind === 'wav' || data.format.kind === 'aiff'} - maxChannels={waveformChannels} /> + maxChannels={waveformChannels} + showBufferLimit /> {/if}
diff --git a/src/lib/modules/flow/utils/nodes.ts b/src/lib/modules/flow/utils/nodes.ts index ec174307..274ae06d 100644 --- a/src/lib/modules/flow/utils/nodes.ts +++ b/src/lib/modules/flow/utils/nodes.ts @@ -205,7 +205,7 @@ export const registry: Record = { component: Waveform, icon: PulseIcon, defaultData: DEFAULT_NODE_DATA['waveform'], - defaultSize: { width: 200, height: 140 } + defaultSize: { width: 256, height: 130 } }), spectrum: entry<'spectrum'>({ kind: 'spectrum', diff --git a/src/lib/modules/overlay/modal/manager.svelte.ts b/src/lib/modules/overlay/modal/manager.svelte.ts index 35fafecf..b1f9f3e8 100644 --- a/src/lib/modules/overlay/modal/manager.svelte.ts +++ b/src/lib/modules/overlay/modal/manager.svelte.ts @@ -9,6 +9,7 @@ export type ModalComponent> = Component void; [key: string]: unknown; } diff --git a/src/lib/modules/overlay/ui/modal/render.svelte b/src/lib/modules/overlay/ui/modal/render.svelte index 0636a11e..8f4a10d8 100644 --- a/src/lib/modules/overlay/ui/modal/render.svelte +++ b/src/lib/modules/overlay/ui/modal/render.svelte @@ -10,7 +10,10 @@ description={entry.params.description as string | undefined} size={entry.params.size as 'sm' | 'md' | 'lg' | 'xl' | undefined} canClose={entry.params.canClose !== false} - onClose={() => modalManager.close(entry.id)} + onClose={() => { + (entry.params.onClose as (() => void) | undefined)?.(); + modalManager.close(entry.id); + }} zIndex={entry.zIndex}> diff --git a/src/lib/modules/updater/ui/update_banner.svelte b/src/lib/modules/updater/ui/update_banner.svelte index 0ced067d..de27b126 100644 --- a/src/lib/modules/updater/ui/update_banner.svelte +++ b/src/lib/modules/updater/ui/update_banner.svelte @@ -5,10 +5,16 @@ import CopyButton from '$lib/components/copy_button.svelte'; import Markdown from '$lib/components/markdown.svelte'; import { Checkmark } from '$lib/components/icons'; - import { getCachedAppInfo } from '$lib/modules/app_info'; + import { getCachedAppInfo, loadAppInfo } from '$lib/modules/app_info'; import { formatBytes } from '$lib/components/format'; - const info = getCachedAppInfo(); + let info = $derived(getCachedAppInfo()); + + $effect(() => { + if (!info) { + loadAppInfo().catch(() => {}); + } + }); let s = $derived(updaterStore.state); diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 14a65a2d..968a137f 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -18,6 +18,7 @@ import { logStore } from '$lib/modules/logs'; import { LogsModal } from '$lib/modules/logs/ui'; import { platform } from '@tauri-apps/plugin-os'; + import { initAnnouncements } from '$lib/modules/announcements'; const isDev = import.meta.env.DEV; @@ -106,6 +107,7 @@ await audioStore.autoActivateOnLaunch(); } catch {} pipelineStore.refresh().catch(() => {}); + initAnnouncements(); if (appSettings.checkUpdatesOnLaunch) checkForUpdates(true).catch(() => {}); listen('menu://action', (e) => handleMenu(e.payload)) .then((fn) => { diff --git a/tests/announcements_matcher.test.ts b/tests/announcements_matcher.test.ts new file mode 100644 index 00000000..61502167 --- /dev/null +++ b/tests/announcements_matcher.test.ts @@ -0,0 +1,191 @@ +import { describe, expect, it } from 'bun:test'; +import { detectChannel, matchesAnnouncement, sortAnnouncements } from '../src/lib/modules/announcements/matcher'; +import { parseAnnouncementsPayload, type Announcement, type EnvironmentContext } from '../src/lib/modules/announcements/types'; + +describe('detectChannel', () => { + it('detects rc channel', () => { + expect(detectChannel('1.3.0-rc.1')).toBe('rc'); + expect(detectChannel('v1.2.1-rc.2')).toBe('rc'); + }); + + it('detects beta channel', () => { + expect(detectChannel('1.3.0-beta.1')).toBe('beta'); + expect(detectChannel('v1.0.0-beta')).toBe('beta'); + }); + + it('detects alpha channel', () => { + expect(detectChannel('1.0.0-alpha.5')).toBe('alpha'); + }); + + it('detects stable channel', () => { + expect(detectChannel('1.2.0')).toBe('stable'); + expect(detectChannel('v1.3.0')).toBe('stable'); + }); +}); + +describe('matchesAnnouncement', () => { + const defaultContext: EnvironmentContext = { + version: '1.3.0-rc.1', + platform: 'macos', + arch: 'aarch64', + now: new Date('2026-09-11T20:00:00Z') + }; + + it('matches wildcard version and all platforms', () => { + const item: Announcement = { + id: 'general-notice', + type: 'banner', + severity: 'info', + title: 'Hello', + message: 'World' + }; + expect(matchesAnnouncement(item, defaultContext)).toBe(true); + }); + + it('matches semver range including pre-release', () => { + const item: Announcement = { + id: 'rc-notice', + type: 'banner', + severity: 'warning', + title: 'RC testing', + message: 'Test message', + filters: { + versions: '>=1.3.0-rc.0 <1.3.0' + } + }; + expect(matchesAnnouncement(item, defaultContext)).toBe(true); + + // Non-matching version + expect(matchesAnnouncement(item, { ...defaultContext, version: '1.2.0' })).toBe(false); + }); + + it('filters by exact version', () => { + const item: Announcement = { + id: 'exact-fix', + type: 'banner', + severity: 'critical', + title: 'Notice for 1.2.1', + message: 'Update info', + filters: { + versions: '1.2.1' + } + }; + expect(matchesAnnouncement(item, { ...defaultContext, version: '1.2.1' })).toBe(true); + expect(matchesAnnouncement(item, { ...defaultContext, version: '1.2.0' })).toBe(false); + }); + + it('filters by platform', () => { + const macOnly: Announcement = { + id: 'mac-only', + type: 'banner', + severity: 'info', + title: 'Mac notice', + message: 'Info', + filters: { + platforms: ['macos'] + } + }; + expect(matchesAnnouncement(macOnly, defaultContext)).toBe(true); + expect(matchesAnnouncement(macOnly, { ...defaultContext, platform: 'windows' })).toBe(false); + }); + + it('filters by pre-release channel', () => { + const rcOnly: Announcement = { + id: 'rc-survey', + type: 'banner', + severity: 'info', + title: 'RC Survey', + message: 'Survey', + filters: { + channels: ['rc'] + } + }; + expect(matchesAnnouncement(rcOnly, defaultContext)).toBe(true); + expect(matchesAnnouncement(rcOnly, { ...defaultContext, version: '1.4.0-rc.2' })).toBe(true); + expect(matchesAnnouncement(rcOnly, { ...defaultContext, version: '1.3.0-beta.1' })).toBe(false); + expect(matchesAnnouncement(rcOnly, { ...defaultContext, version: '1.2.0' })).toBe(false); + }); + + it('respects expiry timestamp', () => { + const expired: Announcement = { + id: 'expired-promo', + type: 'banner', + severity: 'info', + title: 'Old promo', + message: 'Expired', + filters: { + expiresAt: '2026-09-01T00:00:00Z' + } + }; + expect(matchesAnnouncement(expired, defaultContext)).toBe(false); + }); + + it('respects dismissal and updatedAt', () => { + const item: Announcement = { + id: 'dismiss-test', + type: 'banner', + severity: 'info', + title: 'Dismiss test', + message: 'Test' + }; + + const dismissed = { + 'dismiss-test': 1000 + }; + + // Dismissed and not updated + expect(matchesAnnouncement(item, defaultContext, dismissed)).toBe(false); + + // Updated after dismissal + const updatedItem: Announcement = { + ...item, + updatedAt: new Date(2000).toISOString() + }; + expect(matchesAnnouncement(updatedItem, defaultContext, dismissed)).toBe(true); + }); +}); + +describe('sortAnnouncements', () => { + it('sorts by priority first, then severity', () => { + const items: Announcement[] = [ + { id: '1', priority: 10, severity: 'info', type: 'banner', title: '1', message: '' }, + { id: '2', priority: 50, severity: 'warning', type: 'banner', title: '2', message: '' }, + { id: '3', priority: 50, severity: 'critical', type: 'banner', title: '3', message: '' }, + { id: '4', priority: 0, severity: 'critical', type: 'banner', title: '4', message: '' } + ]; + + const sorted = sortAnnouncements(items); + expect(sorted.map((s) => s.id)).toEqual(['3', '2', '1', '4']); + }); +}); + +describe('parseAnnouncementsPayload', () => { + it('keeps valid announcements and rejects malformed entries', () => { + const payload = parseAnnouncementsPayload({ + announcements: [ + { id: 'valid', type: 'banner', severity: 'info', title: 'Title', message: 'Message' }, + { id: 'invalid-type', type: 'toast', severity: 'info', title: 'Title', message: 'Message' }, + { id: 'missing-message', type: 'banner', severity: 'info', title: 'Title' } + ] + }); + + expect(payload?.announcements.map((item) => item.id)).toEqual(['valid']); + }); + + it('keeps only the first announcement with a duplicate id', () => { + const payload = parseAnnouncementsPayload({ + announcements: [ + { id: 'same', type: 'banner', severity: 'info', title: 'First', message: 'Message' }, + { id: 'same', type: 'modal', severity: 'warning', title: 'Second', message: 'Message' } + ] + }); + + expect(payload?.announcements).toHaveLength(1); + expect(payload?.announcements[0]?.title).toBe('First'); + }); + + it('rejects an invalid payload envelope', () => { + expect(parseAnnouncementsPayload({ announcements: null })).toBeNull(); + expect(parseAnnouncementsPayload([])).toBeNull(); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 0b2d8865..5fca5902 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,8 @@ "skipLibCheck": true, "sourceMap": true, "strict": true, - "moduleResolution": "bundler" + "moduleResolution": "bundler", + "types": ["bun-types"] } // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files diff --git a/vite.config.ts b/vite.config.ts index 2f6076b7..26297e3f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,7 +2,6 @@ import { defineConfig } from 'vite'; import { sveltekit } from '@sveltejs/kit/vite'; import tailwindcss from '@tailwindcss/vite'; -// @ts-expect-error process is a nodejs global const host = process.env.TAURI_DEV_HOST; // https://vite.dev/config/