1111 lines
30 KiB
JavaScript
1111 lines
30 KiB
JavaScript
try {
|
||
const isMobile = (
|
||
window.matchMedia("(pointer: coarse)").matches ||
|
||
window.innerWidth <= 768 ||
|
||
/Android|iPhone|iPad|iPod/i.test(navigator.userAgent)
|
||
);
|
||
|
||
let topZ = 100;
|
||
const maximizeIcon = '▢';
|
||
const restoreIcon = '🗗';
|
||
|
||
const startBtn = document.getElementById('start-btn');
|
||
const startMenu = document.getElementById('start-menu');
|
||
const taskbar = document.getElementById('taskbar');
|
||
const windowsContainer = document.getElementById('windows');
|
||
const taskbarWindows = document.getElementById('taskbar-windows');
|
||
const ctx = new ContextMenu();
|
||
const windowCleanup = new Map();
|
||
const username = getCookie('sAMAccountName');
|
||
const LS_KEY = (key) => `${username}:${key}`;
|
||
const MAX_PADDING = { left: 4, top: 4, right: 4, bottom: (56 - 4) };
|
||
|
||
startBtn.addEventListener('click', (evt) => {
|
||
evt.stopPropagation(); // verhindert sofortiges Schließen
|
||
startBtn.classList.toggle('active');
|
||
startMenu.classList.toggle('hidden');
|
||
});
|
||
|
||
function updateStartMenuPosition() {
|
||
const height = taskbar.offsetHeight;
|
||
startMenu.style.bottom = (height + 5) + 'px';
|
||
}
|
||
|
||
const observer = new ResizeObserver(entries => {
|
||
for (let entry of entries) {
|
||
const height = entry.contentRect.height;
|
||
document.documentElement.style.setProperty('--auto-taskbar-height', height + 'px');
|
||
}
|
||
});
|
||
|
||
observer.observe(taskbar);
|
||
|
||
window.addEventListener('resize', updateStartMenuPosition);
|
||
window.addEventListener('load', updateStartMenuPosition);
|
||
|
||
// Launch app when clicking start menu item
|
||
document.addEventListener('click', async (e) => {
|
||
const target = e.target.closest('.start-item');
|
||
const clickedInsideMenu = startMenu.contains(e.target);
|
||
const clickedButton = startBtn.contains(e.target);
|
||
|
||
if(!clickedInsideMenu && !clickedButton) {
|
||
startBtn.classList.remove('active');
|
||
startMenu.classList.add('hidden');
|
||
return;
|
||
}
|
||
if (!target) return;
|
||
|
||
const name = target.dataset.appname;
|
||
const view = target.dataset.appview;
|
||
|
||
|
||
const id = `win-${name}.${view}`;
|
||
const active = target.dataset.active;
|
||
|
||
if(active !== "true") return
|
||
// 👉 WICHTIG: erst prüfen ob Fenster existiert
|
||
const focused = focusWindowById(id);
|
||
if (focused) {
|
||
startMenu.classList.add('hidden');
|
||
return;
|
||
}
|
||
|
||
openApp({
|
||
name,
|
||
view,
|
||
viewLabel: target.dataset.viewlabel
|
||
});
|
||
|
||
startMenu.classList.add('hidden');
|
||
});
|
||
|
||
|
||
/*
|
||
// serverside route
|
||
app.post('/children/Demo/second_frame', async (req, res) => {
|
||
await renderView(app, `${metadata.name}/views/chldren/%name_of_file%`, { ...metadata, tutorial: false }, res)
|
||
});
|
||
|
||
//clientside trigger
|
||
e.g.: onClick: () => openView({ name: 'Demo', view: 'second_frame', viewLabel: 'Second Frame', content: 'Hello world, I\'m the second frame', defaultSize: { width: '900px', height: '300px' } })
|
||
*/
|
||
window.openView = async (payload) => {
|
||
const { name, view, viewLabel } = payload;
|
||
const id = `win-${name}.${view}`;
|
||
const exists = document.querySelector(`[data-winid="${id}"]`);
|
||
|
||
let resume = null;
|
||
|
||
if (exists) {
|
||
resume = {
|
||
left: exists.style.left,
|
||
top: exists.style.top
|
||
};
|
||
exists.remove();
|
||
}
|
||
|
||
const res = await fetch(`/children/${name}/${view}`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload)
|
||
});
|
||
|
||
if (!res.ok) {
|
||
console.warn('Child partial nicht gefunden', `${name}.${view}`);
|
||
return;
|
||
}
|
||
|
||
const html = await res.text();
|
||
|
||
const wrapper = document.createElement('div');
|
||
wrapper.innerHTML = html;
|
||
|
||
const win = wrapper.firstElementChild;
|
||
|
||
win.style.left = resume?.left || (50 + Math.random()*100) + 'px';
|
||
win.style.top = resume?.top || (50 + Math.random()*60) + 'px';
|
||
|
||
const defaultW = payload.defaultSize?.width || 800;
|
||
const defaultH = payload.defaultSize?.height || 600;
|
||
|
||
win.style.width = payload.size?.width || defaultW;
|
||
win.style.height = payload.size?.height || defaultH;
|
||
|
||
win.dataset.winid = id;
|
||
win.dataset.appname = name;
|
||
win.dataset.appview = view;
|
||
win.dataset.viewLabel = viewLabel;
|
||
win.dataset.type = 'view';
|
||
|
||
windowsContainer.appendChild(win);
|
||
|
||
bringToFront(win);
|
||
reloadJS(win);
|
||
wireWindowControls(win);
|
||
}
|
||
|
||
|
||
async function openApp(payload) {
|
||
const { name, view, viewLabel } = payload;
|
||
const saved = JSON.parse(localStorage.getItem(LS_KEY('openWindows')) || '[]')
|
||
|
||
// 🔁 prüfen ob schon offen
|
||
const id = `win-${name}.${view}`;
|
||
const exists = document.querySelector(`[data-winid="${id}"]`);
|
||
if (exists) {
|
||
bringToFront(exists);
|
||
startMenu.classList.add('hidden');
|
||
return;
|
||
}
|
||
|
||
// 📡 Server holen
|
||
const resMeta = await fetch('/api/open_app', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload)
|
||
});
|
||
if (!resMeta.ok) {
|
||
console.warn('open_app failed');
|
||
return;
|
||
}
|
||
|
||
const meta = await resMeta.json();
|
||
const res = await fetch(`/window/${name}/${view}`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload)
|
||
});
|
||
|
||
if (!res.ok) {
|
||
console.warn('Window partial nicht gefunden', `${name}.${view}`);
|
||
return;
|
||
}
|
||
|
||
const html = await res.text();
|
||
|
||
const wrapper = document.createElement('div');
|
||
wrapper.innerHTML = html;
|
||
const win = wrapper.firstElementChild;
|
||
|
||
// 📍 Position
|
||
win.style.left = meta.location?.left || (50 + Math.random()*100) + 'px';
|
||
win.style.top = meta.location?.top || (50 + Math.random()*60) + 'px';
|
||
|
||
// 📐 Größe
|
||
const defaultW = meta.context.defaultSize?.width || 800;
|
||
const defaultH = meta.context.defaultSize?.height || 600;
|
||
|
||
win.style.width = meta.size?.width || defaultW;
|
||
win.style.height = meta.size?.height || defaultH;
|
||
|
||
// 🆔 IDs setzen
|
||
win.dataset.winid = id;
|
||
win.dataset.appname = name;
|
||
win.dataset.appview = view;
|
||
win.dataset.viewLabel = viewLabel;
|
||
win.dataset.type = 'app';
|
||
|
||
windowsContainer.appendChild(win);
|
||
|
||
if (isMobile) {
|
||
const maximizeBtn = win.querySelector('.maximize');
|
||
if (maximizeBtn) maximizeBtn.style.display = 'none';
|
||
}
|
||
|
||
if(!saved.length) {
|
||
payload.zIndex = topZ;
|
||
}
|
||
if (payload.state !== 'minimized') {
|
||
bringToFront(win);
|
||
}
|
||
|
||
if (payload.state !== 'minimized') requestAnimationFrame(() => saveOpenWindows());
|
||
reloadJS(win);
|
||
|
||
// 🧷 Taskbar Button
|
||
const btn = document.createElement('div');
|
||
btn.className = 'taskbar-item focus';
|
||
btn.dataset.winid = id;
|
||
|
||
btn.innerHTML = `${meta.context.menu.label}<br>${
|
||
meta.context.menu.label != viewLabel ? `[${viewLabel}]` : ' '
|
||
}`;
|
||
|
||
btn.oncontextmenu = (evt) => {
|
||
evt.preventDefault();
|
||
if(ctx != null) {
|
||
ctx.setItems([ { label: "Schließen", onClick: () => {
|
||
btn.remove();
|
||
saveOpenWindows()
|
||
handleWindowAction({
|
||
id: win.dataset.winid,
|
||
action: 'close'
|
||
});
|
||
} } ]);
|
||
ctx.show(btn, { position: 'top' });
|
||
}
|
||
}
|
||
|
||
resetFocus(id);
|
||
taskbarWindows.appendChild(btn);
|
||
|
||
wireWindowControls(win, btn);
|
||
restoreWindow(win, meta, btn);
|
||
}
|
||
|
||
|
||
function reloadJS(win) {
|
||
const runtimeId = win.dataset.runtimeId;
|
||
|
||
win.querySelectorAll('script').forEach(oldScript => {
|
||
const script = document.createElement('script');
|
||
|
||
if (!oldScript.src) {
|
||
script.textContent = `(function(runtimeId){${oldScript.textContent}})("${runtimeId}");`;
|
||
}
|
||
|
||
win.appendChild(script);
|
||
oldScript.remove();
|
||
});
|
||
}
|
||
|
||
function handleWindowAction(payload) {
|
||
const win = document.querySelector(`[data-winid="${payload.id}"]`);
|
||
if (!win) return;
|
||
|
||
const tb = document.querySelector(`.taskbar-item[data-winid="${payload.id}"]`);
|
||
|
||
if (tb) resetFocus(payload.id);
|
||
|
||
if (payload.action === 'minimize') {
|
||
win.dataset.state = 'minimized';
|
||
win.style.display = 'none';
|
||
tb?.classList.add('minimized');
|
||
}
|
||
|
||
if (payload.action === 'restore') {
|
||
win.style.display = 'flex';
|
||
win.dataset.state = 'normal';
|
||
bringToFront(win);
|
||
tb?.classList.remove('minimized');
|
||
}
|
||
|
||
if (payload.action === 'close') {
|
||
win.remove();
|
||
tb?.remove();
|
||
}
|
||
|
||
saveOpenWindows();
|
||
}
|
||
|
||
// mainSocket.on('window_action', payload => {
|
||
// // simple forwarding; client-specific behaviour can be implemented
|
||
// const win = document.querySelector(`[data-winid="${payload.id}"]`);
|
||
// if (!win) return;
|
||
|
||
// const tb = document.querySelector(`.taskbar-item[data-winid="${payload.id}"]`);
|
||
|
||
// if(tb) {
|
||
// resetFocus(payload.id);
|
||
// }
|
||
|
||
// if (payload.action === 'minimize') {
|
||
// win.dataset.state = 'minimized';
|
||
// win.style.display = 'none';
|
||
|
||
// if (tb) {
|
||
// tb.classList.add('minimized');
|
||
// }
|
||
|
||
// saveOpenWindows(); // <-- HIER
|
||
// }
|
||
|
||
// if (payload.action === 'restore') {
|
||
// win.style.display = 'flex';
|
||
// win.dataset.state = 'normal';
|
||
// bringToFront();
|
||
// if (tb) {
|
||
// tb.classList.remove('minimized');
|
||
// tb.classList.remove('focus');
|
||
// }
|
||
|
||
// saveOpenWindows(); // <-- HIER
|
||
// }
|
||
// if (payload.action === 'close') {
|
||
// win.remove();
|
||
// if (tb) tb.remove();
|
||
|
||
// saveOpenWindows(); // <-- HIER
|
||
// }
|
||
// });
|
||
|
||
// taskbar click toggles minimize
|
||
// taskbarWindows.addEventListener('click', (e) => {
|
||
// const btn = e.target.closest('.taskbar-item');
|
||
// if (!btn) return;
|
||
|
||
// focusWindowById(btn.dataset.winid);
|
||
// });
|
||
|
||
taskbarWindows.addEventListener('click', (e) => {
|
||
const btn = e.target.closest('.taskbar-item');
|
||
if (!btn) return;
|
||
|
||
const id = btn.dataset.winid;
|
||
const win = document.querySelector(`[data-winid="${id}"]`);
|
||
if (!win) return;
|
||
|
||
const isHidden = win.style.display === 'none';
|
||
const isFocused = btn.classList.contains('focus');
|
||
|
||
// 🔵 1. minimiert → restore + fokus
|
||
if (isHidden) {
|
||
win.style.display = 'flex';
|
||
win.dataset.state = 'normal';
|
||
|
||
bringToFront(win);
|
||
|
||
btn.classList.add('focus');
|
||
btn.classList.remove('minimized');
|
||
|
||
saveOpenWindows();
|
||
return;
|
||
}
|
||
|
||
// 🟢 2. sichtbar + aktiv → MINIMIEREN
|
||
if (isFocused) {
|
||
win.style.display = 'none';
|
||
win.dataset.state = 'minimized';
|
||
|
||
btn.classList.remove('focus');
|
||
btn.classList.add('minimized');
|
||
|
||
saveOpenWindows();
|
||
return;
|
||
}
|
||
|
||
// 🟡 3. sichtbar aber nicht aktiv → FOKUS
|
||
bringToFront(win);
|
||
resetFocus(id);
|
||
|
||
btn.classList.add('focus');
|
||
btn.classList.remove('minimized');
|
||
});
|
||
|
||
|
||
function applyMaximized(win) {
|
||
win.style.left = MAX_PADDING.left + 'px';
|
||
win.style.top = MAX_PADDING.top + 'px';
|
||
win.style.width = `calc(100% - ${MAX_PADDING.left + (MAX_PADDING.right * 1.5)}px)`;
|
||
win.style.height = `calc(100% - ${MAX_PADDING.top + MAX_PADDING.bottom}px)`;
|
||
|
||
win.classList.add('max');
|
||
win.dataset.state = 'maximized';
|
||
}
|
||
|
||
|
||
function focusWindowById(id) {
|
||
const win = document.querySelector(`[data-winid="${id}"]`);
|
||
if (!win) return false;
|
||
|
||
const tb = document.querySelector(`.taskbar-item[data-winid="${id}"]`);
|
||
|
||
// 1. Falls minimiert → wieder anzeigen
|
||
if (win.style.display === 'none') {
|
||
win.style.display = 'flex';
|
||
win.dataset.state = 'normal';
|
||
}
|
||
|
||
// 2. Fokus setzen
|
||
bringToFront(win);
|
||
|
||
if (tb) {
|
||
resetFocus(id);
|
||
tb.classList.add('focus');
|
||
tb.classList.remove('minimized');
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
|
||
function cacheWindowGeometry(win) {
|
||
const rect = win.getBoundingClientRect();
|
||
|
||
win.dataset.lastGeometry = JSON.stringify({
|
||
left: `${rect.left}px`,
|
||
top: `${rect.top}px`,
|
||
width: `${rect.width}px`,
|
||
height: `${rect.height}px`
|
||
});
|
||
}
|
||
|
||
|
||
|
||
function storeNormalGeometry(win) {
|
||
// ❌ NICHT speichern, wenn maximiert oder gesnappt
|
||
if (win.classList.contains('max')) return;
|
||
if (win.dataset.snapped) return;
|
||
|
||
const r = win.getBoundingClientRect();
|
||
win.dataset.normalGeometry = JSON.stringify({
|
||
left: `${r.left}px`,
|
||
top: `${r.top}px`,
|
||
width: `${r.width}px`,
|
||
height: `${r.height}px`,
|
||
});
|
||
}
|
||
|
||
|
||
function makeResizable(win) {
|
||
let resizing = false;
|
||
let dir, startX, startY, startW, startH, startL, startT;
|
||
|
||
const minW = 300;
|
||
const minH = 200;
|
||
|
||
win.querySelectorAll('.window-resize-handle').forEach(handle => {
|
||
handle.addEventListener('mousedown', startResize);
|
||
handle.addEventListener('touchstart', startResize, { passive: false });
|
||
});
|
||
|
||
function startResize(e) {
|
||
e.preventDefault();
|
||
if (win.classList.contains('max')) return;
|
||
|
||
resizing = true;
|
||
dir = e.target.dataset.dir;
|
||
|
||
const r = win.getBoundingClientRect();
|
||
startX = e.clientX || e.touches[0].clientX;
|
||
startY = e.clientY || e.touches[0].clientY;
|
||
startW = r.width;
|
||
startH = r.height;
|
||
startL = r.left;
|
||
startT = r.top;
|
||
|
||
document.addEventListener('mousemove', resize);
|
||
document.addEventListener('mouseup', stopResize);
|
||
document.addEventListener('touchmove', resize, { passive: false });
|
||
document.addEventListener('touchend', stopResize);
|
||
}
|
||
|
||
function resize(e) {
|
||
if (!resizing) return;
|
||
|
||
const x = e.clientX || e.touches[0].clientX;
|
||
const y = e.clientY || e.touches[0].clientY;
|
||
const dx = x - startX;
|
||
const dy = y - startY;
|
||
|
||
if (dir.includes('e')) {
|
||
win.style.width = Math.max(minW, startW + dx) + 'px';
|
||
}
|
||
|
||
if (dir.includes('s')) {
|
||
win.style.height = Math.max(minH, startH + dy) + 'px';
|
||
}
|
||
|
||
if (dir.includes('w')) {
|
||
const w = Math.max(minW, startW - dx);
|
||
win.style.width = w + 'px';
|
||
win.style.left = startL + (startW - w) + 'px';
|
||
}
|
||
|
||
if (dir.includes('n')) {
|
||
const h = Math.max(minH, startH - dy);
|
||
win.style.height = h + 'px';
|
||
win.style.top = startT + (startH - h) + 'px';
|
||
}
|
||
}
|
||
|
||
function stopResize() {
|
||
resizing = false;
|
||
document.removeEventListener('mousemove', resize);
|
||
document.removeEventListener('mouseup', stopResize);
|
||
document.removeEventListener('touchmove', resize);
|
||
document.removeEventListener('touchend', stopResize);
|
||
|
||
storeNormalGeometry(win); // <-- HIER (neu)
|
||
saveOpenWindows();
|
||
}
|
||
}
|
||
|
||
|
||
|
||
function addResizeHandles(win) {
|
||
const dirs = ['n','s','e','w','ne','nw','se','sw'];
|
||
dirs.forEach(dir => {
|
||
const h = document.createElement('div');
|
||
h.classList.add(
|
||
'window-resize-handle',
|
||
`window-resize-${dir}`
|
||
);
|
||
h.dataset.dir = dir;
|
||
win.appendChild(h);
|
||
});
|
||
}
|
||
|
||
|
||
// Simple drag (titlebar)
|
||
function wireWindowControls(win, taskbarBtn = null) {
|
||
const minimize = win.querySelector('.minimize');
|
||
const maximize = win.querySelector('.maximize');
|
||
const close = win.querySelector('.close');
|
||
const titlebar = win.querySelector('.window-titlebar');
|
||
|
||
function isControl(el) {
|
||
return el.closest('.minimize, .maximize, .close, .window-resize-handle');
|
||
}
|
||
|
||
|
||
addResizeHandles(win);
|
||
makeResizable(win);
|
||
|
||
// 🟦 Minimieren
|
||
if(minimize != null) {
|
||
minimize.addEventListener('click', () => {
|
||
cacheWindowGeometry(win); // 🔥 WICHTIG
|
||
win.dataset.state = 'minimized';
|
||
win.style.display = 'none';
|
||
|
||
if (taskbarBtn) {
|
||
taskbarBtn.classList.add('minimized');
|
||
taskbarBtn.classList.remove('focus');
|
||
}
|
||
|
||
saveOpenWindows();
|
||
});
|
||
|
||
}
|
||
|
||
win.addEventListener('mousedown', (e) => {
|
||
if (isControl(e.target)) return;
|
||
if (win.style.display !== 'none') {
|
||
bringToFront(win);
|
||
}
|
||
});
|
||
|
||
win.addEventListener('touchstart', () => {
|
||
if (win.style.display !== 'none') bringToFront(win);
|
||
});
|
||
|
||
|
||
// 🟩 Maximieren / Wiederherstellen
|
||
if(maximize != null) {
|
||
maximize.addEventListener('click', (evt) => {
|
||
if(taskbarBtn != null) {
|
||
taskbarBtn.classList.add('focus');
|
||
maximize.innerHTML = win.dataset.state === 'normal' ? restoreIcon : maximizeIcon;
|
||
resetFocus(win.dataset.winid);
|
||
}
|
||
toggleMaximize();
|
||
saveOpenWindows()
|
||
});
|
||
}
|
||
|
||
// 🟥 Schließen
|
||
if(close != null) {
|
||
close.addEventListener('click', () => {
|
||
destroyWindow(win);
|
||
if (taskbarBtn) taskbarBtn.remove();
|
||
saveOpenWindows();
|
||
handleWindowAction({
|
||
id: win.dataset.winid,
|
||
action: 'close'
|
||
});
|
||
});
|
||
}
|
||
|
||
// 🟨 Doppelklick auf Titelleiste → Maximieren / Wiederherstellen
|
||
let lastTap = 0;
|
||
|
||
if (taskbarBtn !== null) {
|
||
titlebar.addEventListener('dblclick', (evt) => {
|
||
toggleMaximize();
|
||
titlebar.querySelector('.maximize').innerHTML = win.dataset.state === 'maximized' ? restoreIcon : maximizeIcon;
|
||
saveOpenWindows()
|
||
});
|
||
}
|
||
|
||
|
||
titlebar.addEventListener('touchend', (ev) => {
|
||
const current = Date.now();
|
||
const delta = current - lastTap;
|
||
if (delta < 300 && delta > 0) {
|
||
// Double tap → maximize / restore
|
||
titlebar.querySelector('.maximize').innerHTML = win.dataset.state === 'maximized' ? restoreIcon : maximizeIcon;
|
||
toggleMaximize();
|
||
}
|
||
lastTap = current;
|
||
});
|
||
|
||
|
||
function destroyWindow(win) {
|
||
try {
|
||
const runtimeId = win.dataset.runtimeId;
|
||
|
||
// Cleanup ausführen
|
||
if(windowCleanup !== null) {
|
||
if (windowCleanup.has(runtimeId)) {
|
||
windowCleanup.get(runtimeId).forEach(fn => {
|
||
try { fn(); } catch(e) { console.warn(e); }
|
||
});
|
||
windowCleanup.delete(runtimeId);
|
||
}
|
||
}
|
||
|
||
// DOM entfernen
|
||
win.remove()
|
||
} catch (err) {
|
||
alert(err.message)
|
||
}
|
||
|
||
}
|
||
|
||
|
||
// function applyWindowState(win, payload) {
|
||
// const state = payload.state || 'normal';
|
||
|
||
// if (state === 'minimized') {
|
||
// win.style.display = 'none';
|
||
// win.dataset.state = 'minimized';
|
||
// }
|
||
|
||
// if (state === 'maximized') {
|
||
// win.dataset.state = 'maximized';
|
||
// win.classList.add('max');
|
||
|
||
// win.style.left = '8px';
|
||
// win.style.top = '8px';
|
||
// win.style.width = 'calc(100% - 16px)';
|
||
// win.style.height = 'calc(100% - 60px)';
|
||
// }
|
||
|
||
// if (state === 'normal') {
|
||
// win.dataset.state = 'normal';
|
||
// }
|
||
// }
|
||
|
||
|
||
function toggleMaximize() {
|
||
if (!win.classList.contains('max')) {
|
||
// 🔥 VOR dem Maximieren speichern
|
||
storeNormalGeometry(win);
|
||
|
||
applyMaximized(win);
|
||
} else {
|
||
const prev = JSON.parse(win.dataset.normalGeometry || '{}');
|
||
|
||
win.style.left = prev.left || '50px';
|
||
win.style.top = prev.top || '50px';
|
||
win.style.width = prev.width || '800px';
|
||
win.style.height = prev.height || '600px';
|
||
|
||
win.classList.remove('max');
|
||
win.dataset.state = 'normal';
|
||
|
||
// 🔥 WICHTIG: Snap/Drag Reset
|
||
win.dataset.snapped = '';
|
||
}
|
||
|
||
requestAnimationFrame(() => saveOpenWindows());
|
||
}
|
||
|
||
|
||
// 🧲 Snap-to-Side + Drag Handling
|
||
makeDraggableWithSnap(win);
|
||
}
|
||
|
||
function makeDraggableWithSnap(win) {
|
||
if (win.style.display === 'none') return;
|
||
|
||
const title = win.querySelector('.window-titlebar');
|
||
let isDown = false;
|
||
let startX, startY, startLeft, startTop;
|
||
let snapped = false;
|
||
const snapThreshold = 30; // Pixel vom Rand zum Einrasten
|
||
|
||
title.addEventListener('touchstart', (ev) => {
|
||
if (ev.target.closest('.minimize, .maximize, .close')) return;
|
||
isDown = true;
|
||
const t = ev.touches[0];
|
||
startX = t.clientX;
|
||
startY = t.clientY;
|
||
startLeft = parseInt(win.style.left || 0);
|
||
startTop = parseInt(win.style.top || 0);
|
||
document.body.classList.add('dragging');
|
||
}, { passive: true });
|
||
|
||
document.addEventListener('touchmove', (ev) => {
|
||
if (!isDown) return;
|
||
const t = ev.touches[0];
|
||
|
||
const dx = t.clientX - startX;
|
||
const dy = t.clientY - startY;
|
||
|
||
win.style.left = startLeft + dx + 'px';
|
||
win.style.top = startTop + dy + 'px';
|
||
|
||
// Snap
|
||
const screenW = window.innerWidth;
|
||
const screenH = window.innerHeight;
|
||
|
||
if (t.clientX <= snapThreshold) {
|
||
applySnap(win, 'left');
|
||
} else if (t.clientX >= screenW - snapThreshold) {
|
||
applySnap(win, 'right');
|
||
} else if (t.clientY <= snapThreshold) {
|
||
applySnap(win, 'top');
|
||
}
|
||
}, { passive: true });
|
||
|
||
document.addEventListener('touchend', () => {
|
||
isDown = false;
|
||
document.body.classList.remove('dragging');
|
||
saveOpenWindows()
|
||
});
|
||
|
||
title.addEventListener('mousedown', (ev) => {
|
||
if (ev.target.closest('.minimize, .maximize, .close')) return;
|
||
|
||
isDown = true;
|
||
startX = ev.clientX;
|
||
startY = ev.clientY;
|
||
|
||
const wasMax = win.classList.contains('max');
|
||
|
||
let restored = false;
|
||
|
||
const onMove = (moveEv) => {
|
||
if (!wasMax || restored) return;
|
||
|
||
const dy = moveEv.clientY - startY;
|
||
|
||
// 👉 erst wenn 12px NACH UNTEN gezogen
|
||
if (dy > 12) {
|
||
restored = true;
|
||
|
||
// Größe wiederherstellen
|
||
const prev = JSON.parse(win.dataset.normalGeometry || '{}');
|
||
const width = parseFloat(prev.width) || 800;
|
||
const height = parseFloat(prev.height) || 600;
|
||
|
||
let left = moveEv.clientX - width / 2;
|
||
let top = moveEv.clientY - 10;
|
||
|
||
left = Math.max(0, Math.min(left, window.innerWidth - width));
|
||
top = Math.max(0, Math.min(top, window.innerHeight - height));
|
||
|
||
win.style.left = left + 'px';
|
||
win.style.top = top + 'px';
|
||
win.style.width = width + 'px';
|
||
win.style.height = height + 'px';
|
||
|
||
win.classList.remove('max');
|
||
win.dataset.state = 'normal';
|
||
win.dataset.snapped = '';
|
||
|
||
// wichtig: neue drag basis setzen
|
||
startX = moveEv.clientX;
|
||
startY = moveEv.clientY;
|
||
|
||
const rect = win.getBoundingClientRect();
|
||
startLeft = rect.left;
|
||
startTop = rect.top;
|
||
|
||
document.removeEventListener('mousemove', onMove);
|
||
}
|
||
};
|
||
|
||
const onUp = () => {
|
||
document.removeEventListener('mousemove', onMove);
|
||
document.removeEventListener('mouseup', onUp);
|
||
isDown = false;
|
||
document.body.classList.remove('dragging');
|
||
};
|
||
|
||
document.addEventListener('mousemove', onMove);
|
||
document.addEventListener('mouseup', onUp);
|
||
|
||
// 👉 normal drag only if NOT maximized
|
||
if (!wasMax) {
|
||
const rect = win.getBoundingClientRect();
|
||
startLeft = rect.left;
|
||
startTop = rect.top;
|
||
}
|
||
|
||
document.body.classList.add('dragging');
|
||
});
|
||
|
||
|
||
document.addEventListener('mousemove', (ev) => {
|
||
if (!isDown) return;
|
||
|
||
const dx = ev.clientX - startX;
|
||
const dy = ev.clientY - startY;
|
||
|
||
win.style.left = startLeft + dx + 'px';
|
||
win.style.top = startTop + dy + 'px';
|
||
|
||
// Snap to edges
|
||
const screenW = window.innerWidth;
|
||
const screenH = window.innerHeight;
|
||
|
||
if (ev.clientX <= snapThreshold) {
|
||
// Snap left
|
||
applySnap(win, 'left');
|
||
snapped = 'left';
|
||
} else if (ev.clientX >= screenW - snapThreshold) {
|
||
// Snap right
|
||
applySnap(win, 'right');
|
||
snapped = 'right';
|
||
} else if (ev.clientY <= snapThreshold) {
|
||
// Snap top (maximize)
|
||
applySnap(win, 'top');
|
||
win.querySelector('.maximize').innerHTML = '🗗';
|
||
snapped = 'top';
|
||
} else if (snapped && ev.clientX > snapThreshold * 2 && ev.clientX < screenW - snapThreshold * 2) {
|
||
// Wegziehen vom Rand → Restore
|
||
restoreFromSnap(win);
|
||
snapped = false;
|
||
}
|
||
});
|
||
|
||
document.addEventListener('mouseup', () => {
|
||
isDown = false;
|
||
document.body.classList.remove('dragging');
|
||
|
||
saveOpenWindows()
|
||
});
|
||
|
||
function applySnap(win, side) {
|
||
if(win.dataset.type == 'view') return;
|
||
if (!win.dataset.prevstyle) {
|
||
win.dataset.prevstyle = JSON.stringify({
|
||
left: win.style.left,
|
||
top: win.style.top,
|
||
width: `${win.getBoundingClientRect().width}px`,
|
||
height: `${win.getBoundingClientRect().height}px`,
|
||
});
|
||
}
|
||
|
||
win.classList.add('max');
|
||
|
||
|
||
if (side === 'left') {
|
||
win.style.left = '0';
|
||
win.style.top = '8px';
|
||
win.style.width = '50%';
|
||
win.style.height = `calc(100% - 60px)`;
|
||
} else if (side === 'right') {
|
||
win.style.left = '50%';
|
||
win.style.top = '8px';
|
||
win.style.width = '50%';
|
||
win.style.height = `calc(100% - 60px)`;
|
||
} else if (side === 'top') {
|
||
applyMaximized(win);
|
||
}
|
||
|
||
win.dataset.state = side === 'top' ? 'maximized' : 'normal';
|
||
saveOpenWindows(); // <-- HIER (SEHR wichtig!)
|
||
}
|
||
|
||
function restoreFromSnap(win, pointerX, pointerY) {
|
||
// 1️⃣ Alte Normalgröße wiederherstellen
|
||
const prev = JSON.parse(win.dataset.normalGeometry || '{}');
|
||
|
||
const width = parseFloat(prev.width) || 800;
|
||
const height = parseFloat(prev.height) || 600;
|
||
|
||
// 2️⃣ Maus in die Fenster-Mitte setzen
|
||
let left = pointerX - width / 2;
|
||
let top = pointerY - height / 2;
|
||
|
||
// 3️⃣ Fenster darf nicht aus dem Bildschirm rutschen
|
||
const screenW = window.innerWidth;
|
||
const screenH = window.innerHeight;
|
||
left = Math.max(0, Math.min(left, screenW - width));
|
||
top = Math.max(0, Math.min(top, screenH - height));
|
||
|
||
// 4️⃣ Fenster anwenden
|
||
win.style.left = left + 'px';
|
||
win.style.top = top + 'px';
|
||
win.style.width = width + 'px';
|
||
win.style.height = height + 'px';
|
||
|
||
win.classList.remove('max');
|
||
win.querySelector('.maximize').innerHTML = maximizeIcon;
|
||
|
||
win.dataset.state = 'normal';
|
||
win.dataset.snapped = '';
|
||
}
|
||
|
||
|
||
}
|
||
|
||
|
||
function bringToFront(win) {
|
||
// Wenn Fenster minimiert → nix machen
|
||
if (win.style.display === 'none') return;
|
||
const tb = document.querySelector(`.taskbar-item[data-winid="${win.dataset.winid}"]`);
|
||
if(tb) {
|
||
tb.classList.add('focus');
|
||
resetFocus(win.dataset.winid);
|
||
saveOpenWindows();
|
||
}
|
||
const currentZ = parseInt(win.style.zIndex || 0);
|
||
if (currentZ < topZ) {
|
||
topZ += 1;
|
||
win.style.zIndex = topZ;
|
||
}
|
||
saveOpenWindows();
|
||
}
|
||
|
||
function resetFocus(exceptID) {
|
||
Array.from(document.querySelectorAll('.taskbar-item')).filter(btn => btn.dataset.winid != exceptID).forEach(btn => {
|
||
btn.classList.remove('focus')
|
||
})
|
||
}
|
||
|
||
function restoreWindow(win, payload, taskbarBtn = null) {
|
||
const state = payload.state || 'normal';
|
||
|
||
if (payload.location) {
|
||
win.style.left = payload.location.left;
|
||
win.style.top = payload.location.top;
|
||
}
|
||
if (payload.size) {
|
||
win.style.width = payload.size.width;
|
||
win.style.height = payload.size.height;
|
||
}
|
||
|
||
// zIndex wiederherstellen
|
||
if (payload.zIndex) {
|
||
win.style.zIndex = payload.zIndex;
|
||
topZ = Math.max(topZ, payload.zIndex); // topZ anpassen, damit bringToFront funktioniert
|
||
}
|
||
|
||
win.dataset.state = state;
|
||
|
||
if (state === 'minimized') {
|
||
win.style.display = 'none';
|
||
if (taskbarBtn) {
|
||
taskbarBtn.classList.add('minimized');
|
||
taskbarBtn.classList.remove('focus');
|
||
}
|
||
} else {
|
||
win.style.display = 'flex';
|
||
win.querySelector('.maximize').innerHTML = win.dataset.state === 'maximized' ? restoreIcon : maximizeIcon;
|
||
}
|
||
|
||
if (state === 'maximized') {
|
||
win.classList.add('max');
|
||
}
|
||
return;
|
||
}
|
||
|
||
|
||
function saveOpenWindows() {
|
||
let previous = ((v)=>Array.isArray(v)?v:[])(JSON.parse(localStorage.getItem(LS_KEY('openWindows')) || '[]'));
|
||
const openWindows = [];
|
||
|
||
document.querySelectorAll('[data-winid]').forEach(win => {
|
||
if (win.dataset.type !== 'app') return;
|
||
if (!win.dataset.appname || !win.dataset.appview) return;
|
||
|
||
let state = win.dataset.state || 'normal';
|
||
let geometry;
|
||
|
||
if (win.classList.contains('max') && state === 'normal') {
|
||
state = 'maximized';
|
||
}
|
||
|
||
const prev = previous.length == 0 ? {} : previous.find(w =>
|
||
w.name === win.dataset.appname &&
|
||
w.view === win.dataset.appview
|
||
);
|
||
|
||
if (win.dataset.state === 'minimized' && prev?.location && prev?.size) {
|
||
// 🟡 Minimiert → alte gespeicherte Geometrie benutzen
|
||
geometry = {
|
||
left: prev.location.left,
|
||
top: prev.location.top,
|
||
width: prev.size.width,
|
||
height: prev.size.height
|
||
};
|
||
} else {
|
||
// 🟢 Sichtbar → echte Größe messen
|
||
const rect = win.getBoundingClientRect();
|
||
geometry = {
|
||
left: win.style.left,
|
||
top: win.style.top,
|
||
width: win.style.width,
|
||
height: win.style.height
|
||
};
|
||
}
|
||
|
||
// letzte gültige Geometrie merken
|
||
win.dataset.lastGeometry = JSON.stringify(geometry);
|
||
|
||
|
||
//alert(win.dataset.appname + ': ' + state)
|
||
const entry = {
|
||
name: win.dataset.appname,
|
||
view: win.dataset.appview,
|
||
viewLabel: win.dataset.viewLabel,
|
||
state: state,
|
||
zIndex: parseInt(win.style.zIndex || 0)
|
||
};
|
||
|
||
if (win.dataset.state !== 'minimized') {
|
||
entry.location = { left: geometry.left, top: geometry.top };
|
||
entry.size = { width: geometry.width, height: geometry.height };
|
||
} else if (prev?.location && prev?.size) {
|
||
// minimiert → alte geometry trotzdem behalten
|
||
entry.location = prev.location;
|
||
entry.size = prev.size;
|
||
}
|
||
// alert(previous.find(w => w.name === entry.name && w.view === entry.view));
|
||
if(previous.find(w => w.name === entry.name && w.view === entry.view)) {
|
||
// 🔁 In previous ersetzen
|
||
previous[previous.findIndex(w => w.name === entry.name && w.view === entry.view)] = entry;
|
||
} else {
|
||
// ➕ Neu hinzufügen
|
||
previous.push(entry);
|
||
}
|
||
openWindows.push(entry);
|
||
|
||
});
|
||
localStorage.setItem(LS_KEY('openWindows'), JSON.stringify(openWindows));
|
||
}
|
||
|
||
|
||
|
||
window.addEventListener('DOMContentLoaded', () => {
|
||
let saved = JSON.parse(localStorage.getItem(LS_KEY('openWindows')) || '[]')
|
||
|
||
if (saved.length > 0) {
|
||
saved.sort((a,b) => (Number(a.zIndex) || 0) - (Number(b.zIndex) || 0));
|
||
for (const payload of saved) {
|
||
// mainSocket.emit('open_app', payload);
|
||
openApp(payload);
|
||
}
|
||
}
|
||
|
||
if (savedFontFamily) { setFontFamily(savedFontFamily); }
|
||
else { setFontFamily('Arial'); }
|
||
|
||
if (savedFontSize) { setFontSize(savedFontSize); }
|
||
else { setFontSize(18); }
|
||
|
||
if (savedTheme) { loadServerStyles(savedTheme); }
|
||
else { loadServerStyles('dark'); }
|
||
});
|
||
|
||
|
||
} catch( err ) {
|
||
alert(err)
|
||
}
|
||
|