mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-17 02:05:22 -04:00
Odysseus v1.0
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
/**
|
||||
* History-panel subsystem — the floating frosted list of labeled
|
||||
* undo/redo entries that hangs off the topbar History button.
|
||||
*
|
||||
* Same docking pattern as the FX adjustment popups: drag the head to
|
||||
* reposition, click the minimise button to dock into the modalManager
|
||||
* chip chain, click the chip to restore. Esc closes.
|
||||
*
|
||||
* @param {{
|
||||
* undo: () => void,
|
||||
* redo: () => void,
|
||||
* }} deps
|
||||
*
|
||||
* @returns {{
|
||||
* toggleHistoryPanel: () => void,
|
||||
* refreshHistoryPanelIfOpen: () => void,
|
||||
* jumpToHistory: (offset: number) => void,
|
||||
* }}
|
||||
*/
|
||||
import { state } from './state.js';
|
||||
import modalManager from '../modalManager.js';
|
||||
import { HISTORY_ICON, relTime } from './layer-helpers.js';
|
||||
import { historyPanelHTML } from './build/popups.js';
|
||||
|
||||
export function createHistoryPanel({ undo, redo }) {
|
||||
function jumpToHistory(offset) {
|
||||
if (offset === 0) return;
|
||||
if (offset < 0) {
|
||||
for (let i = 0; i < -offset; i++) undo();
|
||||
} else {
|
||||
for (let i = 0; i < offset; i++) redo();
|
||||
}
|
||||
}
|
||||
|
||||
function closeHistoryPanel() {
|
||||
if (state.historyPanelEl) {
|
||||
if (state.historyPanelEl._escHandler) {
|
||||
document.removeEventListener('keydown', state.historyPanelEl._escHandler, true);
|
||||
}
|
||||
if (state.historyPanelEl._awayHandler) {
|
||||
document.removeEventListener('pointerdown', state.historyPanelEl._awayHandler, true);
|
||||
}
|
||||
state.historyPanelEl.remove();
|
||||
state.historyPanelEl = null;
|
||||
}
|
||||
}
|
||||
|
||||
function minimiseHistoryPanel() {
|
||||
if (!state.historyPanelEl) return;
|
||||
const panel = state.historyPanelEl;
|
||||
const r = panel.getBoundingClientRect();
|
||||
panel._stashLeft = r.left;
|
||||
panel._stashTop = r.top;
|
||||
panel.style.display = 'none';
|
||||
state.historyPanelEl = null;
|
||||
const modalId = panel._modalId || 'ge-history-panel-min';
|
||||
panel._modalId = modalId;
|
||||
modalManager.register(modalId, {
|
||||
label: 'History',
|
||||
icon: HISTORY_ICON,
|
||||
restoreFn: () => {
|
||||
panel.style.left = panel._stashLeft + 'px';
|
||||
panel.style.top = panel._stashTop + 'px';
|
||||
panel.style.display = '';
|
||||
state.historyPanelEl = panel;
|
||||
refreshHistoryPanelIfOpen();
|
||||
},
|
||||
closeFn: () => {
|
||||
panel.remove();
|
||||
modalManager.unregister(modalId);
|
||||
},
|
||||
});
|
||||
modalManager.minimize(modalId);
|
||||
}
|
||||
|
||||
function toggleHistoryPanel() {
|
||||
if (state.historyPanelEl) { closeHistoryPanel(); return; }
|
||||
const panel = document.createElement('div');
|
||||
panel.id = 'ge-history-panel';
|
||||
panel.className = 'ge-frosted';
|
||||
panel.innerHTML = historyPanelHTML(HISTORY_ICON);
|
||||
document.body.appendChild(panel);
|
||||
state.historyPanelEl = panel;
|
||||
const btn = document.getElementById('ge-history-btn');
|
||||
if (btn) {
|
||||
const r = btn.getBoundingClientRect();
|
||||
panel.style.top = (r.bottom + 6) + 'px';
|
||||
panel.style.left = Math.max(8, r.left) + 'px';
|
||||
}
|
||||
panel.querySelector('.ge-adj-min').addEventListener('click', minimiseHistoryPanel);
|
||||
// Click anywhere outside the panel (or trigger button) closes it.
|
||||
setTimeout(() => {
|
||||
const onAway = (ev) => {
|
||||
if (!state.historyPanelEl) return;
|
||||
if (state.historyPanelEl.contains(ev.target)) return;
|
||||
if (btn && (ev.target === btn || btn.contains(ev.target))) return;
|
||||
closeHistoryPanel();
|
||||
document.removeEventListener('pointerdown', onAway, true);
|
||||
};
|
||||
document.addEventListener('pointerdown', onAway, true);
|
||||
panel._awayHandler = onAway;
|
||||
}, 0);
|
||||
|
||||
const head = panel.querySelector('[data-history-drag]');
|
||||
head.addEventListener('pointerdown', (e) => {
|
||||
if (e.target.closest('button')) return;
|
||||
e.preventDefault();
|
||||
const startX = e.clientX, startY = e.clientY;
|
||||
const r0 = panel.getBoundingClientRect();
|
||||
head.setPointerCapture(e.pointerId);
|
||||
head.style.cursor = 'grabbing';
|
||||
const onMove = (ev) => {
|
||||
const nx = Math.max(0, Math.min(window.innerWidth - 60, r0.left + (ev.clientX - startX)));
|
||||
const ny = Math.max(0, Math.min(window.innerHeight - 30, r0.top + (ev.clientY - startY)));
|
||||
panel.style.left = nx + 'px';
|
||||
panel.style.top = ny + 'px';
|
||||
};
|
||||
const onUp = () => {
|
||||
head.releasePointerCapture(e.pointerId);
|
||||
head.style.cursor = '';
|
||||
head.removeEventListener('pointermove', onMove);
|
||||
head.removeEventListener('pointerup', onUp);
|
||||
};
|
||||
head.addEventListener('pointermove', onMove);
|
||||
head.addEventListener('pointerup', onUp);
|
||||
});
|
||||
|
||||
const onKey = (ev) => {
|
||||
if (ev.key === 'Escape') {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
closeHistoryPanel();
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', onKey, true);
|
||||
panel._escHandler = onKey;
|
||||
|
||||
refreshHistoryPanelIfOpen();
|
||||
}
|
||||
|
||||
function refreshHistoryPanelIfOpen() {
|
||||
if (!state.historyPanelEl) return;
|
||||
const list = state.historyPanelEl.querySelector('#ge-history-list');
|
||||
if (!list) return;
|
||||
// Chronological order — oldest at top, latest at bottom. Past
|
||||
// (undo) states first, then Current, then future (redo) states.
|
||||
const rows = [];
|
||||
for (let i = 0; i < state.undoStack.length; i++) {
|
||||
const s = state.undoStack[i];
|
||||
rows.push({ offset: -(state.undoStack.length - i), label: s._label || 'Edit', ts: s._ts });
|
||||
}
|
||||
rows.push({ offset: 0, label: 'Current', ts: Date.now(), current: true });
|
||||
for (let i = state.redoStack.length - 1; i >= 0; i--) {
|
||||
const s = state.redoStack[i];
|
||||
rows.push({ offset: (state.redoStack.length - i), label: s._label || 'Edit', ts: s._ts, future: true });
|
||||
}
|
||||
list.innerHTML = rows.map(r => `
|
||||
<button class="ge-history-row${r.current ? ' current' : ''}${r.future ? ' future' : ''}" data-offset="${r.offset}">
|
||||
<span class="ge-history-row-dot"></span>
|
||||
<span class="ge-history-row-label">${(r.label || '').replace(/[<>&]/g,'')}</span>
|
||||
<span class="ge-history-row-time">${relTime(r.ts)}</span>
|
||||
</button>
|
||||
`).join('');
|
||||
list.querySelectorAll('.ge-history-row').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const off = parseInt(btn.dataset.offset, 10);
|
||||
jumpToHistory(off);
|
||||
});
|
||||
});
|
||||
// Scroll the current marker into view.
|
||||
const cur = list.querySelector('.current');
|
||||
if (cur) cur.scrollIntoView({ block: 'center' });
|
||||
}
|
||||
|
||||
return { toggleHistoryPanel, refreshHistoryPanelIfOpen, jumpToHistory };
|
||||
}
|
||||
Reference in New Issue
Block a user