desktop
This commit is contained in:
@@ -109,16 +109,16 @@ h1 {
|
|||||||
|
|
||||||
.desktop-icons {
|
.desktop-icons {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: var(--icon-top);
|
inset: 0;
|
||||||
left: var(--icon-left);
|
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
display: grid;
|
}
|
||||||
grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
|
|
||||||
gap: 16px;
|
.desktop-icons.is-arranging {
|
||||||
width: min(720px, calc(100% - 80px));
|
cursor: progress;
|
||||||
}
|
}
|
||||||
|
|
||||||
.desktop-icon {
|
.desktop-icon {
|
||||||
|
position: absolute;
|
||||||
width: 96px;
|
width: 96px;
|
||||||
padding: 12px 10px;
|
padding: 12px 10px;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
@@ -126,7 +126,9 @@ h1 {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
background: rgba(255, 255, 255, 0.05);
|
background: rgba(255, 255, 255, 0.05);
|
||||||
cursor: pointer;
|
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 {
|
.desktop-icon:hover {
|
||||||
@@ -135,6 +137,13 @@ h1 {
|
|||||||
border-color: var(--shell-border);
|
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 {
|
.desktop-icon-badge {
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
@@ -154,11 +163,10 @@ h1 {
|
|||||||
padding: 3px 8px;
|
padding: 3px 8px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #ffffff;
|
color: var(--desktop-icon-label-color, #0f172a);
|
||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
text-shadow: none;
|
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.22);
|
||||||
mix-blend-mode: difference;
|
transition: color 140ms ease, text-shadow 140ms ease;
|
||||||
isolation: isolate;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.desktop-icon-label::before {
|
.desktop-icon-label::before {
|
||||||
@@ -167,9 +175,56 @@ h1 {
|
|||||||
inset: -2px -4px;
|
inset: -2px -4px;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
border-radius: 8px;
|
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);
|
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,
|
.app-icon-image,
|
||||||
@@ -784,12 +839,24 @@ h1 {
|
|||||||
|
|
||||||
.desktop-icons {
|
.desktop-icons {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: auto;
|
inset: auto;
|
||||||
left: auto;
|
|
||||||
margin: 24px;
|
margin: 24px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
width: calc(100% - 48px);
|
width: calc(100% - 48px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.desktop-icon {
|
||||||
|
position: relative;
|
||||||
|
left: auto !important;
|
||||||
|
top: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-context-menu {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
.widget-zone {
|
.widget-zone {
|
||||||
position: static;
|
position: static;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ const payloadNode = document.getElementById('desktop-payload');
|
|||||||
|
|
||||||
if (payloadNode) {
|
if (payloadNode) {
|
||||||
const payload = JSON.parse(payloadNode.textContent);
|
const payload = JSON.parse(payloadNode.textContent);
|
||||||
|
const desktopStage = document.querySelector('.desktop-stage');
|
||||||
const iconsRoot = document.getElementById('desktop-icons');
|
const iconsRoot = document.getElementById('desktop-icons');
|
||||||
const widgetZoneRoot = document.getElementById('widget-zone');
|
const widgetZoneRoot = document.getElementById('widget-zone');
|
||||||
const windowsRoot = document.getElementById('window-layer');
|
const windowsRoot = document.getElementById('window-layer');
|
||||||
@@ -22,11 +23,14 @@ if (payloadNode) {
|
|||||||
const maximizeOverSystemBar = Boolean(skinProfile.maximize_over_system_bar);
|
const maximizeOverSystemBar = Boolean(skinProfile.maximize_over_system_bar);
|
||||||
const storageScope = payload.desktop.storage_scope || 'guest';
|
const storageScope = payload.desktop.storage_scope || 'guest';
|
||||||
const storageKey = `desktop.kusche.berlin.window-state.${storageScope}`;
|
const storageKey = `desktop.kusche.berlin.window-state.${storageScope}`;
|
||||||
|
const iconStorageKey = `desktop.kusche.berlin.icon-state.${storageScope}.${activeSkin}`;
|
||||||
let zIndex = 20;
|
let zIndex = 20;
|
||||||
let topDesktopInset = 0;
|
let topDesktopInset = 0;
|
||||||
let sideDesktopInset = 0;
|
let sideDesktopInset = 0;
|
||||||
let bottomDesktopInset = 120;
|
let bottomDesktopInset = 120;
|
||||||
|
let iconContextMenu = null;
|
||||||
const windowControlsMode = document.body.dataset.windowControls || 'windows';
|
const windowControlsMode = document.body.dataset.windowControls || 'windows';
|
||||||
|
let wallpaperImagePromise = null;
|
||||||
|
|
||||||
const escapeHtml = (value) => String(value)
|
const escapeHtml = (value) => String(value)
|
||||||
.replaceAll('&', '&')
|
.replaceAll('&', '&')
|
||||||
@@ -45,6 +49,122 @@ if (payloadNode) {
|
|||||||
return `<span class="app-icon-fallback${safeClassName}">${escapeHtml(app.icon || 'AP')}</span>`;
|
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 updateSessionDisplay = () => {
|
||||||
const accountNode = document.querySelector('.tray-pill[data-tray-id="account"]');
|
const accountNode = document.querySelector('.tray-pill[data-tray-id="account"]');
|
||||||
|
|
||||||
@@ -166,6 +286,43 @@ if (payloadNode) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const persistedWindowState = loadWindowState();
|
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 = () => {
|
const measureDesktopBounds = () => {
|
||||||
if (!windowsRoot) {
|
if (!windowsRoot) {
|
||||||
@@ -185,6 +342,137 @@ if (payloadNode) {
|
|||||||
: 120;
|
: 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) => {
|
const setStartMenuOpen = (isOpen) => {
|
||||||
if (!startMenu) {
|
if (!startMenu) {
|
||||||
return;
|
return;
|
||||||
@@ -199,6 +487,60 @@ if (payloadNode) {
|
|||||||
startButton?.setAttribute('aria-expanded', String(isOpen));
|
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 renderClock = () => {
|
||||||
const formatted = new Intl.DateTimeFormat('de-DE', {
|
const formatted = new Intl.DateTimeFormat('de-DE', {
|
||||||
hour: '2-digit',
|
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 createWindow = (definition) => {
|
||||||
const node = document.createElement('article');
|
const node = document.createElement('article');
|
||||||
node.className = 'window';
|
node.className = 'window';
|
||||||
@@ -763,17 +1188,43 @@ if (payloadNode) {
|
|||||||
payload.apps.forEach((app) => {
|
payload.apps.forEach((app) => {
|
||||||
const icon = document.createElement('button');
|
const icon = document.createElement('button');
|
||||||
icon.type = 'button';
|
icon.type = 'button';
|
||||||
icon.className = 'desktop-icon';
|
icon.className = 'desktop-icon is-contrast-dark';
|
||||||
|
icon.dataset.appId = app.app_id;
|
||||||
icon.innerHTML = `
|
icon.innerHTML = `
|
||||||
<span class="desktop-icon-badge">${buildAppIconMarkup(app, 'desktop-app-icon')}</span>
|
<span class="desktop-icon-badge">${buildAppIconMarkup(app, 'desktop-app-icon')}</span>
|
||||||
<span class="desktop-icon-label">${app.title}</span>
|
<span class="desktop-icon-label">${app.title}</span>
|
||||||
`;
|
`;
|
||||||
icon.addEventListener('click', () => openApp(app));
|
wireDesktopIconDragging(icon, app);
|
||||||
iconsRoot?.appendChild(icon);
|
iconsRoot?.appendChild(icon);
|
||||||
});
|
});
|
||||||
|
|
||||||
updateSessionDisplay();
|
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?.setAttribute('aria-expanded', 'false');
|
||||||
startButton?.addEventListener('click', () => {
|
startButton?.addEventListener('click', () => {
|
||||||
if (!startMenu || !startButton) {
|
if (!startMenu || !startButton) {
|
||||||
@@ -789,6 +1240,7 @@ if (payloadNode) {
|
|||||||
renderTraySkins();
|
renderTraySkins();
|
||||||
setStartMenuOpen(false);
|
setStartMenuOpen(false);
|
||||||
measureDesktopBounds();
|
measureDesktopBounds();
|
||||||
|
applyStoredIconLayout();
|
||||||
payload.windows.forEach(createWindow);
|
payload.windows.forEach(createWindow);
|
||||||
renderClock();
|
renderClock();
|
||||||
window.setInterval(renderClock, 1000);
|
window.setInterval(renderClock, 1000);
|
||||||
@@ -799,12 +1251,13 @@ if (payloadNode) {
|
|||||||
clampWindowLayout(record);
|
clampWindowLayout(record);
|
||||||
|
|
||||||
if (record.state.maximized) {
|
if (record.state.maximized) {
|
||||||
|
const maximizedTopInset = maximizeOverSystemBar ? 0 : topDesktopInset;
|
||||||
record.node.style.left = `${sideDesktopInset}px`;
|
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.width = sideDesktopInset === 0 ? '100%' : `calc(100% - ${sideDesktopInset * 2}px)`;
|
||||||
record.node.style.height = activeSkin === 'apple'
|
record.node.style.height = maximizeOverSystemBar
|
||||||
? '100%'
|
? '100%'
|
||||||
: `calc(100% - ${topDesktopInset + bottomDesktopInset}px)`;
|
: `calc(100% - ${maximizedTopInset + bottomDesktopInset}px)`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -812,5 +1265,9 @@ if (payloadNode) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
saveWindowState();
|
saveWindowState();
|
||||||
|
applyStoredIconLayout();
|
||||||
|
closeDesktopContextMenu();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window.refreshDesktopIconContrast = applyDesktopIconContrast;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,10 +27,8 @@ body[data-skin="apple"] .system-bar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body[data-skin="apple"] .desktop-icons {
|
body[data-skin="apple"] .desktop-icons {
|
||||||
left: auto;
|
inset: 0;
|
||||||
right: 24px;
|
width: auto;
|
||||||
justify-items: end;
|
|
||||||
width: min(160px, calc(100% - 48px));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body[data-skin="apple"] .taskbar {
|
body[data-skin="apple"] .taskbar {
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ final class App
|
|||||||
$windows = (new WindowManager($registry))->defaultWindows();
|
$windows = (new WindowManager($registry))->defaultWindows();
|
||||||
$activeWidgetIds = DesktopState::defaultWidgetIds();
|
$activeWidgetIds = DesktopState::defaultWidgetIds();
|
||||||
$displayName = (string) ($authUser['name'] ?? $authUser['username'] ?? 'Gast');
|
$displayName = (string) ($authUser['name'] ?? $authUser['username'] ?? 'Gast');
|
||||||
|
$storageScope = $isAuthenticated
|
||||||
|
? strtolower((string) ($authUser['username'] ?? $authUser['sub'] ?? 'user'))
|
||||||
|
: 'guest';
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'meta' => [
|
'meta' => [
|
||||||
@@ -51,7 +54,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',
|
'storage_scope' => preg_replace('/[^a-z0-9._-]+/i', '-', $storageScope) ?: 'guest',
|
||||||
'workspace' => DesktopState::defaultWorkspace(),
|
'workspace' => DesktopState::defaultWorkspace(),
|
||||||
'login' => [
|
'login' => [
|
||||||
'authenticated' => $isAuthenticated,
|
'authenticated' => $isAuthenticated,
|
||||||
|
|||||||
Reference in New Issue
Block a user