diff --git a/public/assets/desktop/desktop.css b/public/assets/desktop/desktop.css index 67ccaed3..e3929de3 100644 --- a/public/assets/desktop/desktop.css +++ b/public/assets/desktop/desktop.css @@ -178,6 +178,17 @@ h1 { max-height: calc(100% - 120px); } +.window-resize-handle { + position: absolute; + right: 0; + bottom: 0; + width: 18px; + height: 18px; + cursor: nwse-resize; + background: + linear-gradient(135deg, transparent 0 45%, rgba(100, 116, 139, 0.8) 45% 55%, transparent 55% 100%); +} + .window.is-minimized { display: none; } @@ -199,6 +210,10 @@ h1 { overflow: auto; } +.window.is-maximized .window-resize-handle { + display: none; +} + .window-header { display: flex; justify-content: space-between; diff --git a/public/assets/desktop/desktop.js b/public/assets/desktop/desktop.js index c9c8c4de..14769558 100644 --- a/public/assets/desktop/desktop.js +++ b/public/assets/desktop/desktop.js @@ -16,11 +16,41 @@ if (payloadNode) { const windows = new Map(); const activeWidgets = new Set(payload.widgets.active_ids); const activeSkin = payload.meta.active_skin; + const storageScope = payload.desktop.storage_scope || 'guest'; + const storageKey = `desktop.kusche.berlin.window-state.${storageScope}`; let zIndex = 20; let topDesktopInset = 0; let sideDesktopInset = 0; let bottomDesktopInset = 120; + const loadWindowState = () => { + try { + const raw = window.localStorage.getItem(storageKey); + return raw ? JSON.parse(raw) : {}; + } catch { + return {}; + } + }; + + const saveWindowState = () => { + const snapshot = {}; + + windows.forEach((record) => { + snapshot[record.definition.app_id] = { + layout: record.layout, + state: record.state, + }; + }); + + try { + window.localStorage.setItem(storageKey, JSON.stringify(snapshot)); + } catch { + // Ignore storage failures for now. + } + }; + + const persistedWindowState = loadWindowState(); + const measureDesktopBounds = () => { if (!windowsRoot) { return; @@ -115,6 +145,16 @@ if (payloadNode) { record.node.style.height = `${height}px`; }; + const clampWindowLayout = (record) => { + const maxWidth = Math.max(320, window.innerWidth - 48); + const maxHeight = Math.max(220, window.innerHeight - bottomDesktopInset - 24); + + record.layout.width = Math.max(320, Math.min(record.layout.width, maxWidth)); + record.layout.height = Math.max(220, Math.min(record.layout.height, maxHeight)); + record.layout.x = Math.max(0, Math.min(record.layout.x, window.innerWidth - record.layout.width)); + record.layout.y = Math.max(0, Math.min(record.layout.y, window.innerHeight - bottomDesktopInset - record.layout.height)); + }; + const toggleMinimize = (windowId) => { const record = windows.get(windowId); if (!record) { @@ -129,6 +169,8 @@ if (payloadNode) { } else { syncTaskbar(); } + + saveWindowState(); }; const toggleMaximize = (windowId) => { @@ -149,6 +191,7 @@ if (payloadNode) { ? '100%' : `calc(100% - ${topDesktopInset + bottomDesktopInset}px)`; focusWindow(windowId); + saveWindowState(); return; } @@ -156,9 +199,11 @@ if (payloadNode) { record.node.classList.remove('is-maximized'); if (record.restoreLayout) { record.layout = { ...record.restoreLayout }; + clampWindowLayout(record); applyWindowLayout(record); } focusWindow(windowId); + saveWindowState(); }; const closeWindow = (windowId) => { @@ -170,6 +215,7 @@ if (payloadNode) { windows.delete(windowId); record.node.remove(); syncTaskbar(); + saveWindowState(); }; const syncTaskbar = () => { @@ -238,11 +284,57 @@ if (payloadNode) { record.layout.x = originX + event.clientX - startX; record.layout.y = originY + event.clientY - startY; + clampWindowLayout(record); applyWindowLayout(record); }); header.addEventListener('pointerup', () => { dragging = false; + saveWindowState(); + }); + }; + + const wireResizing = (record) => { + const handle = record.node.querySelector('.window-resize-handle'); + if (!handle || window.matchMedia('(max-width: 1100px)').matches) { + return; + } + + let startX = 0; + let startY = 0; + let originWidth = 0; + let originHeight = 0; + let resizing = false; + + handle.addEventListener('pointerdown', (event) => { + if (record.state.maximized) { + return; + } + + resizing = true; + startX = event.clientX; + startY = event.clientY; + originWidth = record.layout.width; + originHeight = record.layout.height; + handle.setPointerCapture(event.pointerId); + focusWindow(record.id); + event.stopPropagation(); + }); + + handle.addEventListener('pointermove', (event) => { + if (!resizing) { + return; + } + + record.layout.width = originWidth + event.clientX - startX; + record.layout.height = originHeight + event.clientY - startY; + clampWindowLayout(record); + applyWindowLayout(record); + }); + + handle.addEventListener('pointerup', () => { + resizing = false; + saveWindowState(); }); }; @@ -262,27 +354,46 @@ if (payloadNode) {
${definition.content}
Route: ${definition.app_id}
+ `; + const persisted = persistedWindowState[definition.app_id] || {}; + const persistedLayout = persisted.layout || {}; + const persistedState = persisted.state || {}; + const record = { id: definition.window_id, title: definition.title, node, definition, layout: { - x: definition.x, - y: definition.y, - width: definition.width, - height: definition.height, + x: persistedLayout.x ?? definition.x, + y: persistedLayout.y ?? definition.y, + width: persistedLayout.width ?? definition.width, + height: persistedLayout.height ?? definition.height, }, restoreLayout: null, state: { - minimized: false, - maximized: false, + minimized: Boolean(persistedState.minimized), + maximized: Boolean(persistedState.maximized), }, }; + clampWindowLayout(record); applyWindowLayout(record); + if (record.state.minimized) { + record.node.classList.add('is-minimized'); + } + if (record.state.maximized) { + record.node.classList.add('is-maximized'); + measureDesktopBounds(); + record.node.style.left = `${sideDesktopInset}px`; + record.node.style.top = `${topDesktopInset}px`; + record.node.style.width = sideDesktopInset === 0 ? '100%' : `calc(100% - ${sideDesktopInset * 2}px)`; + record.node.style.height = activeSkin === 'apple' + ? '100%' + : `calc(100% - ${topDesktopInset + bottomDesktopInset}px)`; + } node.addEventListener('mousedown', () => focusWindow(record.id)); @@ -310,7 +421,12 @@ if (payloadNode) { windows.set(record.id, record); windowsRoot?.appendChild(node); wireDragging(record); - focusWindow(record.id); + wireResizing(record); + if (!record.state.minimized) { + focusWindow(record.id); + } else { + syncTaskbar(); + } }; const openApp = (app) => { @@ -504,6 +620,8 @@ if (payloadNode) { measureDesktopBounds(); windows.forEach((record) => { + clampWindowLayout(record); + if (record.state.maximized) { record.node.style.left = `${sideDesktopInset}px`; record.node.style.top = `${topDesktopInset}px`; @@ -511,7 +629,12 @@ if (payloadNode) { record.node.style.height = activeSkin === 'apple' ? '100%' : `calc(100% - ${topDesktopInset + bottomDesktopInset}px)`; + return; } + + applyWindowLayout(record); }); + + saveWindowState(); }); } diff --git a/src/App/App.php b/src/App/App.php index 8067acb6..5676f8ec 100644 --- a/src/App/App.php +++ b/src/App/App.php @@ -38,6 +38,7 @@ final class App 'desktop' => [ 'greeting' => 'Neue Desktop-Shell fuer Kusche.Berlin', 'wallpaper' => SkinResolver::wallpaper($activeSkin), + 'storage_scope' => 'guest', 'workspace' => DesktopState::defaultWorkspace(), 'login' => [ 'authenticated' => false,