Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,10 @@ if (window.visualizerSettings) {
// Focus Mode: smoothly fade canvas opacity based on user activity
if (typeof window.visualizerSettings.onFocusModeOpacity === "function") {
window.visualizerSettings.onFocusModeOpacity(({ opacity }) => {
const duration = (visualizerState.focusMode && typeof visualizerState.focusMode.transitionDuration === "number")
? visualizerState.focusMode.transitionDuration
: 1.5;
canvas.style.transition = `opacity ${duration}s ease`;
canvas.style.opacity = typeof opacity === "number" ? String(opacity) : "1";
});
}
Expand Down
33 changes: 21 additions & 12 deletions settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -654,33 +654,42 @@ <h3>Theme Automation</h3>

<div class="settings-group">
<h3>Focus Mode</h3>
<p class="setting-desc">Dim the visualizer when you are actively using your computer to reduce distractions, and restore full opacity when idle.</p>
<p class="setting-desc">Dims the visualizer during user activity to keep the desktop clean, and restores full brightness when idle.</p>

<div class="switch-control" style="margin-top: 16px; display: flex; justify-content: space-between; align-items: center;">
<div>
<span style="font-weight: 500; font-size: 15px; color: #e2e8f0;">Enable Focus Mode</span>
<p class="setting-desc" style="margin-top: 4px; margin-bottom: 0;">Dim the visualizer during active user input.</p>
<p class="setting-desc" style="margin-top: 4px; margin-bottom: 0;">Automatically dim visualizer when you are active.</p>
</div>
<label class="switch">
<input type="checkbox" id="focus-mode-enabled-checkbox">
<input type="checkbox" id="focus-mode-checkbox">
<span class="slider-round"></span>
</label>
</div>

<div id="focusModeControls" style="display: none; margin-top: 20px;">
<div class="slider-control" style="margin-bottom: 20px;">
<div id="focus-mode-settings-container" style="display: none; margin-top: 16px;">
<div class="slider-control">
<div class="slider-header">
<label for="focus-mode-dim-opacity-slider">Dimmed Opacity</label>
<span class="slider-value" id="val-focus-mode-dim-opacity">0.10</span>
<label for="focus-mode-dim-opacity">Dimmed Opacity</label>
<span class="slider-value" id="val-focus-mode-dim-opacity">10%</span>
</div>
<input type="range" id="focus-mode-dim-opacity-slider" min="0.0" max="1.0" step="0.05" value="0.10">
<input type="range" id="focus-mode-dim-opacity" min="0" max="100" value="10">
</div>

<div class="slider-control">
<div class="slider-control" style="margin-top: 16px;">
<div class="slider-header">
<label for="focus-mode-idle-timeout">Idle Timeout (seconds)</label>
<span class="slider-value" id="val-focus-mode-idle-timeout">5s</span>
</div>
<input type="range" id="focus-mode-idle-timeout" min="1" max="60" value="5">
</div>

<div class="slider-control" style="margin-top: 16px;">
<div class="slider-header">
<label for="focus-mode-timeout-slider">Activity Idle Timeout (seconds)</label>
<span class="slider-value" id="val-focus-mode-timeout">5</span>
<label for="focus-mode-transition-duration">Fade Duration (seconds)</label>
<span class="slider-value" id="val-focus-mode-transition-duration">1.5s</span>
</div>
<input type="range" id="focus-mode-timeout-slider" min="1" max="60" step="1" value="5">
<input type="range" id="focus-mode-transition-duration" min="0.1" max="10" step="0.1" value="1.5">
</div>
</div>
</div>
Expand Down
113 changes: 65 additions & 48 deletions settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -316,58 +316,64 @@ document.addEventListener('DOMContentLoaded', () => {
// ----------------------------------------
// FOCUS MODE BINDINGS
// ----------------------------------------
const focusModeEnabledCheckbox = document.getElementById('focus-mode-enabled-checkbox');
const focusModeControls = document.getElementById('focusModeControls');
const focusModeDimOpacitySlider = document.getElementById('focus-mode-dim-opacity-slider');
const focusModeTimeoutSlider = document.getElementById('focus-mode-timeout-slider');
const focusModeDimOpacityLabel = document.getElementById('val-focus-mode-dim-opacity');
const focusModeTimeoutLabel = document.getElementById('val-focus-mode-timeout');
const focusModeCheckbox = document.getElementById('focus-mode-checkbox');
const focusModeSettingsContainer = document.getElementById('focus-mode-settings-container');
const focusModeDimOpacity = document.getElementById('focus-mode-dim-opacity');
const focusModeIdleTimeout = document.getElementById('focus-mode-idle-timeout');
const focusModeTransitionDuration = document.getElementById('focus-mode-transition-duration');

function toggleFocusModeControls(isEnabled) {
if (focusModeControls) {
focusModeControls.style.display = isEnabled ? 'block' : 'none';
if (focusModeSettingsContainer) {
focusModeSettingsContainer.style.display = isEnabled ? 'block' : 'none';
}
}

function updateFocusModeSetting(patch) {
if (window.visualizerSettings) {
const currentFocusMode = cachedSettings.focusMode || {};
const nextFocusMode = { ...currentFocusMode, ...patch };
cachedSettings.focusMode = nextFocusMode; // Optimistic local cache update!
cachedSettings.focusMode = nextFocusMode; // Optimistic local cache update
window.visualizerSettings.update({
focusMode: nextFocusMode
});
}
}

if (focusModeEnabledCheckbox) {
focusModeEnabledCheckbox.addEventListener('change', (e) => {
if (focusModeCheckbox) {
focusModeCheckbox.addEventListener('change', (e) => {
const isChecked = e.target.checked;
toggleFocusModeControls(isChecked);
updateFocusModeSetting({ enabled: isChecked });
});
}

if (focusModeDimOpacitySlider) {
focusModeDimOpacitySlider.addEventListener('input', (e) => {
const val = parseFloat(e.target.value);
if (focusModeDimOpacityLabel) {
focusModeDimOpacityLabel.textContent = val.toFixed(2);
}
if (focusModeDimOpacity) {
focusModeDimOpacity.addEventListener('input', (e) => {
const val = parseFloat(e.target.value) / 100;
const valEl = document.getElementById('val-focus-mode-dim-opacity');
if (valEl) valEl.textContent = `${e.target.value}%`;
updateFocusModeSetting({ dimOpacity: val });
});
}

if (focusModeTimeoutSlider) {
focusModeTimeoutSlider.addEventListener('input', (e) => {
const val = parseInt(e.target.value, 10);
if (focusModeTimeoutLabel) {
focusModeTimeoutLabel.textContent = val;
}
if (focusModeIdleTimeout) {
focusModeIdleTimeout.addEventListener('input', (e) => {
const val = parseInt(e.target.value, 10) || 5;
const valEl = document.getElementById('val-focus-mode-idle-timeout');
if (valEl) valEl.textContent = `${val}s`;
updateFocusModeSetting({ idleTimeout: val });
});
}

if (focusModeTransitionDuration) {
focusModeTransitionDuration.addEventListener('input', (e) => {
const val = parseFloat(e.target.value) || 1.5;
const valEl = document.getElementById('val-focus-mode-transition-duration');
if (valEl) valEl.textContent = `${val.toFixed(1)}s`;
updateFocusModeSetting({ transitionDuration: val });
});
}

const themeSelector = document.getElementById('theme-selector');
if (themeSelector) {
themeSelector.addEventListener('change', (e) => {
Expand All @@ -383,6 +389,7 @@ document.addEventListener('DOMContentLoaded', () => {
});
}


const performanceModeSelector = document.getElementById('performance-mode-selector');
if (performanceModeSelector) {
performanceModeSelector.addEventListener('change', (e) => {
Expand Down Expand Up @@ -1356,21 +1363,27 @@ refreshThemeProfiles();
// Load focus mode settings
if (settings.focusMode) {
const fm = settings.focusMode;
if (focusModeEnabledCheckbox) {
focusModeEnabledCheckbox.checked = !!fm.enabled;
if (focusModeCheckbox) {
focusModeCheckbox.checked = !!fm.enabled;
toggleFocusModeControls(fm.enabled);
}
if (focusModeDimOpacitySlider) {
focusModeDimOpacitySlider.value = fm.dimOpacity !== undefined ? fm.dimOpacity : 0.1;
if (focusModeDimOpacityLabel) {
focusModeDimOpacityLabel.textContent = parseFloat(focusModeDimOpacitySlider.value).toFixed(2);
}
if (focusModeDimOpacity) {
const pct = Math.round((fm.dimOpacity !== undefined ? fm.dimOpacity : 0.1) * 100);
focusModeDimOpacity.value = pct;
const valEl = document.getElementById('val-focus-mode-dim-opacity');
if (valEl) valEl.textContent = `${pct}%`;
}
if (focusModeTimeoutSlider) {
focusModeTimeoutSlider.value = fm.idleTimeout !== undefined ? fm.idleTimeout : 5;
if (focusModeTimeoutLabel) {
focusModeTimeoutLabel.textContent = focusModeTimeoutSlider.value;
}
if (focusModeIdleTimeout) {
const secs = fm.idleTimeout !== undefined ? fm.idleTimeout : 5;
focusModeIdleTimeout.value = secs;
const valEl = document.getElementById('val-focus-mode-idle-timeout');
if (valEl) valEl.textContent = `${secs}s`;
}
if (focusModeTransitionDuration) {
const duration = fm.transitionDuration !== undefined ? fm.transitionDuration : 1.5;
focusModeTransitionDuration.value = duration;
const valEl = document.getElementById('val-focus-mode-transition-duration');
if (valEl) valEl.textContent = `${duration.toFixed(1)}s`;
}
}

Expand Down Expand Up @@ -1464,24 +1477,28 @@ refreshThemeProfiles();
updateThemeLabels(dayStart, nightStart);
}

// Sync Focus Mode settings
// Sync focus mode properties if updated from outside
if (nextSettings.focusMode) {
const fm = nextSettings.focusMode;
if (focusModeEnabledCheckbox && fm.enabled !== undefined) {
focusModeEnabledCheckbox.checked = !!fm.enabled;
if (focusModeCheckbox && fm.enabled !== undefined) {
focusModeCheckbox.checked = !!fm.enabled;
toggleFocusModeControls(fm.enabled);
}
if (focusModeDimOpacitySlider && fm.dimOpacity !== undefined) {
focusModeDimOpacitySlider.value = fm.dimOpacity;
if (focusModeDimOpacityLabel) {
focusModeDimOpacityLabel.textContent = parseFloat(fm.dimOpacity).toFixed(2);
}
if (focusModeDimOpacity && fm.dimOpacity !== undefined) {
const pct = Math.round(fm.dimOpacity * 100);
focusModeDimOpacity.value = pct;
const valEl = document.getElementById('val-focus-mode-dim-opacity');
if (valEl) valEl.textContent = `${pct}%`;
}
if (focusModeTimeoutSlider && fm.idleTimeout !== undefined) {
focusModeTimeoutSlider.value = fm.idleTimeout;
if (focusModeTimeoutLabel) {
focusModeTimeoutLabel.textContent = fm.idleTimeout;
}
if (focusModeIdleTimeout && fm.idleTimeout !== undefined) {
focusModeIdleTimeout.value = fm.idleTimeout;
const valEl = document.getElementById('val-focus-mode-idle-timeout');
if (valEl) valEl.textContent = `${fm.idleTimeout}s`;
}
if (focusModeTransitionDuration && fm.transitionDuration !== undefined) {
focusModeTransitionDuration.value = fm.transitionDuration;
const valEl = document.getElementById('val-focus-mode-transition-duration');
if (valEl) valEl.textContent = `${fm.transitionDuration.toFixed(1)}s`;
}
}

Expand Down
16 changes: 10 additions & 6 deletions settingsStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,8 @@ const DEFAULT_SETTINGS = Object.freeze({
focusMode: Object.freeze({
enabled: false,
dimOpacity: 0.1,
idleTimeout: 5
idleTimeout: 5,
transitionDuration: 1.5
}),
auroraDrift: Object.freeze({
// Standard settings
Expand Down Expand Up @@ -572,13 +573,16 @@ function migrateLegacySettings(input = {}) {

function sanitizeFocusMode(input = {}) {
const enabled = typeof input.enabled === "boolean" ? input.enabled : DEFAULT_SETTINGS.focusMode.enabled;
const dimOpacity = typeof input.dimOpacity === "number" && input.dimOpacity >= 0 && input.dimOpacity <= 1
? input.dimOpacity
const dimOpacity = typeof input.dimOpacity === "number" && Number.isFinite(input.dimOpacity)
? Math.max(0, Math.min(1, input.dimOpacity))
: DEFAULT_SETTINGS.focusMode.dimOpacity;
const idleTimeout = typeof input.idleTimeout === "number" && input.idleTimeout >= 1 && input.idleTimeout <= 60
? input.idleTimeout
const idleTimeout = typeof input.idleTimeout === "number" && Number.isFinite(input.idleTimeout)
? Math.max(1, Math.min(60, input.idleTimeout))
: DEFAULT_SETTINGS.focusMode.idleTimeout;
return { enabled, dimOpacity, idleTimeout };
const transitionDuration = typeof input.transitionDuration === "number" && Number.isFinite(input.transitionDuration)
? Math.max(0.1, Math.min(10, input.transitionDuration))
: DEFAULT_SETTINGS.focusMode.transitionDuration;
return { enabled, dimOpacity, idleTimeout, transitionDuration };
}

function sanitizeShortcuts(input) {
Expand Down
33 changes: 33 additions & 0 deletions test/settingsStore.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,36 @@ test("settingsStore - Legacy Theme conversion (sensitivity mapping)", () => {
assert.strictEqual(sanitized.selectedTheme, "reactiveBorder");
assert.strictEqual(sanitized.reactiveBorder.intensity, "high");
});

test("settingsStore - Focus Mode sanitization and clamping", () => {
const input = {
selectedTheme: "ambientWave",
focusMode: {
enabled: true,
dimOpacity: 1.5, // should clamp to 1
idleTimeout: 75, // should clamp to 60
transitionDuration: 15 // should clamp to 10
}
};
const sanitized = sanitizeSettings(input);
assert.strictEqual(sanitized.focusMode.enabled, true);
assert.strictEqual(sanitized.focusMode.dimOpacity, 1);
assert.strictEqual(sanitized.focusMode.idleTimeout, 60);
assert.strictEqual(sanitized.focusMode.transitionDuration, 10);

const inputMin = {
selectedTheme: "ambientWave",
focusMode: {
enabled: false,
dimOpacity: -0.5, // should clamp to 0
idleTimeout: 0, // should clamp to 1
transitionDuration: 0.05 // should clamp to 0.1
}
};
const sanitizedMin = sanitizeSettings(inputMin);
assert.strictEqual(sanitizedMin.focusMode.enabled, false);
assert.strictEqual(sanitizedMin.focusMode.dimOpacity, 0);
assert.strictEqual(sanitizedMin.focusMode.idleTimeout, 1);
assert.strictEqual(sanitizedMin.focusMode.transitionDuration, 0.1);
});

Loading