Desktop
All checks were successful
Deploy / deploy-staging (push) Successful in 8s
Deploy / deploy-production (push) Has been skipped

This commit is contained in:
2026-06-07 01:39:40 +02:00
parent d064f6f81d
commit 8ffaca85fc
3 changed files with 146 additions and 7 deletions

View File

@@ -178,6 +178,17 @@ h1 {
max-height: calc(100% - 120px); 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 { .window.is-minimized {
display: none; display: none;
} }
@@ -199,6 +210,10 @@ h1 {
overflow: auto; overflow: auto;
} }
.window.is-maximized .window-resize-handle {
display: none;
}
.window-header { .window-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;

View File

@@ -16,11 +16,41 @@ if (payloadNode) {
const windows = new Map(); const windows = new Map();
const activeWidgets = new Set(payload.widgets.active_ids); const activeWidgets = new Set(payload.widgets.active_ids);
const activeSkin = payload.meta.active_skin; 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 zIndex = 20;
let topDesktopInset = 0; let topDesktopInset = 0;
let sideDesktopInset = 0; let sideDesktopInset = 0;
let bottomDesktopInset = 120; 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 = () => { const measureDesktopBounds = () => {
if (!windowsRoot) { if (!windowsRoot) {
return; return;
@@ -115,6 +145,16 @@ if (payloadNode) {
record.node.style.height = `${height}px`; 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 toggleMinimize = (windowId) => {
const record = windows.get(windowId); const record = windows.get(windowId);
if (!record) { if (!record) {
@@ -129,6 +169,8 @@ if (payloadNode) {
} else { } else {
syncTaskbar(); syncTaskbar();
} }
saveWindowState();
}; };
const toggleMaximize = (windowId) => { const toggleMaximize = (windowId) => {
@@ -149,6 +191,7 @@ if (payloadNode) {
? '100%' ? '100%'
: `calc(100% - ${topDesktopInset + bottomDesktopInset}px)`; : `calc(100% - ${topDesktopInset + bottomDesktopInset}px)`;
focusWindow(windowId); focusWindow(windowId);
saveWindowState();
return; return;
} }
@@ -156,9 +199,11 @@ if (payloadNode) {
record.node.classList.remove('is-maximized'); record.node.classList.remove('is-maximized');
if (record.restoreLayout) { if (record.restoreLayout) {
record.layout = { ...record.restoreLayout }; record.layout = { ...record.restoreLayout };
clampWindowLayout(record);
applyWindowLayout(record); applyWindowLayout(record);
} }
focusWindow(windowId); focusWindow(windowId);
saveWindowState();
}; };
const closeWindow = (windowId) => { const closeWindow = (windowId) => {
@@ -170,6 +215,7 @@ if (payloadNode) {
windows.delete(windowId); windows.delete(windowId);
record.node.remove(); record.node.remove();
syncTaskbar(); syncTaskbar();
saveWindowState();
}; };
const syncTaskbar = () => { const syncTaskbar = () => {
@@ -238,11 +284,57 @@ if (payloadNode) {
record.layout.x = originX + event.clientX - startX; record.layout.x = originX + event.clientX - startX;
record.layout.y = originY + event.clientY - startY; record.layout.y = originY + event.clientY - startY;
clampWindowLayout(record);
applyWindowLayout(record); applyWindowLayout(record);
}); });
header.addEventListener('pointerup', () => { header.addEventListener('pointerup', () => {
dragging = false; 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) {
<p>${definition.content}</p> <p>${definition.content}</p>
<p><strong>Route:</strong> ${definition.app_id}</p> <p><strong>Route:</strong> ${definition.app_id}</p>
</div> </div>
<div class="window-resize-handle" aria-hidden="true"></div>
`; `;
const persisted = persistedWindowState[definition.app_id] || {};
const persistedLayout = persisted.layout || {};
const persistedState = persisted.state || {};
const record = { const record = {
id: definition.window_id, id: definition.window_id,
title: definition.title, title: definition.title,
node, node,
definition, definition,
layout: { layout: {
x: definition.x, x: persistedLayout.x ?? definition.x,
y: definition.y, y: persistedLayout.y ?? definition.y,
width: definition.width, width: persistedLayout.width ?? definition.width,
height: definition.height, height: persistedLayout.height ?? definition.height,
}, },
restoreLayout: null, restoreLayout: null,
state: { state: {
minimized: false, minimized: Boolean(persistedState.minimized),
maximized: false, maximized: Boolean(persistedState.maximized),
}, },
}; };
clampWindowLayout(record);
applyWindowLayout(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)); node.addEventListener('mousedown', () => focusWindow(record.id));
@@ -310,7 +421,12 @@ if (payloadNode) {
windows.set(record.id, record); windows.set(record.id, record);
windowsRoot?.appendChild(node); windowsRoot?.appendChild(node);
wireDragging(record); wireDragging(record);
focusWindow(record.id); wireResizing(record);
if (!record.state.minimized) {
focusWindow(record.id);
} else {
syncTaskbar();
}
}; };
const openApp = (app) => { const openApp = (app) => {
@@ -504,6 +620,8 @@ if (payloadNode) {
measureDesktopBounds(); measureDesktopBounds();
windows.forEach((record) => { windows.forEach((record) => {
clampWindowLayout(record);
if (record.state.maximized) { if (record.state.maximized) {
record.node.style.left = `${sideDesktopInset}px`; record.node.style.left = `${sideDesktopInset}px`;
record.node.style.top = `${topDesktopInset}px`; record.node.style.top = `${topDesktopInset}px`;
@@ -511,7 +629,12 @@ if (payloadNode) {
record.node.style.height = activeSkin === 'apple' record.node.style.height = activeSkin === 'apple'
? '100%' ? '100%'
: `calc(100% - ${topDesktopInset + bottomDesktopInset}px)`; : `calc(100% - ${topDesktopInset + bottomDesktopInset}px)`;
return;
} }
applyWindowLayout(record);
}); });
saveWindowState();
}); });
} }

View File

@@ -38,6 +38,7 @@ final class App
'desktop' => [ 'desktop' => [
'greeting' => 'Neue Desktop-Shell fuer Kusche.Berlin', 'greeting' => 'Neue Desktop-Shell fuer Kusche.Berlin',
'wallpaper' => SkinResolver::wallpaper($activeSkin), 'wallpaper' => SkinResolver::wallpaper($activeSkin),
'storage_scope' => 'guest',
'workspace' => DesktopState::defaultWorkspace(), 'workspace' => DesktopState::defaultWorkspace(),
'login' => [ 'login' => [
'authenticated' => false, 'authenticated' => false,