Compare commits

..

7 commits
2.3.0 ... main

Author SHA1 Message Date
SpreeHertz
2097ca42b2
Merge pull request #9 from uthvah/uthvah-patch-3
Visual changes
2026-04-05 10:30:28 +05:30
SpreeHertz
ab583f2ac3 update .gitignore 2026-04-05 10:27:03 +05:30
Verity
99ac23834a visual changes
Widget + tiling system, analogue clock, translucent navbar, atmosphere removal
2026-04-04 20:55:18 +01:00
Verity
9ca99c8478
Custom navbar (#8)
* Enhance description in manifest.json

Updated the description to include AES-GCM encryption.

* Custom navbar

- background colour matching
- custom window action buttons
- obsidian frame detection (disabled on native)
2026-04-04 20:48:57 +01:00
Verity
d221521d8e desc change 2026-04-02 12:56:46 +01:00
Verity
39870e9770 fresh install fix 2 2026-04-02 11:56:21 +01:00
Verity
60d9a5479d critical fix
fix fresh install and tamper specific styling
2026-04-02 11:42:57 +01:00
5 changed files with 786 additions and 166 deletions

3
.gitignore vendored
View file

@ -16,5 +16,8 @@ data.json
# Exclude macOS Finder (System Explorer) View States
.DS_Store
# VaultGuard
data.json
styles2.css
locksidian-guard.json

512
index.js
View file

@ -18,7 +18,7 @@ const SALT_LENGTH = 16;
const MAGIC_HEADER = "VAULTGUARD_ENCRYPTED_V1::";
const NOTE_PROCESSING_BATCH_SIZE = 20;
const PBKDF2_ITERATIONS = 250000;
const LS_GUARD_KEY = 'locksidian_v1_guard';
const LS_GUARD_KEY_PREFIX = 'locksidian_v1_guard';
const DEFAULT_SETTINGS = {
autoLockTime: 15,
@ -37,7 +37,13 @@ const DEFAULT_SETTINGS = {
fontWeight: '500',
fontStyle: 'normal',
clockFormat: '24h',
atmosphere: 'minimal',
clockStyle: 'digital',
overlayOpacity: 30,
widgets: {
showDate: true,
showUsername: true,
clock: true,
},
inputRadius: 100,
shakeIntensity: 6,
}
@ -195,6 +201,173 @@ class ProgressModal extends obsidian.Modal {
}
}
// ============================================================================
// ANALOGUE CLOCK HELPERS
// ============================================================================
function buildAnalogueClockSVG() {
const ns = 'http://www.w3.org/2000/svg';
const svg = document.createElementNS(ns, 'svg');
svg.setAttribute('viewBox', '0 0 200 200');
svg.setAttribute('class', 'vg-analogue-clock');
svg.setAttribute('aria-hidden', 'true');
// Outer ring
const ring = document.createElementNS(ns, 'circle');
ring.setAttribute('cx', '100'); ring.setAttribute('cy', '100');
ring.setAttribute('r', '88');
ring.setAttribute('fill', 'none');
ring.setAttribute('stroke', 'rgba(255,255,255,0.30)');
ring.setAttribute('stroke-width', '1.5');
svg.appendChild(ring);
// Hour tick marks — 12 ticks, 4 quarter ticks longer/bolder
for (let i = 0; i < 12; i++) {
const isQ = i % 3 === 0;
const angle = (i * 30) * Math.PI / 180;
const inner = isQ ? 75 : 80;
const tick = document.createElementNS(ns, 'line');
tick.setAttribute('x1', (100 + Math.sin(angle) * inner).toFixed(3));
tick.setAttribute('y1', (100 - Math.cos(angle) * inner).toFixed(3));
tick.setAttribute('x2', (100 + Math.sin(angle) * 86).toFixed(3));
tick.setAttribute('y2', (100 - Math.cos(angle) * 86).toFixed(3));
tick.setAttribute('stroke', isQ ? 'rgba(255,255,255,0.70)' : 'rgba(255,255,255,0.40)');
tick.setAttribute('stroke-width', isQ ? '2.5' : '1.5');
tick.setAttribute('stroke-linecap', 'round');
svg.appendChild(tick);
}
// Hour hand — short and thick
const hourHand = document.createElementNS(ns, 'line');
hourHand.setAttribute('x1', '100'); hourHand.setAttribute('y1', '108');
hourHand.setAttribute('x2', '100'); hourHand.setAttribute('y2', '52');
hourHand.setAttribute('stroke', 'rgba(255,255,255,0.92)');
hourHand.setAttribute('stroke-width', '4.5');
hourHand.setAttribute('stroke-linecap', 'round');
hourHand.setAttribute('class', 'vg-clock-hour-hand');
svg.appendChild(hourHand);
// Minute hand — long and slender
const minuteHand = document.createElementNS(ns, 'line');
minuteHand.setAttribute('x1', '100'); minuteHand.setAttribute('y1', '105');
minuteHand.setAttribute('x2', '100'); minuteHand.setAttribute('y2', '26');
minuteHand.setAttribute('stroke', 'rgba(255,255,255,0.78)');
minuteHand.setAttribute('stroke-width', '2.5');
minuteHand.setAttribute('stroke-linecap', 'round');
minuteHand.setAttribute('class', 'vg-clock-minute-hand');
svg.appendChild(minuteHand);
// Second hand — hair-thin with counterweight tail
const secondHand = document.createElementNS(ns, 'line');
secondHand.setAttribute('x1', '100'); secondHand.setAttribute('y1', '118');
secondHand.setAttribute('x2', '100'); secondHand.setAttribute('y2', '22');
secondHand.setAttribute('stroke', 'rgba(255,255,255,0.58)');
secondHand.setAttribute('stroke-width', '1.5');
secondHand.setAttribute('stroke-linecap', 'round');
secondHand.setAttribute('class', 'vg-clock-second-hand');
svg.appendChild(secondHand);
// Center cap
const cap = document.createElementNS(ns, 'circle');
cap.setAttribute('cx', '100'); cap.setAttribute('cy', '100');
cap.setAttribute('r', '4');
cap.setAttribute('fill', 'rgba(255,255,255,0.90)');
svg.appendChild(cap);
return svg;
}
function updateAnalogueClock(svgEl, dateEl) {
const now = new Date();
const h = now.getHours() % 12;
const m = now.getMinutes();
const s = now.getSeconds();
// Smooth continuous motion — each hand interpolates sub-unit
const hourDeg = h * 30 + m * 0.5 + s * (0.5 / 60);
const minuteDeg = m * 6 + s * 0.1;
const secondDeg = s * 6;
const hourHand = svgEl.querySelector('.vg-clock-hour-hand');
const minuteHand = svgEl.querySelector('.vg-clock-minute-hand');
const secondHand = svgEl.querySelector('.vg-clock-second-hand');
if (hourHand) hourHand.setAttribute('transform', `rotate(${hourDeg.toFixed(3)}, 100, 100)`);
if (minuteHand) minuteHand.setAttribute('transform', `rotate(${minuteDeg.toFixed(3)}, 100, 100)`);
if (secondHand) secondHand.setAttribute('transform', `rotate(${secondDeg.toFixed(3)}, 100, 100)`);
if (dateEl) {
dateEl.textContent = now.toLocaleDateString(undefined, {
weekday: 'long', month: 'long', day: 'numeric'
});
}
}
// ============================================================================
// WIDGET REGISTRY
// Each entry: { id, label, alwaysOn, isEnabled(visual)?, render(plugin) }
// - alwaysOn: never filtered by user prefs
// - isEnabled: optional extra gate (e.g. clock respects clockFormat setting)
// - render: returns an HTMLElement; may set plugin.clockInterval etc.
// To add a new widget: push a new descriptor here and add a toggle in settings.
// ============================================================================
const WIDGET_REGISTRY = [
{
id: 'clock',
label: 'Clock',
alwaysOn: false,
isEnabled: (visual) => visual.clockFormat !== 'off',
render(plugin) {
const visual = plugin.state.settings.visual;
const widgets = visual.widgets ?? {};
const el = document.createElement('div');
el.className = 'vg-time-widget';
let dateEl = null;
if (widgets.showDate !== false) {
dateEl = document.createElement('div');
dateEl.className = 'vaultguard-date';
}
if (visual.clockStyle === 'analogue') {
const svg = buildAnalogueClockSVG();
el.appendChild(svg);
if (dateEl) el.appendChild(dateEl);
updateAnalogueClock(svg, dateEl);
plugin.clockInterval = setInterval(() => updateAnalogueClock(svg, dateEl), 1000);
} else {
const clockEl = document.createElement('div');
clockEl.className = 'vaultguard-clock';
el.appendChild(clockEl);
if (dateEl) el.appendChild(dateEl);
plugin.updateClock(clockEl, dateEl);
plugin.clockInterval = setInterval(() => plugin.updateClock(clockEl, dateEl), 1000);
}
return el;
}
},
{
id: 'login',
label: 'Login',
alwaysOn: true,
render(plugin) {
const visual = plugin.state.settings.visual;
const widgets = visual.widgets ?? {};
const card = document.createElement('div');
card.className = 'vg-login-card';
if (widgets.showUsername !== false) {
const username = document.createElement('div');
username.className = 'vaultguard-username';
username.textContent = visual.username;
card.appendChild(username);
}
card.appendChild(plugin.createPasswordInputElement());
return card;
}
}
];
// ============================================================================
// MAIN PLUGIN CLASS
// ============================================================================
@ -290,13 +463,18 @@ class VaultGuardPlugin extends obsidian.Plugin {
const hasLS = !!(lsGuard?.salt);
if (!hasCredentials && !guardExists && !hasLS) {
// All three stores empty — genuine fresh install, no password ever set
// All three stores empty — genuine fresh install or new vault.
// Because _lsKey is scoped to this vault's path, a stale entry from
// any other vault will never appear here, so this check is trustworthy.
this.state.phase = 'SETUP';
} else if (!hasCredentials) {
// data.json missing but at least one external guard survives — tamper
// At least one external guard survives without credentials — suspicious.
// (guardExists=true means the sentinel file is present despite data.json
// being gone; hasLS=true means this vault's LS record exists without
// matching credentials — either way something is wrong.)
this.state.phase = 'TAMPERED';
console.warn('VaultGuard: credentials missing while guards remain — tamper detected');
console.warn('VaultGuard: guard(s) present but credentials missing — tamper detected');
} else {
// Credentials present — vault has been set up at some point
@ -314,10 +492,11 @@ class VaultGuardPlugin extends obsidian.Plugin {
// data.json salt differs from trusted LS record — possible swap attack
this.state.phase = 'TAMPERED';
console.warn('VaultGuard: salt mismatch between data.json and localStorage — tamper suspected');
} else if (!guardExists) {
// Sentinel deleted — tamper or first run after sentinel feature was introduced
} else if (!guardExists && hasLS) {
// Sentinel missing but LS proves this device previously completed a successful
// unlock — the sentinel should still be present. Suspicious.
this.state.phase = 'TAMPERED';
console.warn('VaultGuard: sentinel missing — possible tamper detected');
console.warn('VaultGuard: sentinel missing on a previously-unlocked device — tamper suspected');
} else {
this.state.phase = 'LOCKED';
}
@ -328,10 +507,33 @@ class VaultGuardPlugin extends obsidian.Plugin {
// SENTINEL GUARD
// ========================================================================
/**
* A localStorage key scoped to this specific vault path.
* Using a global key meant any vault on the same machine could pollute
* another vault's guard record, causing false tamper positives on fresh
* installs. The vault adapter's basePath (desktop) or vault name (mobile)
* gives us a stable, vault-unique identifier.
*/
get _lsKey() {
const adapter = this.app.vault.adapter;
const id = (adapter.basePath ?? this.app.vault.getName())
.replace(/[^a-zA-Z0-9_-]/g, '_')
.slice(-60);
return `${LS_GUARD_KEY_PREFIX}_${id}`;
}
_lsClear() {
try {
window.localStorage.removeItem(this._lsKey);
} catch (e) {
console.warn('VaultGuard: localStorage clear failed', e);
}
}
_lsWrite(salt) {
try {
window.localStorage.setItem(
LS_GUARD_KEY,
this._lsKey,
JSON.stringify({ v: 1, salt })
);
} catch (e) {
@ -341,7 +543,7 @@ class VaultGuardPlugin extends obsidian.Plugin {
_lsRead() {
try {
const raw = window.localStorage.getItem(LS_GUARD_KEY);
const raw = window.localStorage.getItem(this._lsKey);
return raw ? JSON.parse(raw) : null;
} catch {
return null;
@ -750,6 +952,93 @@ class VaultGuardPlugin extends obsidian.Plugin {
await this.app.vault.modify(file, decoded);
}
// ========================================================================
// WINDOW FRAME DETECTION & CONTROLS
// ========================================================================
getFrameStyle() {
// Detected from body classes set by Obsidian at startup — authoritative,
// unlike app.vault.config which does not expose this in the plugin API.
// is-frameless + is-hidden-frameless → 'hidden' (default)
// is-frameless only → 'obsidian' (Obsidian custom bar)
// neither → 'native' (OS titlebar)
const body = document.body;
if (!body.classList.contains('is-frameless')) return 'native';
if (body.classList.contains('is-hidden-frameless')) return 'hidden';
return 'obsidian';
}
_platform() {
try { return process.platform; } catch { return 'unknown'; }
}
// Show a themed navbar for hidden and obsidian styles — native lets the OS handle it.
get needsNavbar() {
return this.getFrameStyle() !== 'native';
}
// On non-macOS with hidden or obsidian frame, our navbar covers Electron's own
// window controls (web content) — render replacements.
// macOS: traffic lights are native OS overlays above all web content, always visible.
get needsWindowControls() {
return this.needsNavbar && this._platform() !== 'darwin';
}
createWindowControls() {
const bar = document.createElement('div');
bar.className = 'vg-window-controls';
const buttons = [
{
cls: 'vg-wc-min',
label: 'Minimize',
svg: '<svg viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"><line x1="1" y1="5" x2="9" y2="5"/></svg>',
action: 'minimize',
},
{
cls: 'vg-wc-max',
label: 'Maximize',
svg: '<svg viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"><rect x="1" y="1" width="8" height="8"/></svg>',
action: 'maximize',
},
{
cls: 'vg-wc-close',
label: 'Close',
svg: '<svg viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"><line x1="1" y1="1" x2="9" y2="9"/><line x1="9" y1="1" x2="1" y2="9"/></svg>',
action: 'close',
},
];
for (const { cls, label, svg, action } of buttons) {
const btn = bar.createEl('button', {
cls: `vg-wc-btn ${cls}`,
attr: { 'aria-label': label },
});
btn.innerHTML = svg;
btn.addEventListener('click', () => this._winAction(action));
}
return bar;
}
_winAction(action) {
// Path 1: Electron remote module (Obsidian ≤ 0.x / some 1.x builds)
try {
const electron = window.require('electron');
const win = electron.remote?.getCurrentWindow?.();
if (win) {
if (action === 'minimize') { win.minimize(); return; }
if (action === 'maximize') { win.isMaximized() ? win.unmaximize() : win.maximize(); return; }
if (action === 'close') { win.close(); return; }
}
} catch {}
// Path 2: Obsidian command fallback (close only)
if (action === 'close') {
try { this.app.commands.executeCommandById('app:quit'); } catch {}
}
}
// ========================================================================
// LOCK SCREEN UI
// ========================================================================
@ -760,31 +1049,27 @@ class VaultGuardPlugin extends obsidian.Plugin {
return;
}
// Create a minimal lock screen ASAP to hide vault content
// Create a minimal lock screen ASAP to hide vault content.
// Keep it as a plain dark screen — the full UI (including any tamper
// warning) is rendered in enhanceLockScreen() once the workspace is ready.
this.lockScreenEl = document.createElement('div');
this.lockScreenEl.className = 'vaultguard-screen vaultguard-screen-loading';
this.lockScreenEl.style.opacity = '1';
this.lockScreenEl.setAttribute('data-atmosphere', this.state.settings.visual.atmosphere || 'minimal');
// Simple background while loading
this.lockScreenEl.style.backgroundColor = 'var(--background-primary)';
if (this.state.phase === 'TAMPERED') {
const warning = document.createElement('div');
warning.className = 'vaultguard-tamper-warning';
warning.appendChild(this._createWarningIcon());
warning.appendChild(document.createTextNode('Vault data modified. Unlock to verify integrity.'));
this.lockScreenEl.appendChild(warning);
}
document.body.appendChild(this.lockScreenEl);
}
_buildLockScreenUI(el) {
el.createDiv({ cls: 'vaultguard-titlebar' });
// Background and overlay are position:absolute on the screen root so they
// extend behind the navbar as well as the main content area.
el.appendChild(this.createBackgroundElement());
el.appendChild(this.createOverlayElement());
if (this.needsNavbar) {
const navbar = el.createDiv({ cls: 'vaultguard-navbar' });
if (this.needsWindowControls) navbar.appendChild(this.createWindowControls());
}
const mainContentEl = el.createDiv({ cls: 'vaultguard-main-content' });
mainContentEl.appendChild(this.createBackgroundElement());
mainContentEl.appendChild(this.createOverlayElement());
mainContentEl.appendChild(this.createContentElement());
setTimeout(() => {
const input = this.lockScreenEl?.querySelector('.vaultguard-input');
@ -801,6 +1086,42 @@ class VaultGuardPlugin extends obsidian.Plugin {
document.addEventListener('focusin', this._focusTrapListener, true);
}
_buildTamperedUI(el) {
if (this.needsNavbar) {
const navbar = el.createDiv({ cls: 'vaultguard-navbar' });
if (this.needsWindowControls) navbar.appendChild(this.createWindowControls());
}
const screen = el.createDiv({ cls: 'vaultguard-tamper-screen' });
const panel = screen.createDiv({ cls: 'vaultguard-tamper-panel' });
// ── Header row: icon + heading ────────────────────────────────────────
const header = panel.createDiv({ cls: 'vaultguard-tamper-panel-header' });
header.appendChild(this._createWarningIcon());
header.createEl('span', { cls: 'vaultguard-tamper-panel-title', text: 'Integrity Warning' });
// ── Body: explanation ─────────────────────────────────────────────────
panel.createEl('p', {
cls: 'vaultguard-tamper-panel-message',
text: 'Plugin data was modified or could not be verified. ' +
'Enter your master password to confirm your identity and restore access.'
});
// ── Password input ────────────────────────────────────────────────────
panel.appendChild(this.createPasswordInputElement());
// Focus the input once the panel is in the DOM
setTimeout(() => panel.querySelector('.vaultguard-input')?.focus(), 100);
// Focus trap — keep keyboard inside the lock screen
this._focusTrapListener = (ev) => {
if (this.lockScreenEl && !this.lockScreenEl.contains(ev.target)) {
this.lockScreenEl.querySelector('.vaultguard-input')?.focus();
}
};
document.addEventListener('focusin', this._focusTrapListener, true);
}
enhanceLockScreen() {
if (!this.lockScreenEl) {
return;
@ -808,8 +1129,15 @@ class VaultGuardPlugin extends obsidian.Plugin {
this.lockScreenEl.classList.remove('vaultguard-screen-loading');
this.lockScreenEl.style.backgroundColor = '';
this.lockScreenEl.innerHTML = '';
this.lockScreenEl.setAttribute('data-atmosphere', this.state.settings.visual.atmosphere || 'minimal');
this._buildLockScreenUI(this.lockScreenEl);
if (this.state.phase === 'TAMPERED') {
// Do not apply user visual preferences — data.json may be absent/compromised.
// Render a stripped-down panel: warning + password input only.
this._buildTamperedUI(this.lockScreenEl);
} else {
this.lockScreenEl.style.setProperty('--vg-overlay-opacity', ((this.state.settings.visual.overlayOpacity ?? 30) / 100).toString());
this._buildLockScreenUI(this.lockScreenEl);
}
}
showLockScreen() {
@ -819,9 +1147,14 @@ class VaultGuardPlugin extends obsidian.Plugin {
}
this.lockScreenEl = document.createElement('div');
this.lockScreenEl.className = 'vaultguard-screen';
this.lockScreenEl.setAttribute('data-atmosphere', this.state.settings.visual.atmosphere || 'minimal');
document.body.appendChild(this.lockScreenEl);
this._buildLockScreenUI(this.lockScreenEl);
if (this.state.phase === 'TAMPERED') {
this._buildTamperedUI(this.lockScreenEl);
} else {
this.lockScreenEl.style.setProperty('--vg-overlay-opacity', ((this.state.settings.visual.overlayOpacity ?? 30) / 100).toString());
this._buildLockScreenUI(this.lockScreenEl);
}
}
removeLockScreen() {
@ -856,15 +1189,10 @@ class VaultGuardPlugin extends obsidian.Plugin {
const content = document.createElement('div');
content.className = 'vaultguard-content';
if (this.state.phase === 'TAMPERED') {
const warning = content.createEl('div', { cls: 'vaultguard-tamper-warning' });
warning.appendChild(this._createWarningIcon());
warning.appendChild(document.createTextNode('Vault data modified. Unlock to verify integrity.'));
}
const visual = this.state.settings.visual;
const widgetPrefs = visual.widgets ?? {};
const visual = this.state.settings.visual;
// Apply CSS custom properties
// CSS custom properties
content.style.setProperty('--vaultguard-ui-scale', (visual.uiScale / 100).toString());
content.style.setProperty('--vaultguard-input-width', `${visual.inputWidth}px`);
content.style.setProperty('--vaultguard-input-radius', String(visual.inputRadius ?? 100));
@ -875,19 +1203,44 @@ class VaultGuardPlugin extends obsidian.Plugin {
content.style.setProperty('--vaultguard-font-weight', visual.fontWeight);
content.style.setProperty('--vaultguard-font-style', visual.fontStyle);
// Clock
const clockEl = content.createEl('div', { cls: 'vaultguard-clock' });
const dateEl = content.createEl('div', { cls: 'vaultguard-date' });
this.updateClock(clockEl, dateEl);
this.clockInterval = setInterval(() => this.updateClock(clockEl, dateEl), 1000);
// Build enabled widget list from registry
const enabled = WIDGET_REGISTRY.filter(w =>
w.alwaysOn ||
(widgetPrefs[w.id] !== false && (!w.isEnabled || w.isEnabled(visual)))
);
// Username display
const username = content.createEl('div', { cls: 'vaultguard-username' });
username.textContent = visual.username;
// Tiling grid — master-stack layout
const grid = document.createElement('div');
grid.className = 'vg-tile-grid';
// Password input
content.appendChild(this.createPasswordInputElement());
if (enabled.length <= 1) {
// Single widget (clock off): full-width fallback
grid.classList.add('vg-tile-single');
const item = document.createElement('div');
item.className = 'vg-tile-item';
item.appendChild(enabled[0].render(this));
grid.appendChild(item);
} else {
// First widget → master pane (left); rest → stack pane (right)
const [master, ...stack] = enabled;
const masterEl = document.createElement('div');
masterEl.className = 'vg-tile-master';
masterEl.appendChild(master.render(this));
grid.appendChild(masterEl);
const stackEl = document.createElement('div');
stackEl.className = 'vg-tile-stack';
for (const widget of stack) {
const item = document.createElement('div');
item.className = 'vg-tile-item';
item.appendChild(widget.render(this));
stackEl.appendChild(item);
}
grid.appendChild(stackEl);
}
content.appendChild(grid);
return content;
}
@ -978,7 +1331,7 @@ class VaultGuardPlugin extends obsidian.Plugin {
const fmt = this.state.settings.visual.clockFormat || '24h';
if (fmt === 'off') {
clockEl.style.display = 'none';
dateEl.style.display = 'none';
if (dateEl) dateEl.style.display = 'none';
return;
}
if (fmt === '12h') {
@ -986,12 +1339,14 @@ class VaultGuardPlugin extends obsidian.Plugin {
} else {
clockEl.textContent = now.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', hour12: false });
}
dateEl.textContent = now.toLocaleDateString(undefined, { weekday: 'long', month: 'long', day: 'numeric' });
if (dateEl) dateEl.textContent = now.toLocaleDateString(undefined, { weekday: 'long', month: 'long', day: 'numeric' });
}
_createWarningIcon() {
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('width', '18');
svg.setAttribute('height', '18');
svg.setAttribute('fill', 'none');
svg.setAttribute('stroke', 'currentColor');
svg.setAttribute('stroke-width', '2');
@ -1326,16 +1681,42 @@ class VaultGuardSettingsTab extends obsidian.PluginSettingTab {
containerEl.createEl('h3', { text: 'Lockscreen' });
new obsidian.Setting(containerEl)
.setName('Atmosphere')
.setDesc('Overall visual style of the lock screen.')
.addDropdown(d => d
.addOption('minimal', 'Minimal')
.addOption('noir', 'Noir')
.addOption('frosted', 'Frosted')
.addOption('vivid', 'Vivid')
.setValue(this.plugin.state.settings.visual.atmosphere)
.setName('Show date')
.setDesc('Display the date below the clock.')
.addToggle(t => t
.setValue(this.plugin.state.settings.visual.widgets?.showDate !== false)
.onChange(async v => {
this.plugin.state.settings.visual.atmosphere = v;
this.plugin.state.settings.visual.widgets = {
...(this.plugin.state.settings.visual.widgets ?? {}),
showDate: v,
};
await this.plugin.saveSettings();
})
);
new obsidian.Setting(containerEl)
.setName('Show username')
.setDesc('Display the username inside the login card.')
.addToggle(t => t
.setValue(this.plugin.state.settings.visual.widgets?.showUsername !== false)
.onChange(async v => {
this.plugin.state.settings.visual.widgets = {
...(this.plugin.state.settings.visual.widgets ?? {}),
showUsername: v,
};
await this.plugin.saveSettings();
})
);
new obsidian.Setting(containerEl)
.setName('Background darkness')
.setDesc('How much the background is darkened behind the lockscreen (0 = none, 60 = dark).')
.addSlider(s => s
.setLimits(0, 60, 1)
.setValue(this.plugin.state.settings.visual.overlayOpacity ?? 30)
.setDynamicTooltip()
.onChange(async v => {
this.plugin.state.settings.visual.overlayOpacity = v;
await this.plugin.saveSettings();
})
);
@ -1354,6 +1735,19 @@ class VaultGuardSettingsTab extends obsidian.PluginSettingTab {
})
);
new obsidian.Setting(containerEl)
.setName('Clock style')
.setDesc('Digital shows a numeric readout. Analogue shows a minimal watch face.')
.addDropdown(d => d
.addOption('digital', 'Digital')
.addOption('analogue', 'Analogue')
.setValue(this.plugin.state.settings.visual.clockStyle ?? 'digital')
.onChange(async v => {
this.plugin.state.settings.visual.clockStyle = v;
await this.plugin.saveSettings();
})
);
new obsidian.Setting(containerEl)
.setName('Lockscreen font')
.setDesc('Applies to all text on the lock screen — clock, date, username.')
@ -1660,4 +2054,4 @@ class VaultGuardSettingsTab extends obsidian.PluginSettingTab {
}
}
export default VaultGuardPlugin;
export default VaultGuardPlugin;

File diff suppressed because one or more lines are too long

View file

@ -3,7 +3,7 @@
"name": "VaultGuard",
"version": "2.3.0",
"minAppVersion": "0.12.0",
"description": "A secure, beautiful, and minimalist lockscreen.",
"description": "Secure your vault with a beautiful lockscreen and industry-standard AES-GCM encryption.",
"author": "Uthvah",
"authorUrl": "https://github.com/uthvah",
"isDesktopOnly": false

View file

@ -20,6 +20,16 @@
display: flex;
flex-direction: column;
overflow: hidden;
/* Shared glass surface tokens
All panels, navbar, and input decorations derive from these.
Changing one variable updates every surface for instant cohesion. */
--vg-glass-bg: rgba(255, 255, 255, 0.10);
--vg-glass-blur: blur(30px) saturate(190%);
--vg-glass-border: rgba(255, 255, 255, 0.18);
--vg-glass-highlight: rgba(255, 255, 255, 0.18);
--vg-glass-shadow: 0 20px 56px rgba(0, 0, 0, 0.50);
--vg-glass-radius: 28px;
}
.vaultguard-screen-loading {
@ -33,25 +43,84 @@
}
.vaultguard-screen.is-leaving {
animation: unlockOut 0.45s cubic-bezier(0.4, 0, 1, 1) forwards;
animation: unlockOut 0.4s cubic-bezier(0.4, 0, 1, 1) forwards;
}
@keyframes unlockOut {
0% { opacity: 1; transform: scale(1); filter: blur(0px); }
100% { opacity: 0; transform: scale(1.04); filter: blur(2px); }
100% { opacity: 0; transform: scale(1.07); filter: blur(6px); }
}
/* ============================================================================
TITLEBAR (for window dragging)
NAVBAR shown for hidden and obsidian frame styles.
Glass treatment is driven by the same surface tokens as the password bar,
so both elements always look visually unified across all atmospheres.
native frame: omitted entirely (OS chrome handles it).
============================================================================ */
.vaultguard-titlebar {
.vaultguard-navbar {
flex-shrink: 0;
height: 30px;
height: 33px;
width: 100%;
-webkit-app-region: drag;
background: transparent;
background: var(--vg-glass-bg);
backdrop-filter: var(--vg-glass-blur);
-webkit-backdrop-filter: var(--vg-glass-blur);
border-bottom: 1px solid var(--vg-glass-border);
/* Top highlight + bottom fade — visually reads as thick glass */
box-shadow:
inset 0 1px 0 var(--vg-glass-highlight),
inset 0 -1px 0 rgba(0, 0, 0, 0.12);
position: relative;
z-index: 1;
display: flex;
align-items: center;
}
/* ============================================================================
WINDOW CONTROLS rendered inside .vaultguard-navbar on Windows/Linux
where Electron's own controls are web content covered by the lockscreen.
macOS: traffic lights are native OS overlays always visible, never needed here.
============================================================================ */
.vg-window-controls {
position: absolute;
top: 0;
right: 0;
height: 100%;
display: flex;
align-items: stretch;
-webkit-app-region: no-drag;
}
.vg-wc-btn {
width: 46px;
border: none;
background: transparent;
color: rgba(255, 255, 255, 0.60);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background-color 0.1s ease, color 0.1s ease;
padding: 0;
-webkit-app-region: no-drag;
}
.vg-wc-btn svg {
width: 10px;
height: 10px;
pointer-events: none;
}
.vg-wc-btn:hover {
background: rgba(255, 255, 255, 0.08);
color: white;
}
.vg-wc-close:hover {
background: #c42b1c;
color: #fff;
}
/* ============================================================================
@ -64,9 +133,8 @@
display: flex;
align-items: center;
justify-content: center;
animation: fadeInContent 0.6s 0.1s ease-in-out forwards;
animation: fadeInContent 0.45s 0.08s ease-in-out forwards;
opacity: 0;
/* Prevent content from being selectable */
user-select: none;
-webkit-user-select: none;
}
@ -102,10 +170,9 @@
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(5px) saturate(120%);
-webkit-backdrop-filter: blur(5px) saturate(120%);
/* Prevent interaction */
background-color: rgba(0, 0, 0, var(--vg-overlay-opacity, 0.30));
backdrop-filter: none;
-webkit-backdrop-filter: none;
pointer-events: none;
user-select: none;
}
@ -117,20 +184,54 @@
.vaultguard-content {
position: relative;
z-index: 10;
display: flex;
flex-direction: column;
align-items: center;
animation: slideUpIn 0.7s 0.2s cubic-bezier(0.25, 1, 0.5, 1) forwards,
fadeInContent 0.7s 0.2s ease-in-out forwards;
transform: translateY(20px);
transition: transform 0.3s ease;
animation: slideUpIn 0.5s 0.12s cubic-bezier(0.25, 1, 0.5, 1) forwards,
fadeInContent 0.5s 0.12s ease-in-out forwards;
transform: translateY(12px);
opacity: 0;
/* Allow interaction with children */
pointer-events: auto;
/* Ensure content is above everything */
isolation: isolate;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
--vaultguard-base-font: 1rem;
font-family: var(--vaultguard-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif);
user-select: none;
-webkit-user-select: none;
}
/* ============================================================================
TILE GRID master-stack tiling layout (Hyprland dwindle style)
First widget = master pane (left, full height)
Remaining widgets = stack pane (right, split equally)
To add a widget: push to WIDGET_REGISTRY in index.js
============================================================================ */
.vg-tile-grid {
display: grid;
grid-template-columns: 3fr 2fr;
gap: 20px;
width: min(900px, 88vw);
height: min(480px, 70vh);
}
.vg-tile-single {
grid-template-columns: 1fr;
width: min(460px, 88vw);
}
.vg-tile-master {
height: 100%;
}
.vg-tile-stack {
display: flex;
flex-direction: column;
gap: 20px;
height: 100%;
}
.vg-tile-item {
flex: 1;
min-height: 0;
}
@keyframes slideUpIn {
@ -139,55 +240,102 @@
}
}
/* ============================================================================
WIDGET PANELS shared glass surface
============================================================================ */
.vg-time-widget,
.vg-login-card {
background: var(--vg-glass-bg);
backdrop-filter: var(--vg-glass-blur);
-webkit-backdrop-filter: var(--vg-glass-blur);
border: 1px solid var(--vg-glass-border);
border-radius: var(--vg-glass-radius);
box-shadow:
var(--vg-glass-shadow),
inset 0 1px 0 var(--vg-glass-highlight);
height: 100%;
box-sizing: border-box;
}
.vg-time-widget {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: calc(10px * var(--vaultguard-ui-scale, 1));
padding: calc(28px * var(--vaultguard-ui-scale, 1)) calc(40px * var(--vaultguard-ui-scale, 1));
}
.vg-login-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: calc(20px * var(--vaultguard-ui-scale, 1));
padding: calc(32px * var(--vaultguard-ui-scale, 1)) calc(36px * var(--vaultguard-ui-scale, 1)) calc(28px * var(--vaultguard-ui-scale, 1));
}
/* ============================================================================
USERNAME DISPLAY
============================================================================ */
.vaultguard-username {
font-size: calc(38px * var(--vaultguard-ui-scale, 1));
margin-bottom: calc(25px * var(--vaultguard-ui-scale, 1));
text-shadow: 0px 2px 12px rgba(0, 0, 0, 0.5);
font-size: calc(34px * var(--vaultguard-ui-scale, 1));
text-shadow: 0px 1px 8px rgba(0, 0, 0, 0.5);
font-family: var(--vaultguard-font-family, inherit);
font-weight: var(--vaultguard-font-weight, 500);
font-style: var(--vaultguard-font-style, normal);
/* Prevent selection */
user-select: none;
-webkit-user-select: none;
/* Ensure it stays above overlay */
position: relative;
z-index: 10;
}
.vaultguard-clock {
font-size: calc(72px * var(--vaultguard-ui-scale, 1));
font-weight: 200;
letter-spacing: -0.02em;
font-weight: 300;
letter-spacing: -0.03em;
color: rgba(255,255,255,0.92);
text-shadow: 0 2px 20px rgba(0,0,0,0.3);
font-variant-numeric: tabular-nums;
line-height: 1;
margin-bottom: 6px;
margin-bottom: 4px;
font-family: var(--vaultguard-font-family, inherit);
}
.vaultguard-date {
font-size: calc(13px * var(--vaultguard-ui-scale, 1));
font-weight: 400;
color: rgba(255,255,255,0.50);
font-size: calc(12px * var(--vaultguard-ui-scale, 1));
font-weight: 500;
color: rgba(255,255,255,0.78);
letter-spacing: 0.08em;
text-transform: uppercase;
margin-bottom: calc(40px * var(--vaultguard-ui-scale, 1));
font-family: var(--vaultguard-font-family, inherit);
text-shadow: 0 1px 6px rgba(0,0,0,0.45);
}
/* ============================================================================
PASSWORD INPUT WRAPPER & INPUT (CORRECTED FOR GLASSMORPHISM)
ANALOGUE CLOCK
============================================================================ */
.vg-analogue-clock {
/* Fills the widget's flex space while staying square */
flex: 1 1 0;
min-height: 0;
width: 100%;
max-width: min(100%, 220px);
aspect-ratio: 1 / 1;
display: block;
overflow: visible;
}
/* ============================================================================
PASSWORD INPUT WRAPPER & INPUT
============================================================================ */
.vaultguard-password-wrapper {
position: relative;
width: var(--vaultguard-input-width, 340px);
max-width: 90vw;
/* Constrained to card width in the tiling layout */
max-width: 100%;
transition: width 0.3s ease;
z-index: 10;
}
@ -196,56 +344,60 @@
animation: shake 0.4s linear;
}
/* This is the corrected rule for the input bar */
/* Input bar — "carved into" the glass card */
.vaultguard-screen .vaultguard-input {
width: 100%;
/* [THE FIX] Dark, translucent background for the glass effect */
background: rgba(10, 10, 10, 0.25) !important;
/* [THE FIX] The essential blur effect */
backdrop-filter: blur(12px) saturate(150%) !important;
-webkit-backdrop-filter: blur(12px) saturate(150%) !important;
/* A subtle border to define the "edge" of the glass */
border: 1px solid rgba(255, 255, 255, 0.18);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
background: rgba(0, 0, 0, 0.22) !important;
backdrop-filter: none !important;
-webkit-backdrop-filter: none !important;
border: 1px solid rgba(255, 255, 255, 0.14);
/* Top shadow reads as a carved recess; bottom edge gives subtle lift */
box-shadow:
inset 0 2px 8px rgba(0, 0, 0, 0.30),
inset 0 1px 2px rgba(0, 0, 0, 0.20),
0 1px 0 rgba(255, 255, 255, 0.06);
border-radius: calc(var(--vaultguard-input-radius, 100) * 1px);
color: white;
padding: calc(16px * var(--vaultguard-ui-scale, 1)) calc(30px * var(--vaultguard-ui-scale, 1));
color: rgba(255, 255, 255, 0.95);
padding: calc(15px * var(--vaultguard-ui-scale, 1)) calc(28px * var(--vaultguard-ui-scale, 1));
font-size: calc(14px * var(--vaultguard-ui-scale, 1));
letter-spacing: 0.5em;
letter-spacing: 0.22em;
text-align: center;
box-sizing: border-box;
outline: none;
transition: all 0.3s ease;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
user-select: text;
-webkit-user-select: text;
/* Adds a subtle shadow to the text for better readability */
text-shadow: 0 1px 3px rgba(0,0,0,0.4);
text-shadow: 0 1px 4px rgba(0,0,0,0.5);
caret-color: rgba(255, 255, 255, 0.80);
}
.vaultguard-input::placeholder {
color: rgba(255, 255, 255, 0.4);
letter-spacing: 0.1em;
.vaultguard-screen .vaultguard-input::placeholder {
color: rgba(255, 255, 255, 0.7) !important;
letter-spacing: 0.06em;
text-shadow: none;
}
/* Ensure the focus state also maintains the glass effect */
.vaultguard-screen .vaultguard-input:focus {
border-color: rgba(255, 255, 255, 0.4);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2),
0 0 0 2px rgba(255, 255, 255, 0.1) inset;
border-color: rgba(255, 255, 255, 0.32);
box-shadow:
inset 0 2px 8px rgba(0, 0, 0, 0.30),
inset 0 1px 2px rgba(0, 0, 0, 0.20),
0 0 0 3px rgba(255, 255, 255, 0.10),
0 1px 0 rgba(255, 255, 255, 0.06);
}
.vaultguard-input.is-error {
border-color: #e74c3c !important;
border-color: rgba(255, 90, 90, 0.65) !important;
box-shadow:
inset 0 2px 8px rgba(0, 0, 0, 0.30),
0 0 0 3px rgba(255, 80, 80, 0.16) !important;
}
.vaultguard-input.is-success {
border-color: #2ecc71 !important;
border-color: rgba(80, 220, 120, 0.65) !important;
box-shadow:
inset 0 2px 8px rgba(0, 0, 0, 0.30),
0 0 0 3px rgba(80, 220, 130, 0.16) !important;
}
@keyframes shake {
@ -603,15 +755,40 @@
RESPONSIVE ADJUSTMENTS
============================================================================ */
@media (max-width: 768px) {
.vaultguard-username {
font-size: calc(32px * var(--vaultguard-ui-scale, 1));
margin-bottom: calc(20px * var(--vaultguard-ui-scale, 1));
/* Small window: stack tiles vertically so neither panel is crushed */
@media (max-width: 640px) {
.vg-tile-grid {
grid-template-columns: 1fr;
width: min(440px, 88vw);
height: auto;
}
.vaultguard-input {
font-size: calc(16px * var(--vaultguard-ui-scale, 1));
padding: calc(14px * var(--vaultguard-ui-scale, 1)) calc(25px * var(--vaultguard-ui-scale, 1));
.vg-tile-master,
.vg-tile-stack {
height: auto;
}
.vg-tile-item {
flex: none;
}
.vg-time-widget,
.vg-login-card {
height: auto;
}
.vg-time-widget {
min-height: 200px;
}
.vg-login-card {
min-height: 140px;
}
}
@media (max-width: 768px) {
.vaultguard-username {
font-size: calc(26px * var(--vaultguard-ui-scale, 1));
}
.vaultguard-thumbnail-grid {
@ -646,7 +823,9 @@
.vaultguard-main-content,
.vaultguard-content,
.vaultguard-input,
.vaultguard-thumbnail-item {
.vaultguard-thumbnail-item,
.vg-time-widget,
.vg-login-card {
animation: none !important;
transition: none !important;
opacity: 1 !important;
@ -658,41 +837,85 @@
}
}
/* === ATMOSPHERE VARIANTS === */
[data-atmosphere="noir"] .vaultguard-overlay {
background-color: rgba(0,0,0,0.75);
backdrop-filter: none;
-webkit-backdrop-filter: none;
animation: none;
}
[data-atmosphere="noir"] .vaultguard-screen .vaultguard-input {
border-radius: 4px !important;
background: rgba(0,0,0,0.65) !important;
border: 1px solid rgba(255,255,255,0.22) !important;
}
[data-atmosphere="frosted"] .vaultguard-overlay {
background-color: rgba(255,255,255,0.10);
backdrop-filter: blur(22px) saturate(180%);
-webkit-backdrop-filter: blur(22px) saturate(180%);
}
[data-atmosphere="frosted"] .vaultguard-screen .vaultguard-input {
background: rgba(255,255,255,0.18) !important;
border-color: rgba(255,255,255,0.30) !important;
}
[data-atmosphere="vivid"] .vaultguard-overlay {
background: linear-gradient(135deg, rgba(99,102,241,0.45) 0%, rgba(168,85,247,0.45) 100%);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
[data-atmosphere="vivid"] .vaultguard-screen .vaultguard-input {
border-color: rgba(168,85,247,0.7) !important;
box-shadow: 0 0 0 1px rgba(168,85,247,0.3), 0 4px 20px rgba(0,0,0,0.2) !important;
}
/* ============================================================================
TAMPER WARNING / DANGER ZONE
============================================================================ */
/* ============================================================================
WARNING ICON (used in tamper panel)
============================================================================ */
.vaultguard-warning-icon {
/* Explicit dimensions prevent the SVG from expanding before CSS loads */
width: 18px;
height: 18px;
flex-shrink: 0;
display: block;
color: #e67e22;
}
/* ============================================================================
TAMPER SCREEN minimal view shown when vault integrity cannot be confirmed
(no user visual preferences are applied here)
============================================================================ */
.vaultguard-tamper-screen {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
background: #0f0f0f;
animation: fadeInContent 0.4s ease forwards;
}
.vaultguard-tamper-panel {
width: 100%;
max-width: 380px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(230, 126, 34, 0.35);
border-radius: 12px;
padding: 28px 28px 24px;
display: flex;
flex-direction: column;
gap: 16px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
}
.vaultguard-tamper-panel-header {
display: flex;
align-items: center;
gap: 10px;
color: #e67e22;
}
.vaultguard-tamper-panel-title {
font-size: 14px;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: #e67e22;
}
.vaultguard-tamper-panel-message {
font-size: 13px;
line-height: 1.6;
color: rgba(255, 255, 255, 0.6);
margin: 0;
}
/* Password input inside the tamper panel inherits the glass style
but needs a fixed width since --vaultguard-input-width isn't set here */
.vaultguard-tamper-panel .vaultguard-password-wrapper {
width: 100%;
max-width: 100%;
}
/* Legacy banner — kept for any callers that may still reference it */
.vaultguard-tamper-warning {
display: flex;
align-items: center;
gap: 8px;
background: rgba(231, 76, 60, 0.15);
border: 1px solid rgba(231, 76, 60, 0.4);
border-radius: 8px;