PageSourceSearch

https://corsch.net/js/core/taskbar-os2045.js

js corsch.net collected 2026-10-09 23:52:59 UTC 22,258 bytes, 425 lines download raw bytes

1// Module: taskbar-os2045 — OS-2045 Taskbar (start button, running apps, tray, clock)
2//
3// Public API: window.OS2045Taskbar = {
4//   mount(container?), update(), refreshClock(), getElement(), pin(appId,opts), unpin(appId)
5// }
6//
7// Behaviour:
8//   - mount() inserts a .os-taskbar element into the desktop container if not present.
9//   - Subscribes to OS2045WM.onChange() to keep running/active button state in sync.
10//   - Click on app button: smart toggle (focus / minimize / open).
11//   - Right-click: contextmenu via OS2045Ctx.
12//   - Clock ticks every 30s.
13
14(function () {
15    'use strict';
16
17    const PIN_STORAGE = 'os2045.taskbar.pinned';
18    const DAYS_SHORT = ['SO','MO','DI','MI','DO','FR','SA'];
19    const MONTHS = ['JAN','FEB','MÄR','APR','MAI','JUN','JUL','AUG','SEP','OKT','NOV','DEZ'];
20
21    let taskbarEl = null;
22    let runningEl = null;
23    let trayClockEl = null;
24    let trayDateEl = null;
25    let clockTimer = null;
26    let pinned = loadPinned();
27    let unsubWM = null;
28
29    function isOs2045() {
30        return document.body && document.body.dataset.themeMode === 'os2045';
31    }
32
33    function loadPinned() {
34        try {
35            const raw = localStorage.getItem(PIN_STORAGE);
36            const list = raw ? JSON.parse(raw) : null;
37            if (!Array.isArray(list)) return list;
38            const revKey = PIN_STORAGE + '.glyphrev';
39            let rev = 0;
40            try { rev = parseInt(localStorage.getItem(revKey) || '0', 10) || 0; } catch (e) {}
41            if (rev < 2) {
42                list.forEach(p => {
43                    if (p && p.id === 'backlog') {
44                        p.icon = backlogIcon();
45                        p.title = p.title === 'Backlog' ? 'Aufgaben' : (p.title || 'Aufgaben');
46                    }
47                });
48                try { localStorage.setItem(PIN_STORAGE, JSON.stringify(list)); } catch (e) {}
49                try { localStorage.setItem(revKey, '2'); } catch (e) {}
50            }
51            return list;
52        } catch (e) { return null; }
53    }
54
55    function savePinned() {
56        try { localStorage.setItem(PIN_STORAGE, JSON.stringify(pinned)); } catch (e) {}
57    }
58
59    function defaultPinned() {
60        // Hartcodierte Default-Pins
61        return [
62            { id: 'dashboard', title: 'Dashboard', icon: dashIcon() },
63            { id: 'backlog',   title: 'Aufgaben',   icon: backlogIcon() },
64            { id: 'firmen',    title: 'Firmen',    icon: firmenIcon() },
65            { id: 'pm2',       title: 'PM2',       icon: pm2Icon() },
66            { id: 'files',     title: 'Dateien',   icon: filesIcon() },
67            { id: 'shell',     title: 'Shell',     icon: shellIcon() }
68        ];
69    }
70
71    // ─── Inline SVG icons (small) ─────────────────────────────────────────
72    function svg(d, opts) {
73        opts = opts || {};
74        return `<svg viewBox="0 0 24 24" fill="${opts.fill||'none'}" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">${d}</svg>`;
75    }
76    // Pure svg symbols — no outer frame. Each icon uses its native 0..24
77    // geometry so it stays large and legible. Shell + SSH still carry their
78    // own 16:18-style frame because the rectangle is part of their glyph
79    // (a screen/terminal silhouette), not a generic decoration.
80    function fsvg(symbol) { return svg(symbol); }
81    // App tiles → pure symbol
82    function dashIcon()    { return fsvg('<rect x="3" y="3" width="7" height="9" rx="1.5"/><rect x="14" y="3" width="7" height="5" rx="1.5"/><rect x="14" y="12" width="7" height="9" rx="1.5"/><rect x="3" y="16" width="7" height="5" rx="1.5"/>'); }
83    function backlogIcon() { return fsvg('<rect x="5" y="3" width="14" height="18" rx="2"/><path d="M9 3h6v2.8H9z"/><path d="M8.5 11.5l2 2 4.2-4.2"/><path d="M8.5 17h7"/>'); }
84    function firmenIcon()  { return fsvg('<rect x="3" y="7" width="18" height="14" rx="1.5"/><path d="M8 7V4h8v3"/>'); }
85    function pm2Icon()     { return fsvg('<rect x="3" y="5" width="18" height="6" rx="1"/><rect x="3" y="13" width="18" height="6" rx="1"/><circle cx="6" cy="8" r="0.7" fill="currentColor"/><circle cx="6" cy="16" r="0.7" fill="currentColor"/>'); }
86    // shell keeps its native rect — it's the screen/terminal silhouette,
87    // not a generic frame.
88    function shellIcon()   { return svg('<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M7 9l3 3-3 3M12 15h6"/>'); }
89    function filesIcon()   { return fsvg('<path d="M3 6a2 2 0 012-2h4l2 2h8a2 2 0 012 2v10a2 2 0 01-2 2H5a2 2 0 01-2-2V6z"/>'); }
90    function clockIcon()   { return fsvg('<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>'); }
91    function shieldIcon()  { return fsvg('<path d="M12 3l8 3v6c0 5-3.5 8.5-8 10-4.5-1.5-8-5-8-10V6l8-3z"/>'); }
92    function bellIcon()    { return fsvg('<path d="M6 8a6 6 0 1112 0v5l1.5 3h-15L6 13z"/><path d="M10 19a2 2 0 004 0"/>'); }
93    function powerIcon()   { return fsvg('<path d="M12 3v8"/><path d="M5.6 8a8 8 0 1012.8 0"/>'); }
94    function qsIcon()      { return fsvg('<rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/>'); }
95    // Start button glyph — 2×2 tile grid (Win11-style). Filled rounded
96    // squares in native 0..24 viewBox geometry, no outer frame.
97    // CorSch-Mark — gleiches Glyph wie auf dem Lockscreen (offener Kreis = "C").
98    function startIcon()   { return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6.5A8 8 0 106 17.5"/></svg>`; }
99    function searchIcon()  { return fsvg('<circle cx="11" cy="11" r="7"/><path d="M21 21l-4-4"/>'); }
100
101    function safeSvgInsert(container, svgString) {
102        if (!svgString) return;
103        const trimmed = String(svgString).trim();
104        // Plain text / emoji icons (e.g. registry icons like 👥): render
105        // as text content. No parser involved → no XSS risk.
106        if (!trimmed.startsWith('<svg')) {
107            container.textContent = trimmed;
108            return;
109        }
110        // SVG strings: strict image/svg+xml parser needs xmlns. Inject if
111        // missing so the SVG actually parses instead of returning a
112        // <parsererror>.
113        const withNs = / xmlns=/.test(trimmed)
114            ? trimmed
115            : trimmed.replace(/^<svg\b/, '<svg xmlns="http://www.w3.org/2000/svg"');
116        const parser = new DOMParser();
117        const doc = parser.parseFromString(withNs, 'image/svg+xml');
118        const svgEl = doc.querySelector('svg');
119        if (!svgEl || doc.querySelector('parsererror')) {
120            container.textContent = trimmed;
121            return;
122        }
123        svgEl.querySelectorAll('script').forEach(s => s.remove());
124        svgEl.querySelectorAll('*').forEach(el => {
125            Array.from(el.attributes).forEach(attr => {
126                if (/^on/i.test(attr.name)) el.removeAttribute(attr.name);
127            });
128        });
129        container.appendChild(document.importNode(svgEl, true));
130    }
131
132    // ─── Build markup ──────────────────────────────────────────────────────
133    function build() {
134        if (taskbarEl) return taskbarEl;
135        taskbarEl = document.createElement('div');
136        taskbarEl.className = 'os-taskbar';
137        taskbarEl.id = 'os2045Taskbar';
138        taskbarEl.innerHTML = `
139            <button class="tb-start" id="os2045StartBtn" title="CorSch Me
139nü" aria-label="Start-Menü öffnen">${startIcon()}</button>
140            <div class="tb-search" id="os2045TbSearch" role="button" tabindex="0" title="Suche öffnen">
141                ${searchIcon()}
142                <span>Suche — Apps, Befehle</span>
143                <span class="kbd">⌘K</span>
144            </div>
145            <div class="tb-sep"></div>
146            <div class="tb-running" id="os2045TbRunning" role="toolbar" aria-label="Geöffnete Anwendungen"></div>
147            <div class="tb-sep"></div>
148            <div class="tray" id="os2045Tray">
149                <div class="tb-nowplaying" id="os2045TbNowPlaying" hidden role="button" tabindex="0" title="Wiedergabe — Klick: Play/Pause · Doppelklick: Cor-Amp">
150                    <span class="np-icon" id="os2045TbNpIcon">▶</span>
151                    <span class="np-title" id="os2045TbNpTitle">—</span>
152                </div>
153                <button class="ti" id="os2045TrayQs" title="Schnell-Einstellungen (Win+A)" aria-label="Schnell-Einstellungen">${qsIcon()}</button>
154                <button class="ti" id="os2045TrayShield" title="Guardian aktiv" aria-label="Guardian Status">${shieldIcon()}</button>
155                <button class="ti" id="os2045TrayBell" title="Benachrichtigungen (Win+N)" aria-label="Benachrichtigungen">${bellIcon()}</button>
156                <button class="ti" id="os2045TrayPower" title="Energie-Optionen (Win+X)" aria-label="Energie-Optionen">${powerIcon()}</button>
157                <div class="tsep"></div>
158                <div class="clock" id="os2045TrayClock">
159                    <span id="os2045ClockTime">--:--</span>
160                    <small id="os2045ClockDate">— · —</small>
161                </div>
162            </div>
163            <button class="tray-ai" id="os2045TrayAi" title="CorSch Copilot" aria-label="Copilot öffnen"></button>
164        `;
165        runningEl = taskbarEl.querySelector('#os2045TbRunning');
166        trayClockEl = taskbarEl.querySelector('#os2045ClockTime');
167        trayDateEl  = taskbarEl.querySelector('#os2045ClockDate');
168
169        // Wire start button
170        taskbarEl.querySelector('#os2045StartBtn').addEventListener('click', (e) => {
171            e.stopPropagation();
172            if (window.OS2045StartMenu) window.OS2045StartMenu.toggle();
173        });
174        // Search opens start menu (placeholder for command palette in future)
175        taskbarEl.querySelector('#os2045TbSearch').addEventListener('click', () => {
176            if (window.OS2045StartMenu) window.OS2045StartMenu.open(true);
177        });
178        // Tray bell → notification center
179        taskbarEl.querySelector('#os2045TrayBell').addEventListener('click', (e) => {
180            e.stopPropagation();
181            if (window.OS2045Notify) window.OS2045Notify.toggle();
182            else if (typeof showToast === 'function') showToast('Keine neuen Benachrichtigungen', 'info');
183        });
184        // Tray quick-settings
185        taskbarEl.querySelector('#os2045TrayQs').addEventListener('click', (e) => {
186            e.stopPropagation();
187            if (window.OS2045QuickSettings) window.OS2045QuickSettings.toggle();
188        });
189        // Tray power
190        taskbarEl.querySelector('#os2045TrayPower').addEventListener('click', (e) => {
191            e.stopPropagation();
192            if (window.OS2045Power) window.OS2045Power.toggle();
193        });
194        // Tray shield → opens Guardian app placeholder
195        taskbarEl.querySelector('#os2045TrayShield').addEventListener('click', () => {
196            if (window.OS2045WM) window.OS2045WM.openApp('guardian', { title: 'Guardian 2FA' });
197        });
198        taskbarEl.querySelector('#os2045TrayAi').addEventListener('click', () => {
199            // Route through the registered app opener so loadAdminModules()
200            // fires and OS2045Copilot.open() actually mounts the chat UI.
201            // Direct OS2045WM.openApp('copilot', { title }) — without a mount
202            // function — falls back to the WM "Phase 3" placeholder.
203            const apps = window.OS2045Apps;
204            if (apps && apps.copilot && typeof apps.copilot.open === 'function') {
205                apps.copilot.open();
206            } else if (window.OS2045Copilot && typeof window.OS2045Copilot.open === 'function') {
207                window.OS2045Copilot.open();
208            } else if (window.OS2045WM) {
209                // Last-resort placeholder so the click still produces a window.
210                window.OS2045WM.openApp('copilot', {
211                    title: 'CorSch Copilot',
212                    html: '<div class="os-window-empty"><div class="os-window-empty-sub">Copilot-Modul wird geladen…</div></div>'
213                });
214            }
215        });
216
217        // Cor-Amp now-playing tray-widget. Hidden until coramp.js dispatches
218        // 'os2045:coramp-state' events (see coramp.js play/pause/load hooks).
219        const npEl = taskbarEl.querySelector('#os2045TbNowPlaying');
220        if (npEl) {
221            // Click → Play/Pause via the public CorAmp API (or a custom event
222            // when the module isn't loaded yet, which coramp.js intercepts).
223            npEl.addEventListener('click', () => {
224                window.dispatchEvent(new CustomEvent('os2045:coramp-toggle-play'));
225            });
226            npEl.addEventListener('dblclick', () => {
227                if (window.OS2045Apps && window.OS2045Apps.corplayer
228                    && typeof window.OS2045Apps.corplayer.open === 'function') {
229                    window.OS2045Apps.corplayer.open();
230                }
231            });
232            window.addEventListener('os2045:coramp-state', (e) => {
233                const d = e.detail || {};
234                if (!d.title) {
235                    npEl.hidden = true;
236                    return;
237                }
238                npEl.hidden = false;
239                const titleEl = npEl.querySelector('#os2045TbNpTitle');
240                const iconEl  = npEl.querySelector('#os2045TbNpIcon');
241                if (titleEl) titleEl.textContent = d.title;
242                if (iconEl)  iconEl.textContent  = d.playing ? '⏸' : '▶';
243                npEl.classList.toggle('is-playing', !!d.playing);
244            });
245        }
246        // Tray icon visibility is permission-aware:
247        //   - Shield: admin only (Guardian config is admin-only feature)
248        //   - Copilot: admin OR a non-admin user whose allowedIcons includes
249        //     'copilot' (admin grants the icon via the Benutzer-Window).
250        const aiBtn = taskbarEl.querySelector('#os2045TrayAi');
251        const shieldBtn = taskbarEl.querySelector('#os2045TrayShield');
252        function syncTrayPermissions() {
253            const loggedIn = !!(window.adminToken && document.body.classList.contains('logged-in'));
254            const role = window.currentUserRole || 'admin';
255            const isAdmin = loggedIn && (!window.currentUserRole || role === 'admin');
256            // Copilot visible if admin, or non-admin user has 'copilot' in allowedIcons
257            let canCopilot = false;
258            if (loggedIn) {
259                if (isAdmin) canCopilot = true;
260                else if (Array.isArray(window.currentUserAllowedIcons)
261                         && window.currentUserAllowedIcons.includes('copilot')) {
262                    canCopilot = true;
263                }
264            }
265            if (aiBtn) aiBtn.style.display = canCopilot ? '' : 'none';
266            if (shieldBtn) shieldBtn.style.display = isAdmin ? '' : 'none';
267        }
268        syncTrayPermissions();
269        if (window.MutationObserver) {
270            new MutationObserver(syncTrayPermissions).observe(document.body, {
271                attributes: true, attributeFilter: ['class']
272            });
273        }
274        // Re-evaluate when allowedIcons change at runtime (admin granted the
275        // icon while user was logged in).
276        window.addEventListener('os2045:allowed-icons-changed', syncTrayPermissions);
277        return taskbarEl;
278    }
279
280    // Mirror of start-menu / shell-bootstrap whitelist for guest sessions.
281    const TASKBAR_GUEST_APPS = new Set([
282        'calculator', 'notes', 'color-picker',
283        'games', 'settings', 'impressum', 'datenschutz'
284    ]);
285    const TASKBAR_NONADMIN_ALWAYS_ON = new Set([
286        'settings', 'account', 'logout', 'impressum', 'datenschutz'
287    ]);
288    function isAdminLoggedIn() {
289        return !!(window.adminToken && document.body && document.body.classList.contains('logged-in')
290                  && (!window.currentUserRole || window.currentUserRole === 'admin'));
291    }
292    function isAppVisibleForRole(appId) {
293        if (isAdminLoggedIn()) return true;
294        if (window.currentUserRole === 'viewer') return TASKBAR_GUEST_APPS.has(appId);
295        if (window.adminToken && window.currentUserRole && window.currentUserRole !== 'admin') {
296            const allowed = window.currentUserAllowedIcons;
297            if (Array.isArray(allowed)) {
298                return allowed.includes(appId) || TASKBAR_NONADMIN_ALWAYS_ON.has(appId);
299            }
300        }
301        return TASKBAR_GUEST_APPS.has(appId);
302    }
303
304    function render() {
305        if (!runningEl) return;
306        const wmState = window.OS2045WM ? window.OS2045WM.getState() : [];
307        const runningIds = new Set(wmState.map(w => w.id));
308        // Defensive copy so we don't mutate the persisted/default array
309        let list = (pinned || defaultPinned()).map(p => Object.assign({}, p));
310        // Guest filter: hide admin app pins. The persisted localStorage list
311        // can carry admin apps from a previous admin session — strip them
312        // when we're running anonymously.
313        list = list.filter(p => p && isAppVisibleForRole(p.id));
314        // Add running apps that aren't pinned (transient buttons).
315        // Resolve icon: window-supplied → OS2045Apps registry → start-menu
316        // catalog → generic files-fallback. Without the catalog lookup the
317        // taskbar showed a blank/default icon for windows whose creator
318        // didn't pass opts.icon (e.g. SSH from the legacy ssh module).
319        wmState.forEach(w => {
320            if (!list.find(p => p.id === w.id)) {
321                // Prefer start-menu catalog over OS2045Apps registry +
322                // window-supplied icon, so taskbar / window-header / desktop
323                // all show the same glyph for one app id.
324                let iconFor = null;
325                if (window.OS2045StartMenu && typeof window.OS2045StartMenu._catalog === 'function') {
326                    try {
327                        const cat = window.OS2045StartMenu._catalog();
328                        const app = Array.isArray(cat) ? cat.find(a => a && a.id === w.id) : null;
329                        if (app && app.icon) iconFor = app.icon;
330                    } catch (_) {}
331                }
332                if (!iconFor && window.OS2045Apps && window.OS2045Apps[w.id] && window.OS2045Apps[w.id].icon) {
333                    iconFor = window.OS2045Apps[w.id].icon;
334                }
335                if (!iconFor) iconFor = w.icon;
336                list.push({ id: w.id, title: w.title, icon: iconFor || filesIcon(), transient: true });
337            }
338        });
339
340        runningEl.innerHTML = '';
341        list.forEach(p => {
342            const btn = document.createElement('button');
343            btn.className = 'tb-btn';
344            btn.dataset.appId = p.id;
345            btn.title = p.title;
346            btn.setAttribute('aria-label', p.title);
347            const tiDiv = document.createElement('div');
348            tiDiv.className = 'ti';
349            safeSvgInsert(tiDiv, p.icon || filesIcon());
350            btn.appendChild(tiDiv);
351            const isRunning = runningIds.has(p.id);
352            const winState  = wmState.find(w => w.id === p.id);
353            if (isRunning) btn.classList.add('running');
354            if (isRunning && !winState.minimized && winState.focused) btn.classList.add('active');
355            btn.addEventListener('click', () => smartToggle(p));
356            runningEl.appendChild(btn);
357        });
358    }
359
360    function smartToggle(p) {
361        const wm = window.OS2045WM;
362        if (!wm) return;
363        if (!wm.isOpen(p.id)) {
364            // Open the app via OS2045Apps if registered, else open empty window
365            const reg = (window.OS2045Apps && window.OS2045Apps[p.id]) || null;
366            if (reg && typeof reg.open === 'function') reg.open();
367            else wm.openApp(p.id, { title: p.title, icon: p.icon });
368            return;
369        }
370        const st = wm.getState().find(w => w.id === p.id);
371        if (!st) return;
372        if (st.minimized) wm.restore(p.id);
373        else if (st.focused) wm.minimize(p.id);
374        else wm.focus(p.id);
375    }
376
377    function pad2(n) { return String(n).padStart(2, '0'); }
378
379    function refreshClock() {
380        const d = new Date();
381        if (trayClockEl) trayClockEl.textContent = pad2(d.getHours()) + ':' + pad2(d.getMinutes());
382        if (trayDateEl)  trayDateEl.textContent  = DAYS_SHORT[d.getDay()] + ' ' + pad2(d.getDate()) + ' ' + MONTHS[d.getMonth()];
383    }
384
385    const api = {
386        mount(container) {
387            if (!isOs2045()) return null;
388            const el = build();
389            const desktop = container || document.getElementById('os2045Desktop');
390            if (desktop && !desktop.contains(el)) desktop.appendChild(el);
391            else if (!desktop && !document.body.contains(el)) document.body.appendChild(el);
392            // Subscribe to WM
393            if (window.OS2045WM && typeof window.OS2045WM.onChange === 'function') {
394                if (typeof unsubWM === 'function') unsubWM();
395                unsubWM = window.OS2045WM.onChange(render);
396            }
397            render();
398            refreshClock();
399            if (clockTimer) clearInterval(clockTimer);
400            clockTimer = setInterval(refreshClock, 30000);
401            return el;
402        },
403        update: render,
404        refreshClock,
405        getElement() { return taskbarEl; },
406        pin(appId, opts) {
407            opts = opts || {};
408            const list = pinned || defaultPinned();
409            if (!list.find(p => p.id === appId)) {
410                list.push({ id: appId, title: opts.title || appId, icon: opts.icon || filesIcon() });
411                pinned = list;
412                savePinned();
413                render();
414            }
415        },
416        unpin(appId) {
417            const list = pinned || defaultPinned();
418            pinned = list.filter(p => p.id !== appId);
419            savePinned();
420            render();
421        }
422    };
423
424    window.OS2045Taskbar = api;
425})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.