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, '"') + '">' + 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.