Desktop
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user