1/** 2 * CitrusOS app icon resolver â dedicated asset, page embed, favicon, then FA. 3 * 4 * Dedicated icons live under /images/CitrusOS/apps/ and are declared per app via 5 * `iconAsset` in appRegistry (e.g. "general/app_music.png"). 6 */ 7(function (global) { 8 'use strict'; 9 10 var DEDICATED_BASE = '/images/CitrusOS/apps/'; 11 12 var GENERIC_URLS = { 13 '/icons/notes.webp': true, 14 'https://pupwife.com/icons/notes.webp': true, 15 '/images/banner.webp': true, 16 'https://pupwife.com/images/banner.webp': true 17 }; 18 19 function normalizeHref(href) { 20 if (!href) return ''; 21 var key = String(href).replace(/^\//, '').split('?')[0].split('#')[0]; 22 if (!/\.html$/i.test(key) && key.indexOf('.') === -1) key += '.html'; 23 return '/' + key; 24 } 25 26 function normalizeAssetUrl(url) { 27 if (!url) return ''; 28 var value = String(url).trim(); 29 if (!value) return ''; 30 if (value.indexOf('//') === 0) return 'https:' + value; 31 if (value.indexOf('http://') === 0 || value.indexOf('https://') === 0) { 32 try { 33 var parsed = new URL(value); 34 if (parsed.hostname === 'pupwife.com' || parsed.hostname === 'www.pupwife.com') { 35 return parsed.pathname + (parsed.search || ''); 36 } 37 } catch (e) { /* keep absolute external URLs */ } 38 return value; 39 } 40 return value.indexOf('/') === 0 ? value : '/' + value; 41 } 42 43 function isGenericUrl(url) { 44 var normalized = normalizeAssetUrl(url); 45 if (!normalized) return true; 46 if (GENERIC_URLS[normalized]) return true; 47 if (GENERIC_URLS['https://pupwife.com' + normalized]) return true; 48 return false; 49 } 50 51 function getManifestEntry(href) { 52 var manifest = global.CITRUS_APP_ICON_MANIFEST || {}; 53 return manifest[normalizeHref(href)] || null; 54 } 55 56 function dedicatedIconUrl(app, allowDedicated) { 57 if (!allowDedicated || !app || !app.iconAsset) return ''; 58 return DEDICATED_BASE + String(app.iconAsset).replace(/^\/+/, ''); 59 } 60 61 function normalizeFaClass(icon, fallback) { 62 var fa = icon || fallback || 'fa-solid fa-window-maximize'; 63 if (fa.indexOf('fa-solid') !== -1 || fa.indexOf('fa-brands') !== -1 || fa.indexOf('fa-regular') !== -1) { 64 return fa; 65 } 66 if (fa.indexOf('fa-') === 0) return 'fa-solid ' + fa; 67 return fallback || 'fa-solid fa-window-maximize'; 68 } 69 70 function faFallbackForApp(app) { 71 if (!app) return 'fa-solid fa-window-maximize'; 72 if (app.iconType === 'fa' || (app.icon && app.icon.indexOf('fa-') !== -1)) { 73 return normalizeFaClass(app.icon); 74 } 75 if (app.icon && (app.icon.indexOf('/') === 0 || app.icon.indexOf('http') === 0)) { 76 return 'fa-solid fa-window-maximize'; 77 } 78 return normalizeFaClass(app.icon); 79 } 80 81 function resolveIconSpec(app, opts) { 82 opts = opts || {}; 83 var allowDedicated = opts.allowDedicated !== false; 84 85 if (global.CitrusFolderIcon && global.CitrusFolderIcon.isFolderIcon(app)
85) { 86 return { kind: 'folder' }; 87 } 88 89 var dedicated = dedicatedIconUrl(app, allowDedicated); 90 if (dedicated) { 91 return { kind: 'img', src: dedicated, tier: 'dedicated' }; 92 } 93 94 var entry = getManifestEntry(app && app.href); 95 if (entry && entry.embed && !isGenericUrl(entry.embed)) { 96 return { kind: 'img', src: normalizeAssetUrl(entry.embed), tier: 'embed' }; 97 } 98 if (entry && entry.favicon && !isGenericUrl(entry.favicon)) { 99 return { kind: 'img', src: normalizeAssetUrl(entry.favicon), tier: 'favicon' }; 100 } 101 102 return { kind: 'fa', fa: faFallbackForApp(app) }; 103 } 104 105 function escapeHtml(value) { 106 return String(value || '') 107 .replace(/&/g, '&') 108 .replace(/</g, '<') 109 .replace(/>/g, '>') 110 .replace(/"/g, '"'); 111 } 112 113 function folderPeekItems(app) { 114 if (!global.CitrusFolderIcon || typeof global.CitrusFolderIcon.resolvePeekItemsForApp !== 'function') { 115 return []; 116 } 117 return global.CitrusFolderIcon.resolvePeekItemsForApp(app); 118 } 119 120 function createFaElement(faClass, className) { 121 var iconFa = document.createElement('i'); 122 iconFa.className = faClass + (className ? ' ' + className : ''); 123 iconFa.setAttribute('aria-hidden', 'true'); 124 return iconFa; 125 } 126 127 function createImgElement(src, className, onFallback) { 128 var img = document.createElement('img'); 129 img.src = src; 130 img.alt = ''; 131 img.draggable = false; 132 if (className) img.className = className; 133 if (typeof onFallback === 'function') { 134 img.addEventListener('error', function onError() { 135 img.removeEventListener('error', onError); 136 onFallback(img); 137 }, { once: true }); 138 } 139 return img; 140 } 141 142 function replaceElement(oldEl, nextEl) { 143 if (!oldEl || !nextEl || oldEl === nextEl) return nextEl; 144 if (oldEl.parentNode) oldEl.parentNode.replaceChild(nextEl, oldEl); 145 return nextEl; 146 } 147 148 function mountIcon(app, parent, opts) { 149 opts = opts || {}; 150 if (!parent) return null; 151 152 if (global.CitrusFolderIcon && global.CitrusFolderIcon.isFolderIcon(app)
152) { 153 var folderEl = global.CitrusFolderIcon.createFolderIcon(opts.folderVariant || 'desktop', folderPeekItems(app)); 154 if (opts.extraClass) folderEl.classList.add(opts.extraClass); 155 parent.appendChild(folderEl); 156 return folderEl; 157 } 158 159 function materializeSpec(nextSpec, replaceTarget) { 160 if (nextSpec.kind === 'fa') { 161 var faEl = createFaElement(nextSpec.fa, opts.faClass); 162 if (replaceTarget) replaceElement(replaceTarget, faEl); 163 else parent.appendChild(faEl); 164 return faEl; 165 } 166 167 var img = createImgElement(nextSpec.src, opts.imgClass, function () { 168 if (nextSpec.tier === 'dedicated') { 169 materializeSpec(resolveIconSpec(app, { allowDedicated: false }), img); 170 return; 171 } 172 if (nextSpec.tier === 'embed') { 173 var entry = getManifestEntry(app && app.href); 174 if (entry && entry.favicon && !isGenericUrl(entry.favicon)) { 175 materializeSpec({ 176 kind: 'img', 177 src: normalizeAssetUrl(entry.favicon), 178 tier: 'favicon' 179 }, img); 180 return; 181 } 182 } 183 materializeSpec({ kind: 'fa', fa: faFallbackForApp(app) }, img); 184 }); 185 186 if (replaceTarget) replaceElement(replaceTarget, img); 187 else parent.appendChild(img); 188 return img; 189 } 190 191 return materializeSpec(resolveIconSpec(app, { allowDedicated: true }), null); 192 } 193 194 function iconMarkup(app, opts) { 195 opts = opts || {}; 196 if (global.CitrusFolderIcon && global.CitrusFolderIcon.isFolderIcon(app)
196) { 197 return global.CitrusFolderIcon.folderIconMarkup(opts.folderVariant || 'start', folderPeekItems(app)); 198 } 199 200 var spec = resolveIconSpec(app, { allowDedicated: true }); 201 if (spec.kind === 'img') { 202 return '<img src="' + escapeHtml(spec.src) + '" alt="" class="' + escapeHtml(opts.imgClass || '') + '" draggable="false">'; 203 } 204 return '<i class="' + escapeHtml(spec.fa + (opts.faClass ? ' ' + opts.faClass : '')) + '" aria-hidden="true"></i>'; 205 } 206 207 function resolveForHref(href, iconHint, iconAsset) { 208 var app = { 209 href: href, 210 icon: iconHint || 'fa-solid fa-window-maximize', 211 iconType: iconHint && iconHint.indexOf('fa-') !== -1 ? 'fa' : null, 212 iconAsset: iconAsset || null 213 }; 214 if (global.CitrusAppRegistry && typeof global.CitrusAppRegistry.getAppByHref === 'function') { 215 var registered = global.CitrusAppRegistry.getAppByHref(href); 216 if (registered) app = registered; 217 else if (iconHint) app.icon = iconHint; 218 } 219 return app; 220 } 221 222 global.CitrusAppIcon = { 223 DEDICATED_BASE: DEDICATED_BASE, 224 normalizeHref: normalizeHref, 225 isGenericUrl: isGenericUrl, 226 resolve: resolveIconSpec, 227 mount: mountIcon, 228 markup: iconMarkup, 229 resolveForHref: resolveForHref, 230 faFallbackForApp: faFallbackForApp 231 }; 232})(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.