From cc955e9288c5f146d0d3a6f8b7e46da562116c96 Mon Sep 17 00:00:00 2001 From: SandObserver <260779319+SandObserver@users.noreply.github.com> Date: Mon, 14 Sep 2026 16:16:01 -0400 Subject: [PATCH 1/2] Document OpenWeather as a weather widget service Add OpenWeather to the weather service list and settings, name it in the widgets index, and sync widget copies with the app. --- public/js/admin-app-form.js | 4 +- public/js/admin-auth.js | 2 +- public/js/admin-color-control.js | 2 +- public/js/admin-list.js | 4 +- public/js/admin-settings.js | 2 +- public/js/admin-shared.js | 24 ++++-- public/js/admin-widget-form.js | 4 +- public/js/admin.js | 14 ++-- public/js/widget-config-form.js | 75 +++++------------- public/widgets/weather/data.js | 94 ++++++++++++++++++----- public/widgets/weather/i18n/de.json | 5 ++ public/widgets/weather/i18n/en.json | 5 ++ public/widgets/weather/i18n/es.json | 5 ++ public/widgets/weather/i18n/fa.json | 5 ++ public/widgets/weather/i18n/fr.json | 5 ++ public/widgets/weather/i18n/zh-Hans.json | 5 ++ public/widgets/weather/widget.json | 27 +++++++ src/content/docs/docs/widgets/index.mdx | 2 +- src/content/docs/docs/widgets/weather.mdx | 12 ++- 19 files changed, 198 insertions(+), 98 deletions(-) diff --git a/public/js/admin-app-form.js b/public/js/admin-app-form.js index ec47e5b..dac5ad6 100644 --- a/public/js/admin-app-form.js +++ b/public/js/admin-app-form.js @@ -21,10 +21,10 @@ import { reveal, setTogDisabled, swapContent, -} from '/js/admin-shared.js?v=5cf446fa'; +} from '/js/admin-shared.js?v=fd784739'; import { createListbox } from '/js/listbox.js?v=a67e9c98'; import { MAX_LABELS } from '/js/badge-logic.js?v=b3c8b6c2'; -import { renderColorControl, BADGE_SWATCHES, BADGE_DEFAULT } from '/js/admin-color-control.js?v=47d9d808'; +import { renderColorControl, BADGE_SWATCHES, BADGE_DEFAULT } from '/js/admin-color-control.js?v=837961fa'; import { badgeErrorAdvice, TONE } from '/js/admin-error.js?v=10f3cdb1'; import { fluidHoverClear, fluidHoverKb } from '/js/fluid-hover.js?v=cb886e86'; diff --git a/public/js/admin-auth.js b/public/js/admin-auth.js index 858ecfe..5d083e1 100644 --- a/public/js/admin-auth.js +++ b/public/js/admin-auth.js @@ -1,4 +1,4 @@ -import { ag, ap } from '/js/admin-shared.js?v=5cf446fa'; +import { ag, ap } from '/js/admin-shared.js?v=fd784739'; import { t } from '/js/i18n.js?v=1f1ea9c1'; import { pwStrength } from '/js/password-strength.js?v=42f45ac7'; import { el, inp as inpById, qa } from '/js/utils.js?v=70cbc405'; diff --git a/public/js/admin-color-control.js b/public/js/admin-color-control.js index a8a1f81..7486796 100644 --- a/public/js/admin-color-control.js +++ b/public/js/admin-color-control.js @@ -1,4 +1,4 @@ -import { PE_SVG, initInlineEdit, toast, reveal } from '/js/admin-shared.js?v=5cf446fa'; +import { PE_SVG, initInlineEdit, toast, reveal } from '/js/admin-shared.js?v=fd784739'; import { t } from '/js/i18n.js?v=1f1ea9c1'; import { html, raw, setHtml } from '/js/html.js?v=c71f8903'; import { qa, q } from '/js/utils.js?v=70cbc405'; diff --git a/public/js/admin-list.js b/public/js/admin-list.js index 1c77e48..17d0f06 100644 --- a/public/js/admin-list.js +++ b/public/js/admin-list.js @@ -15,11 +15,11 @@ import { collapsedFolders, filter, state } from '/js/admin-state.js?v=5a5d655f'; import { snapshotItems } from '/js/admin-save-logic.js?v=4f71ef6c'; import { reorderItems } from '/js/admin-logic.js?v=69e57d35'; import { initDrag, wireRowDrag } from '/js/admin-drag.js?v=85ce4de5'; -import { paintIcon } from '/js/admin-shared.js?v=5cf446fa'; +import { paintIcon } from '/js/admin-shared.js?v=fd784739'; import { clr as rc, el, qa, setUserText } from '/js/utils.js?v=70cbc405'; import { html, raw, setHtml } from '/js/html.js?v=c71f8903'; import { t } from '/js/i18n.js?v=1f1ea9c1'; -import { sizeLabel } from '/js/admin-widget-form.js?v=6c778690'; +import { sizeLabel } from '/js/admin-widget-form.js?v=3b1b3546'; import { widgetGlyph } from '/js/widget-glyphs.js?v=12b0a947'; /** @type {{ openModal: (idx: number|null) => void, diff --git a/public/js/admin-settings.js b/public/js/admin-settings.js index 8be4557..12634d2 100644 --- a/public/js/admin-settings.js +++ b/public/js/admin-settings.js @@ -1,4 +1,4 @@ -import { toast, ag, ap, reveal, swapContent } from '/js/admin-shared.js?v=5cf446fa'; +import { toast, ag, ap, reveal, swapContent } from '/js/admin-shared.js?v=fd784739'; import { pwStrength } from '/js/password-strength.js?v=42f45ac7'; import { t } from '/js/i18n.js?v=1f1ea9c1'; import { diff --git a/public/js/admin-shared.js b/public/js/admin-shared.js index b154ca3..004193b 100644 --- a/public/js/admin-shared.js +++ b/public/js/admin-shared.js @@ -209,11 +209,21 @@ export const PE_SVG = @param {{ type?: string, placeholder?: string | (() => string), onCommit?: (value: string) => void, root?: ParentNode }} [opts] */ export function initInlineEdit(rowId, inputId, { type = 'text', placeholder = '', onCommit, root = document } = {}) { - const ph = () => (typeof placeholder === 'function' ? placeholder() : placeholder); const byId = id => (root === document ? el(id) : root.querySelector('#' + CSS.escape(id))); - const row = byId(rowId); + const row = /** @type {HTMLElement} */ (byId(rowId)); const inp = /** @type {HTMLInputElement} */ (byId(inputId)); if (!row || !inp) return; + wireInlineEdit(row, inp, { type, placeholder, onCommit }); +} + +/** The same row behaviour for elements a caller already holds. `row` needs an id. + `fill: false` keeps the input's own value on open. `render` replaces how the + committed value is shown. + @param {HTMLElement} row @param {HTMLInputElement} inp + @param {{ type?: string, placeholder?: string | (() => string), onCommit?: (value: string) => void, + fill?: boolean, render?: (valEl: Element, value: string) => void }} [opts] */ +export function wireInlineEdit(row, inp, { type = 'text', placeholder = '', onCommit, fill = true, render } = {}) { + const ph = () => (typeof placeholder === 'function' ? placeholder() : placeholder); const valEl = q('.rv', row); const pen = q('.pe', row); if (!valEl || !pen) return; @@ -227,7 +237,7 @@ export function initInlineEdit(rowId, inputId, { type = 'text', placeholder = '' name, and several readers drop it once the field holds a value. */ const labelEl = q('.rl', row); if (labelEl) { - if (!labelEl.id) labelEl.id = `${rowId}-rl`; + if (!labelEl.id) labelEl.id = `${row.id}-rl`; inp.setAttribute('aria-labelledby', labelEl.id); /* The pencil opens this row, so it is named after this row. Taking the name from the label keeps the two in one language, and in step when either @@ -236,11 +246,13 @@ export function initInlineEdit(rowId, inputId, { type = 'text', placeholder = '' } row.insertBefore(inp, pen); + let before = ''; function open() { if (row.classList.contains('editing')) return; row.classList.add('editing'); inp.placeholder = ph(); - inp.value = valEl.classList.contains('is-ph') ? '' : valEl.textContent; + if (fill) inp.value = valEl.classList.contains('is-ph') ? '' : valEl.textContent; + before = inp.value; inp.focus(); inp.select?.(); } @@ -248,7 +260,8 @@ export function initInlineEdit(rowId, inputId, { type = 'text', placeholder = '' if (!row.classList.contains('editing')) return; row.classList.remove('editing'); const v = inp.value.trim(); - if (v) { + if (render) render(valEl, v); + else if (v) { valEl.textContent = v; valEl.classList.remove('is-ph'); } else { @@ -270,6 +283,7 @@ export function initInlineEdit(rowId, inputId, { type = 'text', placeholder = '' } if (e.key === 'Escape') { e.preventDefault(); + inp.value = before; row.classList.remove('editing'); } }, diff --git a/public/js/admin-widget-form.js b/public/js/admin-widget-form.js index 4104a34..91e2b4e 100644 --- a/public/js/admin-widget-form.js +++ b/public/js/admin-widget-form.js @@ -1,7 +1,7 @@ import { state } from '/js/admin-state.js?v=5a5d655f'; -import { PE_SVG, initInlineEdit, swapContent } from '/js/admin-shared.js?v=5cf446fa'; +import { PE_SVG, initInlineEdit, swapContent } from '/js/admin-shared.js?v=fd784739'; import { createListbox } from '/js/listbox.js?v=a67e9c98'; -import { renderWidgetConfigForm } from '/js/widget-config-form.js?v=785f41f6'; +import { renderWidgetConfigForm } from '/js/widget-config-form.js?v=ad9e05ee'; import { html, raw, setHtml } from '/js/html.js?v=c71f8903'; import { sizesForView, widgetConfigMode, rejectionLines, carriesTypedValues } from '/js/admin-logic.js?v=69e57d35'; import { t } from '/js/i18n.js?v=1f1ea9c1'; diff --git a/public/js/admin.js b/public/js/admin.js index b36cd83..42c4d87 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -1,6 +1,6 @@ -import { buildAppForm, buildFolderForm, captureActLabels, serializeKvRows } from '/js/admin-app-form.js?v=0ed92d16'; -import { checkAuth, requireLogin, wirePasswordStrength } from '/js/admin-auth.js?v=8f4673da'; -import { initList, render, syncFilterUI } from '/js/admin-list.js?v=d0669049'; +import { buildAppForm, buildFolderForm, captureActLabels, serializeKvRows } from '/js/admin-app-form.js?v=a53ac0e8'; +import { checkAuth, requireLogin, wirePasswordStrength } from '/js/admin-auth.js?v=c70fb722'; +import { initList, render, syncFilterUI } from '/js/admin-list.js?v=b0923a32'; import { resolveAdminSection } from '/js/admin-logic.js?v=69e57d35'; import { buildAppItem, @@ -10,10 +10,10 @@ import { snapshotItems, upsertItem, } from '/js/admin-save-logic.js?v=4f71ef6c'; -import { loadSettings, settingsDirty, showBgFields, showWallpaperFile } from '/js/admin-settings.js?v=81778efd'; -import { ag, ap, initInlineEdit, paintIcon, reveal, setReauthHandler, toast } from '/js/admin-shared.js?v=5cf446fa'; +import { loadSettings, settingsDirty, showBgFields, showWallpaperFile } from '/js/admin-settings.js?v=079a3d2d'; +import { ag, ap, initInlineEdit, paintIcon, reveal, setReauthHandler, toast } from '/js/admin-shared.js?v=fd784739'; import { collapsedFolders, filter, state } from '/js/admin-state.js?v=5a5d655f'; -import { buildWidgetForm } from '/js/admin-widget-form.js?v=6c778690'; +import { buildWidgetForm } from '/js/admin-widget-form.js?v=3b1b3546'; import { initFluidHover } from '/js/fluid-hover.js?v=cb886e86'; import { initGlideSelect, syncGlideSelect } from '/js/glide-select.js?v=8b39e9d0'; import { createListbox } from '/js/listbox.js?v=a67e9c98'; @@ -33,7 +33,7 @@ import { isMobileLayout, onLayoutChange } from '/js/layout.js?v=e9f4b607'; import { confirmModal, confirmText, openModal as openDialog, promptModal } from '/js/modal.js?v=11fa1eff'; import { readMode, watchSystemTheme, writeMode } from '/js/theme.js?v=787bfdff'; import { el, inp, q, qa, clr as rc, sanitizeCssUrl, setUserText, tgt } from '/js/utils.js?v=70cbc405'; -import { normalizeColorInput } from '/js/admin-color-control.js?v=47d9d808'; +import { normalizeColorInput } from '/js/admin-color-control.js?v=837961fa'; import { parseYamlTolerant, YamlLiteError } from '/js/yaml-lite.js?v=6ebb564c'; import { loadWallpaper, saveWallpaper } from '/js/wallpaper-cache.js?v=c5f8a3e6'; diff --git a/public/js/widget-config-form.js b/public/js/widget-config-form.js index 96a5d2c..8e99c9e 100644 --- a/public/js/widget-config-form.js +++ b/public/js/widget-config-form.js @@ -3,9 +3,9 @@ import { t } from '/js/i18n.js?v=1f1ea9c1'; import { html, raw, setHtml } from '/js/html.js?v=c71f8903'; -import { reveal } from '/js/admin-shared.js?v=5cf446fa'; +import { reveal, wireInlineEdit } from '/js/admin-shared.js?v=fd784739'; import { createListbox } from '/js/listbox.js?v=a67e9c98'; -import { renderColorControl } from '/js/admin-color-control.js?v=47d9d808'; +import { renderColorControl } from '/js/admin-color-control.js?v=837961fa'; import { seedCarried, applyOptionSet, @@ -36,40 +36,17 @@ function _ieRow(field, value, inputType) { const ph = field.placeholder || ''; const row = document.createElement('div'); row.className = 'row ie-row'; + row.id = uniqueId('wcf-ie'); setHtml( row, html`${field.label}${_tag(field)}${has ? value : ph}`, ); - const rv = row.querySelector('.rv'), - inp = qi('.row-inp', row), - pe = row.querySelector('.pe'); - function open() { - row.classList.add('editing'); - inp.style.display = 'block'; - inp.focus(); - inp.select?.(); - } - function commit() { - row.classList.remove('editing'); - inp.style.display = 'none'; - const v = inp.value.trim(); - if (v) { - rv.textContent = v; - rv.classList.remove('is-ph'); - } else { - rv.textContent = ph; - rv.classList.add('is-ph'); - } - inp.dispatchEvent(new Event('change', { bubbles: true })); - } - pe.addEventListener('click', open); - rv.addEventListener('click', open); - inp.addEventListener('blur', commit); - inp.addEventListener('keydown', e => { - if (e.key === 'Enter') { - e.preventDefault(); - inp.blur(); - } + const inp = qi('.row-inp', row); + wireInlineEdit(row, inp, { + type: inputType, + placeholder: ph, + fill: false, + onCommit: () => inp.dispatchEvent(new Event('change', { bubbles: true })), }); const get = () => { const v = inp.value.trim(); @@ -96,33 +73,21 @@ function _ieRow(field, value, inputType) { function _secret(field, isSet) { const row = document.createElement('div'); row.className = 'row ie-row'; + row.id = uniqueId('wcf-secret'); const display = isSet ? t('common.configured') : t('common.notSet'); setHtml( row, - html`${field.label}${_tag(field)}${display}`, + html`${field.label}${_tag(field)}${display}`, ); - const rv = row.querySelector('.rv'), - inp = qi('.row-inp', row), - pe = row.querySelector('.pe'); - const open = () => { - row.classList.add('editing'); - inp.style.display = 'block'; - inp.focus(); - }; - const commit = () => { - row.classList.remove('editing'); - inp.style.display = 'none'; - rv.textContent = inp.value ? t('widgetCfg.newValueSet') : display; - inp.dispatchEvent(new Event('change', { bubbles: true })); - }; - pe.addEventListener('click', open); - rv.addEventListener('click', open); - inp.addEventListener('blur', commit); - inp.addEventListener('keydown', e => { - if (e.key === 'Enter') { - e.preventDefault(); - inp.blur(); - } + const inp = qi('.row-inp', row); + wireInlineEdit(row, inp, { + type: 'password', + placeholder: isSet ? t('widgetCfg.replaceSecret') : field.placeholder || '', + fill: false, + render: (valEl, v) => { + valEl.textContent = v ? t('widgetCfg.newValueSet') : display; + }, + onCommit: () => inp.dispatchEvent(new Event('change', { bubbles: true })), }); const get = () => { const v = inp.value.trim(); diff --git a/public/widgets/weather/data.js b/public/widgets/weather/data.js index 4e76f37..89b40ef 100644 --- a/public/widgets/weather/data.js +++ b/public/widgets/weather/data.js @@ -1,5 +1,6 @@ -/* Open-Meteo, keyless. Returns { temp, units, code, isDay, condition, city }, - where code is the WMO weather code. */ +/* Open-Meteo (keyless) or OpenWeather (free API key). Returns + { temp, units, code, isDay, usedFeels, city }, where code is the WMO weather + code for both providers. The city search always uses Open-Meteo's geocoder. */ async function geocode(ctx) { const q = String(ctx.config.cityQuery || ctx.config.city || '').trim(); @@ -16,23 +17,15 @@ async function geocode(ctx) { return { options }; } -module.exports = async function (ctx) { - const { config, fetchJSON } = ctx; - if (ctx.endpoint === 'geocode') return await geocode(ctx); - const lat = config.lat, - lon = config.lon; - if (lat == null || lon == null || lat === '' || lon === '') ctx.fail('Location not set', { kind: ctx.KIND.INVALID }); - const units = config.units === 'f' ? 'f' : 'c'; - const tempUnit = units === 'f' ? 'fahrenheit' : 'celsius'; - +async function openMeteo(ctx, lat, lon, units) { const url = 'https://api.open-meteo.com/v1/forecast' + `?latitude=${encodeURIComponent(lat)}&longitude=${encodeURIComponent(lon)}` + '¤t=temperature_2m,apparent_temperature,weather_code,is_day' + - `&daily=sunrise,sunset&temperature_unit=${tempUnit}` + + `&daily=sunrise,sunset&temperature_unit=${units === 'f' ? 'fahrenheit' : 'celsius'}` + '&timezone=auto&forecast_days=1'; - const r = await fetchJSON(url, { timeout: 8000 }); + const r = await ctx.fetchJSON(url, { timeout: 8000 }); if (r.status >= 400 || !r.data || !r.data.current) { ctx.fail('Weather unavailable (' + r.status + ')'); } @@ -45,18 +38,79 @@ module.exports = async function (ctx) { const ss = new Date(r.data.daily.sunset[0]).getTime(); isDay = now >= sr && now < ss; } - - const useFeels = config.feelsLike === true || config.feelsLike === 'true'; const real = cur.temperature_2m; - const feels = cur.apparent_temperature != null ? cur.apparent_temperature : real; - const shown = useFeels ? feels : real; + return { + real, + feels: cur.apparent_temperature != null ? cur.apparent_temperature : real, + code: cur.weather_code, + isDay, + }; +} +/* OpenWeather condition id to the WMO code the page renders. */ +function wmoFromOpenWeather(id) { + if (id >= 200 && id < 300) return 95; + if (id >= 300 && id < 400) return 51; + if (id === 511) return 66; + if (id >= 520 && id < 600) return 80; + if (id >= 500 && id < 600) return 61; + if (id >= 620 && id < 700) return 85; + if (id >= 600 && id < 700) return 71; + if (id >= 700 && id < 800) return 45; + if (id === 800) return 0; + if (id === 801) return 1; + if (id === 802) return 2; + return 3; +} + +async function openWeather(ctx, lat, lon, units) { + const key = String(ctx.config.owKey || '').trim(); + if (!key) ctx.fail('Enter an OpenWeather API key.', { kind: ctx.KIND.INVALID }); + const url = + 'https://api.openweathermap.org/data/2.5/weather' + + `?lat=${encodeURIComponent(lat)}&lon=${encodeURIComponent(lon)}` + + `&units=${units === 'f' ? 'imperial' : 'metric'}&appid=${encodeURIComponent(key)}`; + + const r = await ctx.fetchJSON(url, { timeout: 8000 }); + if (r.status === 401) { + ctx.fail('OpenWeather rejected the API key. A new key can take a few hours to activate.', { kind: ctx.KIND.AUTH }); + } + const d = r.data; + if (r.status >= 400 || !d || !d.main || !Array.isArray(d.weather) || !d.weather[0]) { + ctx.fail('Weather unavailable (' + r.status + ')'); + } + + const w = d.weather[0]; + let isDay; + if (typeof w.icon === 'string' && /[dn]$/.test(w.icon)) isDay = w.icon.endsWith('d'); + else isDay = d.sys && d.dt ? d.dt >= d.sys.sunrise && d.dt < d.sys.sunset : true; + const real = d.main.temp; + return { + real, + feels: d.main.feels_like != null ? d.main.feels_like : real, + code: wmoFromOpenWeather(w.id), + isDay, + }; +} + +module.exports = async function (ctx) { + const { config } = ctx; + if (ctx.endpoint === 'geocode') return await geocode(ctx); + const lat = config.lat, + lon = config.lon; + if (lat == null || lon == null || lat === '' || lon === '') ctx.fail('Location not set', { kind: ctx.KIND.INVALID }); + const units = config.units === 'f' ? 'f' : 'c'; + + const read = config.provider === 'openweather' ? openWeather : openMeteo; + const w = await read(ctx, lat, lon, units); + + const useFeels = config.feelsLike === true || config.feelsLike === 'true'; return { - temp: Math.round(shown), + temp: Math.round(useFeels ? w.feels : w.real), usedFeels: useFeels, units: units, - code: cur.weather_code, - isDay: isDay, + code: w.code, + isDay: w.isDay, city: config.city || '', }; }; diff --git a/public/widgets/weather/i18n/de.json b/public/widgets/weather/i18n/de.json index fe53eff..1dcfe56 100644 --- a/public/widgets/weather/i18n/de.json +++ b/public/widgets/weather/i18n/de.json @@ -1,5 +1,10 @@ { "widget.label": "Wetter", + "provider.label": "Dienst", + "provider.opt.openmeteo": "Open-Meteo", + "provider.opt.openweather": "OpenWeather", + "owKey.label": "API-Schlüssel", + "owKey.hint": "Kostenloser Schlüssel von openweathermap.org. Ein neuer Schlüssel kann einige Stunden bis zur Aktivierung brauchen.", "cityQuery.label": "Suche", "cityQuery.placeholder": "z. B. Ottawa", "city.label": "Ort", diff --git a/public/widgets/weather/i18n/en.json b/public/widgets/weather/i18n/en.json index 49b9170..bfe9c6c 100644 --- a/public/widgets/weather/i18n/en.json +++ b/public/widgets/weather/i18n/en.json @@ -1,5 +1,10 @@ { "widget.label": "Weather", + "provider.label": "Service", + "provider.opt.openmeteo": "Open-Meteo", + "provider.opt.openweather": "OpenWeather", + "owKey.label": "API key", + "owKey.hint": "Free key from openweathermap.org. A new key can take a few hours to activate.", "cityQuery.label": "Search", "cityQuery.placeholder": "e.g. Ottawa", "city.label": "Location", diff --git a/public/widgets/weather/i18n/es.json b/public/widgets/weather/i18n/es.json index c232911..84e1319 100644 --- a/public/widgets/weather/i18n/es.json +++ b/public/widgets/weather/i18n/es.json @@ -1,5 +1,10 @@ { "widget.label": "Tiempo", + "provider.label": "Servicio", + "provider.opt.openmeteo": "Open-Meteo", + "provider.opt.openweather": "OpenWeather", + "owKey.label": "Clave de API", + "owKey.hint": "Clave gratuita de openweathermap.org. Una clave nueva puede tardar unas horas en activarse.", "cityQuery.label": "Buscar", "cityQuery.placeholder": "p. ej. Ottawa", "city.label": "Ubicación", diff --git a/public/widgets/weather/i18n/fa.json b/public/widgets/weather/i18n/fa.json index 72a58ca..7a577d9 100644 --- a/public/widgets/weather/i18n/fa.json +++ b/public/widgets/weather/i18n/fa.json @@ -1,5 +1,10 @@ { "widget.label": "آب‌وهوا", + "provider.label": "سرویس", + "provider.opt.openmeteo": "Open-Meteo", + "provider.opt.openweather": "OpenWeather", + "owKey.label": "کلید API", + "owKey.hint": "کلید رایگان را از openweathermap.org بگیرید. فعال شدن کلید تازه ممکن است چند ساعت طول بکشد.", "cityQuery.label": "جستجو", "cityQuery.placeholder": "مثلاً تهران", "city.label": "موقعیت", diff --git a/public/widgets/weather/i18n/fr.json b/public/widgets/weather/i18n/fr.json index e1be307..25c5991 100644 --- a/public/widgets/weather/i18n/fr.json +++ b/public/widgets/weather/i18n/fr.json @@ -1,5 +1,10 @@ { "widget.label": "Météo", + "provider.label": "Service", + "provider.opt.openmeteo": "Open-Meteo", + "provider.opt.openweather": "OpenWeather", + "owKey.label": "Clé d’API", + "owKey.hint": "Clé gratuite sur openweathermap.org. Une nouvelle clé peut mettre quelques heures à s’activer.", "cityQuery.label": "Rechercher", "cityQuery.placeholder": "ex. Ottawa", "city.label": "Lieu", diff --git a/public/widgets/weather/i18n/zh-Hans.json b/public/widgets/weather/i18n/zh-Hans.json index f594503..ca68e60 100644 --- a/public/widgets/weather/i18n/zh-Hans.json +++ b/public/widgets/weather/i18n/zh-Hans.json @@ -1,5 +1,10 @@ { "widget.label": "天气", + "provider.label": "服务", + "provider.opt.openmeteo": "Open-Meteo", + "provider.opt.openweather": "OpenWeather", + "owKey.label": "API 密钥", + "owKey.hint": "在 openweathermap.org 免费获取密钥。新密钥可能需要几个小时才能生效。", "cityQuery.label": "搜索", "cityQuery.placeholder": "例如 Ottawa", "city.label": "位置", diff --git a/public/widgets/weather/widget.json b/public/widgets/weather/widget.json index 3b78017..7911049 100644 --- a/public/widgets/weather/widget.json +++ b/public/widgets/weather/widget.json @@ -6,6 +6,33 @@ "small" ], "fields": [ + { + "key": "provider", + "type": "select", + "variant": "pills", + "label": "provider.label", + "default": "openmeteo", + "options": [ + { + "value": "openmeteo", + "label": "provider.opt.openmeteo" + }, + { + "value": "openweather", + "label": "provider.opt.openweather" + } + ] + }, + { + "key": "owKey", + "type": "secret", + "label": "owKey.label", + "hint": "owKey.hint", + "showIf": { + "field": "provider", + "equals": "openweather" + } + }, { "key": "cityQuery", "type": "text", diff --git a/src/content/docs/docs/widgets/index.mdx b/src/content/docs/docs/widgets/index.mdx index c8f2b0b..2b7595c 100644 --- a/src/content/docs/docs/widgets/index.mdx +++ b/src/content/docs/docs/widgets/index.mdx @@ -33,7 +33,7 @@ Widgets reorder in the same list as apps and folders. See [Apps and folders](/do { href: '/docs/widgets/github/', name: 'GitHub', text: 'Pull requests or a contribution graph.', value: 'GitHub' }, { href: '/docs/widgets/now-playing/', name: 'Now Playing', text: 'What is playing, with progress.', value: 'Plex, Jellyfin, Emby, Navidrome' }, { href: '/docs/widgets/system-summary/', name: 'System summary', text: 'CPU, memory, disk, temperature and network.', value: 'This machine, Glances, Beszel, Unraid' }, - { href: '/docs/widgets/weather/', name: 'Weather', text: 'Current conditions for one place.', value: 'Open-Meteo' }, + { href: '/docs/widgets/weather/', name: 'Weather', text: 'Current conditions for one place.', value: 'Open-Meteo, OpenWeather' }, ]} /> ## Credentials diff --git a/src/content/docs/docs/widgets/weather.mdx b/src/content/docs/docs/widgets/weather.mdx index 1d1571e..6188141 100644 --- a/src/content/docs/docs/widgets/weather.mdx +++ b/src/content/docs/docs/widgets/weather.mdx @@ -1,6 +1,6 @@ --- title: Weather widget -description: Current conditions for any city from Open-Meteo, with no account or API key, on your Stackyard dashboard. +description: Current conditions for any city from Open-Meteo, with no account, or from OpenWeather with a free API key, on your Stackyard dashboard. --- import WidgetGroup from '../../../../components/WidgetGroup.astro'; @@ -29,9 +29,19 @@ Small only. { label: 'open-meteo.com', href: 'https://open-meteo.com/' }, ], }, + { + name: 'OpenWeather', + kind: 'Weather API', + needs: 'API key', + fields: [['API key', 'Free from openweathermap.org. A new key can take a few hours to activate. Stored as a secret.']], + notes: ['Location search still uses Open-Meteo, so it needs no key.'], + links: [{ label: 'openweathermap.org', href: 'https://openweathermap.org/' }], + }, ]} /> Date: Mon, 14 Sep 2026 16:17:56 -0400 Subject: [PATCH 2/2] Add OpenWeather service logo --- public/icons/services/openweather-dark.svg | 1 + public/icons/services/openweather-light.svg | 1 + src/content/docs/docs/widgets/weather.mdx | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 public/icons/services/openweather-dark.svg create mode 100644 public/icons/services/openweather-light.svg diff --git a/public/icons/services/openweather-dark.svg b/public/icons/services/openweather-dark.svg new file mode 100644 index 0000000..d5aeeda --- /dev/null +++ b/public/icons/services/openweather-dark.svg @@ -0,0 +1 @@ + diff --git a/public/icons/services/openweather-light.svg b/public/icons/services/openweather-light.svg new file mode 100644 index 0000000..02346fb --- /dev/null +++ b/public/icons/services/openweather-light.svg @@ -0,0 +1 @@ + diff --git a/src/content/docs/docs/widgets/weather.mdx b/src/content/docs/docs/widgets/weather.mdx index 6188141..f9e131c 100644 --- a/src/content/docs/docs/widgets/weather.mdx +++ b/src/content/docs/docs/widgets/weather.mdx @@ -30,7 +30,7 @@ Small only. ], }, { - name: 'OpenWeather', + name: 'OpenWeather', icon: 'openweather', kind: 'Weather API', needs: 'API key', fields: [['API key', 'Free from openweathermap.org. A new key can take a few hours to activate. Stored as a secret.']],