PageSourceSearch

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

js pupwife.com collected 2026-09-30 15:04:36 UTC 8,889 bytes, 218 lines download raw bytes

1/**
2 * CitrusOS Task Board desktop widget — To do + Doing counts, click opens Task Board.
3 */
4(function (global) {
5    'use strict';
6
7    var WIDGET_BUILD = 'citrusos-taskboard-widget-v2';
8    var FIREBASE_SCRIPTS = [
9        'https://www.gstatic.com/firebasejs/9.22.0/firebase-app-compat.js',
10        'https://www.gstatic.com/firebasejs/9.22.0/firebase-database-compat.js',
11        '/scripts/dependencies/firebaseConfig.js'
12    ];
13    var firebaseLoadPromise = null;
14    var tasksHandler = null;
15    var tasksRef = null;
16    var latestTasks = {};
17
18    function loadScriptOnce(src) {
19        if (global.PupwifeScriptLoad && typeof global.PupwifeScriptLoad.once === 'function') {
20            return global.PupwifeScriptLoad.once(src);
21        }
22        return new Promise(function (resolve, reject) {
23            if (document.querySelector('script[src="' + src + '"]')) {
24                resolve();
25                return;
26            }
27            var script = document.createElement('script');
28            script.src = src;
29            script.async = false;
30            script.onload = function () { resolve(); };
31            script.onerror = function () { reject(new Error('Failed to load ' + src)); };
32            document.head.appendChild(script);
33        });
34    }
35
36    function ensureFirebaseReady() {
37        if (typeof firebase !== 'undefined' && firebase.database) {
38            if (typeof global.ensureFirebaseApp === 'function') {
39                global.ensureFirebaseApp();
40            }
41            return Promise.resolve();
42        }
43        if (!firebaseLoadPromise) {
44            firebaseLoadPromise = FIREBASE_SCRIPTS.reduce(function (chain, src) {
45                return chain.then(function () { return loadScriptOnce(src); });
46            }, Promise.resolve()).then(function () {
47                if (typeof global.ensureFirebaseApp === 'function') {
48                    global.ensureFirebaseApp();
49                }
50            }).catch(function (err) {
51                firebaseLoadPromise = null;
52                throw err;
53            });
54        }
55        return firebaseLoadPromise;
56    }
57
58    function countTasks(tasks) {
59        var todo = 0;
60        var doing = 0;
61        Object.keys(tasks || {}).forEach(function (id) {
62            var status = (tasks[id] || {}).status || 'todo';
63            if (status === 'todo') todo += 1;
64            else if (status === 'doing') doing += 1;
65        });
66        return { todo: todo, doing: doing };
67    }
68
69    function openTaskBoardWindow(ev) {
70        if (ev) {
71            ev.preventDefault();
72            ev.stopPropagation();
73        }
74        var registry = global.CitrusAppRegistry;
75        var desktop = global.CitrusDesktop;
76        if (registry && desktop && typeof registry.getAppByHref === 'function') {
77            var app = registry.getAppByHref('/taskboard.html');
78            if (app && typeof desktop.openWindow === 'function') {
79                desktop.openWindow(app);
80                return;
81            }
82            if (typeof desktop.openHref === 'function') {
83                desktop.openHref('/taskboard.html');
84            }
85        }
86    }
87
88    function renderWidget(mountEl, counts, statusText) {
89        mountEl.innerHTML =
90            '<button type="button" class="citrus-widget-taskboard__panel" data-taskboard-open>' +
91                '<div class="citrus-widget-taskboard__title">' +
92                    '<i class="fa-solid fa-clipboard-list" aria-hidden="true"></i> Task Board' +
93                '</div>' +
94                '<div class="citrus-widget-taskboard__counts">' +
95                    '<span class="citrus-widget-taskboard__count citrus-widget-taskboard__count--todo">' +
96                        '<span class="citrus-widget-taskboard__count-label">To do</span>' +
97                        '<span class="citrus-widget-taskboard__count-value" data-taskboard-todo>' + counts.todo + '</span>' +
98                    '</span>' +
99                    '<span class="citrus-widget-taskboard__count citrus-widget-taskboard__count--doing">' +
100                        '<span class="citrus-widget-taskboard__count-label">Doing</span>' +
101                        '<span class="citrus-widget-taskboard__count-value" data-taskboard-doing>' + counts.doing + '</span>' +
102                    '</span>' +
103                '</div>' +
104                '<p class="citrus-widget-taskboard__status" data-taskboard-status>' + (statusText || 'Click to open') + '</p>' +
105            '</button>';
106
107        var panel = mountEl.querySelector('[data-taskboard-open]');
108        if (panel) {
109            panel.addEventListener('click', openTaskBoardWindow);
110            panel.addEventListener('pointerdown', function (ev) { ev.stopPropagation(); });
111        }
112    }
113
114    function updateCounts(mountEl, counts) {
115        var todoEl = mountEl.querySelector('[data-taskboard-todo]');
116        var doingEl = mountEl.querySelector('[data-taskboard-doing]');
117        var statusEl = mountEl.querySelector('[data-taskboard-status]');
118        if (todoEl) todoEl.textContent = String(counts.todo);
119        if (doingEl) doingEl.textContent = String(counts.doing);
120        if (statusEl) statusEl.textContent = 'Click to open';
121    }
122
123    function subscribeFirebase(mountEl) {
124        if (typeof firebase === 'undefined' || !firebase.database) {
125            return function () {};
126        }
127        tasksRef = firebase.database().ref('todo/tasks');
128        tasksHandler = tasksRef.on('value', function (snapshot) {
129            latestTasks = snapshot.val() || {};
130            var counts = countTasks(latestTasks);
131            if (global.CitrusTaskBoardCounts && typeof global.CitrusTaskBoardCounts.notify === 'function') {
132                global.CitrusTaskBoardCounts.notify(latestTasks);
133            }
134            if (!mountEl.isConnected) return;
135            if (mountEl.querySelector('[data-taskboard-open]')) {
136                updateCounts(mountEl, counts);
137            } else {
138                renderWidget(mountEl, counts);
139            }
140        }, function () {
141            if (!mountEl.isConnected) return;
142            renderWidget(mountEl, { todo: 0, doing: 0 }, 'Board unavailable');
143        });
144        return function () {
145            if (tasksRef && tasksHandler) tasksRef.off('value', tasksHandler);
146            tasksRef = null;
147            tasksHandler = null;
148        };
149    }
150
151    function bootWidget(mountEl) {
152        return ensureFirebaseReady().then(function () {
153            if (!mountEl.isConnected) return null;
154            if (global.__citrusTaskBoardCounts) {
155                renderWidget(mountEl, {
156                    todo: global.__citrusTaskBoardCounts.todo || 0,
157                    doing: global.__citrusTaskBoardCounts.doing || 0
158                });
159            } else {
160                renderWidget(mountEl, countTasks(latestTasks));
161            }
162            return subscribeFirebase(mountEl);
163        }).catch(function () {
164            if (!mountEl.isConnected) return null;
165            renderWidget(mountEl, { todo: 0, doing: 0 }, 'Firebase unavailable');
166            return null;
167        });
168    }
169
170    function mount(body) {
171        body.classList.add('citrus-widget-taskboard');
172        body.setAttribute('data-taskboard-widget-build', WIDGET_BUILD);
173
174        var mountEl = document.createElement('div');
175        mountEl.className = 'citrus-widget-taskboard__mount';
176        body.innerHTML = '';
177        body.appendChild(mountEl);
178
179        renderWidget(mountEl, { todo: 0, doing: 0 }, 'Loading...');
180        var unsubscribe = null;
181        var bootGen = 0;
182        bootWidget(mountEl).then(function (off) {
183            unsubscribe = off;
184        });
185
186        return {
187            destroy: function () {
188                bootGen += 1;
189                if (unsubscribe) unsubscribe();
190                unsubscribe = null;
191            },
192            refresh: function () {
193                bootGen += 1;
194                var localGen = bootGen;
195                if (unsubscribe) unsubscribe();
196                unsubscribe = null;
197                renderWidget(mountEl, { todo: 0, doing: 0 }, 'Loading...');
198                bootWidget(mountEl).then(function (off) {
199                    if (localGen !== bootGen || !mountEl.isConnected) return;
200                    unsubscribe = off;
201                });
202            }
203        };
204    }
205
206    if (global.CitrusWidgetRegistry && typeof global.CitrusWidgetRegistry.register === 'function') {
207        global.CitrusWidgetRegistry.register({
208            type: 'taskboard',
209            label: 'Task Board',
210            icon: 'fa-solid fa-clipboard-list',
211            blurb: 'Live To do and Doing counts from the task board. Click to open the full board.',
212            defaultSize: { w: 200, h: 132 },
213            minSize: { w: 180, h: 120 },
214            resizable: false,
215            mount: mount
216        });
217    }
218})(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.