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