chore: import upstream snapshot with attribution
CI / lint (push) Has been cancelled
CI / js-syntax (push) Successful in 10m24s
CI / deps-audit (push) Has been cancelled
CI / test (push) Failing after 24m55s
CI / sast-bandit (push) Failing after 11m13s
CI / trivy (push) Failing after 9m32s
Docker Publish / build-and-push (push) Failing after 34m3s
CI / lint (push) Has been cancelled
CI / js-syntax (push) Successful in 10m24s
CI / deps-audit (push) Has been cancelled
CI / test (push) Failing after 24m55s
CI / sast-bandit (push) Failing after 11m13s
CI / trivy (push) Failing after 9m32s
Docker Publish / build-and-push (push) Failing after 34m3s
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
// ─── Persistent store (tauri-plugin-store via custom commands) ───
|
||||
//
|
||||
// Falls back to localStorage when running outside Tauri (browser dev mode).
|
||||
// The store is backed by ~/Library/Application Support/app.stemdeck.desktop/user-data.json
|
||||
// on macOS — outside WebKit's reach, so WebView resets can never destroy user data.
|
||||
|
||||
export async function storeGet(key, fallback = null) {
|
||||
if (window.__TAURI__?.core?.invoke) {
|
||||
try {
|
||||
const val = await window.__TAURI__.core.invoke("store_get", { key });
|
||||
return val ?? fallback;
|
||||
} catch (e) { console.warn("[store] get failed for", key, e); return fallback; }
|
||||
}
|
||||
try {
|
||||
const raw = localStorage.getItem(key);
|
||||
return raw !== null ? JSON.parse(raw) : fallback;
|
||||
} catch (e) { console.warn("[store] localStorage get failed for", key, e); return fallback; }
|
||||
}
|
||||
|
||||
export async function storeSet(key, value) {
|
||||
if (window.__TAURI__?.core?.invoke) {
|
||||
try {
|
||||
await window.__TAURI__.core.invoke("store_set", { key, value });
|
||||
} catch (e) { console.warn("[store] set failed for", key, e); }
|
||||
return;
|
||||
}
|
||||
try { localStorage.setItem(key, JSON.stringify(value)); } catch (e) { console.warn("[store] localStorage set failed", e); }
|
||||
}
|
||||
|
||||
// Debounced variant — coalesces rapid writes (e.g. mixer slider moves) into
|
||||
// a single store write ~300ms after the last call. Each call snapshots the
|
||||
// value so no mutation races occur.
|
||||
const _storePending = new Map();
|
||||
export function storeSetDebounced(key, value, delayMs = 300) {
|
||||
if (_storePending.has(key)) clearTimeout(_storePending.get(key));
|
||||
const snapshot = structuredClone(value);
|
||||
_storePending.set(key, setTimeout(() => {
|
||||
_storePending.delete(key);
|
||||
storeSet(key, snapshot);
|
||||
}, delayMs));
|
||||
}
|
||||
|
||||
// Keys that hold critical user data and must be migrated from localStorage
|
||||
// to the store on first launch after this feature ships.
|
||||
const _MIGRATE_KEYS = [
|
||||
"stemdeck.folders",
|
||||
"stemdeck.deleted_jobs",
|
||||
"stemdeck:selected-stems",
|
||||
];
|
||||
|
||||
// One-time bootstrap: copy localStorage → store for existing users.
|
||||
// On fresh installs (no localStorage data) this is a no-op that just writes
|
||||
// the migration flag so future upgrades can safely clear WebKit.
|
||||
export async function runStoreMigrationIfNeeded() {
|
||||
if (!window.__TAURI__?.core?.invoke) return;
|
||||
try {
|
||||
// Check if any critical key is absent from the store but present in localStorage.
|
||||
const needs = await Promise.all(
|
||||
_MIGRATE_KEYS.map(async (k) => {
|
||||
const inStore = await window.__TAURI__.core.invoke("store_get", { key: k });
|
||||
return inStore === null && localStorage.getItem(k) !== null;
|
||||
})
|
||||
).then((r) => r.some(Boolean));
|
||||
|
||||
if (needs) {
|
||||
// Migrate fixed keys.
|
||||
for (const k of _MIGRATE_KEYS) {
|
||||
try {
|
||||
const raw = localStorage.getItem(k);
|
||||
if (raw !== null) {
|
||||
await window.__TAURI__.core.invoke("store_set", { key: k, value: JSON.parse(raw) });
|
||||
}
|
||||
} catch (e) { console.warn("[store] migration failed for key", k, e); }
|
||||
}
|
||||
// Migrate per-job mix keys (stemdeck:mix:<jobId>).
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const k = localStorage.key(i);
|
||||
if (k?.startsWith("stemdeck:mix:")) {
|
||||
try {
|
||||
const raw = localStorage.getItem(k);
|
||||
if (raw !== null) {
|
||||
await window.__TAURI__.core.invoke("store_set", { key: k, value: JSON.parse(raw) });
|
||||
}
|
||||
} catch (e) { console.warn("[store] migration failed for key", k, e); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write the flag regardless of whether migration was needed. This covers
|
||||
// fresh installs (no localStorage data) so future upgrades clear WebKit.
|
||||
await window.__TAURI__.core.invoke("mark_store_migration_done").catch((e) =>
|
||||
console.warn("[store] mark_store_migration_done failed:", e)
|
||||
);
|
||||
} catch (e) { console.warn("[store] migration error:", e); }
|
||||
}
|
||||
|
||||
export function fmtTime(s) {
|
||||
if (!isFinite(s) || s < 0) return "00:00";
|
||||
const m = Math.floor(s / 60);
|
||||
const sec = Math.floor(s % 60).toString().padStart(2, "0");
|
||||
return `${m.toString().padStart(2, "0")}:${sec}`;
|
||||
}
|
||||
|
||||
// Ruler tick: M:SS with no leading zero on the minutes digit ("0:30", "1:00", "12:30").
|
||||
export function fmtTickLabel(s) {
|
||||
if (!isFinite(s) || s < 0) return "0:00";
|
||||
const m = Math.floor(s / 60);
|
||||
const sec = Math.floor(s % 60).toString().padStart(2, "0");
|
||||
return `${m}:${sec}`;
|
||||
}
|
||||
|
||||
// Millisecond-precise timecode "mm:ss.mmm" for the exact-loop inputs. Integer-ms
|
||||
// math avoids a rounding carry bug (e.g. 0.9999s -> "00:01.000", not "00:00.1000").
|
||||
export function fmtTimeMs(s) {
|
||||
if (!isFinite(s) || s < 0) return "00:00.000";
|
||||
const totalMs = Math.round(s * 1000);
|
||||
const m = Math.floor(totalMs / 60000);
|
||||
const sec = Math.floor((totalMs % 60000) / 1000);
|
||||
const ms = totalMs % 1000;
|
||||
return `${m.toString().padStart(2, "0")}:${sec.toString().padStart(2, "0")}.${ms
|
||||
.toString()
|
||||
.padStart(3, "0")}`;
|
||||
}
|
||||
|
||||
// Parse a user-typed loop time. Accepts "mm:ss(.mmm)" (seconds field 0-59) or a
|
||||
// plain decimal-seconds value ("12.48"). Returns seconds, or null if unparseable.
|
||||
export function parseTimecode(str) {
|
||||
const t = String(str ?? "").trim();
|
||||
const colon = /^(\d+):([0-5]?\d(?:\.\d{1,3})?)$/.exec(t);
|
||||
if (colon) return parseInt(colon[1], 10) * 60 + parseFloat(colon[2]);
|
||||
if (/^\d+(?:\.\d+)?$/.test(t)) return parseFloat(t);
|
||||
return null;
|
||||
}
|
||||
|
||||
export const $ = (id) => document.getElementById(id);
|
||||
Reference in New Issue
Block a user