PageSourceSearch

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

js pupwife.com collected 2026-09-30 15:04:25 UTC 9,255 bytes, 236 lines download raw bytes

1/**
2 * CitrusOS desktop widget registry.
3 *
4 * Register new widget types with CitrusWidgetRegistry.register({ ... }).
5 * Add the widget script path to citrusShellManifest.js → widgets (loaded before CitrusWidgets.init).
6 *
7 * User prefs: call ctx.saveSettings({ ... }) from mount(). Settings persist across close/re-add
8 * automatically (key: citrusos_widget_prefs_<type>). Set persistSettings: false for instance-only
9 * widgets (e.g. sticky notes). Override key with globalPrefsKey if needed.
10 */
11(function (global) {
12    'use strict';
13
14    /** @type {Record<string, object>} */
15    var types = Object.create(null);
16
17    var DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
18    var MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
19
20    function ordinal(n) {
21        var s = ['th', 'st', 'nd', 'rd'];
22        var v = n % 100;
23        return n + (s[(v - 20) % 10] || s[v] || s[0]);
24    }
25
26    function register(def) {
27        if (!def || !def.type || typeof def.mount !== 'function') {
28            console.warn('[CitrusWidgetRegistry] invalid widget def', def);
29            return;
30        }
31        types[def.type] = def;
32    }
33
34    function get(type) {
35        return types[type] || null;
36    }
37
38    function list() {
39        return Object.keys(types).map(function (key) {
40            return types[key];
41        });
42    }
43
44    function listGallery() {
45        return list().filter(function (def) {
46            return !def.galleryHidden;
47        });
48    }
49
50    function loadScriptOnce(src) {
51        var id = 'citrus-widget-script-' + src.replace(/[^\w]+/g, '-');
52        if (global.document.getElementById(id)) {
53            return Promise.resolve();
54        }
55        return new Promise(function (resolve, reject) {
56            var script = global.document.createElement('script');
57            script.id = id;
58            script.src = src;
59            script.onload = function () { resolve(); };
60            script.onerror = function () { reject(new Error('Failed to load ' + src)); };
61            global.document.head.appendChild(script);
62        });
63    }
64
65    /* ---- Starter hint (first visit only; not in gallery) ---- */
66    register({
67        type: 'hint',
68        label: 'Getting started',
69        icon: 'fa-solid fa-hand-pointer',
70        galleryHidden: true,
71        persistSettings: false,
72        defaultSize: { w: 220, h: 118 },
73        minSize: { w: 180, h: 96 },
74        resizable: false,
75        mount: function (body) {
76            body.classList.add('citrus-widget-hint');
77            body.innerHTML =
78                '<p class="citrus-widget-hint__lead">' +
79                    '<i class="fa-solid fa-hand-pointer citrus-widget-hint__icon" aria-hidden="true"></i>' +
80                    'Right-click the desktop' +
81                '</p>' +
82                '<p class="citrus-widget-hint__text">' +
83                    'Choose <strong>Widget gallery</strong> or <strong>Add widget</strong> to drop gadgets here.' +
84                '</p>';
85            return {};
86        }
87    });
88
89    /* ---- Clock ---- */
90    register({
91        type: 'clock',
92        label: 'Clock',
93        icon: 'fa-solid fa-clock',
94        blurb: 'Analog and digital time with date and greeting.',
95        defaultSize: { w: 200, h: 168 },
96        minSize: { w: 150, h: 130 },
97        resizable: true,
98        mount: function (body, ctx) {
99            body.classList.add('citrus-widget-clock');
100            body.innerHTML =
101                '<div class="citrus-widget-clock__date">' +
102                    '<span class="citrus-widget-clock__day" data-clock-day>--</span>' +
103                    '<span class="citrus-widget-clock__full" data-clock-full>--</span>' +
104                '</div>' +
105                '<div class="citrus-widget-clock__greeting" data-clock-greeting>good day!</div>' +
106                '<div class="citrus-widget-clock__face">' +
107                    '<div class="citrus-widget-clock__analog" data-clock-analog>' +
108                        '<div class="citrus-widget-clock__hand citrus-widget-clock__hand--hour"></div>' +
109                        '<div class="citrus-widget-clock__hand citrus-widget-clock__hand--minute"></div>' +
110                    '</div>' +
111                    '<div class="citrus-widget-clock__digital" data-clock-digital>--:--</div>' +
112                '</div>';
113
114            var dayEl = body.querySelector('[data-clock-day]');
115            var fullEl = body.querySelector('[data-clock-full]');
116            var greetingEl = body.querySelector('[data-clock-greeting]');
117            var digitalEl = body.querySelector('[data-clock-digital]');
118            var analogEl = body.querySelector('[data-clock-analog]');
119            var hourHand = analogEl && analogEl.querySelector('.citrus-widget-clock__hand--hour');
120            var minHand = analogEl && analogEl.querySelector('.citrus-widget-clock__hand--minute');
121            var timers = [];
122
123            function tick() {
124                var now = new Date();
125                var h = now.getHours();
126                var m = now.getMinutes();
127                var hour12 = h % 12 || 12;
128
129                if (digitalEl) {
130                    digitalEl.innerHTML = hour12 + '<small class="citrus-widget-clock__min">' +
131                        String(m).padStart(2, '0') + '</small>';
132                }
133                if (analogEl && hourHand && minHand) {
134                    hourHand.style.transform = 'rotate(' + ((hour12 * 30) + (m * 0.5)) + 'deg)';
135                    minHand.style.transform = 'rotate(' + (m * 6) + 'deg)';
136                }
137            }
138
139            function tickDate() {
140                if (dayEl) dayEl.textContent = DAYS[new Date().getDay()];
141                if (fullEl) {
142                    var d = new Date();
143                    fullEl.textContent = MONTHS[d.getMonth()] + ' ' + ordinal(d.getDate());
144                }
145                if (greetingEl) {
146                    var hour = new Date().getHours();
147                    if (hour >= 5 && hour < 12) greetingEl.textContent = 'good morning!';
148                    else if (hour >= 12 && hour < 17) greetingEl.textContent = 'good afternoon!';
149                    else greetingEl.textContent = 'good evening!';
150                }
151            }
152
153            tick();
154            tickDate();
155            timers.push(global.setInterval(tick, 1000));
156            timers.push(global.setInterval(tickDate, 60000));
157
158            return {
159                destroy: function () {
160                    for (var i = 0; i < timers.length; i++) {
161                        global.clearInterval(timers[i]);
162                    }
163                }
164            };
165        }
166    });
167
168    /* ---- Sticky note (one document per widget) ---- */
169    register({
170        type: 'note',
171        label: 'New note',
172        icon: 'fa-solid fa-file-lines',
173        blurb: 'Create a new sticky note and open it on the desktop.',
174        persistSettings: false,
175        defaultSize: { w: 260, h: 180 },
176        minSize: { w: 180, h: 120 },
177        resizable: true,
178        galleryAction: 'createNote',
179        mount: function (body, ctx) {
180            body.classList.add('citrus-widget-notes');
181            var noteId = ctx.settings && ctx.settings.noteId;
182            var notesApi = global.CitrusStickyNotes;
183            if (!noteId || !notesApi) {
184                body.innerHTML = '<p class="citrus-widget-notes__missing">Note not found</p>';
185                return {};
186            }
187
188            var note = notesApi.getNote(noteId);
189            if (!note) {
190                body.innerHTML = '<p class="citrus-widget-notes__missing">Note not found</p>';
191                return {};
192            }
193
194            if (typeof ctx.setTitle === 'function') {
195                ctx.setTitle(note.title || notesApi.DEFAULT_TITLE);
196            }
197
198            body.innerHTML =
199                '<textarea id="' + ctx.instanceId + '-note" class="citrus-widget-notes__textarea" ' +
200                    'placeholder="jot something down..." spellcheck="true" aria-label="' +
201                    (note.title || 'Sticky note').replace(/"/g, '&quot;') + '"></textarea>';
202
203            var textarea = body.querySelector('.citrus-widget-notes__textarea');
204            if (!textarea) return {};
205
206            textarea.value = note.text || '';
207            var saveTimer = null;
208
209            textarea.addEventListener('input', function () {
210                global.clearTimeout(saveTimer);
211                saveTimer = global.setTimeout(function () {
212                    var updated = notesApi.updateNote(noteId, { text: textarea.value });
213                    if (updated && typeof ctx.setTitle === 'function') {
214                        ctx.setTitle(updated.title);
215                    }
216                }, 280);
217            });
218            textarea.addEventListener('pointerdown', function (ev) {
219                ev.stopPropagation();
220            });
221
222            return {
223                destroy: function () {
224                    global.clearTimeout(saveTimer);
225                }
226            };
227        }
228    });
229
230    global.CitrusWidgetRegistry = {
231        register: register,
232        get: get,
233        list: list,
234        listGallery: listGallery
235    };
236})(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.