PageSourceSearch

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

js pupwife.com collected 2026-09-30 15:04:16 UTC 8,844 bytes, 232 lines download raw bytes

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, '&lt;')
109            .replace(/>/g, '&gt;')
110            .replace(/"/g, '&quot;');
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.