Add customizable home layout editing and settings

Introduces an edit mode on the home page allowing users to drag and reposition the greeting, search bar, top sites, and 'at a glance' widget. Adds persistent storage for these layout preferences and a settings panel for fine-tuning positions and widget corner. Updates CSS, HTML, and JS to support edit UI, drag logic, and settings integration.
This commit is contained in:
2025-09-06 21:46:08 +12:00
parent 285fc44124
commit 5e3b99fdd6
5 changed files with 302 additions and 0 deletions
+194
View File
@@ -21,6 +21,14 @@ const greetingEl = document.getElementById('greeting');
const resetTopSitesBtn = document.getElementById('resetTopSites');
const clockEl = document.getElementById('clock');
const weatherEl = document.getElementById('weather');
const glanceEl = document.querySelector('.glance');
const searchContainerEl = document.querySelector('.search-container');
const topSitesEl = document.querySelector('.top-sites-card');
const editBtn = document.getElementById('editLayoutBtn');
const greetingTitleEl = document.getElementById('greeting');
const editToolbar = document.getElementById('editToolbar');
const saveEditBtn = document.getElementById('saveEditBtn');
const cancelEditBtn = document.getElementById('cancelEditBtn');
let selectedIcon = initialIcons[0];
let availableIcons = initialIcons;
let currentIconSetKey = 'material';
@@ -678,3 +686,189 @@ window.addEventListener('storage', (e) => {
loadWeather();
}
});
// ---- Home layout preferences ----
const HOME_SEARCH_Y_KEY = 'nebula-home-search-y';
const HOME_BOOKMARKS_Y_KEY = 'nebula-home-bookmarks-y';
const HOME_GLANCE_CORNER_KEY = 'nebula-home-glance-corner';
const HOME_GREETING_Y_KEY = 'nebula-home-greeting-y';
function applyHomeLayoutPrefs() {
try {
const root = document.documentElement;
const greetY = Number(localStorage.getItem(HOME_GREETING_Y_KEY) || 12);
const searchY = Number(localStorage.getItem(HOME_SEARCH_Y_KEY) || 22);
const bmY = Number(localStorage.getItem(HOME_BOOKMARKS_Y_KEY) || 40);
root.style.setProperty('--home-greeting-y', `${greetY}vh`);
root.style.setProperty('--home-search-y', `${searchY}vh`);
root.style.setProperty('--home-bookmarks-y', `${bmY}vh`);
const corner = localStorage.getItem(HOME_GLANCE_CORNER_KEY) || 'br';
if (glanceEl) {
glanceEl.classList.remove('pos-br','pos-bl','pos-tr','pos-tl');
glanceEl.classList.add(`pos-${corner}`);
}
} catch (e) { console.warn('applyHomeLayoutPrefs failed', e); }
}
applyHomeLayoutPrefs();
// React to settings updates via storage or host messages
window.addEventListener('storage', (e) => {
if (!e) return;
if ([HOME_SEARCH_Y_KEY, HOME_BOOKMARKS_Y_KEY, HOME_GLANCE_CORNER_KEY].includes(e.key)) {
applyHomeLayoutPrefs();
}
});
if (window.electronAPI && typeof window.electronAPI.on === 'function') {
window.electronAPI.on('settings-update', (payload) => {
if (!payload) return;
if (payload.searchY != null) document.documentElement.style.setProperty('--home-search-y', `${payload.searchY}vh`);
if (payload.bookmarksY != null) document.documentElement.style.setProperty('--home-bookmarks-y', `${payload.bookmarksY}vh`);
if (payload.glanceCorner && glanceEl) {
glanceEl.classList.remove('pos-br','pos-bl','pos-tr','pos-tl');
glanceEl.classList.add(`pos-${payload.glanceCorner}`);
}
});
}
// ---- Edit mode drag support ----
let editMode = false;
let snapshot = null; // stores values before edits
function setEditMode(on) {
editMode = !!on;
document.body.classList.toggle('edit-mode', editMode);
if (editBtn) editBtn.setAttribute('aria-pressed', String(editMode));
if (editToolbar) editToolbar.hidden = !editMode;
if (editMode) {
// Take a snapshot of current persisted values
snapshot = {
greetY: Number(localStorage.getItem('nebula-home-greeting-y') || 12),
searchY: Number(localStorage.getItem('nebula-home-search-y') || 22),
bmY: Number(localStorage.getItem('nebula-home-bookmarks-y') || 40),
corner: localStorage.getItem('nebula-home-glance-corner') || 'br'
};
} else {
snapshot = null;
}
}
if (editBtn) {
editBtn.addEventListener('click', () => setEditMode(!editMode));
}
function vhFromPx(px) { return (px / window.innerHeight) * 100; }
function clamp(n, min, max) { return Math.max(min, Math.min(max, n)); }
function makeDragY(el, storageKey, cssVar) {
if (!el) return;
let startY = 0; let startTop = 0; let dragging = false;
const onDown = (ev) => {
if (!editMode) return; dragging = true;
startY = (ev.touches ? ev.touches[0].clientY : ev.clientY);
// current computed var
const current = Number((getComputedStyle(document.documentElement).getPropertyValue(cssVar) || '0vh').replace('vh',''));
startTop = isNaN(current) ? 0 : current;
ev.preventDefault();
};
const onMove = (ev) => {
if (!dragging) return;
const y = (ev.touches ? ev.touches[0].clientY : ev.clientY);
const deltaPx = y - startY;
const deltaVh = vhFromPx(deltaPx);
const next = clamp(startTop + deltaVh, 0, 90);
document.documentElement.style.setProperty(cssVar, `${next}vh`);
};
const onUp = () => {
if (!dragging) return; dragging = false;
// Don't persist here; only on Save. Values still applied via CSS var.
};
el.addEventListener('mousedown', onDown);
el.addEventListener('touchstart', onDown, { passive:false });
window.addEventListener('mousemove', onMove);
window.addEventListener('touchmove', onMove, { passive:false });
window.addEventListener('mouseup', onUp);
window.addEventListener('touchend', onUp);
}
function makeDragGlance(el) {
if (!el) return;
let dragging = false; let start;
const onDown = (ev) => {
if (!editMode) return; dragging = true; el.classList.add('dragging');
const p = ev.touches?ev.touches[0]:ev; start = { x:p.clientX, y:p.clientY };
// reset any prior drag offsets
el.style.setProperty('--drag-x','0px'); el.style.setProperty('--drag-y','0px');
ev.preventDefault();
};
const onMove = (ev) => {
if (!dragging) return; const p = ev.touches?ev.touches[0]:ev;
const dx = p.clientX - start.x; const dy = p.clientY - start.y;
el.style.setProperty('--drag-x', `${dx}px`);
el.style.setProperty('--drag-y', `${dy}px`);
};
const onUp = (ev) => {
if (!dragging) return; dragging = false; el.classList.remove('dragging');
el.style.removeProperty('--drag-x'); el.style.removeProperty('--drag-y');
const p = ev.changedTouches?ev.changedTouches[0]:ev;
const x = p.clientX; const y = p.clientY;
// snap to nearest corner
const left = x < window.innerWidth/2;
const top = y < window.innerHeight/2;
const corner = top ? (left ? 'tl' : 'tr') : (left ? 'bl' : 'br');
// Only store corner on Save; temporarily apply class for preview
if (glanceEl) {
glanceEl.classList.remove('pos-br','pos-bl','pos-tr','pos-tl');
glanceEl.classList.add(`pos-${corner}`);
// Stash pending corner choice on the element during edit mode
glanceEl.dataset.pendingCorner = corner;
}
};
el.addEventListener('mousedown', onDown);
el.addEventListener('touchstart', onDown, { passive:false });
window.addEventListener('mousemove', onMove);
window.addEventListener('touchmove', onMove, { passive:false });
window.addEventListener('mouseup', onUp);
window.addEventListener('touchend', onUp);
}
makeDragY(searchContainerEl, 'nebula-home-search-y', '--home-search-y');
makeDragY(topSitesEl, 'nebula-home-bookmarks-y', '--home-bookmarks-y');
makeDragGlance(glanceEl);
makeDragY(greetingTitleEl, 'nebula-home-greeting-y', '--home-greeting-y');
// Save/Cancel handlers
if (saveEditBtn) saveEditBtn.addEventListener('click', () => {
// Persist current CSS variable values and pending corner
const rootStyle = getComputedStyle(document.documentElement);
const getVh = (v) => Math.round(Number((v || '0vh').replace('vh','')));
const gy = getVh(rootStyle.getPropertyValue('--home-greeting-y'));
const sy = getVh(rootStyle.getPropertyValue('--home-search-y'));
const by = getVh(rootStyle.getPropertyValue('--home-bookmarks-y'));
try {
localStorage.setItem('nebula-home-greeting-y', String(gy));
localStorage.setItem('nebula-home-search-y', String(sy));
localStorage.setItem('nebula-home-bookmarks-y', String(by));
} catch {}
const corner = glanceEl?.dataset?.pendingCorner || localStorage.getItem(HOME_GLANCE_CORNER_KEY) || 'br';
try { localStorage.setItem(HOME_GLANCE_CORNER_KEY, corner); } catch {}
if (glanceEl) delete glanceEl.dataset.pendingCorner;
setEditMode(false);
});
if (cancelEditBtn) cancelEditBtn.addEventListener('click', () => {
// Revert CSS vars and glance corner to snapshot
if (snapshot) {
document.documentElement.style.setProperty('--home-greeting-y', `${snapshot.greetY}vh`);
document.documentElement.style.setProperty('--home-search-y', `${snapshot.searchY}vh`);
document.documentElement.style.setProperty('--home-bookmarks-y', `${snapshot.bmY}vh`);
if (glanceEl) {
glanceEl.classList.remove('pos-br','pos-bl','pos-tr','pos-tl');
glanceEl.classList.add(`pos-${snapshot.corner}`);
delete glanceEl.dataset.pendingCorner;
}
} else {
applyHomeLayoutPrefs();
}
setEditMode(false);
});