mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-16 03:42:20 +02:00
Photoshop-parity editor client: layers, selection/masking, brushes,
smart filters, LUT import, Quick Edit, mobile gestures, workspaces,
batch operations, and the service-worker versioning (v978->v1006).
Source: vendor/photocraft (ad1cff44) photo-editor client files, merged
onto upstream/dev (a8c147b2). Conflict resolution documented in
pr_work/A_conflicts.md; editor files taken from vendor/photocraft
(upstream's editor is an earlier snapshot of the same feature).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
175 lines
6.1 KiB
JavaScript
175 lines
6.1 KiB
JavaScript
/**
|
|
* Side-panel visibility.
|
|
*
|
|
* Photoshop-style View > Panels toggles for every docked side panel (plus the
|
|
* pinned Layers strip and the Tool Options bar). Visibility is a per-user,
|
|
* document-independent preference persisted in localStorage so it survives
|
|
* reloads and applies to every document.
|
|
*
|
|
* The dock itself has no visibility API, so we apply visibility by tagging the
|
|
* panel wrapper, its tab strip item, and (when a whole group is hidden) the
|
|
* group element with `data-panel-hidden="1"`; CSS hides tagged nodes.
|
|
*/
|
|
export const PANELS = Object.freeze([
|
|
{ id: 'swatches', label: 'Swatches', group: 'Color' },
|
|
{ id: 'gradients', label: 'Gradients', group: 'Color' },
|
|
{ id: 'patterns', label: 'Patterns', group: 'Color' },
|
|
{ id: 'properties', label: 'Properties', group: 'Adjustments' },
|
|
{ id: 'adjustments', label: 'Adjustments', group: 'Adjustments' },
|
|
{ id: 'histogram', label: 'Histogram', group: 'Adjustments' },
|
|
{ id: 'info', label: 'Info', group: 'Adjustments' },
|
|
{ id: 'channels', label: 'Channels', group: 'Panels' },
|
|
{ id: 'paths', label: 'Paths', group: 'Panels' },
|
|
{ id: 'character', label: 'Character', group: 'Panels' },
|
|
{ id: 'navigator', label: 'Navigator', group: 'Panels' },
|
|
{ id: 'history', label: 'History', group: 'History' },
|
|
{ id: 'actions', label: 'Actions', group: 'History' },
|
|
{ id: 'layers', label: 'Layers', group: 'Pinned' },
|
|
{ id: 'options', label: 'Tool Options', group: 'Pinned' },
|
|
]);
|
|
|
|
export const PANEL_IDS = PANELS.map(panel => panel.id);
|
|
const STORAGE_KEY = 'ge-panel-visibility';
|
|
const DOCK_GROUPS = ['Color', 'Adjustments', 'Panels', 'History'];
|
|
const subscribers = new Set();
|
|
|
|
let hidden = null;
|
|
|
|
function readStored() {
|
|
try {
|
|
const raw = localStorage.getItem(STORAGE_KEY);
|
|
if (!raw) return {};
|
|
const parsed = JSON.parse(raw);
|
|
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
} catch (_) {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
function ensureLoaded() {
|
|
if (hidden) return;
|
|
const stored = readStored();
|
|
hidden = new Set(PANEL_IDS.filter(id => stored[id] === false));
|
|
}
|
|
|
|
function persist() {
|
|
const map = {};
|
|
for (const id of PANEL_IDS) map[id] = !hidden.has(id);
|
|
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(map)); } catch (_) {}
|
|
}
|
|
|
|
export function isVisible(id) {
|
|
ensureLoaded();
|
|
return !hidden.has(id);
|
|
}
|
|
|
|
export function list() {
|
|
ensureLoaded();
|
|
return PANELS.map(panel => ({ ...panel, visible: !hidden.has(panel.id) }));
|
|
}
|
|
|
|
function notify() {
|
|
for (const fn of subscribers) { try { fn(); } catch (_) {} }
|
|
}
|
|
|
|
function markHidden(el, hide) {
|
|
if (!el) return;
|
|
if (hide) el.setAttribute('data-panel-hidden', '1');
|
|
else el.removeAttribute('data-panel-hidden');
|
|
}
|
|
|
|
export function apply() {
|
|
ensureLoaded();
|
|
for (const id of PANEL_IDS) {
|
|
const hide = hidden.has(id);
|
|
document.querySelectorAll(`.ge-dock-panel[data-panel="${id}"]`).forEach(el => markHidden(el, hide));
|
|
document.querySelectorAll(`.ge-dock-tab-item[data-panel="${id}"]`).forEach(el => markHidden(el, hide));
|
|
document.querySelectorAll(`.ge-dock-select option[value="${id}"]`).forEach(el => markHidden(el, hide));
|
|
}
|
|
// Collapse a whole group (and the divider that precedes it) when every one
|
|
// of its panels is hidden, so the rail reflows without an empty gap.
|
|
for (const group of DOCK_GROUPS) {
|
|
const members = PANELS.filter(panel => panel.group === group).map(panel => panel.id);
|
|
const allHidden = members.every(id => hidden.has(id));
|
|
document.querySelectorAll(`.ge-dock-group[data-group="${group}"]`).forEach(el => markHidden(el, allHidden));
|
|
}
|
|
document.querySelectorAll('.ge-dock-divider').forEach(div => {
|
|
const next = div.nextElementSibling;
|
|
markHidden(div, !!(next && next.matches('.ge-dock-group[data-panel-hidden="1"]')));
|
|
});
|
|
// Pinned panels live outside the dock.
|
|
markHidden(document.querySelector('.ge-layers'), hidden.has('layers'));
|
|
markHidden(document.querySelector('.ge-controls'), hidden.has('options'));
|
|
// If every dock group is hidden, collapse the dock entirely so the rail
|
|
// doesn't leave an empty column of blank space; hide the whole rail once
|
|
// the pinned panels are hidden too (the canvas takes the freed width).
|
|
const anyDockVisible = DOCK_GROUPS.some(group =>
|
|
PANELS.some(panel => panel.group === group && !hidden.has(panel.id)));
|
|
document.querySelectorAll('.ge-dock').forEach(el => markHidden(el, !anyDockVisible));
|
|
const railEmpty = !anyDockVisible && hidden.has('layers') && hidden.has('options');
|
|
document.querySelectorAll('.ge-right-panel').forEach(el => {
|
|
markHidden(el, railEmpty);
|
|
if (!anyDockVisible) el.setAttribute('data-panel-dock-hidden', '1');
|
|
else el.removeAttribute('data-panel-dock-hidden');
|
|
});
|
|
// Never leave a group showing a hidden panel: activate the first visible tab.
|
|
for (const group of DOCK_GROUPS) {
|
|
const members = PANELS.filter(panel => panel.group === group).map(panel => panel.id);
|
|
if (members.every(id => hidden.has(id))) continue;
|
|
const groupEl = document.querySelector(`.ge-dock-group[data-group="${group}"]`);
|
|
if (!groupEl) continue;
|
|
const activeTab = groupEl.querySelector('.ge-dock-tab-item.active');
|
|
const activeId = activeTab?.dataset.panel;
|
|
if (activeId && hidden.has(activeId)) {
|
|
const firstVisible = members.find(id => !hidden.has(id));
|
|
groupEl.querySelector(`.ge-dock-tab-item[data-panel="${firstVisible}"] .ge-dock-tab`)?.click();
|
|
}
|
|
}
|
|
}
|
|
|
|
export function setVisible(id, visible) {
|
|
ensureLoaded();
|
|
if (!PANEL_IDS.includes(id)) return false;
|
|
const on = visible !== false;
|
|
if (on) hidden.delete(id);
|
|
else hidden.add(id);
|
|
persist();
|
|
apply();
|
|
notify();
|
|
return on;
|
|
}
|
|
|
|
export function toggle(id) {
|
|
return setVisible(id, !isVisible(id));
|
|
}
|
|
|
|
export function showAll() {
|
|
ensureLoaded();
|
|
hidden.clear();
|
|
persist();
|
|
apply();
|
|
notify();
|
|
}
|
|
|
|
export function hideAll() {
|
|
ensureLoaded();
|
|
hidden = new Set(PANEL_IDS);
|
|
persist();
|
|
apply();
|
|
notify();
|
|
}
|
|
|
|
export function reset() {
|
|
return showAll();
|
|
}
|
|
|
|
export function subscribe(fn) {
|
|
subscribers.add(fn);
|
|
return () => subscribers.delete(fn);
|
|
}
|
|
|
|
// Test seam: drop the in-memory cache so the next read re-loads localStorage.
|
|
export function _resetCache() {
|
|
hidden = null;
|
|
}
|