1/** 2 * CitrusOS app registry â desktop / Start menu apps and launch tiers. 3 */ 4(function (global) { 5 'use strict'; 6 7 /** Apps that leave CitrusOS and open as a full page (too heavy for in-shell loader). */ 8 var EXTERNAL_LAUNCH = {}; 9 10 var DEFAULT_SIZE = { w: 920, h: 640 }; 11 12 /** Apps that support F11 / titlebar maximize immersive focus inside CitrusOS. */ 13 var LEAF_IMMERSIVE_FOCUS = { 14 'pupocalypse.html': true, 15 'daw.html': true 16 }; 17 18 /** Apps that support F11 immersive focus only (no titlebar maximize button). */ 19 var LEAF_IMMERSIVE_KEYBOARD = { 20 'dnd.html': true 21 }; 22 23 var LEAF_NO_RESIZE = { 24 'daw.html': true, 25 'noisegenerator.html': true, 26 'palette.html': true, 27 'gradientgenerator.html': true, 28 'frameflipper.html': true, 29 'whoshere.html': true, 30 'trimmer.html': true, 31 'workout.html': true, 32 'plong.html': true, 33 'kittysensory.html': true, 34 'cookbook.html': true, 35 'studyhall.html': true, 36 'terrarium.html': true, 37 'dnd.html': true, 38 'taskboard.html': true, 39 'adminpanel.html': true, 40 'aboutme.html': true, 41 'contact.html': true, 42 'terms.html': true, 43 'privacy.html': true, 44 'attributions.html': true, 45 'bookings.html': true, 46 'changelog.html': true 47 }; 48 49 var LEAF_DEFAULT_SIZES = { 50 'pupocalypse.html': { w: 900, h: 560 }, 51 'weather.html': { w: 520, h: 640 }, 52 'sortlab.html': { w: 720, h: 520 }, 53 'timezones.html': { w: 900, h: 620 }, 54 'trimmer.html': { w: 500, h: 540 }, 55 'normalMapping.html': { w: 760, h: 560 }, 56 'uvmapping.html': { w: 800, h: 560 }, 57 'mememaker.html': { w: 820, h: 600 }, 58 'music.html': { w: 900, h: 600 }, 59 'daw.html': { w: 1000, h: 380 }, 60 'strudel.html': { w: 960, h: 640 }, 61 'noisegenerator.html': { w: 300, h: 175 }, 62 'palette.html': { w: 520, h: 370 }, 63 'gradientgenerator.html': { w: 400, h: 300 }, 64 'frameflipper.html': { w: 880, h: 720 }, 65 'whoshere.html': { w: 300, h: 480 }, 66 'workout.html': { w: 680, h: 536 }, 67 'terrarium.html': { w: 680, h: 540 }, 68 'plong.html': { w: 420, h: 580 }, 69 'kittysensory.html': { w: 1000, h: 720 }, 70 'cookbook.html': { w: 1100, h: 620 }, 71 'studyhall.html': { w: 800, h: 600 }, 72 'dnd.html': { w: 1100, h: 720 }, 73 'taskboard.html': { w: 900, h: 640 }, 74 'adminpanel.html': { w: 880, h: 580 }, 75 'aboutme.html': { w: 420, h: 580 }, 76 'contact.html': { w: 420, h: 580 }, 77 'terms.html': { w: 420, h: 580 }, 78 'privacy.html': { w: 420, h: 580 }, 79 'attributions.html': { w: 420, h: 580 }, 80 'bookings.html': { w: 420, h: 580 }, 81 'changelog.html': { w: 480, h: 560 } 82 }; 83 84 var LEAF_LAUNCH_TIERS = { 85 'puppypaint.html': 'fullscreen' 86 }; 87 88 var DESKTOP_APPS = [ 89 { id: 'arcade', label: 'Arcade', href: '/arcade.html', iconType: 'citrus-folder', launchTier: 'folder', folderId: 'arcade', defaultSize: { w: 680, h: 500 }, resizable: false }, 90 { id: 'silly', label: 'Silly Things', href: '/silly.html', iconType: 'citrus-folder', launchTier: 'folder', folderId: 'silly', defaultSize: { w: 680, h: 500 }, resizable: false }, 91 { id: 'casino', label: 'Casino', href: '/casino.html', iconType: 'citrus-folder', launchTier: 'folder', folderId: 'casino', defaultSize: { w: 760, h: 560 }, resizable: false }, 92 { id: 'shrines', label: 'Shrines', href: '/shrines.html', iconType: 'citrus-folder', launchTier: 'folder', folderId: 'shrines', defaultSize: { w: 560, h: 420 }, resizable: false }, 93 { id: 'stickynotes', label: 'Sticky Notes', href: '/stickynotes.html', iconType: 'citrus-folder', launchTier: 'folder', folderId: 'stickynotes', defaultSize: { w: 520, h: 420 }, resizable: false }, 94 { id: 'siteinfo', label: 'Site Info', href: '/siteinfo.html', iconType: 'citrus-folder', launchTier: 'folder', folderId: 'siteinfo', defaultSize: { w: 560, h: 480 }, resizable: false }, 95 { id: 'music', label: 'Music', href: '/music.html', iconAsset: 'general/app_music.png', icon: 'fa-solid fa-music', iconType: 'fa', launchTier: 'window', defaultSize: { w: 900, h: 600 }, singleton: true, resizable: false }, 96 { id: 'friends', label: 'Friends', href: '/friends.html', iconAsset: 'general/app_friends.png', icon: 'fa-solid fa-user-group', iconType: 'fa', launchTier: 'window' }, 97 { id: 'blog', label: 'Blog', href: '/blog.html', icon: 'fa-solid fa-newspaper', iconType: 'fa', launchTier: 'window', defaultSize: { w: 760, h: 620 }, resizable: false }, 98 { id: 'studyhall', label: 'Study Hall', href: '/studyhall.html', iconAsset: 'general/app_studyhall.png', icon: 'fa-solid fa-graduation-cap', iconType: 'fa', launchTier: 'window', defaultSize: { w: 800, h: 600 }, resizable: false }, 99 { id: 'pupocalypse', label: 'Pupocalypse', href: '/pupocalypse.html', iconAsset: 'games/app_pupocalypse.png', icon: 'fa-solid fa-gamepad', iconType: 'fa', launchTier: 'window', defaultSize: { w: 900, h: 560 }, singleton: true, immersiveFocus: true }, 100 { id: 'cookbook', label: 'Cookbook', href: '/cookbook.html', iconAsset: 'general/app_cookbook.png', icon: 'fa-solid fa-book', iconType: 'fa', launchTier: 'window', defaultSize: { w: 1100, h: 620 }, singleton: true, resizable: false }, 101 { id: 'daw', label: 'Virtual Keyboard', href: '/daw.html', icon: 'fa-solid fa-keyboard', iconType: 'fa', launchTier: 'window', defaultSize: { w: 1000, h: 380 }, singleton: true, immersiveFocus: true, resizable: false }, 102 { id: 'strudel', label: 'Strudel', href: '/strudel.html', icon: 'fa-solid fa-code', iconType: 'fa', launchTier: 'window', defaultSize: { w: 960, h: 640 }, singleton: true, resizable: true }, 103 { id: 'chat', label: 'Chat', href: '/chatroom.html', icon: 'fa-solid fa-comments', iconType: 'fa', launchTier: 'window', defaultSize: { w: 420, h: 560 }, singleton: true, resizable: false }, 104 { id: 'plong', label: 'Plong', href: '/plong.html', iconAsset: 'games/app_plong.png', icon: 'fa-solid fa-table-tennis-paddle-ball', iconType: 'fa', launchTier: 'window', defaultSize: { w: 420, h: 580 }, singleton: true, resizable: false }, 105 { id: 'kittysensory', label: 'Kitty Sensory Corner', href: '/kittysensory.html', icon: 'fa-solid fa-paw', iconType: 'fa', launchTier: 'window', defaultSize: { w: 1000, h: 720 }, singleton: true, resizable: false }, 106 { id: 'taskboard', label: 'Task Board', href: '/taskboard.html', icon: 'fa-solid fa-clipboard-list', iconType: 'fa', launchTier: 'window', defaultSize: { w: 900, h: 640 }, singleton: true, resizable: false }, 107 { id: 'changelog', label: 'Changelog', href: '/changelog.html', icon: 'fa-solid fa-clock-rotate-left', iconType: 'fa', launchTier: 'window', defaultSize: { w: 480, h: 560 }, singleton: true, resizable: false, startMenuHidden: true } 108 ]; 109 110 /** Owner-only desktop icon â not listed in Start menu search. */ 111 var OWNER_ONLY_APPS = [ 112 { 113 id: 'moderation', 114 label: 'Moderation', 115 windowLabel: 'Admin Panel', 116 href: '/adminpanel.html', 117 icon: 'fa-solid fa-computer', 118 iconType: 'fa', 119 launchTier: 'window', 120 defaultSize: { w: 880, h: 580 }, 121 singleton: true, 122 resizable: false, 123 ownerOnly: true, 124 startMenuHidden: true, 125 windowClass: 'citrus-window--admin' 126 } 127 ]; 128 129 var OWNER_NAME = 'puppy'; 130 131 function isSiteOwner() { 132 try { 133 var username = localStorage.getItem('puppy_shared_username') || ''; 134 return username.trim().toLowerCase() === OWNER_NAME; 135 } catch (e) { 136 return false;
137 } 138 } 139 140 function normalizeHref(href) { 141 if (!href) return ''; 142 var key = href.replace(/^\//, '').split('?')[0].split('#')[0]; 143 if (!/\.html$/i.test(key) && key.indexOf('.') === -1) key += '.html'; 144 return '/' + key; 145 } 146 147 function hrefToId(href) { 148 var key = normalizeHref(href).replace(/^\//, ''); 149 return key.replace(/\.html$/i, '').replace(/\//g, '-'); 150 } 151 152 function enrichApp(app) { 153 var entry = { 154 id: app.id, 155 label: app.label, 156 href: normalizeHref(app.href), 157 icon: app.icon, 158 iconAsset: app.iconAsset || null, 159 iconType: app.iconType || null, 160 launchTier: app.launchTier || 'window', 161 folderId: app.folderId || null, 162 folderPath: app.folderPath || null, 163 noteId: app.noteId || null, 164 defaultSize: app.defaultSize || DEFAULT_SIZE, 165 singleton: app.singleton !== false, 166 immersiveFocus: !!app.immersiveFocus, 167 immersiveKeyboard: !!app.immersiveKeyboard, 168 resizable: app.launchTier === 'folder' ? false : (app.resizable !== false), 169 ownerOnly: !!app.ownerOnly, 170 startMenuHidden: !!app.startMenuHidden, 171 windowLabel: app.windowLabel || null, 172 windowClass: app.windowClass || null 173 }; 174 if (global.CitrusFolderIcon && typeof global.CitrusFolderIcon.normalizeFolderApp === 'function') { 175 global.CitrusFolderIcon.normalizeFolderApp(entry); 176 } 177 return entry; 178 } 179 180 function buildApps() { 181 var apps = DESKTOP_APPS.map(enrichApp); 182 if (isSiteOwner()) { 183 apps = apps.concat(OWNER_ONLY_APPS.map(enrichApp)); 184 } 185 return apps; 186 } 187 188 function getStartMenuApps() { 189 return buildApps().filter(function (app) { 190 return !app.startMenuHidden; 191 }); 192 } 193 194 function getApp(id) { 195 var apps = buildApps(); 196 for (var i = 0; i < apps.length; i++) { 197 if (apps[i].id === id) return apps[i]; 198 } 199 if (id && id.indexOf('folder:') === 0) { 200 var folderId = id.replace('folder:', ''); 201 var meta = global.CitrusFolderRegistry && global.CitrusFolderRegistry.FOLDER_META 202 ? global.CitrusFolderRegistry.FOLDER_META[folderId] 203 : null; 204 if (meta) { 205 return enrichApp({ 206 id: id, 207 label: meta.label, 208 href: '/' + folderId + '.html', 209 icon: meta.icon, 210 iconType: meta.iconType || 'citrus-folder', 211 launchTier: 'folder', 212 folderId: folderId, 213 defaultSize: folderId === 'casino' ? { w: 760, h: 560 } : { w: 680, h: 500 } 214 }); 215 } 216 } 217 return null; 218 } 219 220 function getAppById(id, hrefHint) { 221 if (!id) return null; 222 var app = getApp(id); 223 if (app) return app; 224 if (hrefHint) { 225 app = getAppByHref(hrefHint); 226 if (app) return app; 227 } 228 if (id.indexOf('-') !== -1) { 229 app = getAppByHref('/' + id.replace(/-/g, '/') + '.html'); 230 if (app) return app; 231 } 232 return getAppByHref('/' + id + '.html'); 233 } 234 235 function getAppByHref(href) { 236 var key = normalizeHref(href); 237 var apps = buildApps(); 238 for (var i = 0; i < apps.length; i++) { 239 if (apps[i].href === key) return apps[i]; 240 } 241 return getLeafApp(href); 242 } 243 244 function getLeafApp(href) { 245 var key = normalizeHref(href).replace(/^\//, ''); 246 var fileKey = key;
247 var label = key.replace(/\.html$/i, '').replace(/casino\//, '').replace(/\//g, ' '); 248 var icon = 'fa-solid fa-window-maximize'; 249 var external = !!EXTERNAL_LAUNCH[fileKey.split('/').pop()]; 250 var fileName = fileKey.split('/').pop(); 251 var leafSize = LEAF_DEFAULT_SIZES[fileName] || DEFAULT_SIZE; 252 var launchTier = LEAF_LAUNCH_TIERS[fileName] || 'window'; 253 var immersiveFocus = !!LEAF_IMMERSIVE_FOCUS[fileName]; 254 var immersiveKeyboard = !!LEAF_IMMERSIVE_KEYBOARD[fileName]; 255 var resizable = !LEAF_NO_RESIZE[fileName]; 256 257 if (global.CitrusFolderRegistry && global.CitrusFolderRegistry.findAppInFolders) { 258 var found = global.CitrusFolderRegistry.findAppInFolders(href); 259 if (found) { 260 label = found.label; 261 if (found.iconType === 'fa') icon = found.icon; 262 external = found.external; 263 } 264 } 265 266 if (global.ArcadeHubMeta && global.ArcadeHubMeta.getGame) { 267 var arcadeGame = global.ArcadeHubMeta.getGame(fileKey); 268 if (arcadeGame) { 269 label = arcadeGame.title; 270 icon = 'fa-solid ' + (arcadeGame.icon || 'fa-gamepad'); 271 } 272 } 273 274 return enrichApp({ 275 id: hrefToId(href), 276 label: label, 277 href: normalizeHref(href), 278 icon: icon.indexOf('/') === 0 || icon.indexOf('http') === 0 ? icon : icon, 279 launchTier: external ? 'external' : launchTier, 280 defaultSize: leafSize, 281 singleton: true, 282 immersiveFocus: immersiveFocus, 283 immersiveKeyboard: immersiveKeyboard, 284 resizable: resizable 285 }); 286 } 287 288 function matchesFolderIndexItem(item, q) { 289 if (!q) return false; 290 if (item.label.toLowerCase().indexOf(q) !== -1) return true; 291 if (item.href && item.href.toLowerCase().indexOf(q) !== -1) return true; 292 if (item.ancestors && item.ancestors.length) { 293 if (item.ancestors.join(' ').toLowerCase().indexOf(q) !== -1) return true; 294 } 295 if (item.path && item.path.length && item.path.join('/').toLowerCase().indexOf(q) !== -1) return true; 296 return false; 297 } 298 299 function searchApps(query) { 300 var q = (query || '').trim().toLowerCase(); 301 var results = []; 302 var seen = {}; 303 304 function pushResult(entry) { 305 if (!entry || seen[entry.id]) return; 306 seen[entry.id] = true; 307 results.push(entry); 308 } 309 310 var apps = getStartMenuApps(); 311 for (var i = 0; i < apps.length; i++) { 312 var app = apps[i]; 313 if (!q || 314 app.label.toLowerCase().indexOf(q) !== -1 || 315 app.id.toLowerCase().indexOf(q) !== -1 || 316 app.href.toLowerCase().indexOf(q) !== -1) { 317 pushResult(app); 318 } 319 } 320 321 if (q && global.CitrusFolderRegistry && typeof global.CitrusFolderRegistry.flattenSearchIndex === 'function') { 322 var index = global.CitrusFolderRegistry.flattenSearchIndex(); 323 var folderMeta = global.CitrusFolderRegistry.FOLDER_META || {}; 324 for (var j = 0; j < index.length; j++) { 325 var item = index[j]; 326 if (!matchesFolderIndexItem(item, q)) continue; 327 if (item.type === 'app') { 328 pushResult(getLeafApp(item.href)); 329 } else if (item.type === 'note') { 330 pushResult(enrichApp({ 331 id: 'note:' + item.noteId, 332 label: item.label + ' (Sticky Notes)', 333 href: '/stickynotes.html', 334 icon: item.icon || 'fa-solid fa-file-lines', 335 launchTier: 'note', 336 noteId: item.noteId, 337 singleton: false 338 })); 339 } else if (item.type === 'folder') { 340 var pathKey = item.path && item.path.length ? item.path.join('/') : ''; 341 var hubLabel = (folderMeta[item.folderId] || {}).label || item.folderId; 342 pushResult(enrichApp({ 343 id: 'folder:' + item.folderId + (pathKey ? ':' + pathKey : ''), 344 label: item.label + ' (' + hubLabel + ')', 345 href: '/' + item.folderId + '.html', 346 icon: item.icon, 347 launchTier: 'folder', 348 folderId: item.folderId, 349 folderPath: item.path ? item.path.slice() : [] 350 })); 351 } 352 } 353 } 354 355 return results; 356 } 357 358 global.CitrusAppRegistry = { 359 getApps: buildApps, 360 getStartMenuApps: getStartMenuApps, 361 isSiteOwner: isSiteOwner, 362 getApp: getApp, 363 getAppById: getAppById, 364 getAppByHref: getAppByHref, 365 getLeafApp: getLeafApp, 366 search: searchApps,
367 isExternalLaunch: function (routeId) { 368 return !!EXTERNAL_LAUNCH[routeId.replace(/^\//, '').split('/').pop()]; 369 } 370 }; 371})(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.