Front-to-back QC pass turned up two independent missing-module
showstoppers (workspace did not compile; frontend did not load) plus a
handful of HANDOVER-claimed features that were wired but dead. Fixes:
P0 — gets the app booting again:
- Add the never-committed src/lib/utils/runtime.js (hasTauriRuntime
detection); 5 imports were resolving to nothing.
- Add the never-committed crates/audio/src/streaming_resample.rs
(rubato-backed StreamingResampler with new/push_samples/flush);
declared in lib.rs and used 3x by live.rs but had no impl.
- Drop the duplicate hasTauriRuntime import in routes/+layout.svelte.
- Allow the transcript-viewer window to use the default capability
(was missing from capabilities/default.json:windows, so the viewer
window could open but not invoke any Tauri command).
P1 — features documented as working but actually dead:
- Pump MicrophoneCapture::take_error_rx() into LiveStatusMessage::
Warning each loop iteration in commands/live.rs. The HANDOVER
promised cpal stream errors would surface as toasts; the channel
was created and never read.
- Replace .expect() on the WebKit media-permission setup with a
logged warning. Failure no longer aborts the whole process.
- Toast on save_preferences failure (preferences.svelte.js had a
silent console.error — now warns once per failure run via the
existing toasts store).
P2 — correctness/robustness:
- add_dictionary_entry: switch INSERT OR IGNORE to ON CONFLICT
DO UPDATE ... RETURNING id so duplicate terms get the real row id
instead of a stale auto-increment.
- search_transcripts: qualify ORDER BY fts.rank.
- InsertTranscriptParams + TranscriptRow: bump sample_rate /
audio_channels from i32 to i64 to match the Tauri DTO and avoid
silent truncation at the boundary.
- Drop the unused tauri-plugin-mcp dependency.
- Promote sqlx in src-tauri/Cargo.toml from linux-only to
unconditional (lib.rs names sqlx::SqlitePool unconditionally —
macOS/Windows builds were latently broken).
- hotkey/linux.rs: stop panicking the hotplug task on inotify
failure; degrade to "no hotplug" with a stderr warning.
- layout.svelte: store the global error/unhandledrejection handler
refs and remove them in onDestroy so HMR/window teardown doesn't
leak listeners.
Verified: cargo check -p kon-core -p kon-storage -p kon-cloud-providers
passes. cargo check on src-tauri/kon-audio/kon-hotkey requires alsa +
gtk system libs not present in this sandbox; their changes are
syntactically and type-checked against the rest of the workspace.
svelte-check requires npm install which is not available here.
https://claude.ai/code/session_018ozAs4UcRC8jbJbddqJtEw
131 lines
4.2 KiB
JavaScript
131 lines
4.2 KiB
JavaScript
// src/lib/stores/preferences.svelte.js
|
|
import { invoke } from '@tauri-apps/api/core';
|
|
import { toasts } from './toasts.svelte.js';
|
|
|
|
const DEFAULTS = {
|
|
theme: 'dark',
|
|
zone: 'default',
|
|
accessibility: {
|
|
fontFamily: 'lexend',
|
|
fontSize: 16,
|
|
letterSpacing: 0,
|
|
lineHeight: 1.5,
|
|
transcriptSize: 16,
|
|
bionicReading: false,
|
|
reduceMotion: 'system'
|
|
}
|
|
};
|
|
|
|
const FONT_FAMILIES = {
|
|
lexend: "'Lexend', system-ui, sans-serif",
|
|
atkinson: "'Atkinson Hyperlegible Next', system-ui, sans-serif",
|
|
opendyslexic: "'OpenDyslexic', system-ui, sans-serif"
|
|
};
|
|
|
|
function readFromDOM() {
|
|
const el = document.documentElement;
|
|
return {
|
|
theme: el.dataset.theme || DEFAULTS.theme,
|
|
zone: el.dataset.zone || DEFAULTS.zone,
|
|
accessibility: {
|
|
fontFamily: el.dataset.fontFamily || DEFAULTS.accessibility.fontFamily,
|
|
fontSize: parseFloat(el.style.getPropertyValue('--font-size-body')) || DEFAULTS.accessibility.fontSize,
|
|
letterSpacing: parseFloat(el.style.getPropertyValue('--letter-spacing-body')) || DEFAULTS.accessibility.letterSpacing,
|
|
lineHeight: parseFloat(el.style.getPropertyValue('--line-height-body')) || DEFAULTS.accessibility.lineHeight,
|
|
transcriptSize: DEFAULTS.accessibility.transcriptSize,
|
|
bionicReading: el.dataset.bionicReading === 'true',
|
|
reduceMotion: el.dataset.reduceMotion || DEFAULTS.accessibility.reduceMotion
|
|
}
|
|
};
|
|
}
|
|
|
|
function applyToDOM(prefs) {
|
|
const el = document.documentElement;
|
|
|
|
// Theme — resolve 'system' to actual value
|
|
el.dataset.theme = prefs.theme === 'system'
|
|
? (window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark')
|
|
: prefs.theme;
|
|
|
|
// Zone
|
|
if (prefs.zone === 'default') {
|
|
delete el.dataset.zone;
|
|
} else {
|
|
el.dataset.zone = prefs.zone;
|
|
}
|
|
|
|
// Accessibility — inline styles for highest specificity
|
|
const a = prefs.accessibility;
|
|
el.style.setProperty('--font-family-body', FONT_FAMILIES[a.fontFamily] || FONT_FAMILIES.lexend);
|
|
el.style.setProperty('--font-size-body', `${a.fontSize}px`);
|
|
el.style.setProperty('--letter-spacing-body', `${a.letterSpacing}em`);
|
|
el.style.setProperty('--line-height-body', String(a.lineHeight));
|
|
|
|
el.dataset.bionicReading = String(a.bionicReading);
|
|
el.dataset.fontFamily = a.fontFamily;
|
|
|
|
// Reduce motion — three-value resolution
|
|
const motionReduced = a.reduceMotion === 'on'
|
|
|| (a.reduceMotion === 'system' && window.matchMedia('(prefers-reduced-motion: reduce)').matches);
|
|
if (motionReduced) {
|
|
el.dataset.reduceMotion = 'true';
|
|
} else {
|
|
delete el.dataset.reduceMotion;
|
|
}
|
|
}
|
|
|
|
let saveTimeout = null;
|
|
// Show the failure toast at most once per process so a stuck SQLite path
|
|
// doesn't spam the user every time they nudge a slider.
|
|
let saveFailureToastShown = false;
|
|
function persistToSQLite(prefs) {
|
|
clearTimeout(saveTimeout);
|
|
saveTimeout = setTimeout(async () => {
|
|
try {
|
|
await invoke('save_preferences', { preferences: JSON.stringify(prefs) });
|
|
saveFailureToastShown = false;
|
|
} catch (e) {
|
|
console.error('Failed to save preferences:', e);
|
|
if (!saveFailureToastShown) {
|
|
const msg = typeof e === 'string' ? e : (e?.message ?? String(e));
|
|
toasts.warn(
|
|
'Could not save preferences',
|
|
`${msg}. Your changes still apply for this session.`,
|
|
);
|
|
saveFailureToastShown = true;
|
|
}
|
|
}
|
|
}, 500);
|
|
}
|
|
|
|
let preferences = $state(readFromDOM());
|
|
|
|
export function getPreferences() {
|
|
return preferences;
|
|
}
|
|
|
|
export function updatePreferences(updates) {
|
|
preferences = { ...preferences, ...updates };
|
|
applyToDOM(preferences);
|
|
persistToSQLite(preferences);
|
|
}
|
|
|
|
export function updateAccessibility(updates) {
|
|
preferences = {
|
|
...preferences,
|
|
accessibility: { ...preferences.accessibility, ...updates }
|
|
};
|
|
applyToDOM(preferences);
|
|
persistToSQLite(preferences);
|
|
}
|
|
|
|
// Re-resolve when OS preferences change
|
|
if (typeof window !== 'undefined') {
|
|
window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', () => {
|
|
if (preferences.theme === 'system') applyToDOM(preferences);
|
|
});
|
|
window.matchMedia('(prefers-reduced-motion: reduce)').addEventListener('change', () => {
|
|
if (preferences.accessibility.reduceMotion === 'system') applyToDOM(preferences);
|
|
});
|
|
}
|