Add lock settings.
This commit is contained in:
parent
9d9e599c09
commit
5a240b081d
948
dist/index.html
vendored
948
dist/index.html
vendored
File diff suppressed because it is too large
Load Diff
@ -3,6 +3,7 @@
|
|||||||
import { deriveKey, createTestPayload, verifyPassword } from '../lib/crypto/crypto.js'
|
import { deriveKey, createTestPayload, verifyPassword } from '../lib/crypto/crypto.js'
|
||||||
import { saveVaultMeta, loadVaultMeta, isVaultInitialized, ensureTrashGroup } from '../lib/storage/db.js'
|
import { saveVaultMeta, loadVaultMeta, isVaultInitialized, ensureTrashGroup } from '../lib/storage/db.js'
|
||||||
import { startAutoLock } from '../lib/stores/security.svelte.js'
|
import { startAutoLock } from '../lib/stores/security.svelte.js'
|
||||||
|
import { settings } from '../lib/stores/settings.svelte.js'
|
||||||
import { autofocus } from '../lib/autofocus.js'
|
import { autofocus } from '../lib/autofocus.js'
|
||||||
|
|
||||||
let masterPassword = $state('')
|
let masterPassword = $state('')
|
||||||
@ -42,6 +43,7 @@
|
|||||||
|
|
||||||
await saveVaultMeta(salt, testEncrypted, testPlaintext)
|
await saveVaultMeta(salt, testEncrypted, testPlaintext)
|
||||||
await ensureTrashGroup()
|
await ensureTrashGroup()
|
||||||
|
await settings.load()
|
||||||
app.isUnlocked = true
|
app.isUnlocked = true
|
||||||
startAutoLock()
|
startAutoLock()
|
||||||
} else {
|
} else {
|
||||||
@ -64,6 +66,7 @@
|
|||||||
|
|
||||||
app.salt = meta.salt
|
app.salt = meta.salt
|
||||||
app.encryptionKey = key
|
app.encryptionKey = key
|
||||||
|
await settings.load()
|
||||||
app.isUnlocked = true
|
app.isUnlocked = true
|
||||||
startAutoLock()
|
startAutoLock()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,9 +8,10 @@
|
|||||||
import EntryDetail from './EntryDetail.svelte'
|
import EntryDetail from './EntryDetail.svelte'
|
||||||
import EntryForm from './EntryForm.svelte'
|
import EntryForm from './EntryForm.svelte'
|
||||||
import ImportExport from './ImportExport.svelte'
|
import ImportExport from './ImportExport.svelte'
|
||||||
|
import SettingsDialog from './SettingsDialog.svelte'
|
||||||
|
|
||||||
let sidebarOpen = $state(false)
|
let sidebarOpen = $state(false)
|
||||||
let viewMode = $state('list') // 'list' | 'detail' | 'form'
|
let viewMode = $state('list') // 'list' | 'detail' | 'form' | 'settings'
|
||||||
let selectedEntryId = $state(null)
|
let selectedEntryId = $state(null)
|
||||||
let showEmptyTrashConfirm = $state(false)
|
let showEmptyTrashConfirm = $state(false)
|
||||||
let emptyingTrash = $state(false)
|
let emptyingTrash = $state(false)
|
||||||
@ -35,9 +36,14 @@
|
|||||||
sidebarOpen = false
|
sidebarOpen = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function goSettings() {
|
||||||
|
viewMode = 'settings'
|
||||||
|
sidebarOpen = false
|
||||||
|
}
|
||||||
|
|
||||||
function handleBack() {
|
function handleBack() {
|
||||||
if (viewMode === 'form') {
|
if (viewMode === 'form' || viewMode === 'settings') {
|
||||||
goDetail(selectedEntryId)
|
goList()
|
||||||
} else {
|
} else {
|
||||||
goList()
|
goList()
|
||||||
}
|
}
|
||||||
@ -94,6 +100,8 @@
|
|||||||
<h1>Entry Details</h1>
|
<h1>Entry Details</h1>
|
||||||
{:else if viewMode === 'form'}
|
{:else if viewMode === 'form'}
|
||||||
<h1>{selectedEntryId ? 'Edit Entry' : 'New Entry'}</h1>
|
<h1>{selectedEntryId ? 'Edit Entry' : 'New Entry'}</h1>
|
||||||
|
{:else if viewMode === 'settings'}
|
||||||
|
<h1>Settings</h1>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="top-bar-actions">
|
<div class="top-bar-actions">
|
||||||
@ -106,6 +114,7 @@
|
|||||||
<button class="btn btn-primary btn-sm" onclick={() => goForm(null)}>+ New Entry</button>
|
<button class="btn btn-primary btn-sm" onclick={() => goForm(null)}>+ New Entry</button>
|
||||||
{/if}
|
{/if}
|
||||||
<ImportExport />
|
<ImportExport />
|
||||||
|
<button class="btn btn-ghost btn-sm" onclick={goSettings} title="Settings">⚙️</button>
|
||||||
<button class="btn btn-ghost btn-sm" onclick={handleLock} title="Lock vault">🔒</button>
|
<button class="btn btn-ghost btn-sm" onclick={handleLock} title="Lock vault">🔒</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -126,6 +135,8 @@
|
|||||||
onSave={goList}
|
onSave={goList}
|
||||||
onCancel={handleBack}
|
onCancel={handleBack}
|
||||||
/>
|
/>
|
||||||
|
{:else if viewMode === 'settings'}
|
||||||
|
<SettingsDialog onBack={goList} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
149
src/components/SettingsDialog.svelte
Normal file
149
src/components/SettingsDialog.svelte
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
<script>
|
||||||
|
import { settings } from '../lib/stores/settings.svelte.js'
|
||||||
|
import { startAutoLock } from '../lib/stores/security.svelte.js'
|
||||||
|
|
||||||
|
let { onBack } = $props()
|
||||||
|
|
||||||
|
// Local copies so the user can cancel without losing values
|
||||||
|
let minutes = $state(settings.autoLockMinutes)
|
||||||
|
let lockOnTabSwitch = $state(settings.lockOnTabSwitch)
|
||||||
|
let saving = $state(false)
|
||||||
|
|
||||||
|
const minuteOptions = [1, 5, 10, 15, 30, 60]
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
saving = true
|
||||||
|
try {
|
||||||
|
settings.autoLockMinutes = minutes
|
||||||
|
settings.lockOnTabSwitch = lockOnTabSwitch
|
||||||
|
await settings.save()
|
||||||
|
startAutoLock()
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to save settings:', e)
|
||||||
|
}
|
||||||
|
saving = false
|
||||||
|
onBack()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync local values on mount
|
||||||
|
$effect(() => {
|
||||||
|
minutes = settings.autoLockMinutes
|
||||||
|
lockOnTabSwitch = settings.lockOnTabSwitch
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="settings-panel">
|
||||||
|
<form class="form-card" onsubmit={(e) => { e.preventDefault(); handleSave(); }}>
|
||||||
|
<h3>Settings</h3>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="auto-lock-minutes">Auto-lock after</label>
|
||||||
|
<select id="auto-lock-minutes" bind:value={minutes}>
|
||||||
|
{#each minuteOptions as m}
|
||||||
|
<option value={m}>{m} {m === 1 ? 'minute' : 'minutes'}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
<p class="text-muted text-xs mt-1">
|
||||||
|
Vault locks after {minutes} {minutes === 1 ? 'minute' : 'minutes'} of inactivity.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="toggle-label" for="lock-tab-switch">
|
||||||
|
<input
|
||||||
|
id="lock-tab-switch"
|
||||||
|
type="checkbox"
|
||||||
|
bind:checked={lockOnTabSwitch}
|
||||||
|
/>
|
||||||
|
<span class="toggle-track">
|
||||||
|
<span class="toggle-thumb"></span>
|
||||||
|
</span>
|
||||||
|
<span class="toggle-text">Lock when tab loses focus</span>
|
||||||
|
</label>
|
||||||
|
<p class="text-muted text-xs mt-1">
|
||||||
|
{lockOnTabSwitch
|
||||||
|
? 'The vault locks immediately when you switch to another tab.'
|
||||||
|
: 'The vault stays unlocked even when you switch tabs.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn-primary" disabled={saving}>
|
||||||
|
{saving ? 'Saving...' : 'Save'}
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-ghost" onclick={onBack}>Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.settings-panel {
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-card {
|
||||||
|
background: var(--color-surface);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-card h3 {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-label input[type="checkbox"] {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-track {
|
||||||
|
width: 40px;
|
||||||
|
height: 22px;
|
||||||
|
background: var(--color-border);
|
||||||
|
border-radius: 11px;
|
||||||
|
position: relative;
|
||||||
|
transition: background-color 150ms;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-track .toggle-thumb {
|
||||||
|
position: absolute;
|
||||||
|
top: 3px;
|
||||||
|
left: 3px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: transform 150ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-label input:checked + .toggle-track {
|
||||||
|
background: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-label input:checked + .toggle-track .toggle-thumb {
|
||||||
|
transform: translateX(18px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-text {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -117,6 +117,31 @@ export async function isVaultInitialized() {
|
|||||||
return meta.salt !== null
|
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
|
// Groups
|
||||||
// ========================
|
// ========================
|
||||||
|
|||||||
@ -3,24 +3,26 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { app } from './app.svelte.js'
|
import { app } from './app.svelte.js'
|
||||||
|
import { settings } from './settings.svelte.js'
|
||||||
const DEFAULT_AUTO_LOCK_MINUTES = 5
|
|
||||||
|
|
||||||
let autoLockTimer = null
|
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.
|
* Start (or restart) the auto-lock timer using current settings.
|
||||||
* @param {number} minutes - Auto-lock after N minutes of inactivity
|
|
||||||
*/
|
*/
|
||||||
export function startAutoLock(minutes = DEFAULT_AUTO_LOCK_MINUTES) {
|
export function startAutoLock() {
|
||||||
autoLockMinutes = minutes
|
|
||||||
resetAutoLock()
|
resetAutoLock()
|
||||||
|
|
||||||
// Listen for user activity to reset the timer
|
// Listen for user activity to reset the timer
|
||||||
const events = ['mousedown', 'keydown', 'scroll', 'touchstart']
|
if (!activityHandler) {
|
||||||
const handler = () => resetAutoLock()
|
activityEvents = ['mousedown', 'keydown', 'scroll', 'touchstart']
|
||||||
events.forEach(evt => window.addEventListener(evt, handler, { passive: true }))
|
activityHandler = () => resetAutoLock()
|
||||||
|
activityEvents.forEach(evt => window.addEventListener(evt, activityHandler, { passive: true }))
|
||||||
|
}
|
||||||
|
|
||||||
// Listen for visibility change (user switches tabs)
|
// Listen for visibility change (user switches tabs)
|
||||||
document.addEventListener('visibilitychange', handleVisibilityChange)
|
document.addEventListener('visibilitychange', handleVisibilityChange)
|
||||||
@ -29,29 +31,26 @@ export function startAutoLock(minutes = DEFAULT_AUTO_LOCK_MINUTES) {
|
|||||||
window.addEventListener('beforeunload', clearKeyOnExit)
|
window.addEventListener('beforeunload', clearKeyOnExit)
|
||||||
|
|
||||||
// Store cleanup function
|
// Store cleanup function
|
||||||
window.__vaultCleanup = () => {
|
window.__vaultCleanup = stopAutoLock
|
||||||
events.forEach(evt => window.removeEventListener(evt, handler))
|
|
||||||
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
|
||||||
window.removeEventListener('beforeunload', clearKeyOnExit)
|
|
||||||
if (autoLockTimer) clearTimeout(autoLockTimer)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset the auto-lock timer.
|
* Reset the auto-lock timer using the current settings value.
|
||||||
*/
|
*/
|
||||||
function resetAutoLock() {
|
function resetAutoLock() {
|
||||||
if (autoLockTimer) clearTimeout(autoLockTimer)
|
if (autoLockTimer) clearTimeout(autoLockTimer)
|
||||||
|
const minutes = settings.autoLockMinutes ?? 5
|
||||||
autoLockTimer = setTimeout(() => {
|
autoLockTimer = setTimeout(() => {
|
||||||
app.lockVault()
|
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() {
|
function handleVisibilityChange() {
|
||||||
if (document.hidden && app.isUnlocked) {
|
if (document.hidden && app.isUnlocked && settings.lockOnTabSwitch) {
|
||||||
app.lockVault()
|
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() {
|
export function stopAutoLock() {
|
||||||
if (autoLockTimer) {
|
if (autoLockTimer) {
|
||||||
clearTimeout(autoLockTimer)
|
clearTimeout(autoLockTimer)
|
||||||
autoLockTimer = null
|
autoLockTimer = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (activityHandler && activityEvents) {
|
||||||
|
activityEvents.forEach(evt => window.removeEventListener(evt, activityHandler))
|
||||||
|
activityHandler = null
|
||||||
|
activityEvents = null
|
||||||
|
}
|
||||||
|
|
||||||
|
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
||||||
|
window.removeEventListener('beforeunload', clearKeyOnExit)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
35
src/lib/stores/settings.svelte.js
Normal file
35
src/lib/stores/settings.svelte.js
Normal file
@ -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()
|
||||||
Loading…
x
Reference in New Issue
Block a user