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);
}
.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;

View File

@@ -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) {
<p>${definition.content}</p>
<p><strong>Route:</strong> ${definition.app_id}</p>
</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 = {
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();
});
}

View File

@@ -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,