PageSourceSearch

https://pupwife.com/scripts/desktop/citrusFolderWindow.js

js pupwife.com collected 2026-09-30 15:04:20 UTC 23,858 bytes, 572 lines download raw bytes

1/**
2 * CitrusOS folder windows — daedalOS-style explorer inside normal window chrome.
3 */
4(function (global) {
5    'use strict';
6
7    /** @type {object|null} */
8    var bridge = null;
9    /** @type {Record<string, { path: string[] }>} */
10    var folderPaths = {};
11    /** @type {WeakMap<HTMLElement, { folderId: string, casinoBound: boolean }>} */
12    var mountState = new WeakMap();
13
14    function folderWinId(folderId) {
15        return 'folder:' + folderId;
16    }
17
18    function ensureStylesheet(href) {
19        if (document.querySelector('link[href="' + href + '"], link[data-spa-stylesheet="' + href + '"]')) {
20            return Promise.resolve();
21        }
22        return new Promise(function (resolve) {
23            var link = document.createElement('link');
24            link.rel = 'stylesheet';
25            link.href = href;
26            link.setAttribute('data-spa-stylesheet', href);
27            link.onload = function () { resolve(); };
28            link.onerror = function () { resolve(); };
29            document.head.appendChild(link);
30            global.setTimeout(resolve, 1500);
31        });
32    }
33
34    function loadScript(src) {
35        if (global.PupwifeScriptLoad && typeof global.PupwifeScriptLoad.once === 'function') {
36            return global.PupwifeScriptLoad.once(src);
37        }
38        return new Promise(function (resolve, reject) {
39            if (document.querySelector('script[src="' + src + '"]')) {
40                resolve();
41                return;
42            }
43            var s = document.createElement('script');
44            s.src = src;
45            s.async = false;
46            s.onload = function () { resolve(); };
47            s.onerror = function () { reject(new Error('Failed to load ' + src)); };
48            document.body.appendChild(s);
49        });
50    }
51
52    function ensureCasinoScripts() {
53        var scripts = [
54            '/scripts/casino/casinoCore.js',
55            '/scripts/casino/casinoLuck.js',
56            '/scripts/casino/casinoWallet.js',
57            '/scripts/casino/casinoShell.js'
58        ];
59        var chain = Promise.resolve();
60        for (var i = 0; i < scripts.length; i++) {
61            (function (src) {
62                chain = chain.then(function () { return loadScript(src); });
63            })(scripts[i]);
64        }
65        return chain;
66    }
67
68    function buildCasinoHeader() {
69        var header = document.createElement('div');
70        header.className = 'citrus-folder-casino-lobby';
71        header.setAttribute('data-casino-lobby', '1');
72        header.innerHTML =
73            '<div class="citrus-folder-casino-lobby__row">' +
74            '  <div class="casino-top-wallet" data-casino-wallet-block hidden>' +
75            '    <div class="casino-balance">' +
76            '      <i class="fa-solid fa-coins" aria-hidden="true"></i>' +
77            '      <span data-casino-balance>—</span>' +
78            '    </div>' +
79            '    <button type="button" class="casino-btn casino-btn--gold citrus-folder-casino-btn" data-casino-daily-claim>' +
80            '      <i class="fa-solid fa-gift" aria-hidden="true"></i>' +
81            '      <span data-casino-claim-label>Claim daily</span>' +
82            '    </button>' +
83            '  </div>' +
84            '  <p class="citrus-folder-casino-guest" data-casino-guest-banner hidden>' +
85            '    <button type="button" class="casino-btn citrus-folder-casino-btn" data-casino-sign-in>Sign in</button> to bet coins' +
86            '  </p>' +
87            '</div>';
88        return header;
89    }
90
91    function bindCasinoLobby(root) {
92        if (!root || !global.CasinoWallet || !global.CasinoShell) return Promise.resolve();
93        return global.CasinoWallet.init().then(function () {
94            global.CasinoShell.initPage(root);
95        });
96    }
97
98    function destroyCasinoLobby(root) {
99        if (!root) return;
100        if (global.CasinoShell && typeof global.CasinoShell.destroy === 'function') {
101            global.CasinoShell.destroy();
102        }
103        if (global.CasinoWallet && typeof global.CasinoWallet.destroy === 'function') {
104            global.CasinoWallet.destroy();
105        }
106    }
107
108    function iconMarkup(item) {
109        if (global.CitrusFolderIcon && global.CitrusFolderIcon.isFolderIcon(item)) {
110            var peek = global.CitrusFolderRegistry && typeof global.CitrusFolderRegistry.getPeekItemsForNode === 'function'
111                ? global.CitrusFolderRegistry.getPeekItemsForNode(item)
112                : [];
113            return global.CitrusFolderIcon.folderIconMarkup('grid', peek);
114        }
115        if (global.CitrusAppIcon && typeof global.CitrusAppIcon.markup === 'function' && item.href) {
116            var app = global.CitrusAppIcon.resolveForHref(item.href, item.icon);
117            return global.CitrusAppIcon.markup(app, {
118                folderVariant: 'grid',
119                imgClass: 'citrus-folder-icon__img',
120                faClass: 'citrus-folder-icon__fa'
121            });
122        }
123        if (item.iconType === 'img' || (item.icon && (item.icon.indexOf('/') !== -1 || item.icon.indexOf('http') === 0))) {
124            return '<img class="citrus-folder-icon__img" src="' + item.icon + '" alt="" draggable="false">';
125        }
126        var fa = item.icon || 'fa-solid fa-folder';
127        if (fa.indexOf('fa-') === 0) fa = 'fa-solid ' + fa;
128        return '<i class="' + fa + ' citrus-folder-icon__fa" aria-hidden="true"></i>';
129    }
130
131    function renderBreadcrumb(breadcrumb) {
132        if (!breadcrumb || breadcrumb.length <= 1) return '';
133        var parts = [];
134        for (var i = 1; i < breadcrumb.length; i++) {
135            parts.push(breadcrumb[i].label);
136        }
137        return parts.join(' › ');
138    }
139
140    function renderGrid(listing) {
141        var html = '';
142        for (var i = 0; i < listing.length; i++) {
143            var item = listing[i];
144            var badge = '';
145            if (item.badge) {
146                badge = '<span class="citrus-folder-icon__badge">' + item.badge + '</span>';
147            } else if (item.pay) {
148                badge = '<span class="citrus-folder-icon__badge citrus-folder-icon__badge--pay">' + item.pay + '</span>';
149            }
150            html +=
151                '<button type="button" class="citrus-folder-icon" data-entry-id="' + item.id + '" data-entry-type="' + item.type + '"' +
152                (item.href ? ' data-entry-href="' + item.href + '"' : '') +
153                (item.external ? ' data-entry-external="1"' : '') +
154                (item.userFolderId ? ' data-user-folder-id="' + item.userFolderId + '"' : '') +
155                ' title="' + item.label.replace(/"/g, '&quot;') + '">' +
156                '<span class="citrus-folder-icon__graphic">' + iconMarkup(item) + badge + '</span>' +
157                '<span class="citrus-folder-icon__label">' + item.label + '</span>' +
158                '</button>';
159        }
160        return html;
161    }
162
163    function clearSelection(gridEl) {
164        if (!gridEl) return;
165        var selected = gridEl.querySelectorAll('.citrus-folder-icon--selected');
166        for (var i = 0; i < selected.length; i++) {
167            selected[i].classList.remove('citrus-folder-icon--selected');
168        }
169    }
170
171    function navigateFolder(win, folderId, pathSegments) {
172        if (!win || !win.contentEl) return;
173        var resolved = global.CitrusFolderRegistry.resolvePath(folderId, pathSegments);
174        folderPaths[folderId] = { path: pathSegments.slice() };
175
176        var shell = win.contentEl.querySelector('.citrus-folder-window');
177        if (!shell) return;
178
179        var backBtn = shell.querySelector('[data-folder-back]');
180        var upBtn = shell.querySelector('[data-folder-up]');
181        var crumb = shell.querySelector('[data-folder-crumb]');
182        var grid = shell.querySelector('[data-folder-grid]');
183        var status = shell.querySelector('[data-folder-status]');
184
185        if (backBtn) backBtn.disabled = pathSegments.length === 0;
186        if (upBtn) upBtn.disabled = pathSegments.length === 0;
187        if (crumb) crumb.textContent = renderBreadcrumb(resolved.breadcrumb);
188
189        if (grid) {
190            clearSelection(grid);
191            grid.innerHTML = renderGrid(resolved.listing);
192            bindGridDnD(grid, { containerId: folderId, path: pathSegments.slice() });
193        }
194
195        if (status) {
196            var count = resolved.listing.length;
197            status.textContent = count + ' item' + (count === 1 ? '' : 's');
198        }
199
200        syncStickyNotesToolbar(shell, grid);
201    }
202
203    function openEntry(win, folderId, entryEl) {
204        if (!entryEl) return;
205        var type = entryEl.getAttribute('data-entry-type');
206        var entryId = entryEl.getAttribute('data-entry-id');
207
208        if (type === 'folder') {
209            var userFolderId = entryEl.getAttribute('data-user-folder-id');
210            var path = (folderPaths[folderId] && folderPaths[folderId].path) ? folderPaths[folderId].path.slice() : [];
211            if (userFolderId && String(folderId).indexOf('user:') !== 0) {
212                open('user:' + userFolderId);
213                return;
214            }
215            if (userFolderId) {
216                path.push(userFolderId);
217                navigateFolder(win, folderId, path);
218                return;
219            }
220            path.push(entryId);
221            navigateFolder(win, folderId, path);
222            return;
223        }
224
225        if (type === 'note') {
226            if (global.CitrusWidgets && typeof global.CitrusWidgets.openNote === 'function') {
227                global.CitrusWidgets.openNote(entryId);
228            }
229            return;
230        }
231
232        if (type === 'app') {
233            var href = entryEl.getAttribute('data-entry-href');
234            var external = entryEl.getAttribute('data-entry-external') === '1';
235            if (!href) return;
236            if (external) {
237                global.location.assign(href);
238            } else if (bridge && typeof bridge.openHref === 'function') {
239                bridge.openHref(href);
240            }
241            var shell = win.contentEl && win.contentEl.querySelector('.citrus-folder-window');
242            var grid = shell && shell.querySelector('[data-folder-grid]');
243            clearSelection(grid);
244            syncStickyNotesToolbar(shell, grid);
245        }
246    }
247
248    function syncStickyNotesToolbar(shell, grid) {
249        if (!shell || shell.getAttribute('data-folder-id') !== 'stickynotes') return;
250        var deleteBtn = shell.querySelector('[data-folder-delete-note]');
251        if (!deleteBtn || !grid) return;
252        var selected = grid.querySelector('.citrus-folder-icon--selected');
253        var canDelete = !!(selected && selected.getAttribute('data-entry-type') === 'note');
254        deleteBtn.disabled = !canDelete;
255    }
256
257    function deleteSelectedStickyNote(win, folderId, grid) {
258        if (folderId !== 'stickynotes' || !grid) return;
259        var selected = grid.querySelector('.citrus-folder-icon--selected');
260        if (!selected || selected.getAttribute('data-entry-type') !== 'note') return;
261
262        var noteId = selected.getAttribute('data-entry-id');
263        var note = global.CitrusStickyNotes && global.CitrusStickyNotes.getNote
264            ? global.CitrusStickyNotes.getNote(noteId)
265            : null;
266        var label = note && note.title ? note.title : 'this note';
267        function doDelete() {
268            if (global.CitrusWidgets && typeof global.CitrusWidgets.closeNoteWidgets === 'function') {
269                global.CitrusWidgets.closeNoteWidgets(noteId);
270            }
271            if (global.CitrusStickyNotes && typeof global.CitrusStickyNotes.deleteNote === 'function') {
272                global.CitrusStickyNotes.deleteNote(noteId);
273            }
274            navigateFolder(win, folderId, []);
275        }
276
277        if (global.CitrusShellDialogs && typeof global.CitrusShellDialogs.confirm === 'function') {
278            global.CitrusShellDialogs.confirm({
279                title: 'Delete note?',
280                message: 'Delete "' + label + '" permanently? This cannot be undone.',
281                confirmLabel: 'Delete',
282                danger: true
283            }).then(function (ok) {
284                if (ok) doDelete();
285            });
286            return;
287        }
288
289        if (!global.confirm('Delete "' + label + '" permanently? This cannot be undone.')) {
290            return;
291        }
292        doDelete();
293    }
294
295    function bindGridDnD(grid, context) {
296        if (!grid || !global.CitrusDesktopDnD) return;
297        var icons = grid.querySelectorAll('.citrus-folder-icon');
298        for (var i = 0; i < icons.length; i++) {
299            if (icons[i].getAttribute('data-dnd-bound') === '1') continue;
300            icons[i].setAttribute('data-dnd-bound', '1');
301            global.CitrusDesktopDnD.bindFolderIconDrag(icons[i], context);
302        }
303        global.CitrusDesktopDnD.bindFolderGridDropZone(grid, context);
304    }
305
306    function refreshOpenFolders() {
307        if (!bridge || typeof bridge.getWindow !== 'function') return;
308        var keys = Object.keys(folderPaths);
309        for (var i = 0; i < keys.length; i++) {
310            var folderId = keys[i];
311            var win = bridge.getWindow(folderWinId(folderId));
312            if (!win) continue;
313            var path = folderPaths[folderId] ? folderPaths[folderId].path.slice() : [];
314            navigateFolder(win, folderId, path);
315        }
316    }
317
318    function resolveFolderMeta(folderId) {
319        if (String(folderId).indexOf('user:') === 0) {
320            var uf = global.CitrusUserFolders && global.CitrusUserFolders.getFolder
321                ? global.CitrusUserFolders.getFolder(folderId.slice(5))
322                : null;
323            return {
324                label: uf ? uf.label : 'Folder',
325                icon: (uf && uf.icon) || 'fa-solid fa-folder',
326                iconType: 'fa'
327            };
328        }
329        return global.CitrusFolderRegistry.FOLDER_META[folderId] || { label: folderId, iconType: 'citrus-folder' };
330    }
331
332    function bindGridEvents(win, folderId, grid) {
333        if (!grid || grid.getAttribute('data-folder-grid-bound') === '1') return;
334        grid.setAttribute('data-folder-grid-bound', '1');
335
336        var lastTap = { time: 0, id: null };
337
338        grid.addEventListener('click', function (ev) {
339            var btn = ev.target.closest('.citrus-folder-icon');
340            if (!btn) {
341                clearSelection(grid);
342                syncStickyNotesToolbar(win.contentEl && win.contentEl.querySelector('.citrus-folder-window'), grid);
343                return;
344            }
345            ev.stopPropagation();
346            clearSelection(grid);
347            btn.classList.add('citrus-folder-icon--selected');
348            syncStickyNotesToolbar(win.contentEl && win.contentEl.querySelector('.citrus-folder-window'), grid);
349        });
350
351        grid.addEventListener('dblclick', function (ev) {
352            var btn = ev.target.closest('.citrus-folder-icon');
353            if (!btn) return;
354            ev.preventDefault();
355            openEntry(win, folderId, btn);
356        });
357
358        grid.addEventListener('pointerup', function (ev) {
359            var btn = ev.target.closest('.citrus-folder-icon');
360            if (!btn || ev.pointerType === 'mouse') return;
361            var now = Date.now();
362            var id = btn.getAttribute('data-entry-id');
363            if (lastTap.time && now - lastTap.time < 450 && lastTap.id === id) {
364                lastTap.time = 0;
365                openEntry(win, folderId, btn);
366            } else {
367                lastTap.time = now;
368                lastTap.id = id;
369            }
370        });
371    }
372
373    function mountFolderBody(win, folderId, initialPath) {
374        if (!win || !win.contentEl) return;
375
376        var meta = resolveFolderMeta(folderId);
377        var path = initialPath || [];
378        folderPaths[folderId] = { path: path.slice() };
379
380        var resolved = global.CitrusFolderRegistry.resolvePath(folderId, path);
381        var isCasino = folderId === 'casino';
382        var isStickyNotes = folderId === 'stickynotes';
383
384        var shell = document.createElement('div');
385        shell.className = 'citrus-folder-window';
386        shell.setAttribute('data-folder-id', folderId);
387
388        var toolbar = document.createElement('div');
389        toolbar.className = 'citrus-folder-toolbar';
390        toolbar.innerHTML =
391            '<button type="button" class="citrus-folder-toolbar-btn" data-folder-back aria-label="Back" dis
391abled>' +
392            '<i class="fa-solid fa-arrow-left" aria-hidden="true"></i></button>' +
393            '<button type="button" class="citrus-folder-toolbar-btn" data-folder-up aria-label="Up" disabled>' +
394            '<i class="fa-solid fa-arrow-up" aria-hidden="true"></i></button>' +
395            '<span class="citrus-folder-toolbar__crumb" data-folder-crumb></span>';
396
397        if (isStickyNotes) {
398            toolbar.innerHTML +=
399                '<span class="citrus-folder-toolbar__spacer" aria-hidden="true"></span>' +
400                '<button type="button" class="citrus-folder-toolbar-btn citrus-folder-toolbar-btn--text" data-folder-new-note>' +
401                '<i class="fa-solid fa-file-circle-plus" aria-hidden="true"></i> New note</button>' +
402                '<button type="button" class="citrus-folder-toolbar-btn citrus-folder-toolbar-btn--text" data-folder-delete-note disabled>' +
403                '<i class="fa-solid fa-trash-can" aria-hidden="true"></i> Delete</button>';
404        }
405
406        shell.appendChild(toolbar);
407
408        if (isCasino) {
409            shell.appendChild(buildCasinoHeader());
410        }
411
412        var body = document.createElement('div');
413        body.className = 'citrus-folder-body';
414
415        var grid = document.createElement('div');
416        grid.className = 'citrus-folder-grid';
417        grid.setAttribute('data-folder-grid', '1');
418        grid.innerHTML = renderGrid(resolved.listing);
419        body.appendChild(grid);
420
421        var statusBar = document.createElement('div');
422        statusBar.className = 'citrus-folder-status';
423        statusBar.setAttribute('data-folder-status', '1');
424        var count = resolved.listing.length;
425        statusBar.textContent = count + ' item' + (count === 1 ? '' : 's');
426        body.appendChild(statusBar);
427
428        shell.appendChild(body);
429
430        win.contentEl.innerHTML = '';
431        win.contentEl.appendChild(shell);
432
433        bindGridEvents(win, folderId, grid);
434        bindGridDnD(grid, { containerId: folderId, path: path.slice() });
435
436        var backBtn = shell.querySelector('[data-folder-back]');
437        var upBtn = shell.querySelector('[data-folder-up]');
438        var crumbEl = shell.querySelector('[data-folder-crumb]');
439        if (crumbEl) crumbEl.textContent = renderBreadcrumb(resolved.breadcrumb);
440
441        function goBack() {
442            var current = folderPaths[folderId] ? folderPaths[folderId].path.slice() : [];
443            if (!current.length) return;
444            current.pop();
445            navigateFolder(win, folderId, current);
446        }
447
448        if (backBtn) backBtn.addEventListener('click', goBack);
449        if (upBtn) upBtn.addEventListener('click', goBack);
450
451        var newNoteBtn = shell.querySelector('[data-folder-new-note]');
452        if (newNoteBtn) {
453            newNoteBtn.addEventListener('click', function () {
454                if (global.CitrusWidgets && typeof global.CitrusWidgets.createNoteAndOpen === 'function') {
455                    global.CitrusWidgets.createNoteAndOpen();
456                }
457            });
458        }
459
460        var deleteNoteBtn = shell.querySelector('[data-folder-delete-note]');
461        if (deleteNoteBtn) {
462            deleteNoteBtn.addEventListener('click', function () {
463                deleteSelectedStickyNote(win, folderId, grid);
464            });
465        }
466
467        shell.addEventListener('click', function (ev) {
468            if (ev.target.closest('.citrus-folder-icon')) return;
469            if (ev.target.closest('.citrus-folder-toolbar')) return;
470            if (ev.target.closest('.citrus-folder-casino-lobby')) return;
471            clearSelection(grid);
472            syncStickyNotesToolbar(shell, grid);
473        });
474
475        mountState.set(win.contentEl, { folderId: folderId, casinoBound: false });
476
477        if (isCasino) {
478            ensureStylesheet('/css/casino.css').then(function () {
479                return ensureCasinoScripts();
480            }).then(function () {
481                var lobby = shell.querySelector('[data-casino-lobby]');
482                if (lobby) {
483                    return bindCasinoLobby(lobby);
484                }
485            }).then(function () {
486                var state = mountState.get(win.contentEl);
487                if (state) state.casinoBound = true;
488            }).catch(function (err) {
489                console.warn('[CitrusFolderWindow] casino lobby', err);
490            });
491        }
492    }
493
494    function open(folderId, opts) {
495        if (!bridge || !global.CitrusFolderRegistry) return null;
496        opts = opts || {};
497
498        var winId = folderWinId(folderId);
499        var existing = bridge.getWindow(winId);
500        if (existing) {
501            if (existing.minimized && bridge.restoreWindow) bridge.restoreWindow(winId);
502            if (!opts.silent) bridge.focusWindow(winId);
503            if (opts.path) navigateFolder(existing, folderId, opts.path);
504            return existing;
505        }
506
507        var meta = resolveFolderMeta(folderId);
508        var app = {
509            id: winId,
510            label: meta.label,
511            icon: meta.icon,
512            iconType: meta.iconType || 'citrus-folder',
513            launchTier: 'folder',
514            folderId: folderId,
515            defaultSize: folderId === 'casino'
516                ? { w: 760, h: 560 }
517                : (folderId === 'stickynotes' ? { w: 520, h: 420 } : { w: 680, h: 500 }),
518            singleton: String(folderId).indexOf('user:') !== 0,
519            resizable: false,
520            isFolder: true
521        };
522        if (global.CitrusFolderIcon && typeof global.CitrusFolderIcon.normalizeFolderApp === 'function') {
523            global.CitrusFolderIcon.normalizeFolderApp(app);
524        }
525
526        var win = bridge.openWindow(app, opts);
527        if (!win) return null;
528        mountFolderBody(win, folderId, opts.path || []);
529        return win;
530    }
531
532    function onWindowClose(win) {
533        if (!win || !win.contentEl) return;
534        var shell = win.contentEl.querySelector('.citrus-folder-window');
535        var grid = shell && shell.querySelector('[data-folder-grid]');
536        clearSelection(grid);
537        var state = mountState.get(win.contentEl);
538        if (state && state.folderId === 'casino') {
539            var lobby = win.contentEl.querySelector('[data-casino-lobby]');
540            destroyCasinoLobby(lobby);
541        }
542        if (state) mountState.delete(win.contentEl);
543    }
544
545    function getFolderPath(folderId) {
546        return folderPaths[folderId] ? folderPaths[folderId].path.slice() : [];
547    }
548
549    function init(bridgeApi) {
550        bridge = bridgeApi;
551        if (global.__citrusStickyNotesFolderListener) return;
552        global.__citrusStickyNotesFolderListener = true;
553        global.addEventListener('pupwife:citrus-sticky-notes-changed', function () {
554            if (!bridge || typeof bridge.getWindow !== 'function') return;
555            var win = bridge.getWindow(folderWinId('stickynotes'));
556            if (!win || !win.contentEl) return;
557            var path = folderPaths.stickynotes ? folderPaths.stickynotes.path.slice() : [];
558            navigateFolder(win, 'stickynotes', path);
559        });
560    }
561
562    global.CitrusFolderWindow = {
563        init: init,
564        open: open,
565        onWindowClose: onWindowClose,
566        folderWinId: folderWinId,
567        getFolderPath: getFolderPath,
568        navigateFolder: navigateFolder,
569        refreshOpenFolders: refreshOpenFolders,
570        resolveFolderMeta: resolveFolderMeta
571    };
572})(typeof window !== 'undefined' ? window : this);

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.