PageSourceSearch

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

js pupwife.com collected 2026-09-30 15:04:37 UTC 9,706 bytes, 244 lines download raw bytes

1/**
2 * CitrusOS Fish Food desktop widget — score, falling-food toggle, leaderboard window.
3 * Register path: citrusShellManifest.js → widgets (after fishFood.js + foodCounter.js)
4 */
5(function (global) {
6    'use strict';
7
8    var LEADERBOARD_AUX_ID = 'fishfood-leaderboard';
9    var PREFS_KEY = 'citrusos_widget_prefs_fishfood';
10    var LEADERBOARD_SCRIPT = '/scripts/dependencies/fishFoodLeaderboard.js';
11
12    function loadScriptOnce(src) {
13        if (global.PupwifeScriptLoad && typeof global.PupwifeScriptLoad.once === 'function') {
14            return global.PupwifeScriptLoad.once(src);
15        }
16        return new Promise(function (resolve, reject) {
17            if (global.document.querySelector('script[src="' + src + '"]')) {
18                resolve();
19                return;
20            }
21            var script = global.document.createElement('script');
22            script.src = src;
23            script.async = false;
24            script.onload = function () { resolve(); };
25            script.onerror = function () { reject(new Error('Failed to load ' + src)); };
26            global.document.body.appendChild(script);
27        });
28    }
29
30    function readGlobalPrefs() {
31        if (global.CitrusPrefs && typeof global.CitrusPrefs.load === 'function') {
32            return global.CitrusPrefs.load(PREFS_KEY);
33        }
34        try {
35            var raw = global.localStorage.getItem(PREFS_KEY);
36            if (!raw) return {};
37            var parsed = JSON.parse(raw);
38            return parsed && typeof parsed === 'object' ? parsed : {};
39        } catch (e) {
40            return {};
41        }
42    }
43
44    function hideBottomCounter() {
45        if (global.__puppyFoodCounter && typeof global.__puppyFoodCounter.setVisible === 'function') {
46            global.__puppyFoodCounter.setVisible(false);
47            return;
48        }
49        var counter = global.document.getElementById('food-counter');
50        if (counter) counter.style.display = 'none';
51    }
52
53    function applyFallingEnabled(enabled) {
54        if (global.FishFood && typeof global.FishFood.setEnabled === 'function') {
55            global.FishFood.setEnabled(!!enabled);
56        }
57    }
58
59    function applyCitrusShellFishFood() {
60        if (!global.document.body || !global.document.body.classList.contains('page-citrusos')) return;
61        hideBottomCounter();
62        var prefs = readGlobalPrefs();
63        applyFallingEnabled(!!prefs.fallingEnabled);
64    }
65
66    function getScore() {
67        if (global.__puppyFoodCounter && typeof global.__puppyFoodCounter.count === 'number') {
68            return global.__puppyFoodCounter.count;
69        }
70        try {
71            var stored = global.localStorage.getItem('puppy_food_count');
72            return stored === null ? 0 : (parseInt(stored, 10) || 0);
73        } catch (e) {
74            return 0;
75        }
76    }
77
78    function syncScoreFromStorage() {
79        if (global.__puppyFoodCounter && typeof global.__puppyFoodCounter.syncFromStorage === 'function') {
80            return global.__puppyFoodCounter.syncFromStorage();
81        }
82        return getScore();
83    }
84
85    function renderToggleButton(btn, enabled) {
86        if (!btn) return;
87        btn.classList.toggle('citrus-widget-fishfood__btn--active', !!enabled);
88        btn.setAttribute('aria-pressed', enabled ? 'true' : 'false');
89        btn.innerHTML =
90            '<i class="fa-solid ' + (enabled ? 'fa-pause' : 'fa-arrow-down') + '" aria-hidden="true"></i>' +
91            '<span>' + (enabled ? 'Stop food' : 'Drop food') + '</span>';
92    }
93
94    function openLeaderboardWindow(anchorEl) {
95        var desktop = global.CitrusDesktop;
96        if (!desktop || typeof desktop.openAuxWindow !== 'function') return;
97
98        var shellW = 300;
99        var shellH = 340;
100        var rect = anchorEl && anchorEl.getBoundingClientRect
101            ? anchorEl.getBoundingClientRect()
102            : { left: 120, top: 80, right: 220, bottom: 120 };
103        var posX = Math.min(rect.right + 8, Math.max(8, global.innerWidth - shellW - 8));
104        var posY = Math.max(8, Math.min(rect.top, global.innerHeight - shellH - 72));
105
106        desktop.openAuxWindow({
107            id: LEADERBOARD_AUX_ID,
108            label: 'Fish Food Leaderboard',
109            icon: 'fa-solid fa-fish',
110            w: shellW,
111            h: shellH,
112            x: posX,
113            y: posY,
114            nearChat: false,
115            onMount: function (contentEl) {
116                contentEl.innerHTML =
117                    '<div class="citrus-fishfood-lb-window">' +
118                        '<p class="citrus-fishfood-lb-window__hint">Top feeders on pupwife.com</p>' +
119                        '<div class="citrus-fishfood-lb-window__mount" data-fishfood-lb-mount></div>' +
120                    '</div>';
121                var mount = contentEl.querySelector('[data-fishfood-lb-mount]');
122                loadScriptOnce(LEADERBOARD_SCRIPT).then(function () {
123                    if (global.FishFoodLeaderboard && typeof global.FishFoodLeaderboard.mountPanel === 'function') {
124                        global.FishFoodLeaderboard.mountPanel(mount);
125                    } else if (global.FishFoodLeaderboard && typeof global.FishFoodLeaderboard.init === 'function') {
126                        global.FishFoodLeaderboard.init();
127                    }
128                }).catch(function () {
129                    if (mount) {
130                        mount.innerHTML = '<p class="citrus-fishfood-lb__loading">Could not load leaderboard.</p>';
131                    }
132                });
133            },
134            onClose: function () {
135                if (global.FishFoodLeaderboard && typeof global.FishFoodLeaderboard.clearActivePanel === 'function') {
136                    global.FishFoodLeaderboard.clearActivePanel();
137                }
138            }
139        });
140    }
141
142    function mount(body, ctx) {
143        body.classList.add('citrus-widget-fishfood');
144        var settings = ctx.settings || {};
145        var fallingEnabled = !!settings.fallingEnabled;
146
147        body.innerHTML =
148            '<div class="citrus-widget-fishfood__main">' +
149                '<div class="citrus-widget-fishfood__score-row">' +
150                    '<i class="fa-solid fa-fish citrus-widget-fishfood__icon" aria-hidden="true"></i>' +
151                    '<span class="citrus-widget-fishfood__score" data-ff-score>0</span>' +
152                    '<span class="citrus-widget-fishfood__label">fish food</span>' +
153                '</div>' +
154                '<div class="citrus-widget-fishfood__actions">' +
155                    '<button type="button" class="citrus-widget-fishfood__btn" data-ff-toggle aria-pressed="false"></button>' +
156                    '<button type="button" class="citrus-widget-fishfood__btn" data-ff-lb>' +
157                        '<i class="fa-solid fa-trophy" aria-hidden="true"></i>' +
158                        '<span>Leaderboard</span>' +
159                    '</button>' +
160                '</div>' +
161            '</div>';
162
163        var scoreEl = body.querySelector('[data-ff-score]');
164        var toggleBtn = body.querySelector('[data-ff-toggle]');
165        var lbBtn = body.querySelector('[data-ff-lb]');
166
167        function paintScore() {
168            if (scoreEl) scoreEl.textContent = String(syncScoreFromStorage());
169        }
170
171        function setFalling(enabled, persist) {
172            fallingEnabled = !!enabled;
173            applyFallingEnabled(fallingEnabled);
174            renderToggleButton(toggleBtn, fallingEnabled);
175            if (persist && typeof ctx.saveSettings === 'function') {
176                ctx.saveSettings({ fallingEnabled: fallingEnabled });
177            }
178        }
179
180        hideBottomCounter();
181        paintScore();
182        setFalling(fallingEnabled, false);
183
184        function onCollected() {
185            paintScore();
186        }
187
188        function onStorage(ev) {
189            if (!ev || ev.key !== 'puppy_food_count') return;
190            paintScore();
191        }
192
193        if (toggleBtn) {
194            toggleBtn.addEventListener('click', function (ev) {
195                ev.stopPropagation();
196                setFalling(!fallingEnabled, true);
197            });
198        }
199
200        if (lbBtn) {
201            lbBtn.addEventListener('click', function (ev) {
202                ev.stopPropagation();
203                openLeaderboardWindow(body);
204            });
205        }
206
207        global.window.addEventListener('fishfood:collected', onCollected);
208        global.window.addEventListener('storage', onStorage);
209
210        return {
211            destroy: function () {
212                global.window.removeEventListener('fishfood:collected', onCollected);
213                global.window.removeEventListener('storage', onStorage);
214            }
215        };
216    }
217
218    if (global.CitrusWidgetRegistry && typeof global.CitrusWidgetRegistry.register === 'function') {
219        global.CitrusWidgetRegistry.register({
220            type: 'fishfood',
221            label: 'Fish Food',
222            icon: 'fa-solid fa-fish',
223            blurb: 'Your fish food score, a falling-food toggle, and the top 5 leaderboard.',
224            defaultSize: { w: 210, h: 132 },
225            minSize: { w: 190, h: 120 },
226            resizable: false,
227            mount: mount
228        });
229    }
230
231    global.CitrusFishFoodShell = {
232        apply: applyCitrusShellFishFood
233    };
234
235    function scheduleShellApply() {
236        if (!global.document.body || !global.document.body.classList.contains('page-citrusos')) return;
237        applyCitrusShellFishFood();
238        if (global.document.readyState === 'loading') {
239            global.document.addEventListener('DOMContentLoaded', applyCitrusShellFishFood);
240        }
241    }
242
243    scheduleShellApply();
244})(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.