} group
* @param {V} __value
@@ -4749,91 +4765,219 @@ if (typeof window !== "undefined") ((window.__svelte ??= {}).v ??= /* @__PURE__
//#region node_modules/svelte/src/internal/flags/legacy.js
enable_legacy_mode_flag();
//#endregion
-//#region src/lib/stores/security.svelte.js
-var DEFAULT_AUTO_LOCK_MINUTES = 5;
-var autoLockTimer = null;
-var autoLockMinutes = 5;
-function startAutoLock(minutes = DEFAULT_AUTO_LOCK_MINUTES) {
- autoLockMinutes = minutes;
- resetAutoLock();
- const events = [
- "mousedown",
- "keydown",
- "scroll",
- "touchstart"
- ];
- const handler = () => resetAutoLock();
- events.forEach((evt) => window.addEventListener(evt, handler, { passive: true }));
- document.addEventListener("visibilitychange", handleVisibilityChange);
- window.addEventListener("beforeunload", clearKeyOnExit);
- window.__vaultCleanup = () => {
- events.forEach((evt) => window.removeEventListener(evt, handler));
- document.removeEventListener("visibilitychange", handleVisibilityChange);
- window.removeEventListener("beforeunload", clearKeyOnExit);
- if (autoLockTimer) clearTimeout(autoLockTimer);
- };
+//#region node_modules/idb/build/index.js
+var instanceOfAny = (object, constructors) => constructors.some((c) => object instanceof c);
+var idbProxyableTypes;
+var cursorAdvanceMethods;
+function getIdbProxyableTypes() {
+ return idbProxyableTypes || (idbProxyableTypes = [
+ IDBDatabase,
+ IDBObjectStore,
+ IDBIndex,
+ IDBCursor,
+ IDBTransaction
+ ]);
}
-/**
-* Reset the auto-lock timer.
-*/
-function resetAutoLock() {
- if (autoLockTimer) clearTimeout(autoLockTimer);
- autoLockTimer = setTimeout(() => {
- app$1.lockVault();
- }, autoLockMinutes * 60 * 1e3);
+function getCursorAdvanceMethods() {
+ return cursorAdvanceMethods || (cursorAdvanceMethods = [
+ IDBCursor.prototype.advance,
+ IDBCursor.prototype.continue,
+ IDBCursor.prototype.continuePrimaryKey
+ ]);
}
-/**
-* Handle visibility change â lock when user switches away from tab.
-*/
-function handleVisibilityChange() {
- if (document.hidden && app$1.isUnlocked) app$1.lockVault();
+var transactionDoneMap = /* @__PURE__ */ new WeakMap();
+var transformCache = /* @__PURE__ */ new WeakMap();
+var reverseTransformCache = /* @__PURE__ */ new WeakMap();
+function promisifyRequest(request) {
+ const promise = new Promise((resolve, reject) => {
+ const unlisten = () => {
+ request.removeEventListener("success", success);
+ request.removeEventListener("error", error);
+ };
+ const success = () => {
+ resolve(wrap(request.result));
+ unlisten();
+ };
+ const error = () => {
+ reject(request.error);
+ unlisten();
+ };
+ request.addEventListener("success", success);
+ request.addEventListener("error", error);
+ });
+ reverseTransformCache.set(promise, request);
+ return promise;
}
-/**
-* Clear the encryption key when the page is closing.
-*/
-function clearKeyOnExit() {
- app$1.encryptionKey = null;
+function cacheDonePromiseForTransaction(tx) {
+ if (transactionDoneMap.has(tx)) return;
+ const done = new Promise((resolve, reject) => {
+ const unlisten = () => {
+ tx.removeEventListener("complete", complete);
+ tx.removeEventListener("error", error);
+ tx.removeEventListener("abort", error);
+ };
+ const complete = () => {
+ resolve();
+ unlisten();
+ };
+ const error = () => {
+ reject(tx.error || new DOMException("AbortError", "AbortError"));
+ unlisten();
+ };
+ tx.addEventListener("complete", complete);
+ tx.addEventListener("error", error);
+ tx.addEventListener("abort", error);
+ });
+ transactionDoneMap.set(tx, done);
}
-/**
-* Stop auto-lock (e.g., when manually locking).
-*/
-function stopAutoLock() {
- if (autoLockTimer) {
- clearTimeout(autoLockTimer);
- autoLockTimer = null;
- }
-}
-//#endregion
-//#region src/lib/stores/app.svelte.js
-var AppStore = class {
- #isUnlocked = /* @__PURE__ */ state(false);
- get isUnlocked() {
- return get(this.#isUnlocked);
- }
- set isUnlocked(value) {
- set(this.#isUnlocked, value, true);
- }
- #encryptionKey = /* @__PURE__ */ state(null);
- get encryptionKey() {
- return get(this.#encryptionKey);
- }
- set encryptionKey(value) {
- set(this.#encryptionKey, value, true);
- }
- #salt = /* @__PURE__ */ state(null);
- get salt() {
- return get(this.#salt);
- }
- set salt(value) {
- set(this.#salt, value, true);
- }
- lockVault() {
- stopAutoLock();
- this.encryptionKey = null;
- this.isUnlocked = false;
+var idbProxyTraps = {
+ get(target, prop, receiver) {
+ if (target instanceof IDBTransaction) {
+ if (prop === "done") return transactionDoneMap.get(target);
+ if (prop === "store") return receiver.objectStoreNames[1] ? void 0 : receiver.objectStore(receiver.objectStoreNames[0]);
+ }
+ return wrap(target[prop]);
+ },
+ set(target, prop, value) {
+ target[prop] = value;
+ return true;
+ },
+ has(target, prop) {
+ if (target instanceof IDBTransaction && (prop === "done" || prop === "store")) return true;
+ return prop in target;
}
};
-var app$1 = new AppStore();
+function replaceTraps(callback) {
+ idbProxyTraps = callback(idbProxyTraps);
+}
+function wrapFunction(func) {
+ if (getCursorAdvanceMethods().includes(func)) return function(...args) {
+ func.apply(unwrap(this), args);
+ return wrap(this.request);
+ };
+ return function(...args) {
+ return wrap(func.apply(unwrap(this), args));
+ };
+}
+function transformCachableValue(value) {
+ if (typeof value === "function") return wrapFunction(value);
+ if (value instanceof IDBTransaction) cacheDonePromiseForTransaction(value);
+ if (instanceOfAny(value, getIdbProxyableTypes())) return new Proxy(value, idbProxyTraps);
+ return value;
+}
+function wrap(value) {
+ if (value instanceof IDBRequest) return promisifyRequest(value);
+ if (transformCache.has(value)) return transformCache.get(value);
+ const newValue = transformCachableValue(value);
+ if (newValue !== value) {
+ transformCache.set(value, newValue);
+ reverseTransformCache.set(newValue, value);
+ }
+ return newValue;
+}
+var unwrap = (value) => reverseTransformCache.get(value);
+/**
+* Open a database.
+*
+* @param name Name of the database.
+* @param version Schema version.
+* @param callbacks Additional callbacks.
+*/
+function openDB(name, version, { blocked, upgrade, blocking, terminated } = {}) {
+ const request = indexedDB.open(name, version);
+ const openPromise = wrap(request);
+ if (upgrade) request.addEventListener("upgradeneeded", (event) => {
+ upgrade(wrap(request.result), event.oldVersion, event.newVersion, wrap(request.transaction), event);
+ });
+ if (blocked) request.addEventListener("blocked", (event) => blocked(event.oldVersion, event.newVersion, event));
+ openPromise.then((db) => {
+ if (terminated) db.addEventListener("close", () => terminated());
+ if (blocking) db.addEventListener("versionchange", (event) => blocking(event.oldVersion, event.newVersion, event));
+ }).catch(() => {});
+ return openPromise;
+}
+var readMethods = [
+ "get",
+ "getKey",
+ "getAll",
+ "getAllKeys",
+ "count"
+];
+var writeMethods = [
+ "put",
+ "add",
+ "delete",
+ "clear"
+];
+var cachedMethods = /* @__PURE__ */ new Map();
+function getMethod(target, prop) {
+ if (!(target instanceof IDBDatabase && !(prop in target) && typeof prop === "string")) return;
+ if (cachedMethods.get(prop)) return cachedMethods.get(prop);
+ const targetFuncName = prop.replace(/FromIndex$/, "");
+ const useIndex = prop !== targetFuncName;
+ const isWrite = writeMethods.includes(targetFuncName);
+ if (!(targetFuncName in (useIndex ? IDBIndex : IDBObjectStore).prototype) || !(isWrite || readMethods.includes(targetFuncName))) return;
+ const method = async function(storeName, ...args) {
+ const tx = this.transaction(storeName, isWrite ? "readwrite" : "readonly");
+ let target = tx.store;
+ if (useIndex) target = target.index(args.shift());
+ return (await Promise.all([target[targetFuncName](...args), isWrite && tx.done]))[0];
+ };
+ cachedMethods.set(prop, method);
+ return method;
+}
+replaceTraps((oldTraps) => ({
+ ...oldTraps,
+ get: (target, prop, receiver) => getMethod(target, prop) || oldTraps.get(target, prop, receiver),
+ has: (target, prop) => !!getMethod(target, prop) || oldTraps.has(target, prop)
+}));
+var advanceMethodProps = [
+ "continue",
+ "continuePrimaryKey",
+ "advance"
+];
+var methodMap = {};
+var advanceResults = /* @__PURE__ */ new WeakMap();
+var ittrProxiedCursorToOriginalProxy = /* @__PURE__ */ new WeakMap();
+var cursorIteratorTraps = { get(target, prop) {
+ if (!advanceMethodProps.includes(prop)) return target[prop];
+ let cachedFunc = methodMap[prop];
+ if (!cachedFunc) cachedFunc = methodMap[prop] = function(...args) {
+ advanceResults.set(this, ittrProxiedCursorToOriginalProxy.get(this)[prop](...args));
+ };
+ return cachedFunc;
+} };
+async function* iterate(...args) {
+ let cursor = this;
+ if (!(cursor instanceof IDBCursor)) cursor = await cursor.openCursor(...args);
+ if (!cursor) return;
+ cursor = cursor;
+ const proxiedCursor = new Proxy(cursor, cursorIteratorTraps);
+ ittrProxiedCursorToOriginalProxy.set(proxiedCursor, cursor);
+ reverseTransformCache.set(proxiedCursor, unwrap(cursor));
+ while (cursor) {
+ yield proxiedCursor;
+ cursor = await (advanceResults.get(proxiedCursor) || cursor.continue());
+ advanceResults.delete(proxiedCursor);
+ }
+}
+function isIteratorProp(target, prop) {
+ return prop === Symbol.asyncIterator && instanceOfAny(target, [
+ IDBIndex,
+ IDBObjectStore,
+ IDBCursor
+ ]) || prop === "iterate" && instanceOfAny(target, [IDBIndex, IDBObjectStore]);
+}
+replaceTraps((oldTraps) => ({
+ ...oldTraps,
+ get(target, prop, receiver) {
+ if (isIteratorProp(target, prop)) return iterate;
+ return oldTraps.get(target, prop, receiver);
+ },
+ has(target, prop) {
+ return isIteratorProp(target, prop) || oldTraps.has(target, prop);
+ }
+}));
//#endregion
//#region src/lib/models/schema.js
/**
@@ -5164,220 +5308,6 @@ function generatePassword({ length = 16, uppercase = true, lowercase = true, dig
return password;
}
//#endregion
-//#region node_modules/idb/build/index.js
-var instanceOfAny = (object, constructors) => constructors.some((c) => object instanceof c);
-var idbProxyableTypes;
-var cursorAdvanceMethods;
-function getIdbProxyableTypes() {
- return idbProxyableTypes || (idbProxyableTypes = [
- IDBDatabase,
- IDBObjectStore,
- IDBIndex,
- IDBCursor,
- IDBTransaction
- ]);
-}
-function getCursorAdvanceMethods() {
- return cursorAdvanceMethods || (cursorAdvanceMethods = [
- IDBCursor.prototype.advance,
- IDBCursor.prototype.continue,
- IDBCursor.prototype.continuePrimaryKey
- ]);
-}
-var transactionDoneMap = /* @__PURE__ */ new WeakMap();
-var transformCache = /* @__PURE__ */ new WeakMap();
-var reverseTransformCache = /* @__PURE__ */ new WeakMap();
-function promisifyRequest(request) {
- const promise = new Promise((resolve, reject) => {
- const unlisten = () => {
- request.removeEventListener("success", success);
- request.removeEventListener("error", error);
- };
- const success = () => {
- resolve(wrap(request.result));
- unlisten();
- };
- const error = () => {
- reject(request.error);
- unlisten();
- };
- request.addEventListener("success", success);
- request.addEventListener("error", error);
- });
- reverseTransformCache.set(promise, request);
- return promise;
-}
-function cacheDonePromiseForTransaction(tx) {
- if (transactionDoneMap.has(tx)) return;
- const done = new Promise((resolve, reject) => {
- const unlisten = () => {
- tx.removeEventListener("complete", complete);
- tx.removeEventListener("error", error);
- tx.removeEventListener("abort", error);
- };
- const complete = () => {
- resolve();
- unlisten();
- };
- const error = () => {
- reject(tx.error || new DOMException("AbortError", "AbortError"));
- unlisten();
- };
- tx.addEventListener("complete", complete);
- tx.addEventListener("error", error);
- tx.addEventListener("abort", error);
- });
- transactionDoneMap.set(tx, done);
-}
-var idbProxyTraps = {
- get(target, prop, receiver) {
- if (target instanceof IDBTransaction) {
- if (prop === "done") return transactionDoneMap.get(target);
- if (prop === "store") return receiver.objectStoreNames[1] ? void 0 : receiver.objectStore(receiver.objectStoreNames[0]);
- }
- return wrap(target[prop]);
- },
- set(target, prop, value) {
- target[prop] = value;
- return true;
- },
- has(target, prop) {
- if (target instanceof IDBTransaction && (prop === "done" || prop === "store")) return true;
- return prop in target;
- }
-};
-function replaceTraps(callback) {
- idbProxyTraps = callback(idbProxyTraps);
-}
-function wrapFunction(func) {
- if (getCursorAdvanceMethods().includes(func)) return function(...args) {
- func.apply(unwrap(this), args);
- return wrap(this.request);
- };
- return function(...args) {
- return wrap(func.apply(unwrap(this), args));
- };
-}
-function transformCachableValue(value) {
- if (typeof value === "function") return wrapFunction(value);
- if (value instanceof IDBTransaction) cacheDonePromiseForTransaction(value);
- if (instanceOfAny(value, getIdbProxyableTypes())) return new Proxy(value, idbProxyTraps);
- return value;
-}
-function wrap(value) {
- if (value instanceof IDBRequest) return promisifyRequest(value);
- if (transformCache.has(value)) return transformCache.get(value);
- const newValue = transformCachableValue(value);
- if (newValue !== value) {
- transformCache.set(value, newValue);
- reverseTransformCache.set(newValue, value);
- }
- return newValue;
-}
-var unwrap = (value) => reverseTransformCache.get(value);
-/**
-* Open a database.
-*
-* @param name Name of the database.
-* @param version Schema version.
-* @param callbacks Additional callbacks.
-*/
-function openDB(name, version, { blocked, upgrade, blocking, terminated } = {}) {
- const request = indexedDB.open(name, version);
- const openPromise = wrap(request);
- if (upgrade) request.addEventListener("upgradeneeded", (event) => {
- upgrade(wrap(request.result), event.oldVersion, event.newVersion, wrap(request.transaction), event);
- });
- if (blocked) request.addEventListener("blocked", (event) => blocked(event.oldVersion, event.newVersion, event));
- openPromise.then((db) => {
- if (terminated) db.addEventListener("close", () => terminated());
- if (blocking) db.addEventListener("versionchange", (event) => blocking(event.oldVersion, event.newVersion, event));
- }).catch(() => {});
- return openPromise;
-}
-var readMethods = [
- "get",
- "getKey",
- "getAll",
- "getAllKeys",
- "count"
-];
-var writeMethods = [
- "put",
- "add",
- "delete",
- "clear"
-];
-var cachedMethods = /* @__PURE__ */ new Map();
-function getMethod(target, prop) {
- if (!(target instanceof IDBDatabase && !(prop in target) && typeof prop === "string")) return;
- if (cachedMethods.get(prop)) return cachedMethods.get(prop);
- const targetFuncName = prop.replace(/FromIndex$/, "");
- const useIndex = prop !== targetFuncName;
- const isWrite = writeMethods.includes(targetFuncName);
- if (!(targetFuncName in (useIndex ? IDBIndex : IDBObjectStore).prototype) || !(isWrite || readMethods.includes(targetFuncName))) return;
- const method = async function(storeName, ...args) {
- const tx = this.transaction(storeName, isWrite ? "readwrite" : "readonly");
- let target = tx.store;
- if (useIndex) target = target.index(args.shift());
- return (await Promise.all([target[targetFuncName](...args), isWrite && tx.done]))[0];
- };
- cachedMethods.set(prop, method);
- return method;
-}
-replaceTraps((oldTraps) => ({
- ...oldTraps,
- get: (target, prop, receiver) => getMethod(target, prop) || oldTraps.get(target, prop, receiver),
- has: (target, prop) => !!getMethod(target, prop) || oldTraps.has(target, prop)
-}));
-var advanceMethodProps = [
- "continue",
- "continuePrimaryKey",
- "advance"
-];
-var methodMap = {};
-var advanceResults = /* @__PURE__ */ new WeakMap();
-var ittrProxiedCursorToOriginalProxy = /* @__PURE__ */ new WeakMap();
-var cursorIteratorTraps = { get(target, prop) {
- if (!advanceMethodProps.includes(prop)) return target[prop];
- let cachedFunc = methodMap[prop];
- if (!cachedFunc) cachedFunc = methodMap[prop] = function(...args) {
- advanceResults.set(this, ittrProxiedCursorToOriginalProxy.get(this)[prop](...args));
- };
- return cachedFunc;
-} };
-async function* iterate(...args) {
- let cursor = this;
- if (!(cursor instanceof IDBCursor)) cursor = await cursor.openCursor(...args);
- if (!cursor) return;
- cursor = cursor;
- const proxiedCursor = new Proxy(cursor, cursorIteratorTraps);
- ittrProxiedCursorToOriginalProxy.set(proxiedCursor, cursor);
- reverseTransformCache.set(proxiedCursor, unwrap(cursor));
- while (cursor) {
- yield proxiedCursor;
- cursor = await (advanceResults.get(proxiedCursor) || cursor.continue());
- advanceResults.delete(proxiedCursor);
- }
-}
-function isIteratorProp(target, prop) {
- return prop === Symbol.asyncIterator && instanceOfAny(target, [
- IDBIndex,
- IDBObjectStore,
- IDBCursor
- ]) || prop === "iterate" && instanceOfAny(target, [IDBIndex, IDBObjectStore]);
-}
-replaceTraps((oldTraps) => ({
- ...oldTraps,
- get(target, prop, receiver) {
- if (isIteratorProp(target, prop)) return iterate;
- return oldTraps.get(target, prop, receiver);
- },
- has(target, prop) {
- return isIteratorProp(target, prop) || oldTraps.has(target, prop);
- }
-}));
-//#endregion
//#region src/lib/storage/db.js
/**
* IndexedDB storage layer using the `idb` wrapper.
@@ -5469,6 +5399,25 @@ async function isVaultInitialized() {
return (await loadVaultMeta()).salt !== null;
}
/**
+* Save a single setting as a key/value pair in the meta store.
+* @param {string} key
+* @param {*} value
+*/
+async function saveSetting(key, value) {
+ await (await getDb()).put("meta", {
+ key: "setting:" + key,
+ value
+ });
+}
+/**
+* Load a single setting from the meta store.
+* @param {string} key
+* @returns {Promise<*>} The value, or undefined if not set
+*/
+async function getSetting(key) {
+ return (await (await getDb()).get("meta", "setting:" + key))?.value ?? void 0;
+}
+/**
* @typedef {import('../models/schema.js').Group} Group
*/
/**
@@ -5701,6 +5650,129 @@ async function importAll(data, mode = "merge", sourcePassword = "", targetKey =
};
}
//#endregion
+//#region src/lib/stores/settings.svelte.js
+var SettingsStore = class {
+ #autoLockMinutes = /* @__PURE__ */ state(5);
+ get autoLockMinutes() {
+ return get(this.#autoLockMinutes);
+ }
+ set autoLockMinutes(value) {
+ set(this.#autoLockMinutes, value, true);
+ }
+ #lockOnTabSwitch = /* @__PURE__ */ state(true);
+ get lockOnTabSwitch() {
+ return get(this.#lockOnTabSwitch);
+ }
+ set lockOnTabSwitch(value) {
+ set(this.#lockOnTabSwitch, value, true);
+ }
+ async load() {
+ const minutes = await getSetting("autoLockMinutes");
+ const tabSwitch = await getSetting("lockOnTabSwitch");
+ this.autoLockMinutes = minutes != null ? Number(minutes) : 5;
+ this.lockOnTabSwitch = tabSwitch != null ? Boolean(tabSwitch) : true;
+ }
+ /**
+ * Persist current settings to IndexedDB.
+ */
+ async save() {
+ await saveSetting("autoLockMinutes", this.autoLockMinutes);
+ await saveSetting("lockOnTabSwitch", this.lockOnTabSwitch);
+ }
+};
+var settings = new SettingsStore();
+//#endregion
+//#region src/lib/stores/security.svelte.js
+var autoLockTimer = null;
+var activityHandler = null;
+var activityEvents = null;
+function startAutoLock() {
+ resetAutoLock();
+ if (!activityHandler) {
+ activityEvents = [
+ "mousedown",
+ "keydown",
+ "scroll",
+ "touchstart"
+ ];
+ activityHandler = () => resetAutoLock();
+ activityEvents.forEach((evt) => window.addEventListener(evt, activityHandler, { passive: true }));
+ }
+ document.addEventListener("visibilitychange", handleVisibilityChange);
+ window.addEventListener("beforeunload", clearKeyOnExit);
+ window.__vaultCleanup = stopAutoLock;
+}
+/**
+* Reset the auto-lock timer using the current settings value.
+*/
+function resetAutoLock() {
+ if (autoLockTimer) clearTimeout(autoLockTimer);
+ const minutes = settings.autoLockMinutes ?? 5;
+ autoLockTimer = setTimeout(() => {
+ app$1.lockVault();
+ }, minutes * 60 * 1e3);
+}
+/**
+* Handle visibility change â lock when user switches away from tab
+* (only if lockOnTabSwitch is enabled).
+*/
+function handleVisibilityChange() {
+ if (document.hidden && app$1.isUnlocked && settings.lockOnTabSwitch) app$1.lockVault();
+}
+/**
+* Clear the encryption key when the page is closing.
+*/
+function clearKeyOnExit() {
+ app$1.encryptionKey = null;
+}
+/**
+* Stop auto-lock and remove all listeners.
+*/
+function stopAutoLock() {
+ if (autoLockTimer) {
+ clearTimeout(autoLockTimer);
+ autoLockTimer = null;
+ }
+ if (activityHandler && activityEvents) {
+ activityEvents.forEach((evt) => window.removeEventListener(evt, activityHandler));
+ activityHandler = null;
+ activityEvents = null;
+ }
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
+ window.removeEventListener("beforeunload", clearKeyOnExit);
+}
+//#endregion
+//#region src/lib/stores/app.svelte.js
+var AppStore = class {
+ #isUnlocked = /* @__PURE__ */ state(false);
+ get isUnlocked() {
+ return get(this.#isUnlocked);
+ }
+ set isUnlocked(value) {
+ set(this.#isUnlocked, value, true);
+ }
+ #encryptionKey = /* @__PURE__ */ state(null);
+ get encryptionKey() {
+ return get(this.#encryptionKey);
+ }
+ set encryptionKey(value) {
+ set(this.#encryptionKey, value, true);
+ }
+ #salt = /* @__PURE__ */ state(null);
+ get salt() {
+ return get(this.#salt);
+ }
+ set salt(value) {
+ set(this.#salt, value, true);
+ }
+ lockVault() {
+ stopAutoLock();
+ this.encryptionKey = null;
+ this.isUnlocked = false;
+ }
+};
+var app$1 = new AppStore();
+//#endregion
//#region src/lib/autofocus.js
/**
* Action that autofocuses an element after mount when the condition is truthy.
@@ -5711,9 +5783,9 @@ function autofocus(node, condition = true) {
}
//#endregion
//#region src/components/LockScreen.svelte
-var root_1$7 = /* @__PURE__ */ from_html(`
`);
+var root_1$8 = /* @__PURE__ */ from_html(`
`);
var root_2$6 = /* @__PURE__ */ from_html(`
`);
-var root$7 = /* @__PURE__ */ from_html(``);
+var root$8 = /* @__PURE__ */ from_html(``);
function LockScreen($$anchor, $$props) {
push($$props, true);
let masterPassword = /* @__PURE__ */ state("");
@@ -5745,6 +5817,7 @@ function LockScreen($$anchor, $$props) {
app$1.encryptionKey = await deriveKey(get(masterPassword), salt);
await saveVaultMeta(salt, testEncrypted, testPlaintext);
await ensureTrashGroup();
+ await settings.load();
app$1.isUnlocked = true;
startAutoLock();
} else {
@@ -5762,6 +5835,7 @@ function LockScreen($$anchor, $$props) {
}
app$1.salt = meta.salt;
app$1.encryptionKey = key;
+ await settings.load();
app$1.isUnlocked = true;
startAutoLock();
}
@@ -5773,14 +5847,14 @@ function LockScreen($$anchor, $$props) {
set(masterPassword, "");
set(confirmPassword, "");
}
- var div = root$7();
+ var div = root$8();
var div_1 = child(div);
var p = sibling(child(div_1), 4);
var text = child(p, true);
reset(p);
var node = sibling(p, 2);
var consequent = ($$anchor) => {
- var div_2 = root_1$7();
+ var div_2 = root_1$8();
var text_1 = child(div_2, true);
reset(div_2);
template_effect(() => set_text(text_1, get(error)));
@@ -5894,7 +5968,7 @@ var root_4$5 = /* @__PURE__ */ from_html(``);
var root_6$4 = /* @__PURE__ */ from_html(`Delete Group
Delete " "? Entries in this group will become ungrouped.
`);
-var root$6 = /* @__PURE__ */ from_html(``);
+var root$7 = /* @__PURE__ */ from_html(``);
function Sidebar($$anchor, $$props) {
push($$props, true);
let groups = /* @__PURE__ */ state(proxy([]));
@@ -5989,7 +6063,7 @@ function Sidebar($$anchor, $$props) {
set(groupError, "Failed to delete group: " + e.message);
}
}
- var div = root$6();
+ var div = root$7();
var div_1 = sibling(child(div), 2);
var input = child(div_1);
remove_input_defaults(input);
@@ -6162,7 +6236,7 @@ function Sidebar($$anchor, $$props) {
delegate(["input", "click"]);
//#endregion
//#region src/components/EntryList.svelte
-var root_1$6 = /* @__PURE__ */ from_html(`Loading entries...
`);
+var root_1$7 = /* @__PURE__ */ from_html(`Loading entries...
`);
var root_2$4 = /* @__PURE__ */ from_html(`
`);
var root_4$4 = /* @__PURE__ */ from_html(``);
var root_3$4 = /* @__PURE__ */ from_html(``);
@@ -6170,9 +6244,9 @@ var root_6$3 = /* @__PURE__ */ from_html(`matching " "`, 1);
var root_7$4 = /* @__PURE__ */ from_html(` | `);
var root_9$1 = /* @__PURE__ */ from_html(`â ŋ`);
var root_10$1 = /* @__PURE__ */ from_html(` | `);
-var root_8$1 = /* @__PURE__ */ from_html(`| | | | |
`);
+var root_8$2 = /* @__PURE__ */ from_html(`| | | | |
`);
var root_5$3 = /* @__PURE__ */ from_html(`
`, 1);
-var root$5 = /* @__PURE__ */ from_html(``);
+var root$6 = /* @__PURE__ */ from_html(``);
function EntryList($$anchor, $$props) {
push($$props, true);
let entries = /* @__PURE__ */ state(proxy([]));
@@ -6211,10 +6285,10 @@ function EntryList($$anchor, $$props) {
search.refreshTrigger;
loadEntries();
});
- var div = root$5();
+ var div = root$6();
var node = child(div);
var consequent = ($$anchor) => {
- append($$anchor, root_1$6());
+ append($$anchor, root_1$7());
};
var consequent_1 = ($$anchor) => {
var div_2 = root_2$4();
@@ -6287,7 +6361,7 @@ function EntryList($$anchor, $$props) {
reset(thead);
var tbody = sibling(thead);
each(tbody, 21, () => get(entries), (entry) => entry.id, ($$anchor, entry) => {
- var tr_1 = root_8$1();
+ var tr_1 = root_8$2();
var td = child(tr_1);
var node_4 = child(td);
var consequent_6 = ($$anchor) => {
@@ -6369,19 +6443,19 @@ function EntryList($$anchor, $$props) {
delegate(["click"]);
//#endregion
//#region src/components/EntryDetail.svelte
-var root_1$5 = /* @__PURE__ */ from_html(`
`);
+var root_1$6 = /* @__PURE__ */ from_html(`
`);
var root_2$3 = /* @__PURE__ */ from_html(`Loading...
`);
var root_3$3 = /* @__PURE__ */ from_html(`
`);
var root_4$3 = /* @__PURE__ */ from_html(`Entry not found
`);
var root_6$2 = /* @__PURE__ */ from_html(` `, 1);
var root_7$3 = /* @__PURE__ */ from_html(` `, 1);
-var root_8 = /* @__PURE__ */ from_html(``);
+var root_8$1 = /* @__PURE__ */ from_html(``);
var root_9 = /* @__PURE__ */ from_html(``);
var root_10 = /* @__PURE__ */ from_html(``);
-var root_11$1 = /* @__PURE__ */ from_html(`Move to Trash
Move " " to the trash? You can restore it later.
`);
+var root_11 = /* @__PURE__ */ from_html(`Move to Trash
Move " " to the trash? You can restore it later.
`);
var root_12 = /* @__PURE__ */ from_html(`Delete Forever
Permanently delete " "? This cannot be undone.
`);
var root_5$2 = /* @__PURE__ */ from_html(` `, 1);
-var root$4 = /* @__PURE__ */ from_html(`
`);
+var root$5 = /* @__PURE__ */ from_html(`
`);
function EntryDetail($$anchor, $$props) {
push($$props, true);
let entry = /* @__PURE__ */ state(null);
@@ -6458,10 +6532,10 @@ function EntryDetail($$anchor, $$props) {
set(deleting, false);
set(showPermanentDeleteConfirm, false);
}
- var div = root$4();
+ var div = root$5();
var node = child(div);
var consequent = ($$anchor) => {
- var div_1 = root_1$5();
+ var div_1 = root_1$6();
var text_1 = child(div_1, true);
reset(div_1);
template_effect(() => set_text(text_1, get(toast)));
@@ -6518,7 +6592,7 @@ function EntryDetail($$anchor, $$props) {
var div_8 = sibling(div_6, 2);
var node_3 = child(div_8);
var consequent_5 = ($$anchor) => {
- var div_9 = root_8();
+ var div_9 = root_8$1();
var div_10 = sibling(child(div_9), 2);
var span = child(div_10);
var text_4 = child(span, true);
@@ -6589,7 +6663,7 @@ function EntryDetail($$anchor, $$props) {
reset(div_5);
var node_6 = sibling(div_5, 2);
var consequent_8 = ($$anchor) => {
- var div_18 = root_11$1();
+ var div_18 = root_11();
var div_19 = child(div_18);
var p = sibling(child(div_19), 2);
var strong = sibling(child(p));
@@ -6676,13 +6750,13 @@ delegate(["click"]);
delegate(["click"]);
//#endregion
//#region src/components/EntryForm.svelte
-var root_1$3 = /* @__PURE__ */ from_html(`Loading...
`);
+var root_1$4 = /* @__PURE__ */ from_html(`Loading...
`);
var root_3$2 = /* @__PURE__ */ from_html(`
`);
var root_5$1 = /* @__PURE__ */ from_html(`
`);
var root_4$2 = /* @__PURE__ */ from_html(``);
var root_7$2 = /* @__PURE__ */ from_html(``);
var root_2$2 = /* @__PURE__ */ from_html(` `, 1);
-var root$2 = /* @__PURE__ */ from_html(``);
+var root$3 = /* @__PURE__ */ from_html(``);
function EntryForm($$anchor, $$props) {
push($$props, true);
let title = /* @__PURE__ */ state("");
@@ -6758,10 +6832,10 @@ function EntryForm($$anchor, $$props) {
}
set(saving, false);
}
- var div = root$2();
+ var div = root$3();
var node = child(div);
var consequent = ($$anchor) => {
- append($$anchor, root_1$3());
+ append($$anchor, root_1$4());
};
var alternate = ($$anchor) => {
var fragment = root_2$2();
@@ -6887,13 +6961,13 @@ function EntryForm($$anchor, $$props) {
delegate(["click"]);
//#endregion
//#region src/components/ImportExport.svelte
-var root_1$2 = /* @__PURE__ */ from_html(`Export Vault
All entries and groups will be exported. You'll need the source vault's master password when importing into another vault.
`);
+var root_1$3 = /* @__PURE__ */ from_html(`Export Vault
All entries and groups will be exported. You'll need the source vault's master password when importing into another vault.
`);
var root_3$1 = /* @__PURE__ */ from_html(`
`);
var root_4$1 = /* @__PURE__ */ from_html(`
`);
var root_6$1 = /* @__PURE__ */ from_html(`File loaded. Enter the source vault's master password to decrypt and re-encrypt entries under your current vault.
`, 1);
var root_7$1 = /* @__PURE__ */ from_html(`Select how to handle existing data:
`, 1);
var root_2$1 = /* @__PURE__ */ from_html(``);
-var root$1 = /* @__PURE__ */ from_html(`
`);
+var root$2 = /* @__PURE__ */ from_html(`
`);
function ImportExport($$anchor, $$props) {
push($$props, true);
const binding_group = [];
@@ -6963,12 +7037,12 @@ function ImportExport($$anchor, $$props) {
}
set(importing, false);
}
- var div = root$1();
+ var div = root$2();
var button = child(div);
var button_1 = sibling(button, 2);
var node = sibling(button_1, 2);
var consequent = ($$anchor) => {
- var div_1 = root_1$2();
+ var div_1 = root_1$3();
var div_2 = child(div_1);
var div_3 = sibling(child(div_2), 4);
var button_2 = child(div_3);
@@ -7118,16 +7192,108 @@ function ImportExport($$anchor, $$props) {
}
delegate(["click", "change"]);
//#endregion
+//#region src/components/SettingsDialog.svelte
+var root_1$2 = /* @__PURE__ */ from_html(``);
+var root$1 = /* @__PURE__ */ from_html(``);
+function SettingsDialog($$anchor, $$props) {
+ push($$props, true);
+ let minutes = /* @__PURE__ */ state(proxy(settings.autoLockMinutes));
+ let lockOnTabSwitch = /* @__PURE__ */ state(proxy(settings.lockOnTabSwitch));
+ let saving = /* @__PURE__ */ state(false);
+ const minuteOptions = [
+ 1,
+ 5,
+ 10,
+ 15,
+ 30,
+ 60
+ ];
+ async function handleSave() {
+ set(saving, true);
+ try {
+ settings.autoLockMinutes = get(minutes);
+ settings.lockOnTabSwitch = get(lockOnTabSwitch);
+ await settings.save();
+ startAutoLock();
+ } catch (e) {
+ console.error("Failed to save settings:", e);
+ }
+ set(saving, false);
+ $$props.onBack();
+ }
+ user_effect(() => {
+ set(minutes, settings.autoLockMinutes, true);
+ set(lockOnTabSwitch, settings.lockOnTabSwitch, true);
+ });
+ var div = root$1();
+ var form = child(div);
+ var div_1 = sibling(child(form), 2);
+ var select = sibling(child(div_1), 2);
+ each(select, 21, () => minuteOptions, index, ($$anchor, m) => {
+ var option = root_1$2();
+ var text = child(option);
+ reset(option);
+ var option_value = {};
+ template_effect(() => {
+ set_text(text, `${get(m) ?? ""} ${get(m) === 1 ? "minute" : "minutes"}`);
+ if (option_value !== (option_value = get(m))) option.value = (option.__value = get(m)) ?? "";
+ });
+ append($$anchor, option);
+ });
+ reset(select);
+ var p = sibling(select, 2);
+ var text_1 = child(p);
+ reset(p);
+ reset(div_1);
+ var div_2 = sibling(div_1, 2);
+ var label = child(div_2);
+ var input = child(label);
+ remove_input_defaults(input);
+ next(4);
+ reset(label);
+ var p_1 = sibling(label, 2);
+ var text_2 = child(p_1, true);
+ reset(p_1);
+ reset(div_2);
+ var div_3 = sibling(div_2, 2);
+ var button = child(div_3);
+ var text_3 = child(button, true);
+ reset(button);
+ var button_1 = sibling(button, 2);
+ reset(div_3);
+ reset(form);
+ reset(div);
+ template_effect(() => {
+ set_text(text_1, `Vault locks after ${get(minutes) ?? ""} ${get(minutes) === 1 ? "minute" : "minutes"} of inactivity.`);
+ set_text(text_2, get(lockOnTabSwitch) ? "The vault locks immediately when you switch to another tab." : "The vault stays unlocked even when you switch tabs.");
+ button.disabled = get(saving);
+ set_text(text_3, get(saving) ? "Saving..." : "Save");
+ });
+ event("submit", form, (e) => {
+ e.preventDefault();
+ handleSave();
+ });
+ bind_select_value(select, () => get(minutes), ($$value) => set(minutes, $$value));
+ bind_checked(input, () => get(lockOnTabSwitch), ($$value) => set(lockOnTabSwitch, $$value));
+ delegated("click", button_1, function(...$$args) {
+ $$props.onBack?.apply(this, $$args);
+ });
+ append($$anchor, div);
+ pop();
+}
+delegate(["click"]);
+//#endregion
//#region src/components/MainLayout.svelte
var root_1$1 = /* @__PURE__ */ from_html(``);
var root_2 = /* @__PURE__ */ from_html(``);
var root_3 = /* @__PURE__ */ from_html(`
`);
var root_4 = /* @__PURE__ */ from_html(`Entry Details
`);
var root_5 = /* @__PURE__ */ from_html(`
`);
-var root_6 = /* @__PURE__ */ from_html(``);
-var root_7 = /* @__PURE__ */ from_html(``);
-var root_11 = /* @__PURE__ */ from_html(`Empty Trash
Permanently delete all entries from the trash? This cannot be undone.
`);
-var root = /* @__PURE__ */ from_html(``);
+var root_6 = /* @__PURE__ */ from_html(`Settings
`);
+var root_7 = /* @__PURE__ */ from_html(``);
+var root_8 = /* @__PURE__ */ from_html(``);
+var root_13 = /* @__PURE__ */ from_html(`Empty Trash
Permanently delete all entries from the trash? This cannot be undone.
`);
+var root = /* @__PURE__ */ from_html(``);
function MainLayout($$anchor, $$props) {
push($$props, true);
let sidebarOpen = /* @__PURE__ */ state(false);
@@ -7151,8 +7317,12 @@ function MainLayout($$anchor, $$props) {
set(viewMode, "form");
set(sidebarOpen, false);
}
+ function goSettings() {
+ set(viewMode, "settings");
+ set(sidebarOpen, false);
+ }
function handleBack() {
- if (get(viewMode) === "form") goDetail(get(selectedEntryId));
+ if (get(viewMode) === "form" || get(viewMode) === "settings") goList();
else goList();
}
async function handleEmptyTrash() {
@@ -7216,16 +7386,20 @@ function MainLayout($$anchor, $$props) {
template_effect(() => set_text(text_1, get(selectedEntryId) ? "Edit Entry" : "New Entry"));
append($$anchor, h1_2);
};
+ var consequent_5 = ($$anchor) => {
+ append($$anchor, root_6());
+ };
if_block(node_3, ($$render) => {
if (get(viewMode) === "list") $$render(consequent_2);
else if (get(viewMode) === "detail") $$render(consequent_3, 1);
else if (get(viewMode) === "form") $$render(consequent_4, 2);
+ else if (get(viewMode) === "settings") $$render(consequent_5, 3);
});
reset(div_3);
var div_4 = sibling(div_3, 2);
var node_4 = child(div_4);
- var consequent_5 = ($$anchor) => {
- var button_4 = root_6();
+ var consequent_6 = ($$anchor) => {
+ var button_4 = root_7();
var text_2 = child(button_4, true);
reset(button_4);
template_effect(() => {
@@ -7236,31 +7410,32 @@ function MainLayout($$anchor, $$props) {
append($$anchor, button_4);
};
if_block(node_4, ($$render) => {
- if (get(viewMode) === "list" && get(isTrashView)) $$render(consequent_5);
+ if (get(viewMode) === "list" && get(isTrashView)) $$render(consequent_6);
});
var node_5 = sibling(node_4, 2);
- var consequent_6 = ($$anchor) => {
- var button_5 = root_7();
+ var consequent_7 = ($$anchor) => {
+ var button_5 = root_8();
delegated("click", button_5, () => goForm(null));
append($$anchor, button_5);
};
if_block(node_5, ($$render) => {
- if (get(viewMode) === "list" && !get(isTrashView)) $$render(consequent_6);
+ if (get(viewMode) === "list" && !get(isTrashView)) $$render(consequent_7);
});
var node_6 = sibling(node_5, 2);
ImportExport(node_6, {});
var button_6 = sibling(node_6, 2);
+ var button_7 = sibling(button_6, 2);
reset(div_4);
reset(div_2);
var div_5 = sibling(div_2, 2);
var node_7 = child(div_5);
- var consequent_7 = ($$anchor) => {
+ var consequent_8 = ($$anchor) => {
EntryList($$anchor, {
onSelect: goDetail,
onAdd: () => goForm(null)
});
};
- var consequent_8 = ($$anchor) => {
+ var consequent_9 = ($$anchor) => {
EntryDetail($$anchor, {
get entryId() {
return get(selectedEntryId);
@@ -7269,7 +7444,7 @@ function MainLayout($$anchor, $$props) {
onBack: goList
});
};
- var consequent_9 = ($$anchor) => {
+ var consequent_10 = ($$anchor) => {
EntryForm($$anchor, {
get entryId() {
return get(selectedEntryId);
@@ -7278,43 +7453,48 @@ function MainLayout($$anchor, $$props) {
onCancel: handleBack
});
};
+ var consequent_11 = ($$anchor) => {
+ SettingsDialog($$anchor, { onBack: goList });
+ };
if_block(node_7, ($$render) => {
- if (get(viewMode) === "list") $$render(consequent_7);
- else if (get(viewMode) === "detail" && get(selectedEntryId)) $$render(consequent_8, 1);
- else if (get(viewMode) === "form") $$render(consequent_9, 2);
+ if (get(viewMode) === "list") $$render(consequent_8);
+ else if (get(viewMode) === "detail" && get(selectedEntryId)) $$render(consequent_9, 1);
+ else if (get(viewMode) === "form") $$render(consequent_10, 2);
+ else if (get(viewMode) === "settings") $$render(consequent_11, 3);
});
reset(div_5);
reset(main);
var node_8 = sibling(main, 2);
- var consequent_10 = ($$anchor) => {
- var div_6 = root_11();
+ var consequent_12 = ($$anchor) => {
+ var div_6 = root_13();
var div_7 = child(div_6);
var div_8 = sibling(child(div_7), 4);
- var button_7 = child(div_8);
- var text_3 = child(button_7, true);
- reset(button_7);
- var button_8 = sibling(button_7, 2);
+ var button_8 = child(div_8);
+ var text_3 = child(button_8, true);
+ reset(button_8);
+ var button_9 = sibling(button_8, 2);
reset(div_8);
reset(div_7);
reset(div_6);
template_effect(() => {
- button_7.disabled = get(emptyingTrash);
+ button_8.disabled = get(emptyingTrash);
set_text(text_3, get(emptyingTrash) ? "Emptying..." : "Yes, empty trash");
});
delegated("click", div_6, () => set(showEmptyTrashConfirm, false));
delegated("click", div_7, (e) => e.stopPropagation());
- delegated("click", button_7, handleEmptyTrash);
- delegated("click", button_8, () => set(showEmptyTrashConfirm, false));
+ delegated("click", button_8, handleEmptyTrash);
+ delegated("click", button_9, () => set(showEmptyTrashConfirm, false));
append($$anchor, div_6);
};
if_block(node_8, ($$render) => {
- if (get(showEmptyTrashConfirm)) $$render(consequent_10);
+ if (get(showEmptyTrashConfirm)) $$render(consequent_12);
});
reset(div);
template_effect(() => set_class(aside, 1, `sidebar ${get(sidebarOpen) ? "open" : ""}`, "svelte-1ybayt"));
delegated("click", button, () => set(sidebarOpen, !get(sidebarOpen)));
delegated("click", button_1, handleLock);
- delegated("click", button_6, handleLock);
+ delegated("click", button_6, goSettings);
+ delegated("click", button_7, handleLock);
append($$anchor, div);
pop();
}
@@ -8265,6 +8445,76 @@ label {
border-color: var(--color-primary);
}
+ .settings-panel.svelte-1koizbb {
+ max-width: 500px;
+ }
+
+ .form-card.svelte-1koizbb {
+ background: var(--color-surface);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-lg);
+ padding: 24px;
+ }
+
+ .form-card.svelte-1koizbb h3:where(.svelte-1koizbb) {
+ margin-bottom: 16px;
+ }
+
+ .form-actions.svelte-1koizbb {
+ display: flex;
+ gap: 8px;
+ margin-top: 20px;
+ }
+
+ .toggle-label.svelte-1koizbb {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ cursor: pointer;
+ margin-bottom: 0;
+ }
+
+ .toggle-label.svelte-1koizbb input[type="checkbox"]:where(.svelte-1koizbb) {
+ position: absolute;
+ opacity: 0;
+ width: 0;
+ height: 0;
+ }
+
+ .toggle-track.svelte-1koizbb {
+ width: 40px;
+ height: 22px;
+ background: var(--color-border);
+ border-radius: 11px;
+ position: relative;
+ transition: background-color 150ms;
+ flex-shrink: 0;
+ }
+
+ .toggle-track.svelte-1koizbb .toggle-thumb:where(.svelte-1koizbb) {
+ position: absolute;
+ top: 3px;
+ left: 3px;
+ width: 16px;
+ height: 16px;
+ background: #fff;
+ border-radius: 50%;
+ transition: transform 150ms;
+ }
+
+ .toggle-label.svelte-1koizbb input:where(.svelte-1koizbb):checked + .toggle-track:where(.svelte-1koizbb) {
+ background: var(--color-primary);
+ }
+
+ .toggle-label.svelte-1koizbb input:where(.svelte-1koizbb):checked + .toggle-track:where(.svelte-1koizbb) .toggle-thumb:where(.svelte-1koizbb) {
+ transform: translateX(18px);
+ }
+
+ .toggle-text.svelte-1koizbb {
+ font-size: 0.875rem;
+ color: var(--color-text);
+ }
+
.app-shell.svelte-1ybayt {
display: flex;
min-height: 100vh;
diff --git a/src/components/LockScreen.svelte b/src/components/LockScreen.svelte
index 1305276..d3b89cd 100644
--- a/src/components/LockScreen.svelte
+++ b/src/components/LockScreen.svelte
@@ -3,6 +3,7 @@
import { deriveKey, createTestPayload, verifyPassword } from '../lib/crypto/crypto.js'
import { saveVaultMeta, loadVaultMeta, isVaultInitialized, ensureTrashGroup } from '../lib/storage/db.js'
import { startAutoLock } from '../lib/stores/security.svelte.js'
+ import { settings } from '../lib/stores/settings.svelte.js'
import { autofocus } from '../lib/autofocus.js'
let masterPassword = $state('')
@@ -42,6 +43,7 @@
await saveVaultMeta(salt, testEncrypted, testPlaintext)
await ensureTrashGroup()
+ await settings.load()
app.isUnlocked = true
startAutoLock()
} else {
@@ -64,6 +66,7 @@
app.salt = meta.salt
app.encryptionKey = key
+ await settings.load()
app.isUnlocked = true
startAutoLock()
}
diff --git a/src/components/MainLayout.svelte b/src/components/MainLayout.svelte
index 2fbb060..d64b31c 100644
--- a/src/components/MainLayout.svelte
+++ b/src/components/MainLayout.svelte
@@ -8,9 +8,10 @@
import EntryDetail from './EntryDetail.svelte'
import EntryForm from './EntryForm.svelte'
import ImportExport from './ImportExport.svelte'
+ import SettingsDialog from './SettingsDialog.svelte'
let sidebarOpen = $state(false)
- let viewMode = $state('list') // 'list' | 'detail' | 'form'
+ let viewMode = $state('list') // 'list' | 'detail' | 'form' | 'settings'
let selectedEntryId = $state(null)
let showEmptyTrashConfirm = $state(false)
let emptyingTrash = $state(false)
@@ -35,9 +36,14 @@
sidebarOpen = false
}
+ function goSettings() {
+ viewMode = 'settings'
+ sidebarOpen = false
+ }
+
function handleBack() {
- if (viewMode === 'form') {
- goDetail(selectedEntryId)
+ if (viewMode === 'form' || viewMode === 'settings') {
+ goList()
} else {
goList()
}
@@ -94,6 +100,8 @@
Entry Details
{:else if viewMode === 'form'}
{selectedEntryId ? 'Edit Entry' : 'New Entry'}
+ {:else if viewMode === 'settings'}
+ Settings
{/if}
@@ -106,6 +114,7 @@
{/if}
+
@@ -126,6 +135,8 @@
onSave={goList}
onCancel={handleBack}
/>
+ {:else if viewMode === 'settings'}
+
{/if}
diff --git a/src/components/SettingsDialog.svelte b/src/components/SettingsDialog.svelte
new file mode 100644
index 0000000..520a1c2
--- /dev/null
+++ b/src/components/SettingsDialog.svelte
@@ -0,0 +1,149 @@
+
+
+
+
+
+
+
diff --git a/src/lib/storage/db.js b/src/lib/storage/db.js
index 1b4b2fd..3ea50ec 100644
--- a/src/lib/storage/db.js
+++ b/src/lib/storage/db.js
@@ -117,6 +117,31 @@ export async function isVaultInitialized() {
return meta.salt !== null
}
+// ========================
+// Settings (user preferences in meta store)
+// ========================
+
+/**
+ * Save a single setting as a key/value pair in the meta store.
+ * @param {string} key
+ * @param {*} value
+ */
+export async function saveSetting(key, value) {
+ const db = await getDb()
+ await db.put('meta', { key: 'setting:' + key, value })
+}
+
+/**
+ * Load a single setting from the meta store.
+ * @param {string} key
+ * @returns {Promise<*>} The value, or undefined if not set
+ */
+export async function getSetting(key) {
+ const db = await getDb()
+ const row = await db.get('meta', 'setting:' + key)
+ return row?.value ?? undefined
+}
+
// ========================
// Groups
// ========================
diff --git a/src/lib/stores/security.svelte.js b/src/lib/stores/security.svelte.js
index 2ef808d..1b4bc6d 100644
--- a/src/lib/stores/security.svelte.js
+++ b/src/lib/stores/security.svelte.js
@@ -3,24 +3,26 @@
*/
import { app } from './app.svelte.js'
-
-const DEFAULT_AUTO_LOCK_MINUTES = 5
+import { settings } from './settings.svelte.js'
let autoLockTimer = null
-let autoLockMinutes = 5
+
+// Activity listeners â registered once, cleaned up on stop
+let activityHandler = null
+let activityEvents = null
/**
- * Start the auto-lock timer. Resets every time the user interacts.
- * @param {number} minutes - Auto-lock after N minutes of inactivity
+ * Start (or restart) the auto-lock timer using current settings.
*/
-export function startAutoLock(minutes = DEFAULT_AUTO_LOCK_MINUTES) {
- autoLockMinutes = minutes
+export function startAutoLock() {
resetAutoLock()
// Listen for user activity to reset the timer
- const events = ['mousedown', 'keydown', 'scroll', 'touchstart']
- const handler = () => resetAutoLock()
- events.forEach(evt => window.addEventListener(evt, handler, { passive: true }))
+ if (!activityHandler) {
+ activityEvents = ['mousedown', 'keydown', 'scroll', 'touchstart']
+ activityHandler = () => resetAutoLock()
+ activityEvents.forEach(evt => window.addEventListener(evt, activityHandler, { passive: true }))
+ }
// Listen for visibility change (user switches tabs)
document.addEventListener('visibilitychange', handleVisibilityChange)
@@ -29,29 +31,26 @@ export function startAutoLock(minutes = DEFAULT_AUTO_LOCK_MINUTES) {
window.addEventListener('beforeunload', clearKeyOnExit)
// Store cleanup function
- window.__vaultCleanup = () => {
- events.forEach(evt => window.removeEventListener(evt, handler))
- document.removeEventListener('visibilitychange', handleVisibilityChange)
- window.removeEventListener('beforeunload', clearKeyOnExit)
- if (autoLockTimer) clearTimeout(autoLockTimer)
- }
+ window.__vaultCleanup = stopAutoLock
}
/**
- * Reset the auto-lock timer.
+ * Reset the auto-lock timer using the current settings value.
*/
function resetAutoLock() {
if (autoLockTimer) clearTimeout(autoLockTimer)
+ const minutes = settings.autoLockMinutes ?? 5
autoLockTimer = setTimeout(() => {
app.lockVault()
- }, autoLockMinutes * 60 * 1000)
+ }, minutes * 60 * 1000)
}
/**
- * Handle visibility change â lock when user switches away from tab.
+ * Handle visibility change â lock when user switches away from tab
+ * (only if lockOnTabSwitch is enabled).
*/
function handleVisibilityChange() {
- if (document.hidden && app.isUnlocked) {
+ if (document.hidden && app.isUnlocked && settings.lockOnTabSwitch) {
app.lockVault()
}
}
@@ -64,13 +63,22 @@ function clearKeyOnExit() {
}
/**
- * Stop auto-lock (e.g., when manually locking).
+ * Stop auto-lock and remove all listeners.
*/
export function stopAutoLock() {
if (autoLockTimer) {
clearTimeout(autoLockTimer)
autoLockTimer = null
}
+
+ if (activityHandler && activityEvents) {
+ activityEvents.forEach(evt => window.removeEventListener(evt, activityHandler))
+ activityHandler = null
+ activityEvents = null
+ }
+
+ document.removeEventListener('visibilitychange', handleVisibilityChange)
+ window.removeEventListener('beforeunload', clearKeyOnExit)
}
/**
diff --git a/src/lib/stores/settings.svelte.js b/src/lib/stores/settings.svelte.js
new file mode 100644
index 0000000..5ee613c
--- /dev/null
+++ b/src/lib/stores/settings.svelte.js
@@ -0,0 +1,35 @@
+/**
+ * Reactive settings store for vault security preferences.
+ *
+ * Settings are persisted in IndexedDB (meta store) so they survive
+ * page reloads. Defaults are used until the user explicitly saves.
+ */
+
+import { getSetting, saveSetting } from '../storage/db.js'
+
+export class SettingsStore {
+ autoLockMinutes = $state(5)
+ lockOnTabSwitch = $state(true)
+
+ /**
+ * Load persisted settings from IndexedDB.
+ * Falls back to defaults for any missing keys.
+ */
+ async load() {
+ const minutes = await getSetting('autoLockMinutes')
+ const tabSwitch = await getSetting('lockOnTabSwitch')
+
+ this.autoLockMinutes = minutes != null ? Number(minutes) : 5
+ this.lockOnTabSwitch = tabSwitch != null ? Boolean(tabSwitch) : true
+ }
+
+ /**
+ * Persist current settings to IndexedDB.
+ */
+ async save() {
+ await saveSetting('autoLockMinutes', this.autoLockMinutes)
+ await saveSetting('lockOnTabSwitch', this.lockOnTabSwitch)
+ }
+}
+
+export const settings = new SettingsStore()