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.