desktop
This commit is contained in:
@@ -109,16 +109,16 @@ h1 {
|
||||
|
||||
.desktop-icons {
|
||||
position: absolute;
|
||||
top: var(--icon-top);
|
||||
left: var(--icon-left);
|
||||
inset: 0;
|
||||
z-index: 10;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
|
||||
gap: 16px;
|
||||
width: min(720px, calc(100% - 80px));
|
||||
}
|
||||
|
||||
.desktop-icons.is-arranging {
|
||||
cursor: progress;
|
||||
}
|
||||
|
||||
.desktop-icon {
|
||||
position: absolute;
|
||||
width: 96px;
|
||||
padding: 12px 10px;
|
||||
border: 1px solid transparent;
|
||||
@@ -126,7 +126,9 @@ h1 {
|
||||
text-align: center;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
cursor: pointer;
|
||||
transition: transform 140ms ease, background-color 140ms ease, border-color 140ms ease;
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
transition: transform 140ms ease, background-color 140ms ease, border-color 140ms ease, box-shadow 140ms ease;
|
||||
}
|
||||
|
||||
.desktop-icon:hover {
|
||||
@@ -135,6 +137,13 @@ h1 {
|
||||
border-color: var(--shell-border);
|
||||
}
|
||||
|
||||
.desktop-icon.is-dragging {
|
||||
transform: scale(1.03);
|
||||
box-shadow: 0 16px 36px rgba(15, 23, 42, 0.28);
|
||||
background: rgba(255, 255, 255, 0.16);
|
||||
border-color: rgba(255, 255, 255, 0.28);
|
||||
}
|
||||
|
||||
.desktop-icon-badge {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
@@ -154,11 +163,10 @@ h1 {
|
||||
padding: 3px 8px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
color: #ffffff;
|
||||
color: var(--desktop-icon-label-color, #0f172a);
|
||||
line-height: 1.25;
|
||||
text-shadow: none;
|
||||
mix-blend-mode: difference;
|
||||
isolation: isolate;
|
||||
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.22);
|
||||
transition: color 140ms ease, text-shadow 140ms ease;
|
||||
}
|
||||
|
||||
.desktop-icon-label::before {
|
||||
@@ -167,9 +175,56 @@ h1 {
|
||||
inset: -2px -4px;
|
||||
z-index: -1;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
background: var(--desktop-icon-label-bg, rgba(255, 255, 255, 0.48));
|
||||
backdrop-filter: blur(8px) saturate(1.08);
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
|
||||
box-shadow: inset 0 0 0 1px var(--desktop-icon-label-border, rgba(255, 255, 255, 0.12));
|
||||
}
|
||||
|
||||
.desktop-icon.is-contrast-light .desktop-icon-label {
|
||||
--desktop-icon-label-color: #f8fafc;
|
||||
--desktop-icon-label-bg: rgba(15, 23, 42, 0.44);
|
||||
--desktop-icon-label-border: rgba(255, 255, 255, 0.12);
|
||||
text-shadow: 0 1px 2px rgba(2, 6, 23, 0.38);
|
||||
}
|
||||
|
||||
.desktop-icon.is-contrast-dark .desktop-icon-label {
|
||||
--desktop-icon-label-color: #0f172a;
|
||||
--desktop-icon-label-bg: rgba(255, 255, 255, 0.52);
|
||||
--desktop-icon-label-border: rgba(255, 255, 255, 0.22);
|
||||
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.28);
|
||||
}
|
||||
|
||||
.desktop-context-menu {
|
||||
position: fixed;
|
||||
z-index: 180;
|
||||
min-width: 220px;
|
||||
padding: 10px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
background: rgba(20, 28, 45, 0.82);
|
||||
color: #f8fafc;
|
||||
backdrop-filter: blur(20px);
|
||||
box-shadow: 0 20px 48px rgba(15, 23, 42, 0.32);
|
||||
}
|
||||
|
||||
.desktop-context-menu[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.desktop-context-menu-button {
|
||||
width: 100%;
|
||||
padding: 11px 12px;
|
||||
border: 0;
|
||||
border-radius: 12px;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
text-align: left;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.desktop-context-menu-button:hover {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.app-icon-image,
|
||||
@@ -784,12 +839,24 @@ h1 {
|
||||
|
||||
.desktop-icons {
|
||||
position: relative;
|
||||
top: auto;
|
||||
left: auto;
|
||||
inset: auto;
|
||||
margin: 24px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
|
||||
gap: 16px;
|
||||
width: calc(100% - 48px);
|
||||
}
|
||||
|
||||
.desktop-icon {
|
||||
position: relative;
|
||||
left: auto !important;
|
||||
top: auto !important;
|
||||
}
|
||||
|
||||
.desktop-context-menu {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.widget-zone {
|
||||
position: static;
|
||||
width: 100%;
|
||||
|
||||
@@ -2,6 +2,7 @@ const payloadNode = document.getElementById('desktop-payload');
|
||||
|
||||
if (payloadNode) {
|
||||
const payload = JSON.parse(payloadNode.textContent);
|
||||
const desktopStage = document.querySelector('.desktop-stage');
|
||||
const iconsRoot = document.getElementById('desktop-icons');
|
||||
const widgetZoneRoot = document.getElementById('widget-zone');
|
||||
const windowsRoot = document.getElementById('window-layer');
|
||||
@@ -22,11 +23,14 @@ if (payloadNode) {
|
||||
const maximizeOverSystemBar = Boolean(skinProfile.maximize_over_system_bar);
|
||||
const storageScope = payload.desktop.storage_scope || 'guest';
|
||||
const storageKey = `desktop.kusche.berlin.window-state.${storageScope}`;
|
||||
const iconStorageKey = `desktop.kusche.berlin.icon-state.${storageScope}.${activeSkin}`;
|
||||
let zIndex = 20;
|
||||
let topDesktopInset = 0;
|
||||
let sideDesktopInset = 0;
|
||||
let bottomDesktopInset = 120;
|
||||
let iconContextMenu = null;
|
||||
const windowControlsMode = document.body.dataset.windowControls || 'windows';
|
||||
let wallpaperImagePromise = null;
|
||||
|
||||
const escapeHtml = (value) => String(value)
|
||||
.replaceAll('&', '&')
|
||||
@@ -45,6 +49,122 @@ if (payloadNode) {
|
||||
return `<span class="app-icon-fallback${safeClassName}">${escapeHtml(app.icon || 'AP')}</span>`;
|
||||
};
|
||||
|
||||
const extractWallpaperImageUrl = () => {
|
||||
const wallpaper = getComputedStyle(document.body).getPropertyValue('--wallpaper') || '';
|
||||
const match = wallpaper.match(/url\((['"]?)(.*?)\1\)/i);
|
||||
return match && match[2] ? match[2] : null;
|
||||
};
|
||||
|
||||
const loadWallpaperImage = () => {
|
||||
const wallpaperUrl = extractWallpaperImageUrl();
|
||||
|
||||
if (!wallpaperUrl) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
if (wallpaperImagePromise) {
|
||||
return wallpaperImagePromise;
|
||||
}
|
||||
|
||||
wallpaperImagePromise = new Promise((resolve) => {
|
||||
const image = new Image();
|
||||
image.decoding = 'async';
|
||||
image.crossOrigin = 'anonymous';
|
||||
image.onload = () => resolve(image);
|
||||
image.onerror = () => resolve(null);
|
||||
image.src = wallpaperUrl;
|
||||
});
|
||||
|
||||
return wallpaperImagePromise;
|
||||
};
|
||||
|
||||
const computeCoverSamplePoint = (image, viewportX, viewportY) => {
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
const scale = Math.max(viewportWidth / image.naturalWidth, viewportHeight / image.naturalHeight);
|
||||
const renderWidth = image.naturalWidth * scale;
|
||||
const renderHeight = image.naturalHeight * scale;
|
||||
const offsetX = (viewportWidth - renderWidth) / 2;
|
||||
const offsetY = (viewportHeight - renderHeight) / 2;
|
||||
const imageX = Math.max(0, Math.min(image.naturalWidth - 1, Math.round((viewportX - offsetX) / scale)));
|
||||
const imageY = Math.max(0, Math.min(image.naturalHeight - 1, Math.round((viewportY - offsetY) / scale)));
|
||||
|
||||
return { imageX, imageY };
|
||||
};
|
||||
|
||||
const createWallpaperSampler = (image) => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = image.naturalWidth;
|
||||
canvas.height = image.naturalHeight;
|
||||
const context = canvas.getContext('2d', { willReadFrequently: true });
|
||||
|
||||
if (!context) {
|
||||
return null;
|
||||
}
|
||||
|
||||
context.drawImage(image, 0, 0);
|
||||
|
||||
const sampleLuminance = (rect) => {
|
||||
const samplePoints = [
|
||||
[rect.left + rect.width * 0.2, rect.top + rect.height * 0.2],
|
||||
[rect.left + rect.width * 0.8, rect.top + rect.height * 0.2],
|
||||
[rect.left + rect.width * 0.5, rect.top + rect.height * 0.5],
|
||||
[rect.left + rect.width * 0.2, rect.top + rect.height * 0.8],
|
||||
[rect.left + rect.width * 0.8, rect.top + rect.height * 0.8],
|
||||
];
|
||||
|
||||
let luminanceTotal = 0;
|
||||
|
||||
samplePoints.forEach(([x, y]) => {
|
||||
const { imageX, imageY } = computeCoverSamplePoint(image, x, y);
|
||||
const pixel = context.getImageData(imageX, imageY, 1, 1).data;
|
||||
luminanceTotal += (0.2126 * pixel[0]) + (0.7152 * pixel[1]) + (0.0722 * pixel[2]);
|
||||
});
|
||||
|
||||
return luminanceTotal / samplePoints.length;
|
||||
};
|
||||
|
||||
return { sampleLuminance };
|
||||
};
|
||||
|
||||
const applyDesktopIconContrast = async () => {
|
||||
const icons = Array.from(document.querySelectorAll('.desktop-icon'));
|
||||
|
||||
if (icons.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const image = await loadWallpaperImage();
|
||||
|
||||
if (!image) {
|
||||
icons.forEach((icon) => {
|
||||
icon.classList.remove('is-contrast-light');
|
||||
icon.classList.add('is-contrast-dark');
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const sampler = createWallpaperSampler(image);
|
||||
|
||||
if (!sampler) {
|
||||
return;
|
||||
}
|
||||
|
||||
icons.forEach((icon) => {
|
||||
const label = icon.querySelector('.desktop-icon-label');
|
||||
|
||||
if (!(label instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = label.getBoundingClientRect();
|
||||
const luminance = sampler.sampleLuminance(rect);
|
||||
const useDarkText = luminance >= 150;
|
||||
icon.classList.toggle('is-contrast-dark', useDarkText);
|
||||
icon.classList.toggle('is-contrast-light', !useDarkText);
|
||||
});
|
||||
};
|
||||
|
||||
const updateSessionDisplay = () => {
|
||||
const accountNode = document.querySelector('.tray-pill[data-tray-id="account"]');
|
||||
|
||||
@@ -166,6 +286,43 @@ if (payloadNode) {
|
||||
};
|
||||
|
||||
const persistedWindowState = loadWindowState();
|
||||
const loadIconState = () => {
|
||||
try {
|
||||
const raw = window.localStorage.getItem(iconStorageKey);
|
||||
const parsed = raw ? JSON.parse(raw) : null;
|
||||
|
||||
if (!parsed || typeof parsed !== 'object') {
|
||||
return {
|
||||
mode: 'auto',
|
||||
positions: {},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
mode: parsed.mode === 'manual' ? 'manual' : 'auto',
|
||||
positions: typeof parsed.positions === 'object' && parsed.positions !== null ? parsed.positions : {},
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
mode: 'auto',
|
||||
positions: {},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const persistedIconState = loadIconState();
|
||||
const iconState = {
|
||||
mode: persistedIconState.mode,
|
||||
positions: { ...persistedIconState.positions },
|
||||
};
|
||||
|
||||
const saveIconState = () => {
|
||||
try {
|
||||
window.localStorage.setItem(iconStorageKey, JSON.stringify(iconState));
|
||||
} catch {
|
||||
// Ignore storage failures for now.
|
||||
}
|
||||
};
|
||||
|
||||
const measureDesktopBounds = () => {
|
||||
if (!windowsRoot) {
|
||||
@@ -185,6 +342,137 @@ if (payloadNode) {
|
||||
: 120;
|
||||
};
|
||||
|
||||
const shouldUseDesktopGrid = () => !window.matchMedia('(max-width: 1100px)').matches;
|
||||
|
||||
const getDesktopStageRect = () => desktopStage instanceof HTMLElement
|
||||
? desktopStage.getBoundingClientRect()
|
||||
: new DOMRect(0, 0, window.innerWidth, window.innerHeight);
|
||||
|
||||
const getIconMetrics = () => ({
|
||||
width: 112,
|
||||
height: 108,
|
||||
marginX: 18,
|
||||
marginY: 18,
|
||||
});
|
||||
|
||||
const getIconLayoutBounds = () => {
|
||||
const stageRect = getDesktopStageRect();
|
||||
const { width, height, marginX, marginY } = getIconMetrics();
|
||||
const top = topDesktopInset + Math.max(24, marginY);
|
||||
const left = Math.max(24, marginX);
|
||||
const right = Math.max(24, marginX);
|
||||
const bottom = Math.max(24, bottomDesktopInset + 12);
|
||||
const availableWidth = Math.max(width, Math.round(stageRect.width - left - right));
|
||||
const availableHeight = Math.max(height, Math.round(stageRect.height - top - bottom));
|
||||
|
||||
return {
|
||||
top,
|
||||
left,
|
||||
right,
|
||||
bottom,
|
||||
width,
|
||||
height,
|
||||
availableWidth,
|
||||
availableHeight,
|
||||
maxX: Math.max(left, Math.round(stageRect.width - right - width)),
|
||||
maxY: Math.max(top, Math.round(stageRect.height - bottom - height)),
|
||||
};
|
||||
};
|
||||
|
||||
const buildAutoIconPositions = () => {
|
||||
const bounds = getIconLayoutBounds();
|
||||
const metrics = getIconMetrics();
|
||||
const columns = Math.max(1, Math.floor((bounds.availableWidth + metrics.marginX) / (metrics.width + metrics.marginX)));
|
||||
const rows = Math.max(1, Math.floor((bounds.availableHeight + metrics.marginY) / (metrics.height + metrics.marginY)));
|
||||
const rightAligned = activeSkin === 'apple' || skinProfile.family === 'apple';
|
||||
const positions = {};
|
||||
|
||||
payload.apps.forEach((app, index) => {
|
||||
const column = Math.floor(index / rows);
|
||||
const row = index % rows;
|
||||
const x = rightAligned
|
||||
? bounds.maxX - (column * (metrics.width + metrics.marginX))
|
||||
: bounds.left + (column * (metrics.width + metrics.marginX));
|
||||
const y = bounds.top + (row * (metrics.height + metrics.marginY));
|
||||
|
||||
positions[app.app_id] = {
|
||||
x: Math.max(bounds.left, Math.min(bounds.maxX, x)),
|
||||
y: Math.max(bounds.top, Math.min(bounds.maxY, y)),
|
||||
};
|
||||
});
|
||||
|
||||
return positions;
|
||||
};
|
||||
|
||||
const clampIconPosition = (position) => {
|
||||
const bounds = getIconLayoutBounds();
|
||||
|
||||
return {
|
||||
x: Math.max(bounds.left, Math.min(bounds.maxX, Math.round(position.x))),
|
||||
y: Math.max(bounds.top, Math.min(bounds.maxY, Math.round(position.y))),
|
||||
};
|
||||
};
|
||||
|
||||
const applyIconPosition = (icon, position) => {
|
||||
const next = clampIconPosition(position);
|
||||
icon.style.left = `${next.x}px`;
|
||||
icon.style.top = `${next.y}px`;
|
||||
};
|
||||
|
||||
const getIconPositionMap = () => {
|
||||
if (iconState.mode === 'manual') {
|
||||
const autoPositions = buildAutoIconPositions();
|
||||
const positions = {};
|
||||
|
||||
payload.apps.forEach((app) => {
|
||||
positions[app.app_id] = clampIconPosition(iconState.positions[app.app_id] || autoPositions[app.app_id] || { x: 24, y: 24 });
|
||||
});
|
||||
|
||||
return positions;
|
||||
}
|
||||
|
||||
return buildAutoIconPositions();
|
||||
};
|
||||
|
||||
const applyStoredIconLayout = () => {
|
||||
if (!iconsRoot) {
|
||||
return;
|
||||
}
|
||||
|
||||
const icons = Array.from(iconsRoot.querySelectorAll('.desktop-icon'));
|
||||
|
||||
if (!shouldUseDesktopGrid()) {
|
||||
icons.forEach((icon) => {
|
||||
icon.style.left = '';
|
||||
icon.style.top = '';
|
||||
});
|
||||
applyDesktopIconContrast();
|
||||
return;
|
||||
}
|
||||
|
||||
const positions = getIconPositionMap();
|
||||
|
||||
icons.forEach((icon) => {
|
||||
const appId = icon.dataset.appId;
|
||||
const position = appId ? positions[appId] : null;
|
||||
|
||||
if (!position) {
|
||||
return;
|
||||
}
|
||||
|
||||
applyIconPosition(icon, position);
|
||||
});
|
||||
|
||||
applyDesktopIconContrast();
|
||||
};
|
||||
|
||||
const resetIconsToAutoLayout = () => {
|
||||
iconState.mode = 'auto';
|
||||
iconState.positions = {};
|
||||
saveIconState();
|
||||
applyStoredIconLayout();
|
||||
};
|
||||
|
||||
const setStartMenuOpen = (isOpen) => {
|
||||
if (!startMenu) {
|
||||
return;
|
||||
@@ -199,6 +487,60 @@ if (payloadNode) {
|
||||
startButton?.setAttribute('aria-expanded', String(isOpen));
|
||||
};
|
||||
|
||||
const ensureDesktopContextMenu = () => {
|
||||
if (iconContextMenu || !(desktopStage instanceof HTMLElement)) {
|
||||
return iconContextMenu;
|
||||
}
|
||||
|
||||
const menu = document.createElement('div');
|
||||
menu.className = 'desktop-context-menu';
|
||||
menu.hidden = true;
|
||||
menu.innerHTML = `
|
||||
<button class="desktop-context-menu-button" type="button" data-action="auto-arrange-icons">
|
||||
Symbole automatisch anordnen
|
||||
</button>
|
||||
`;
|
||||
|
||||
menu.addEventListener('click', (event) => {
|
||||
const action = event.target instanceof Element
|
||||
? event.target.closest('.desktop-context-menu-button')?.dataset.action
|
||||
: null;
|
||||
|
||||
if (action === 'auto-arrange-icons') {
|
||||
resetIconsToAutoLayout();
|
||||
}
|
||||
|
||||
menu.hidden = true;
|
||||
});
|
||||
|
||||
desktopStage.appendChild(menu);
|
||||
iconContextMenu = menu;
|
||||
return menu;
|
||||
};
|
||||
|
||||
const closeDesktopContextMenu = () => {
|
||||
if (iconContextMenu) {
|
||||
iconContextMenu.hidden = true;
|
||||
}
|
||||
};
|
||||
|
||||
const openDesktopContextMenu = (clientX, clientY) => {
|
||||
const menu = ensureDesktopContextMenu();
|
||||
|
||||
if (!(menu instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
menu.hidden = false;
|
||||
|
||||
const menuRect = menu.getBoundingClientRect();
|
||||
const maxLeft = Math.max(12, window.innerWidth - menuRect.width - 12);
|
||||
const maxTop = Math.max(12, window.innerHeight - menuRect.height - 12);
|
||||
|
||||
menu.style.left = `${Math.min(clientX, maxLeft)}px`;
|
||||
menu.style.top = `${Math.min(clientY, maxTop)}px`;
|
||||
};
|
||||
|
||||
const renderClock = () => {
|
||||
const formatted = new Intl.DateTimeFormat('de-DE', {
|
||||
hour: '2-digit',
|
||||
@@ -489,6 +831,89 @@ if (payloadNode) {
|
||||
});
|
||||
};
|
||||
|
||||
const wireDesktopIconDragging = (icon, app) => {
|
||||
let startX = 0;
|
||||
let startY = 0;
|
||||
let originX = 0;
|
||||
let originY = 0;
|
||||
let dragging = false;
|
||||
let moved = false;
|
||||
|
||||
icon.addEventListener('pointerdown', (event) => {
|
||||
if (event.button !== 0 || !shouldUseDesktopGrid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
startX = event.clientX;
|
||||
startY = event.clientY;
|
||||
originX = parseFloat(icon.style.left || '0');
|
||||
originY = parseFloat(icon.style.top || '0');
|
||||
dragging = true;
|
||||
moved = false;
|
||||
icon.classList.add('is-dragging');
|
||||
icon.setPointerCapture(event.pointerId);
|
||||
closeDesktopContextMenu();
|
||||
});
|
||||
|
||||
icon.addEventListener('pointermove', (event) => {
|
||||
if (!dragging) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextX = originX + (event.clientX - startX);
|
||||
const nextY = originY + (event.clientY - startY);
|
||||
const distance = Math.abs(event.clientX - startX) + Math.abs(event.clientY - startY);
|
||||
|
||||
if (distance > 4) {
|
||||
moved = true;
|
||||
}
|
||||
|
||||
applyIconPosition(icon, {
|
||||
x: nextX,
|
||||
y: nextY,
|
||||
});
|
||||
});
|
||||
|
||||
icon.addEventListener('pointerup', () => {
|
||||
if (!dragging) {
|
||||
return;
|
||||
}
|
||||
|
||||
dragging = false;
|
||||
icon.classList.remove('is-dragging');
|
||||
|
||||
if (moved) {
|
||||
const nextPosition = clampIconPosition({
|
||||
x: parseFloat(icon.style.left || '0'),
|
||||
y: parseFloat(icon.style.top || '0'),
|
||||
});
|
||||
|
||||
iconState.mode = 'manual';
|
||||
iconState.positions[app.app_id] = nextPosition;
|
||||
saveIconState();
|
||||
applyIconPosition(icon, nextPosition);
|
||||
icon.dataset.suppressClick = 'true';
|
||||
applyDesktopIconContrast();
|
||||
}
|
||||
});
|
||||
|
||||
icon.addEventListener('pointercancel', () => {
|
||||
dragging = false;
|
||||
icon.classList.remove('is-dragging');
|
||||
});
|
||||
|
||||
icon.addEventListener('click', (event) => {
|
||||
if (icon.dataset.suppressClick === 'true') {
|
||||
icon.dataset.suppressClick = 'false';
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
openApp(app);
|
||||
});
|
||||
};
|
||||
|
||||
const createWindow = (definition) => {
|
||||
const node = document.createElement('article');
|
||||
node.className = 'window';
|
||||
@@ -763,17 +1188,43 @@ if (payloadNode) {
|
||||
payload.apps.forEach((app) => {
|
||||
const icon = document.createElement('button');
|
||||
icon.type = 'button';
|
||||
icon.className = 'desktop-icon';
|
||||
icon.className = 'desktop-icon is-contrast-dark';
|
||||
icon.dataset.appId = app.app_id;
|
||||
icon.innerHTML = `
|
||||
<span class="desktop-icon-badge">${buildAppIconMarkup(app, 'desktop-app-icon')}</span>
|
||||
<span class="desktop-icon-label">${app.title}</span>
|
||||
`;
|
||||
icon.addEventListener('click', () => openApp(app));
|
||||
wireDesktopIconDragging(icon, app);
|
||||
iconsRoot?.appendChild(icon);
|
||||
});
|
||||
|
||||
updateSessionDisplay();
|
||||
|
||||
desktopStage?.addEventListener('contextmenu', (event) => {
|
||||
if (!shouldUseDesktopGrid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.target instanceof Element && event.target.closest('.window, .taskbar, .start-menu, .system-bar')) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
openDesktopContextMenu(event.clientX, event.clientY);
|
||||
});
|
||||
|
||||
document.addEventListener('click', (event) => {
|
||||
if (!(event.target instanceof Element) || !event.target.closest('.desktop-context-menu')) {
|
||||
closeDesktopContextMenu();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
closeDesktopContextMenu();
|
||||
}
|
||||
});
|
||||
|
||||
startButton?.setAttribute('aria-expanded', 'false');
|
||||
startButton?.addEventListener('click', () => {
|
||||
if (!startMenu || !startButton) {
|
||||
@@ -789,6 +1240,7 @@ if (payloadNode) {
|
||||
renderTraySkins();
|
||||
setStartMenuOpen(false);
|
||||
measureDesktopBounds();
|
||||
applyStoredIconLayout();
|
||||
payload.windows.forEach(createWindow);
|
||||
renderClock();
|
||||
window.setInterval(renderClock, 1000);
|
||||
@@ -799,12 +1251,13 @@ if (payloadNode) {
|
||||
clampWindowLayout(record);
|
||||
|
||||
if (record.state.maximized) {
|
||||
const maximizedTopInset = maximizeOverSystemBar ? 0 : topDesktopInset;
|
||||
record.node.style.left = `${sideDesktopInset}px`;
|
||||
record.node.style.top = `${topDesktopInset}px`;
|
||||
record.node.style.top = `${maximizedTopInset}px`;
|
||||
record.node.style.width = sideDesktopInset === 0 ? '100%' : `calc(100% - ${sideDesktopInset * 2}px)`;
|
||||
record.node.style.height = activeSkin === 'apple'
|
||||
record.node.style.height = maximizeOverSystemBar
|
||||
? '100%'
|
||||
: `calc(100% - ${topDesktopInset + bottomDesktopInset}px)`;
|
||||
: `calc(100% - ${maximizedTopInset + bottomDesktopInset}px)`;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -812,5 +1265,9 @@ if (payloadNode) {
|
||||
});
|
||||
|
||||
saveWindowState();
|
||||
applyStoredIconLayout();
|
||||
closeDesktopContextMenu();
|
||||
});
|
||||
|
||||
window.refreshDesktopIconContrast = applyDesktopIconContrast;
|
||||
}
|
||||
|
||||
@@ -27,10 +27,8 @@ body[data-skin="apple"] .system-bar {
|
||||
}
|
||||
|
||||
body[data-skin="apple"] .desktop-icons {
|
||||
left: auto;
|
||||
right: 24px;
|
||||
justify-items: end;
|
||||
width: min(160px, calc(100% - 48px));
|
||||
inset: 0;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
body[data-skin="apple"] .taskbar {
|
||||
|
||||
@@ -39,6 +39,9 @@ final class App
|
||||
$windows = (new WindowManager($registry))->defaultWindows();
|
||||
$activeWidgetIds = DesktopState::defaultWidgetIds();
|
||||
$displayName = (string) ($authUser['name'] ?? $authUser['username'] ?? 'Gast');
|
||||
$storageScope = $isAuthenticated
|
||||
? strtolower((string) ($authUser['username'] ?? $authUser['sub'] ?? 'user'))
|
||||
: 'guest';
|
||||
|
||||
return [
|
||||
'meta' => [
|
||||
@@ -51,7 +54,7 @@ final class App
|
||||
'desktop' => [
|
||||
'greeting' => 'Neue Desktop-Shell fuer Kusche.Berlin',
|
||||
'wallpaper' => SkinResolver::wallpaper($activeSkin),
|
||||
'storage_scope' => 'guest',
|
||||
'storage_scope' => preg_replace('/[^a-z0-9._-]+/i', '-', $storageScope) ?: 'guest',
|
||||
'workspace' => DesktopState::defaultWorkspace(),
|
||||
'login' => [
|
||||
'authenticated' => $isAuthenticated,
|
||||
|
||||
Reference in New Issue
Block a user