PageSourceSearch

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

js pupwife.com collected 2026-09-30 15:04:32 UTC 13,528 bytes, 359 lines download raw bytes

1/**
2 * CitrusOS Yo-Yo desktop widget — global overlay toggle + physics aux window.
3 * Register path: citrusShellManifest.js → widgets (after yoyo.js)
4 */
5(function (global) {
6    'use strict';
7
8    var PREFS_KEY = 'citrusos_widget_prefs_yoyo';
9    var PHYSICS_AUX_ID = 'yoyo-physics';
10
11    var SLIDER_SPECS = [
12        { key: 'gravity', label: 'Gravity', min: 0, max: 1, step: 0.05, decimals: 2 },
13        { key: 'stiffness', label: 'Stiffness', min: 0.001, max: 0.1, step: 0.001, decimals: 3 },
14        { key: 'damping', label: 'Damping', min: 0.5, max: 0.99, step: 0.01, decimals: 2 },
15        { key: 'radius', label: 'Ball size', min: 5, max: 30, step: 1, decimals: 0 },
16        { key: 'mass', label: 'Ball mass', min: 0.5, max: 5, step: 0.1, decimals: 1 }
17    ];
18
19    function readGlobalPrefs() {
20        if (global.CitrusPrefs && typeof global.CitrusPrefs.load === 'function') {
21            return global.CitrusPrefs.load(PREFS_KEY);
22        }
23        try {
24            var raw = global.localStorage.getItem(PREFS_KEY);
25            if (!raw) return {};
26            var parsed = JSON.parse(raw);
27            return parsed && typeof parsed === 'object' ? parsed : {};
28        } catch (e) {
29            return {};
30        }
31    }
32
33    function saveGlobalPrefs(partial) {
34        if (global.CitrusPrefs && typeof global.CitrusPrefs.save === 'function') {
35            return global.CitrusPrefs.save(PREFS_KEY, partial);
36        }
37        try {
38            var next = Object.assign({}, readGlobalPrefs(), partial || {});
39            global.localStorage.setItem(PREFS_KEY, JSON.stringify(next));
40            return next;
41        } catch (e) {
42            return readGlobalPrefs();
43        }
44    }
45
46    function getYoyoApi() {
47        return global.CitrusYoyo || null;
48    }
49
50    function readPhysicsFromPrefs(prefs) {
51        var api = getYoyoApi();
52        var defaults = api && api.DEFAULTS ? api.DEFAULTS : {
53            gravity: 0.4,
54            stiffness: 0.02,
55            damping: 0.95,
56            radius: 12,
57            mass: 2
58        };
59        prefs = prefs || {};
60        return {
61            gravity: typeof prefs.gravity === 'number' ? prefs.gravity : defaults.gravity,
62            stiffness: typeof prefs.stiffness === 'number' ? prefs.stiffness : defaults.stiffness,
63            damping: typeof prefs.damping === 'number' ? prefs.damping : defaults.damping,
64            radius: typeof prefs.radius === 'number' ? prefs.radius : defaults.radius,
65            mass: typeof prefs.mass === 'number' ? prefs.mass : defaults.mass
66        };
67    }
68
69    function physicsToPrefs(physics) {
70        return {
71            gravity: physics.gravity,
72            stiffness: physics.stiffness,
73            damping: physics.damping,
74            radius: physics.radius,
75            mass: physics.mass
76        };
77    }
78
79    function applyPhysics(physics) {
80        var api = getYoyoApi();
81        if (!api || typeof api.setPhysics !== 'function') return physics;
82        return api.setPhysics(physics);
83    }
84
85    function applyEnabled(enabled) {
86        var api = getYoyoApi();
87        if (!api) return !!enabled;
88        if (enabled) {
89            if (typeof api.enable === 'function') api.enable();
90        } else if (typeof api.disable === 'function') {
91            api.disable();
92        }
93        return api.isEnabled ? !!api.isEnabled() : !!enabled;
94    }
95
96    function applyCitrusShellYoyo() {
97        if (!global.document.body || !global.document.body.classList.contains('page-citrusos')) return;
98        var api = getYoyoApi();
99        if (!api) return;
100        var prefs = readGlobalPrefs();
101        applyPhysics(readPhysicsFromPrefs(prefs));
102        applyEnabled(!!prefs.enabled);
103    }
104
105    function formatValue(value, decimals) {
106        if (decimals === 0) return String(Math.round(value));
107        return Number(value).toFixed(decimals);
108    }
109
110    function renderToggleButton(btn, enabled) {
111        if (!btn) return;
112        btn.classList.toggle('citrus-widget-yoyo__btn--active', !!enabled);
113        btn.setAttribute('aria-pressed', enabled ? 'true' : 'false');
114        btn.innerHTML =
115            '<i class="fa-solid ' + (enabled ? 'fa-circle-stop' : 'fa-circle-notch') + '" aria-hidden="true"></i>' +
116            '<span>' + (enabled ? 'Hide yoyo' : 'Show yoyo') + '</span>';
117    }
118
119    function renderStatus(el, enabled) {
120        if (!el) return;
121        el.textContent = enabled ? 'Yoyo is out!' : 'Yoyo is tucked away';
122        el.classList.toggle('citrus-widget-yoyo__status--on', !!enabled);
123    }
124
125    function knobRow(spec, val) {
126        return '<label class="noise-knob" data-yoyo-ui>' +
127            '<span class="noise-knob__meta">' +
128                '<span class="noise-knob__label">' + spec.label + '</span>' +
129                '<span class="noise-knob__value" data-yoyo-val="' + spec.key + '">' + formatValue(val, spec.decimals) + '</span>' +
130            '</span>' +
131            '<input type="range" class="noise-knob__input" data-yoyo-key="' + spec.key + '" ' +
132                'min="' + spec.min + '" max="' + spec.max + '" step="' + spec.step + '" value="' + val + '" ' +
133                'aria-label="' + spec.label + '">' +
134        '</label>';
135    }
136
137    function mountPhysicsPanel(mountEl, onChange) {
138        if (!mountEl) return function () {};
139
140        var api = getYoyoApi();
141        var physics = api && typeof api.getPhysics === 'function'
142            ? api.getPhysics()
143            : readPhysicsFromPrefs(readGlobalPrefs());
144
145        var rows = SLIDER_SPECS.map(function (spec) {
146            return knobRow(spec, physics[spec.key]);
147        }).join('');
148
149        var html = '<div class="citrus-yoyo-physics">' +
150            '<div class="noise-knob-grid citrus-yoyo-physics__knobs">' + rows + '</div>' +
151            '<button type="button" class="citrus-tool-btn citrus-yoyo-physics__reset" data-yoyo-reset>Reset to defaults</button>' +
152            '</div>';
153
154        mountEl.innerHTML = html;
155
156        var sliders = mountEl.querySelectorAll('[data-yoyo-key]');
157        var valueEls = {};
158        SLIDER_SPECS.forEach(function (spec) {
159            valueEls[spec.key] = mountEl.querySelector('[data-yoyo-val="' + spec.key + '"]');
160        });
161
162        function syncDisplay(nextPhysics) {
163            SLIDER_SPECS.forEach(function (spec) {
164                var slider = mountEl.querySelector('[data-yoyo-key="' + spec.key + '"]');
165                if (slider) slider.value = String(nextPhysics[spec.key]);
166                if (valueEls[spec.key]) {
167                    valueEls[spec.key].textContent = formatValue(nextPhysics[spec.key], spec.decimals);
168                }
169            });
170        }
171
172        function handleInput(ev) {
173            var key = ev.target.getAttribute('data-yoyo-key');
174            if (!key) return;
175            var partial = {};
176            partial[key] = parseFloat(ev.target.value);
177            var next = applyPhysics(Object.assign({}, physics, partial));
178            physics = next;
179            var spec = SLIDER_SPECS.filter(function (s) { return s.key === key; })[0];
180            if (spec && valueEls[key]) {
181                valueEls[key].textContent = formatValue(next[key], spec.decimals);
182            }
183            if (typeof onChange === 'function') {
184                onChange(next);
185            }
186        }
187
188        for (var i = 0; i < sliders.length; i++) {
189            sliders[i].addEventListener('input', handleInput);
190        }
191
192        var resetBtn = mountEl.querySelector('[data-yoyo-reset]');
193        if (resetBtn) {
194            resetBtn.addEventListener('click', function (ev) {
195                ev.stopPropagation();
196                var apiRef = getYoyoApi();
197                physics = apiRef && typeof apiRef.resetPhysics === 'function'
198                    ? apiRef.resetPhysics()
199                    : readPhysicsFromPrefs({});
200                syncDisplay(physics);
201                if (typeof onChange === 'function') {
202                    onChange(physics);
203                }
204            });
205        }
206
207        return function teardown() {
208            for (var j = 0; j < sliders.length; j++) {
209                sliders[j].removeEventListener('input', handleInput);
210            }
211        };
212    }
213
214    var PHYSICS_WIN_W = 300;
215    var PHYSICS_WIN_H = 296;
216
217    function openPhysicsWindow(anchorEl, persistPhysics) {
218        var desktop = global.CitrusDesktop;
219        if (!desktop || typeof desktop.openAuxWindow !== 'function') return;
220
221        var shellW = PHYSICS_WIN_W;
222        var shellH = PHYSICS_WIN_H;
223        var rect = anchorEl && anchorEl.getBoundingClientRect
224            ? anchorEl.getBoundingClientRect()
225            : { left: 120, top: 80, right: 220, bottom: 120 };
226        var posX = Math.min(rect.right + 8, Math.max(8, global.innerWidth - shellW - 8));
227        var posY = Math.max(8, Math.min(rect.top, global.innerHe
227ight - shellH - 72));
228
229        var teardownPanel = null;
230
231        desktop.openAuxWindow({
232            id: PHYSICS_AUX_ID,
233            label: 'Yoyo Physics',
234            icon: 'fa-solid fa-sliders',
235            w: shellW,
236            h: shellH,
237            x: posX,
238            y: posY,
239            nearChat: false,
240            resizable: false,
241            auxClass: 'citrus-window--yoyo-physics',
242            onMount: function (contentEl) {
243                contentEl.classList.add('page-yoyo-physics', 'citrusos-app-active');
244                teardownPanel = mountPhysicsPanel(contentEl, function (physics) {
245                    if (typeof persistPhysics === 'function') {
246                        persistPhysics(physics);
247                    } else {
248                        saveGlobalPrefs(physicsToPrefs(physics));
249                    }
250                });
251            },
252            onClose: function () {
253                if (typeof teardownPanel === 'function') {
254                    teardownPanel();
255                }
256                teardownPanel = null;
257            }
258        });
259    }
260
261    function mount(body, ctx) {
262        body.classList.add('citrus-widget-yoyo');
263
264        var settings = ctx.settings || {};
265        var enabled = !!settings.enabled;
266        var physics = readPhysicsFromPrefs(settings);
267
268        applyPhysics(physics);
269        enabled = applyEnabled(enabled);
270
271        body.innerHTML =
272            '<div class="citrus-widget-yoyo__main">' +
273                '<div class="citrus-widget-yoyo__head">' +
274                    '<i class="fa-solid fa-circle-notch citrus-widget-yoyo__icon" aria-hidden="true"></i>' +
275                    '<span class="citrus-widget-yoyo__status" data-yoyo-status>Yoyo is tucked away</span>' +
276                '</div>' +
277                '<div class="citrus-widget-yoyo__actions">' +
278                    '<button type="button" class="citrus-widget-yoyo__btn" data-yoyo-toggle aria-pressed="false"></button>' +
279                    '<button type="button" class="citrus-widget-yoyo__btn" data-yoyo-physics>' +
280                        '<i class="fa-solid fa-sliders" aria-hidden="true"></i>' +
281                        '<span>Physics</span>' +
282                    '</button>' +
283                '</div>' +
284            '</div>';
285
286        var statusEl = body.querySelector('[data-yoyo-status]');
287        var toggleBtn = body.querySelector('[data-yoyo-toggle]');
288        var physicsBtn = body.querySelector('[data-yoyo-physics]');
289
290        function persistAll(partial) {
291            if (typeof ctx.saveSettings === 'function') {
292                ctx.saveSettings(partial);
293            }
294            saveGlobalPrefs(partial);
295        }
296
297        function setEnabled(nextEnabled, persist) {
298            enabled = !!nextEnabled;
299            enabled = applyEnabled(enabled);
300            renderToggleButton(toggleBtn, enabled);
301            renderStatus(statusEl, enabled);
302            if (persist) {
303                persistAll({ enabled: enabled });
304            }
305        }
306
307        function persistPhysics(nextPhysics) {
308            persistAll(physicsToPrefs(nextPhysics));
309        }
310
311        renderToggleButton(toggleBtn, enabled);
312        renderStatus(statusEl, enabled);
313
314        if (toggleBtn) {
315            toggleBtn.addEventListener('click', function (ev) {
316                ev.stopPropagation();
317                setEnabled(!enabled, true);
318            });
319        }
320
321        if (physicsBtn) {
322            physicsBtn.addEventListener('click', function (ev) {
323                ev.stopPropagation();
324                openPhysicsWindow(body, persistPhysics);
325            });
326        }
327
328        return {
329            destroy: function () { /* global yoyo stays on like fish food */ }
330        };
331    }
332
333    if (global.CitrusWidgetRegistry && typeof global.CitrusWidgetRegistry.register === 'function') {
334        global.CitrusWidgetRegistry.register({
335            type: 'yoyo',
336            label: 'Yo-Yo',
337            icon: 'fa-solid fa-circle-notch',
338            blurb: 'Toggle the springy yoyo overlay and tune its physics.',
339            defaultSize: { w: 210, h: 118 },
340            minSize: { w: 190, h: 108 },
341            resizable: false,
342            mount: mount
343        });
344    }
345
346    global.CitrusYoyoShell = {
347        apply: applyCitrusShellYoyo
348    };
349
350    function scheduleShellApply() {
351        if (!global.document.body || !global.document.body.classList.contains('page-citrusos')) return;
352        applyCitrusShellYoyo();
353        if (global.document.readyState === 'loading') {
354            global.document.addEventListener('DOMContentLoaded', applyCitrusShellYoyo);
355        }
356    }
357
358    scheduleShellApply();
359})(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.