1469 lines
50 KiB
JavaScript
1469 lines
50 KiB
JavaScript
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');
|
|
const taskbarRoot = document.getElementById('taskbar-apps');
|
|
const startButton = document.getElementById('start-button');
|
|
const startMenu = document.getElementById('start-menu');
|
|
const startMenuApps = document.getElementById('start-menu-apps');
|
|
const startMenuFunctions = document.getElementById('start-menu-functions');
|
|
const startMenuSettingsButton = document.getElementById('start-menu-settings-button');
|
|
const startMenuUserCard = document.getElementById('start-menu-user-card');
|
|
const startMenuUserName = document.getElementById('start-menu-user-name');
|
|
const startMenuUserAvatar = document.getElementById('start-menu-user-avatar');
|
|
const startMenuSessionAction = document.getElementById('start-menu-session-action');
|
|
const accountMenu = document.getElementById('tray-account-menu');
|
|
const clockNode = document.getElementById('clock');
|
|
const clockTopNode = document.getElementById('clock-top');
|
|
const windows = new Map();
|
|
const activeWidgets = new Set(payload.widgets.active_ids);
|
|
const activeSkin = payload.meta.active_skin;
|
|
const skinProfile = payload.meta.skin_profile || {};
|
|
const settingsApi = payload.desktop.settings_api || '';
|
|
let currentUserPreferences = payload.desktop.user_preferences || {};
|
|
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;
|
|
let activeFunctionArea = 'programs';
|
|
const windowControlsMode = document.body.dataset.windowControls || 'windows';
|
|
let wallpaperImagePromise = null;
|
|
|
|
const escapeHtml = (value) => String(value)
|
|
.replaceAll('&', '&')
|
|
.replaceAll('<', '<')
|
|
.replaceAll('>', '>')
|
|
.replaceAll('"', '"')
|
|
.replaceAll("'", ''');
|
|
|
|
const buildAppIconMarkup = (app, className = '') => {
|
|
const safeClassName = className ? ` ${className}` : '';
|
|
|
|
if (app.icon_path) {
|
|
return `<img class="app-icon-image${safeClassName}" src="${escapeHtml(app.icon_path)}" alt="">`;
|
|
}
|
|
|
|
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"]');
|
|
|
|
const displayName = currentUserPreferences?.profile?.name || payload.session?.display_name;
|
|
|
|
if (accountNode && typeof displayName === 'string' && displayName.trim() !== '') {
|
|
accountNode.textContent = displayName;
|
|
}
|
|
|
|
if (startMenuUserName && typeof displayName === 'string' && displayName.trim() !== '') {
|
|
startMenuUserName.textContent = displayName;
|
|
}
|
|
|
|
if (startMenuUserAvatar) {
|
|
startMenuUserAvatar.textContent = buildInitials(displayName);
|
|
}
|
|
};
|
|
|
|
const buildInitials = (value) => {
|
|
const normalized = typeof value === 'string' ? value.trim() : '';
|
|
|
|
if (normalized === '') {
|
|
return 'GU';
|
|
}
|
|
|
|
const parts = normalized.split(/\s+/).filter(Boolean);
|
|
|
|
if (parts.length === 1) {
|
|
return parts[0].slice(0, 2).toUpperCase();
|
|
}
|
|
|
|
return `${parts[0][0] || ''}${parts[1][0] || ''}`.toUpperCase();
|
|
};
|
|
|
|
const setAccountMenuOpen = (isOpen) => {
|
|
const accountButton = document.querySelector('.tray-pill[data-tray-id="account"]');
|
|
|
|
if (!(accountMenu instanceof HTMLElement) || !(accountButton instanceof HTMLElement)) {
|
|
return;
|
|
}
|
|
|
|
accountMenu.hidden = !isOpen;
|
|
accountButton.setAttribute('aria-expanded', String(isOpen));
|
|
};
|
|
|
|
const findAppById = (appId) => payload.apps.find((app) => app.app_id === appId) || null;
|
|
|
|
const dispatchUserSettingsUpdate = (detail) => {
|
|
window.dispatchEvent(new CustomEvent('desktop:user-settings-updated', { detail }));
|
|
};
|
|
|
|
const persistSettingsPatch = async (patch) => {
|
|
if (!settingsApi) {
|
|
return null;
|
|
}
|
|
|
|
const response = await window.fetch(settingsApi, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
Accept: 'application/json',
|
|
},
|
|
body: JSON.stringify(patch),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`settings-save-failed:${response.status}`);
|
|
}
|
|
|
|
return response.json();
|
|
};
|
|
|
|
const buildTaskbarButtonContent = (record) => `
|
|
<span class="taskbar-app-button-badge">${buildAppIconMarkup(record.definition, 'taskbar-app-icon')}</span>
|
|
<span class="taskbar-app-button-label">${escapeHtml(record.title)}</span>
|
|
`;
|
|
|
|
const buildWindowActions = () => {
|
|
if (windowControlsMode === 'mac') {
|
|
return `
|
|
<div class="window-actions window-actions--mac">
|
|
<button class="window-action" data-action="close" type="button" aria-label="Schliessen"></button>
|
|
<button class="window-action" data-action="minimize" type="button" aria-label="Minimieren"></button>
|
|
<button class="window-action" data-action="maximize" type="button" aria-label="Maximieren"></button>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
const maximizeGlyph = windowControlsMode === 'linux' ? '▢' : '□';
|
|
|
|
return `
|
|
<div class="window-actions window-actions--${windowControlsMode}">
|
|
<button class="window-action" data-action="minimize" type="button" aria-label="Minimieren"><span>-</span></button>
|
|
<button class="window-action" data-action="maximize" type="button" aria-label="Maximieren"><span>${maximizeGlyph}</span></button>
|
|
<button class="window-action" data-action="close" type="button" aria-label="Schliessen"><span>×</span></button>
|
|
</div>
|
|
`;
|
|
};
|
|
|
|
const buildWindowHeader = (definition) => {
|
|
const title = escapeHtml(definition.title);
|
|
|
|
if (windowControlsMode === 'mac') {
|
|
return `
|
|
<header class="window-header window-header--mac">
|
|
${buildWindowActions()}
|
|
<strong class="window-title">${title}</strong>
|
|
<span class="window-header-spacer" aria-hidden="true"></span>
|
|
</header>
|
|
`;
|
|
}
|
|
|
|
return `
|
|
<header class="window-header window-header--${windowControlsMode}">
|
|
<strong class="window-title">${title}</strong>
|
|
${buildWindowActions()}
|
|
</header>
|
|
`;
|
|
};
|
|
|
|
const buildWindowBody = (definition) => {
|
|
if (definition.content_mode === 'native-module' && definition.app_id === 'mining-checker') {
|
|
return `
|
|
<div class="window-content window-content--embedded window-content--module">
|
|
<div id="mining-checker-app" class="window-module-host" data-module-app="mining-checker"></div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
if (definition.content_mode === 'native-module' && definition.app_id === 'user-self-management') {
|
|
return `
|
|
<div class="window-content window-content--embedded window-content--module">
|
|
<div id="user-self-management-app" class="window-module-host" data-module-app="user-self-management"></div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
if (definition.content_mode === 'iframe' && typeof definition.entry_route === 'string' && definition.entry_route !== '') {
|
|
const title = escapeHtml(definition.title);
|
|
const src = escapeHtml(definition.entry_route);
|
|
|
|
return `
|
|
<div class="window-content window-content--embedded">
|
|
<iframe class="window-app-frame" src="${src}" title="${title}"></iframe>
|
|
<div class="window-embed-fallback">
|
|
<p><strong>${title}</strong> wird geladen.</p>
|
|
<p>Falls die Einbettung leer bleibt, direkt oeffnen: <a href="${src}" target="_blank" rel="noopener noreferrer">${src}</a></p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
return `
|
|
<div class="window-content">
|
|
<p>${escapeHtml(definition.content)}</p>
|
|
<p><strong>Route:</strong> ${escapeHtml(definition.app_id)}</p>
|
|
</div>
|
|
`;
|
|
};
|
|
|
|
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 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) {
|
|
return;
|
|
}
|
|
|
|
const taskbar = document.querySelector('.taskbar');
|
|
const systemBar = document.querySelector('.system-bar');
|
|
const stageRect = windowsRoot.getBoundingClientRect();
|
|
const taskbarRect = taskbar ? taskbar.getBoundingClientRect() : null;
|
|
const systemBarRect = systemBar ? systemBar.getBoundingClientRect() : null;
|
|
|
|
topDesktopInset = systemBarRect ? Math.round(systemBarRect.height) : 0;
|
|
sideDesktopInset = 0;
|
|
bottomDesktopInset = taskbarRect
|
|
? Math.max(0, Math.round(stageRect.bottom - taskbarRect.top))
|
|
: 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: 148,
|
|
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;
|
|
}
|
|
|
|
if (isOpen) {
|
|
startMenu.removeAttribute('hidden');
|
|
} else {
|
|
startMenu.setAttribute('hidden', 'hidden');
|
|
}
|
|
|
|
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',
|
|
minute: '2-digit',
|
|
second: '2-digit',
|
|
}).format(new Date());
|
|
|
|
if (clockNode) {
|
|
clockNode.textContent = formatted;
|
|
}
|
|
|
|
if (clockTopNode) {
|
|
clockTopNode.textContent = formatted;
|
|
}
|
|
};
|
|
|
|
const focusWindow = (windowId) => {
|
|
windows.forEach((record) => {
|
|
record.node.classList.remove('is-focused');
|
|
});
|
|
|
|
const record = windows.get(windowId);
|
|
if (!record) {
|
|
return;
|
|
}
|
|
|
|
record.state.minimized = false;
|
|
record.node.classList.remove('is-minimized');
|
|
zIndex += 1;
|
|
record.node.style.zIndex = String(record.state.maximized && activeSkin === 'apple' ? 1000 + zIndex : zIndex);
|
|
record.node.classList.add('is-focused');
|
|
syncTaskbar();
|
|
};
|
|
|
|
const closeStartMenu = () => {
|
|
setStartMenuOpen(false);
|
|
};
|
|
|
|
const openAccountSettings = () => {
|
|
const app = findAppById('user-self-management');
|
|
if (app) {
|
|
openApp(app);
|
|
}
|
|
setAccountMenuOpen(false);
|
|
};
|
|
|
|
const applyWindowLayout = (record) => {
|
|
const { x, y, width, height } = record.layout;
|
|
record.node.style.left = `${x}px`;
|
|
record.node.style.top = `${y}px`;
|
|
record.node.style.width = `${width}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 record = windows.get(windowId);
|
|
if (!record) {
|
|
return;
|
|
}
|
|
|
|
record.state.minimized = !record.state.minimized;
|
|
record.node.classList.toggle('is-minimized', record.state.minimized);
|
|
|
|
if (!record.state.minimized) {
|
|
focusWindow(windowId);
|
|
} else {
|
|
syncTaskbar();
|
|
}
|
|
|
|
saveWindowState();
|
|
};
|
|
|
|
const toggleMaximize = (windowId) => {
|
|
const record = windows.get(windowId);
|
|
if (!record) {
|
|
return;
|
|
}
|
|
|
|
if (!record.state.maximized) {
|
|
record.restoreLayout = { ...record.layout };
|
|
record.state.maximized = true;
|
|
record.node.classList.add('is-maximized');
|
|
measureDesktopBounds();
|
|
const maximizedTopInset = maximizeOverSystemBar ? 0 : topDesktopInset;
|
|
record.node.style.left = `${sideDesktopInset}px`;
|
|
record.node.style.top = `${maximizedTopInset}px`;
|
|
record.node.style.width = sideDesktopInset === 0 ? '100%' : `calc(100% - ${sideDesktopInset * 2}px)`;
|
|
record.node.style.height = maximizeOverSystemBar
|
|
? '100%'
|
|
: `calc(100% - ${maximizedTopInset + bottomDesktopInset}px)`;
|
|
focusWindow(windowId);
|
|
saveWindowState();
|
|
return;
|
|
}
|
|
|
|
record.state.maximized = false;
|
|
record.node.classList.remove('is-maximized');
|
|
if (record.restoreLayout) {
|
|
record.layout = { ...record.restoreLayout };
|
|
clampWindowLayout(record);
|
|
applyWindowLayout(record);
|
|
}
|
|
focusWindow(windowId);
|
|
saveWindowState();
|
|
};
|
|
|
|
const closeWindow = (windowId) => {
|
|
const record = windows.get(windowId);
|
|
if (!record) {
|
|
return;
|
|
}
|
|
|
|
windows.delete(windowId);
|
|
record.node.remove();
|
|
syncTaskbar();
|
|
saveWindowState();
|
|
};
|
|
|
|
const syncTaskbar = () => {
|
|
if (!taskbarRoot) {
|
|
return;
|
|
}
|
|
|
|
taskbarRoot.innerHTML = '';
|
|
|
|
windows.forEach((record) => {
|
|
const button = document.createElement('button');
|
|
button.type = 'button';
|
|
button.className = 'taskbar-app-button';
|
|
if (record.state.minimized) {
|
|
button.classList.add('is-minimized');
|
|
} else if (record.node.classList.contains('is-focused')) {
|
|
button.classList.add('is-focused');
|
|
}
|
|
button.innerHTML = buildTaskbarButtonContent(record);
|
|
button.addEventListener('click', () => {
|
|
if (record.state.minimized) {
|
|
toggleMinimize(record.id);
|
|
return;
|
|
}
|
|
|
|
focusWindow(record.id);
|
|
});
|
|
taskbarRoot.appendChild(button);
|
|
});
|
|
};
|
|
|
|
const wireDragging = (record) => {
|
|
const header = record.node.querySelector('.window-header');
|
|
if (!header || window.matchMedia('(max-width: 1100px)').matches) {
|
|
return;
|
|
}
|
|
|
|
let startX = 0;
|
|
let startY = 0;
|
|
let originX = 0;
|
|
let originY = 0;
|
|
let dragging = false;
|
|
|
|
header.addEventListener('pointerdown', (event) => {
|
|
if (record.state.maximized) {
|
|
return;
|
|
}
|
|
|
|
if (event.target instanceof Element && event.target.closest('.window-action')) {
|
|
return;
|
|
}
|
|
|
|
dragging = true;
|
|
startX = event.clientX;
|
|
startY = event.clientY;
|
|
originX = record.layout.x;
|
|
originY = record.layout.y;
|
|
header.setPointerCapture(event.pointerId);
|
|
focusWindow(record.id);
|
|
});
|
|
|
|
header.addEventListener('pointermove', (event) => {
|
|
if (!dragging) {
|
|
return;
|
|
}
|
|
|
|
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 handles = record.node.querySelectorAll('.window-resize-handle');
|
|
if (!handles.length || window.matchMedia('(max-width: 1100px)').matches) {
|
|
return;
|
|
}
|
|
|
|
handles.forEach((handle) => {
|
|
let startX = 0;
|
|
let startY = 0;
|
|
let originX = 0;
|
|
let originY = 0;
|
|
let originWidth = 0;
|
|
let originHeight = 0;
|
|
let resizing = false;
|
|
const edge = handle.dataset.edge;
|
|
|
|
handle.addEventListener('pointerdown', (event) => {
|
|
if (record.state.maximized) {
|
|
return;
|
|
}
|
|
|
|
resizing = true;
|
|
startX = event.clientX;
|
|
startY = event.clientY;
|
|
originX = record.layout.x;
|
|
originY = record.layout.y;
|
|
originWidth = record.layout.width;
|
|
originHeight = record.layout.height;
|
|
handle.setPointerCapture(event.pointerId);
|
|
focusWindow(record.id);
|
|
event.stopPropagation();
|
|
});
|
|
|
|
handle.addEventListener('pointermove', (event) => {
|
|
if (!resizing) {
|
|
return;
|
|
}
|
|
|
|
const deltaX = event.clientX - startX;
|
|
const deltaY = event.clientY - startY;
|
|
|
|
if (edge === 'right' || edge === 'corner') {
|
|
record.layout.width = originWidth + deltaX;
|
|
}
|
|
|
|
if (edge === 'bottom' || edge === 'corner') {
|
|
record.layout.height = originHeight + deltaY;
|
|
}
|
|
|
|
if (edge === 'left') {
|
|
record.layout.x = originX + deltaX;
|
|
record.layout.width = originWidth - deltaX;
|
|
}
|
|
|
|
if (edge === 'top') {
|
|
record.layout.y = originY + deltaY;
|
|
record.layout.height = originHeight - deltaY;
|
|
}
|
|
|
|
clampWindowLayout(record);
|
|
applyWindowLayout(record);
|
|
});
|
|
|
|
handle.addEventListener('pointerup', () => {
|
|
resizing = false;
|
|
saveWindowState();
|
|
});
|
|
});
|
|
};
|
|
|
|
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';
|
|
node.dataset.windowControls = windowControlsMode;
|
|
node.innerHTML = `
|
|
${buildWindowHeader(definition)}
|
|
${buildWindowBody(definition)}
|
|
<div class="window-resize-handle" data-edge="top" aria-hidden="true"></div>
|
|
<div class="window-resize-handle" data-edge="right" aria-hidden="true"></div>
|
|
<div class="window-resize-handle" data-edge="bottom" aria-hidden="true"></div>
|
|
<div class="window-resize-handle" data-edge="left" aria-hidden="true"></div>
|
|
<div class="window-resize-handle" data-edge="corner" 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: persistedLayout.x ?? definition.x,
|
|
y: persistedLayout.y ?? definition.y,
|
|
width: persistedLayout.width ?? definition.width,
|
|
height: persistedLayout.height ?? definition.height,
|
|
},
|
|
restoreLayout: null,
|
|
state: {
|
|
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();
|
|
const maximizedTopInset = maximizeOverSystemBar ? 0 : topDesktopInset;
|
|
record.node.style.left = `${sideDesktopInset}px`;
|
|
record.node.style.top = `${maximizedTopInset}px`;
|
|
record.node.style.width = sideDesktopInset === 0 ? '100%' : `calc(100% - ${sideDesktopInset * 2}px)`;
|
|
record.node.style.height = maximizeOverSystemBar
|
|
? '100%'
|
|
: `calc(100% - ${maximizedTopInset + bottomDesktopInset}px)`;
|
|
}
|
|
|
|
node.addEventListener('mousedown', () => focusWindow(record.id));
|
|
|
|
const frame = node.querySelector('.window-app-frame');
|
|
const frameFallback = node.querySelector('.window-embed-fallback');
|
|
if (frame && frameFallback) {
|
|
frame.addEventListener('load', () => {
|
|
frameFallback.remove();
|
|
}, { once: true });
|
|
}
|
|
|
|
const moduleHost = node.querySelector('.window-module-host');
|
|
if (moduleHost && definition.content_mode === 'native-module' && definition.app_id === 'mining-checker') {
|
|
const moduleOptions = definition.module_options || {};
|
|
if (typeof window.initMiningCheckerApp === 'function') {
|
|
window.initMiningCheckerApp(moduleHost, {
|
|
apiBase: '/api/mining-checker/index.php/v1',
|
|
defaultProjectKey: moduleOptions.default_project_key || 'doge-main',
|
|
activeView: moduleOptions.active_view || 'overview',
|
|
sections: Array.isArray(moduleOptions.sections) ? moduleOptions.sections : [],
|
|
moduleDebugEnabled: false,
|
|
});
|
|
} else {
|
|
moduleHost.innerHTML = '<div class="window-module-error">Mining-Checker Assets konnten nicht initialisiert werden.</div>';
|
|
}
|
|
}
|
|
|
|
if (moduleHost && definition.content_mode === 'native-module' && definition.app_id === 'user-self-management') {
|
|
if (typeof window.initUserSelfManagementApp === 'function') {
|
|
window.initUserSelfManagementApp(moduleHost, {
|
|
apiBase: settingsApi,
|
|
activeSkin,
|
|
});
|
|
} else {
|
|
moduleHost.innerHTML = '<div class="window-module-error">User Self Management konnte nicht initialisiert werden.</div>';
|
|
}
|
|
}
|
|
|
|
node.querySelectorAll('.window-action').forEach((action) => {
|
|
action.addEventListener('click', (event) => {
|
|
event.stopPropagation();
|
|
|
|
const actionName = action.dataset.action;
|
|
if (actionName === 'close') {
|
|
closeWindow(record.id);
|
|
return;
|
|
}
|
|
|
|
if (actionName === 'minimize') {
|
|
toggleMinimize(record.id);
|
|
return;
|
|
}
|
|
|
|
if (actionName === 'maximize') {
|
|
toggleMaximize(record.id);
|
|
}
|
|
});
|
|
});
|
|
|
|
windows.set(record.id, record);
|
|
windowsRoot?.appendChild(node);
|
|
wireDragging(record);
|
|
wireResizing(record);
|
|
if (!record.state.minimized) {
|
|
focusWindow(record.id);
|
|
} else {
|
|
syncTaskbar();
|
|
}
|
|
};
|
|
|
|
const openApp = (app) => {
|
|
const existing = Array.from(windows.values()).find((record) => record.definition.app_id === app.app_id);
|
|
|
|
if (existing) {
|
|
focusWindow(existing.id);
|
|
return;
|
|
}
|
|
|
|
createWindow({
|
|
window_id: `window-${app.app_id}-${Date.now()}`,
|
|
app_id: app.app_id,
|
|
title: app.title,
|
|
x: 64 + Math.round(Math.random() * 80),
|
|
y: 70 + Math.round(Math.random() * 60),
|
|
width: app.default_width || 640,
|
|
height: app.default_height || 420,
|
|
content: app.summary || '',
|
|
entry_route: app.entry_route || '',
|
|
content_mode: app.content_mode || 'summary',
|
|
module_options: app.module_options || null,
|
|
});
|
|
};
|
|
|
|
const renderWidgets = () => {
|
|
if (!widgetZoneRoot) {
|
|
return;
|
|
}
|
|
|
|
widgetZoneRoot.innerHTML = '';
|
|
|
|
payload.widgets.registry
|
|
.filter((widget) => activeWidgets.has(widget.widget_id))
|
|
.forEach((widget) => {
|
|
const card = document.createElement('article');
|
|
card.className = 'widget-card';
|
|
const actionMarkup = widget.launch_app_id
|
|
? `
|
|
<div class="widget-card-actions">
|
|
<button class="widget-card-action" type="button" data-app-id="${escapeHtml(widget.launch_app_id)}">
|
|
${escapeHtml(widget.action_label || 'Oeffnen')}
|
|
</button>
|
|
</div>
|
|
`
|
|
: '';
|
|
card.innerHTML = `
|
|
<div class="widget-card-header">
|
|
<div>
|
|
<h3>${widget.title}</h3>
|
|
<p>${widget.summary || ''}</p>
|
|
</div>
|
|
<span class="widget-badge">${widget.icon || 'WG'}</span>
|
|
</div>
|
|
<p>${widget.content || ''}</p>
|
|
${actionMarkup}
|
|
`;
|
|
|
|
const actionButton = card.querySelector('.widget-card-action');
|
|
if (actionButton instanceof HTMLButtonElement && widget.launch_app_id) {
|
|
actionButton.addEventListener('click', () => {
|
|
const app = findAppById(widget.launch_app_id);
|
|
if (app) {
|
|
openApp(app);
|
|
}
|
|
});
|
|
}
|
|
|
|
widgetZoneRoot.appendChild(card);
|
|
});
|
|
};
|
|
|
|
const renderFunctionArea = () => {
|
|
if (!startMenuFunctions) {
|
|
return;
|
|
}
|
|
|
|
startMenuFunctions.querySelectorAll('[data-function-id]').forEach((button) => {
|
|
const isActive = button instanceof HTMLElement && button.dataset.functionId === activeFunctionArea;
|
|
button.classList.toggle('is-active', isActive);
|
|
});
|
|
};
|
|
|
|
const renderSelectionArea = () => {
|
|
if (!startMenuApps) {
|
|
return;
|
|
}
|
|
|
|
startMenuApps.innerHTML = '';
|
|
|
|
if (activeFunctionArea !== 'programs') {
|
|
return;
|
|
}
|
|
|
|
payload.apps.forEach((app) => {
|
|
const button = document.createElement('button');
|
|
button.type = 'button';
|
|
button.className = 'start-menu-entry start-menu-entry--app';
|
|
button.innerHTML = `
|
|
<span class="start-menu-entry-badge">${buildAppIconMarkup(app, 'start-menu-app-icon')}</span>
|
|
<span>
|
|
<strong>${app.title}</strong>
|
|
<small>${app.summary || ''}</small>
|
|
</span>
|
|
<span class="start-menu-entry-toggle">Open</span>
|
|
`;
|
|
button.addEventListener('click', () => {
|
|
openApp(app);
|
|
closeStartMenu();
|
|
});
|
|
startMenuApps.appendChild(button);
|
|
});
|
|
};
|
|
|
|
const runQuickAction = (actionId) => {
|
|
if (actionId === 'open-user-self-management') {
|
|
const userSetup = findAppById('user-self-management');
|
|
if (userSetup) {
|
|
openApp(userSetup);
|
|
closeStartMenu();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (actionId === 'open-public-dashboard') {
|
|
const publicDashboard = findAppById('public-dashboard');
|
|
if (publicDashboard) {
|
|
openApp(publicDashboard);
|
|
closeStartMenu();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (actionId === 'toggle-all-widgets') {
|
|
const shouldShowAll = activeWidgets.size !== payload.widgets.registry.length;
|
|
|
|
activeWidgets.clear();
|
|
if (shouldShowAll) {
|
|
payload.widgets.registry.forEach((widget) => activeWidgets.add(widget.widget_id));
|
|
}
|
|
|
|
renderWidgets();
|
|
renderStartMenu();
|
|
persistSettingsPatch({
|
|
widgets: {
|
|
active_ids: Array.from(activeWidgets),
|
|
},
|
|
}).catch(() => {
|
|
// Keep current desktop state even if persistence fails.
|
|
});
|
|
}
|
|
};
|
|
|
|
const renderStartMenu = () => {
|
|
if (!startMenuApps) {
|
|
return;
|
|
}
|
|
|
|
renderFunctionArea();
|
|
renderSelectionArea();
|
|
};
|
|
|
|
payload.apps.forEach((app) => {
|
|
const icon = document.createElement('button');
|
|
icon.type = 'button';
|
|
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>
|
|
`;
|
|
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();
|
|
}
|
|
|
|
if (!(event.target instanceof Element) || !event.target.closest('#start-menu, #start-button')) {
|
|
setStartMenuOpen(false);
|
|
}
|
|
|
|
if (!(event.target instanceof Element) || !event.target.closest('.tray-pill[data-tray-id="account"], #tray-account-menu')) {
|
|
setAccountMenuOpen(false);
|
|
}
|
|
});
|
|
|
|
document.addEventListener('keydown', (event) => {
|
|
if (event.key === 'Escape') {
|
|
closeDesktopContextMenu();
|
|
setAccountMenuOpen(false);
|
|
}
|
|
});
|
|
|
|
startButton?.setAttribute('aria-expanded', 'false');
|
|
startButton?.addEventListener('click', () => {
|
|
if (!startMenu || !startButton) {
|
|
return;
|
|
}
|
|
|
|
const isClosed = startMenu.hasAttribute('hidden');
|
|
setStartMenuOpen(isClosed);
|
|
setAccountMenuOpen(false);
|
|
});
|
|
|
|
document.querySelector('.tray-pill[data-tray-id="account"]')?.addEventListener('click', () => {
|
|
if (!(accountMenu instanceof HTMLElement)) {
|
|
return;
|
|
}
|
|
|
|
setStartMenuOpen(false);
|
|
setAccountMenuOpen(accountMenu.hidden);
|
|
});
|
|
|
|
accountMenu?.addEventListener('click', (event) => {
|
|
const target = event.target instanceof Element ? event.target.closest('[data-account-action]') : null;
|
|
|
|
if (!target) {
|
|
return;
|
|
}
|
|
|
|
const action = target.getAttribute('data-account-action');
|
|
|
|
if (action === 'settings') {
|
|
event.preventDefault();
|
|
openAccountSettings();
|
|
return;
|
|
}
|
|
|
|
if (action === 'login') {
|
|
event.preventDefault();
|
|
const loginApp = findAppById('desktop-login');
|
|
if (loginApp) {
|
|
openApp(loginApp);
|
|
}
|
|
setAccountMenuOpen(false);
|
|
}
|
|
});
|
|
|
|
startMenuSettingsButton?.addEventListener('click', () => {
|
|
openAccountSettings();
|
|
});
|
|
|
|
startMenuUserCard?.addEventListener('click', () => {
|
|
openAccountSettings();
|
|
});
|
|
|
|
startMenuSessionAction?.addEventListener('click', (event) => {
|
|
const target = event.currentTarget;
|
|
|
|
if (!(target instanceof HTMLElement) || target.dataset.sessionAction !== 'login') {
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
const loginApp = findAppById('desktop-login');
|
|
if (loginApp) {
|
|
openApp(loginApp);
|
|
}
|
|
closeStartMenu();
|
|
});
|
|
|
|
startMenuFunctions?.addEventListener('click', (event) => {
|
|
const target = event.target instanceof Element ? event.target.closest('[data-function-id]') : null;
|
|
|
|
if (!(target instanceof HTMLElement)) {
|
|
return;
|
|
}
|
|
|
|
activeFunctionArea = target.dataset.functionId || 'programs';
|
|
renderStartMenu();
|
|
});
|
|
|
|
startMenuFunctions?.addEventListener('mouseover', (event) => {
|
|
const target = event.target instanceof Element ? event.target.closest('[data-function-id]') : null;
|
|
|
|
if (!(target instanceof HTMLElement)) {
|
|
return;
|
|
}
|
|
|
|
const nextFunctionArea = target.dataset.functionId || 'programs';
|
|
if (nextFunctionArea === activeFunctionArea) {
|
|
return;
|
|
}
|
|
|
|
activeFunctionArea = nextFunctionArea;
|
|
renderStartMenu();
|
|
});
|
|
|
|
window.addEventListener('desktop:user-settings-updated', (event) => {
|
|
const preferences = event.detail?.preferences;
|
|
const requiresReload = Boolean(event.detail?.requiresReload);
|
|
|
|
if (preferences && typeof preferences === 'object') {
|
|
currentUserPreferences = preferences;
|
|
|
|
const nextWidgetIds = Array.isArray(preferences.widgets?.active_ids)
|
|
? preferences.widgets.active_ids
|
|
: [];
|
|
|
|
activeWidgets.clear();
|
|
nextWidgetIds.forEach((widgetId) => activeWidgets.add(widgetId));
|
|
renderWidgets();
|
|
updateSessionDisplay();
|
|
renderStartMenu();
|
|
}
|
|
|
|
if (requiresReload) {
|
|
const nextSkin = preferences?.desktop?.active_skin || activeSkin;
|
|
const nextUrl = `${window.location.pathname}?skin=${encodeURIComponent(nextSkin)}`;
|
|
window.location.assign(nextUrl);
|
|
}
|
|
});
|
|
|
|
renderWidgets();
|
|
renderStartMenu();
|
|
setStartMenuOpen(false);
|
|
measureDesktopBounds();
|
|
applyStoredIconLayout();
|
|
payload.windows.forEach(createWindow);
|
|
renderClock();
|
|
window.setInterval(renderClock, 1000);
|
|
window.addEventListener('resize', () => {
|
|
measureDesktopBounds();
|
|
|
|
windows.forEach((record) => {
|
|
clampWindowLayout(record);
|
|
|
|
if (record.state.maximized) {
|
|
const maximizedTopInset = maximizeOverSystemBar ? 0 : topDesktopInset;
|
|
record.node.style.left = `${sideDesktopInset}px`;
|
|
record.node.style.top = `${maximizedTopInset}px`;
|
|
record.node.style.width = sideDesktopInset === 0 ? '100%' : `calc(100% - ${sideDesktopInset * 2}px)`;
|
|
record.node.style.height = maximizeOverSystemBar
|
|
? '100%'
|
|
: `calc(100% - ${maximizedTopInset + bottomDesktopInset}px)`;
|
|
return;
|
|
}
|
|
|
|
applyWindowLayout(record);
|
|
});
|
|
|
|
saveWindowState();
|
|
applyStoredIconLayout();
|
|
closeDesktopContextMenu();
|
|
});
|
|
|
|
window.refreshDesktopIconContrast = applyDesktopIconContrast;
|
|
window.desktopShell = {
|
|
openAppById: (appId) => {
|
|
const app = findAppById(appId);
|
|
if (app) {
|
|
openApp(app);
|
|
}
|
|
},
|
|
persistSettingsPatch,
|
|
dispatchUserSettingsUpdate,
|
|
};
|
|
}
|