PageSourceSearch

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

js pupwife.com collected 2026-09-30 15:04:36 UTC 10,074 bytes, 228 lines download raw bytes

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.