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.