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 { 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()
|
||||
}
|
||||
|
||||
@ -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 @@
|
||||
<h1>Entry Details</h1>
|
||||
{:else if viewMode === 'form'}
|
||||
<h1>{selectedEntryId ? 'Edit Entry' : 'New Entry'}</h1>
|
||||
{:else if viewMode === 'settings'}
|
||||
<h1>Settings</h1>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="top-bar-actions">
|
||||
@ -106,6 +114,7 @@
|
||||
<button class="btn btn-primary btn-sm" onclick={() => goForm(null)}>+ New Entry</button>
|
||||
{/if}
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@ -126,6 +135,8 @@
|
||||
onSave={goList}
|
||||
onCancel={handleBack}
|
||||
/>
|
||||
{:else if viewMode === 'settings'}
|
||||
<SettingsDialog onBack={goList} />
|
||||
{/if}
|
||||
</div>
|
||||
</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
|
||||
}
|
||||
|
||||
// ========================
|
||||
// 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
|
||||
// ========================
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
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