1/** 2 * CitrusOS Weather desktop widget (index.html / #citrusos-widgets). 3 * Self-contained UI â does not share weatherDetector's dashboard compact layout. 4 * Register path: citrusShellManifest.js â widgets 5 */ 6(function (global) { 7 'use strict'; 8 9 var WIDGET_BUILD = 'citrusos-weather-widget-v5'; 10 var Loc = global.PupwifeWeatherLocation; 11 12 function apiConfig() { 13 return global.PupwifeWeatherApi || { KEY: '', BASE: 'https://api.weatherapi.com/v1' }; 14 } 15 16 function isValidPayload(data) { 17 return !!(data && data.current && data.location && data.current.condition); 18 } 19 20 function fetchWidgetWeather(skipCache) { 21 if (!Loc) return Promise.reject(new Error('Weather location module missing')); 22 if (!apiConfig().KEY) return Promise.reject(new Error('Weather API key missing')); 23 return Loc.fetchForecast({ 24 days: 1, 25 aqi: false, 26 skipCache: !!skipCache 27 }).then(function (data) { 28 if (!isValidPayload(data)) throw new Error('WeatherAPI returned unexpected data'); 29 return data; 30 }); 31 } 32 33 function loadUnit() { 34 try { 35 var saved = localStorage.getItem('weatherUnit') || localStorage.getItem('weatherPageUnit'); 36 return saved === 'C' || saved === 'F' ? saved : 'F'; 37 } catch (e) { 38 return 'F'; 39 } 40 } 41 42 function saveUnit(unit) { 43 try { 44 localStorage.setItem('weatherUnit', unit); 45 localStorage.setItem('weatherPageUnit', unit); 46 } catch (e) { /* ignore */ } 47 } 48 49 function openForecastWindow(ev) { 50 if (ev) { 51 ev.preventDefault(); 52 ev.stopPropagation(); 53 } 54 var registry = global.CitrusAppRegistry; 55 var desktop = global.CitrusDesktop; 56 if (registry && desktop && typeof registry.getAppByHref === 'function') { 57 var app = registry.getAppByHref('/weather.html'); 58 if (app && typeof desktop.openWindow === 'function') { 59 desktop.openWindow(app); 60 return; 61 } 62 if (typeof desktop.openHref === 'function') { 63 desktop.openHref('/weather.html'); 64 } 65 } 66 } 67 68 function buildView(data, unit) { 69 if (!data || !data.current || !data.location) { 70 throw new Error('Invalid weather data'); 71 } 72 var current = data.current; 73 var condition = current.condition || {}; 74 var iconUrl = condition.icon ? String(condition.icon) : ''; 75 if (iconUrl.indexOf('//') === 0) iconUrl = 'https:' + iconUrl; 76 var temp = unit === 'F' ? Math.round(current.temp_f) : Math.round(current.temp_c); 77 var feelsLike = unit === 'F' ? Math.round(current.feelslike_f) : Math.round(current.feelslike_c); 78 var locationDisplay = Loc 79 ? Loc.getLocationDisplay(data) 80 : { 81 iconClass: 'fa-location-dot', 82 iconTitle: 'Location', 83 label: [data.location.name, data.location.region || data.location.country] 84 .filter(Boolean) 85 .join(', ') || 'Unknown' 86 }; 87 return { 88 iconUrl: iconUrl, 89 temp: temp, 90 condition: condition.text || 'Unknown', 91 feelsLike: feelsLike, 92 locationName: locationDisplay.label, 93 locationIconClass: locationDisplay.iconClass, 94 locationIconTitle: locationDisplay.iconTitle 95 }; 96 } 97 98 function renderWidget(mountEl, view, unit, rawData) { 99 var iconHtml = view.iconUrl 100 ? '<img class="citrus-widget-weather__icon-img" src="' + view.iconUrl + '" alt="" width="52" height="52">' 101 : '<i class="fa-solid fa-cloud-sun citrus-widget-weather__icon-fallback" aria-hidden="true"></i>'; 102 103 mountEl.innerHTML = 104 '<div class="citrus-widget-weather__panel" data-weather-widget-build="' + WIDGET_BUILD + '">' + 105 '<div class="citrus-widget-weather__current">' + 106 '<button type="button" class="citrus-widget-weather-unit-btn citrus-widget-weather-unit-btn--corner" data-weather-unit-toggle aria-label="Switch temperature unit">°' + 107 (unit === 'F' ? 'C' : 'F') + '</button>' + 108 '<div class="citrus-widget-weather__main-temp">' + 109 iconHtml + 110 '<div class="citrus-widget-weather__temp-info">' + 111 '<div class="citrus-widget-weather__temp-large">' + view.temp + '°' + unit + '</div>' + 112 '<div class="citrus-widget-weather__condition-text">' + view.condition + '</div>' + 113 '<div class="citrus-widget-weather__feels-like">Feels like ' + view.feelsLike + '°' + unit + '</div>' + 114 '</div>' + 115 '</div>' + 116 '</div>' + 117 '<div class="citrus-widget-weather__location">' + 118 '<i class="fa-solid ' + view.locationIconClass + ' citrus-widget-weather__location-icon" aria-hidden="true" title="' + view.locationIconTitle + '"></i> ' + 119 '<span class="citrus-widget-weather__location-label">' + view.locationName + '</span>' + 120 '</div>' + 121 '<div class="citrus-widget-weather__actions">' + 122 '<button type="button" class="citrus-widget-weather-forecast-btn" data-weather-open-app>Full forecast</button>' + 123 '</div>' + 124 '</div>'; 125 126 var unitBtn = mountEl.querySelector('[data-weather-unit-toggle]'); 127 var forecastBtn = mountEl.querySelector('[data-weather-open-app]'); 128 129 if (unitBtn) { 130 unitBtn.addEventListener('click', function (ev) { 131 ev.stopPropagation(); 132 var next = unit === 'F' ? 'C' : 'F'; 133 saveUnit(next); 134 renderWidget(mountEl, buildView(rawData, next), next, rawData); 135 }); 136 unitBtn.addEventListener('pointerdown', function (ev) { ev.stopPropagation(); }); 137 } 138 139 if (forecastBtn) { 140 forecastBtn.addEventListener('click', openForecastWindow); 141 forecastBtn.addEventListener('pointerdown', function (ev) { ev.stopPropagation(); }); 142 } 143 } 144 145 function renderError(mountEl, message) { 146 mountEl.innerHTML = 147 '<div class="citrus-widget-weather__panel citrus-widget-weather__panel--error" data-weather-widget-build="' + WIDGET_BUILD + '">' + 148 '<p class="citrus-widget-weather__error"><i class="fa-solid fa-circle-exclamation" aria-hidden="true"></i> weather unavailable</p>' + 149 '<p class="citrus-widget-weather__error-detail">' + (message || 'check your connection') + '</p>' + 150 '<div class="citrus-widget-weather__actions">' + 151 '<button type="button" class="citrus-widget-weather-forecast-btn" data-weather-open-app>Full forecast</button>' + 152 '<button type="button" class="citrus-widget-weather-retry" data-weather-retry>Retry</button>' + 153 '</div>' + 154 '</div>'; 155 156 var forecastBtn = mountEl.querySelector('[data-weather-open-app]'); 157 var retryBtn = mountEl.querySelector('[data-weather-retry]'); 158 if (forecastBtn) { 159 forecastBtn.addEventListener('click', openForecastWindow); 160 forecastBtn.addEventListener('pointerdown', function (ev) { ev.stopPropagation(); }); 161 } 162 if (retryBtn) { 163 retryBtn.addEventListener('click', function () { 164 bootWidget(mountEl); 165 }); 166 retryBtn.addEventListener('pointerdown', function (ev) { ev.stopPropagation(); }); 167 } 168 } 169 170 function bootWidget(mountEl, skipCache) { 171 var gen = bootWidget._gen = (bootWidget._gen || 0) + 1; 172 var localGen = gen; 173 mountEl.innerHTML = '<p class="citrus-widget-weather__loading"><i class="fa-solid fa-cloud-sun" aria-hidden="true"></i> loading weather...</p>'; 174 175 fetchWidgetWeather(skipCache).then(function (data) { 176 if (localGen !== bootWidget._gen || !mountEl.isConnected) return; 177 var unit = loadUnit(); 178 renderWidget(mountEl, buildView(data, unit), unit, data); 179 }).catch(function (err) { 180 if (localGen !== bootWidget._gen || !mountEl.isConnected) return; 181 var msg = err && err.message ? err.message : 'check your connection'; 182 renderError(mountEl, msg); 183 }); 184 } 185 186 function mount(body) { 187 body.classList.add('citrus-widget-weather'); 188 body.setAttribute('data-weather-widget-build', WIDGET_BUILD); 189 190 var mountEl = document.createElement('div'); 191 mountEl.className = 'citrus-widget-weather__mount'; 192 mountEl.setAttribute('data-citrus-weather-widget', '1'); 193 body.innerHTML = ''; 194 body.appendChild(mountEl); 195 196 var offLocationChange = null; 197 if (Loc) { 198 offLocationChange = Loc.onChange(function () { 199 bootWidget(mountEl, true); 200 }); 201 } 202 203 bootWidget(mountEl); 204 205 return { 206 destroy: function () { 207 bootWidget._gen = (bootWidget._gen || 0) + 1; 208 if (offLocationChange) offLocationChange(); 209 }, 210 refresh: function () { 211 bootWidget(mountEl, true); 212 } 213 }; 214 } 215 216 if (global.CitrusWidgetRegistry && typeof global.CitrusWidgetRegistry.register === 'function') { 217 global.CitrusWidgetRegistry.register({ 218 type: 'weather', 219 label: 'Weather', 220 icon: 'fa-solid fa-cloud-sun', 221 blurb: 'Forecast from WeatherAPI.com. Auto location or search for a city in the full forecast window.', 222 defaultSize: { w: 228, h: 176 }, 223 minSize: { w: 200, h: 156 }, 224 resizable: false, 225 mount: mount 226 }); 227 } 228})(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.