diff --git a/daily-wallpaper/README.md b/daily-wallpaper/README.md index d8097eda..53060306 100644 --- a/daily-wallpaper/README.md +++ b/daily-wallpaper/README.md @@ -1,14 +1,19 @@ # Daily Wallpaper -Daily Wallpaper fetches Bing's image of the day or NASA's image of the day and -applies it through Noctalia's wallpaper API. +Daily Wallpaper fetches Bing's image of the day or NASA's image of the day, +applies it through Noctalia's wallpaper API, and shows the caption/credit that +came with it — on the bar, in a panel, and on the desktop. ## Plugin | Field | Value | | --- | --- | | ID | `nzlov/daily-wallpaper` | -| Entry | Service: `service` | +| Entries | Service: `service`; bar widget: `widget`; panel: `panel`; desktop widget: `desktop` | + +## Requirements + +Install `xdg-utils` on `PATH` (for the panel's "Learn more" button, via `xdg-open`). ## Usage @@ -19,12 +24,27 @@ per source and Bing locale each day. Choose Bing or NASA under the plugin's settings. Bing accepts a market locale such as `en-US`, `de-DE`, or `fr-FR`; NASA ignores the locale setting. +Add the `widget` bar entry and/or the `desktop` desktop widget from Settings +to see today's caption. Clicking the bar glyph (or running +`noctalia msg panel-toggle nzlov/daily-wallpaper:panel`) opens the full panel +with the caption, credit (Bing only), and a "Learn more" link — Bing's quiz +page, or NASA's full article about the image. + ## Settings | Setting | Type | Default | Description | | --- | --- | --- | --- | | `source` | `select` | `bing` | Selects Bing or NASA as the daily image source. | | `locale` | `string` | *(automatic)* | Bing market locale; an empty value uses the service default. | +| `widget.glyph` | `glyph` | `photo` | Bar icon. | +| `widget.show_text` | `bool` | `true` | Show a truncated caption next to the bar glyph. | +| `widget.max_chars` | `int` | `28` | Max caption length shown on the bar before truncating with an ellipsis. | + +## IPC + +```sh +noctalia msg plugin nzlov/daily-wallpaper:service all refresh +``` ## Notes @@ -32,3 +52,11 @@ The service contacts the selected provider and downloads images into a dedicated `daily-wallpaper` cache directory. It removes cached images older than five days. Repeated failures are logged, but error notifications are limited to once per day. + +Neither source gives a wallpaper-application step any reason to keep the +caption text that comes with the image, so it is captured separately at the +point each source resolves and published as plugin state for the bar widget, +panel, and desktop widget to display. Bing's `copyright` field is one caption +line, e.g. "Runners at the base of Victoria Falls, Zimbabwe (© Byron.../Getty +Images)", which is split into a title and a credit; NASA's gallery-item +caption is a single sentence with no separate credit line. diff --git a/daily-wallpaper/daily_wallpaper.luau b/daily-wallpaper/daily_wallpaper.luau index dce80ede..cb8c426e 100644 --- a/daily-wallpaper/daily_wallpaper.luau +++ b/daily-wallpaper/daily_wallpaper.luau @@ -1,7 +1,12 @@ --!nonstrict -- Daily Wallpaper service for Noctalia 5. -- Fetches one Bing or NASA image per day, caches it locally, and applies it via --- the native Noctalia wallpaper API. +-- the native Noctalia wallpaper API. Also captures each source's caption text +-- (Bing's `copyright` field; NASA's gallery-item caption) and publishes it as +-- "today" state for widget.luau/panel.luau/desktop_widget.luau to display — +-- neither source's raw API gives a wallpaper-application step any reason to +-- keep this text, so it has to be captured here, at the point each source is +-- resolved, or it is lost. local CHECK_INTERVAL_MS = 10 * 60 * 1000 local RETENTION_SECONDS = 5 * 24 * 60 * 60 @@ -49,10 +54,43 @@ end local function decodeHtml(value) local decoded = value:gsub("&", "&") decoded = decoded:gsub(""", '"') - decoded = decoded:gsub("'", "'") + -- Numeric entities generally (covers both ' and ', which NASA's + -- markup uses for apostrophes in captions). + decoded = decoded:gsub("&#(%d+);", function(n) + local code = tonumber(n) + return (code and code < 256) and string.char(code) or "" + end) return decoded end +-- Bing's `copyright` field reads like "Caption text (© Photographer/Source)" — +-- split the trailing parenthesized attribution off into its own field so +-- title and credit can be shown separately. +local function splitCopyright(copyright) + local body, credit = copyright:match("^(.-)%s*%(([^()]*)%)%s*$") + if body and credit then + return decodeHtml(body), decodeHtml(credit) + end + return decodeHtml(copyright), "" +end + +-- Publishes today's caption/credit/"learn more" link for the bar widget, +-- panel, and desktop widget. Independent of whether the image itself still +-- needs downloading — metadata is available as soon as a source resolves. +local function publishToday(selectedSource, date, info) + if type(info) ~= "table" then + return + end + noctalia.state.set("today", { + date = date, + source = selectedSource, + title = info.title or "", + credit = info.credit or "", + link = info.link or "", + }) + noctalia.log(`Daily Wallpaper caption: {info.title or ""}`) +end + local function absoluteNasaUrl(url) if url == nil or url == "" then return "" @@ -170,7 +208,12 @@ local function resolveBing(locale, done) return end - done(`bing-{locale}`, `https://www.bing.com{urlBase}_UHD.jpg`, `https://www.bing.com{urlBase}_1920x1080.jpg`) + local title, credit = splitCopyright(first.copyright or "") + local link = (type(first.copyrightlink) == "string" and first.copyrightlink ~= "") + and first.copyrightlink or "" + + done(`bing-{locale}`, `https://www.bing.com{urlBase}_UHD.jpg`, `https://www.bing.com{urlBase}_1920x1080.jpg`, + { title = title, credit = credit, link = link }) end) if not started then @@ -199,7 +242,16 @@ local function resolveNasa(done) return end - done("nasa", primary, fallback) + -- NASA gives one caption sentence per gallery item and no separate + -- credit line (unlike Bing's "(© ...)" suffix), so title is the whole + -- caption and credit stays empty. The gallery item's own link goes to + -- NASA's full article about the image — a much richer "learn more" + -- target than Bing's quiz page. + local caption = page:match('class="hds%-gallery%-item%-caption[^"]*">([^<]*)') + local link = page:match('class="hds%-gallery%-item%-link[^"]*"%s+href="([^"]+)"') + + done("nasa", primary, fallback, + { title = decodeHtml(caption or ""), credit = "", link = link or "" }) end) if not started then @@ -244,10 +296,22 @@ local function fetchAndApply(force) checking = false cleanupOldWallpapers(dir) applyWallpaper(dest, false) + -- The image was already cached (e.g. from before a restart), but this + -- process may not have captured its caption yet — re-resolve for + -- metadata only, without re-downloading the (already-cached) image. + local function onInfoOnly(_prefix, _primaryUrl, _fallbackUrl, info) + publishToday(selectedSource, date, info) + end + if selectedSource == "nasa" then + resolveNasa(onInfoOnly) + else + resolveBing(locale, onInfoOnly) + end return end - local function onResolved(prefix, primaryUrl, fallbackUrl) + local function onResolved(prefix, primaryUrl, fallbackUrl, info) + publishToday(selectedSource, date, info) local resolvedDest = cachePath(prefix, date) downloadAndApply(primaryUrl, fallbackUrl, resolvedDest) end diff --git a/daily-wallpaper/desktop_widget.luau b/daily-wallpaper/desktop_widget.luau new file mode 100644 index 00000000..fc9a51f7 --- /dev/null +++ b/daily-wallpaper/desktop_widget.luau @@ -0,0 +1,37 @@ +--!nonstrict +-- Desktop widget: a small always-visible card showing today's wallpaper +-- caption and credit. Pure view over daily_wallpaper.luau's "today"/"status" +-- state — static, so no onFrameTick is needed. + +local function tr(key, args) return noctalia.tr(key, args) end + +local WRAP = 220 + +local function render() + local today = noctalia.state.get("today") + local status = noctalia.state.get("status") + + local rows = { + ui.row({ gap = 6, align = "center" }, { + ui.glyph({ name = "photo", size = 16, color = "secondary" }), + ui.label({ text = tr("widget.title"), fontSize = 12, fontWeight = "bold", color = "on_surface_variant" }), + }), + } + + if type(today) == "table" and type(today.title) == "string" and today.title ~= "" then + rows[#rows + 1] = ui.label({ text = today.title, fontSize = 14, fontWeight = "medium", maxWidth = WRAP, maxLines = 4 }) + if today.credit and today.credit ~= "" then + rows[#rows + 1] = ui.label({ text = today.credit, fontSize = 11, opacity = 0.7, maxWidth = WRAP, maxLines = 2 }) + end + else + local msg = (type(status) == "table" and status.state == "error") + and tr("widget.tooltip_error") or tr("widget.tooltip_loading") + rows[#rows + 1] = ui.label({ text = msg, fontSize = 12, opacity = 0.7, maxWidth = WRAP }) + end + + desktopWidget.render(ui.column({ gap = 4, padding = 10 }, rows)) +end + +noctalia.state.watch("today", render) +noctalia.state.watch("status", render) +render() diff --git a/daily-wallpaper/panel.luau b/daily-wallpaper/panel.luau new file mode 100644 index 00000000..24e46b38 --- /dev/null +++ b/daily-wallpaper/panel.luau @@ -0,0 +1,75 @@ +--!nonstrict +-- Daily Wallpaper panel — today's full caption, credit, and a "learn more" +-- link out to the source's own page for it (Bing's quiz page, or NASA's full +-- article). Opens on a bar-widget click, or via +-- `noctalia msg panel-toggle nzlov/daily-wallpaper:panel`. Pure subscriber of +-- daily_wallpaper.luau's "today"/"status" state; the refresh button just asks +-- the service to re-check now (onIpc "refresh"). + +local function tr(key, args) return noctalia.tr(key, args) end + +local WRAP = 380 + +local SOURCE_LABEL = { bing = "Bing", nasa = "NASA" } + +local function shellQuote(s) + return "'" .. s:gsub("'", "'\\''") .. "'" +end + +local function render() + local today = noctalia.state.get("today") + local status = noctalia.state.get("status") + + local rows = { + ui.row({ align = "center", justify = "space_between", gap = 8 }, { + ui.label({ text = tr("panel.title"), fontSize = 16, fontWeight = "bold", flexGrow = 1 }), + ui.button({ glyph = "refresh", variant = "ghost", onClick = "onRefresh" }), + ui.button({ glyph = "close", onClick = "onCloseClicked" }), + }), + ui.separator({}), + } + + if type(today) ~= "table" or type(today.title) ~= "string" or today.title == "" then + local msg = (type(status) == "table" and status.state == "error") + and (status.message or tr("panel.error")) + or tr("panel.loading") + rows[#rows + 1] = ui.label({ text = msg, maxWidth = WRAP, opacity = 0.7 }) + else + rows[#rows + 1] = ui.label({ text = today.title, fontSize = 15, fontWeight = "medium", maxWidth = WRAP }) + if today.credit and today.credit ~= "" then + rows[#rows + 1] = ui.label({ text = today.credit, maxWidth = WRAP, opacity = 0.7 }) + end + local sourceLabel = SOURCE_LABEL[today.source] or today.source or "?" + rows[#rows + 1] = ui.label({ + text = tr("panel.date", { source = sourceLabel, date = today.date }), + maxWidth = WRAP, fontSize = 12, opacity = 0.6, + }) + if today.link and today.link ~= "" then + rows[#rows + 1] = ui.button({ text = tr("panel.learn_more"), variant = "ghost", onClick = "onOpenLink" }) + end + end + + panel.render(ui.column({ padding = 14, gap = 10, flexGrow = 1 }, rows)) +end + +function onOpen(_context) + render() +end + +function onCloseClicked() + panel.close() +end + +function onRefresh() + noctalia.runAsync("noctalia msg plugin 'nzlov/daily-wallpaper:service' all refresh") +end + +function onOpenLink() + local today = noctalia.state.get("today") + if type(today) == "table" and type(today.link) == "string" and today.link ~= "" then + noctalia.runAsync("xdg-open " .. shellQuote(today.link) .. " >/dev/null 2>&1") + end +end + +noctalia.state.watch("today", render) +noctalia.state.watch("status", render) diff --git a/daily-wallpaper/plugin.toml b/daily-wallpaper/plugin.toml index 56ddde89..58174021 100644 --- a/daily-wallpaper/plugin.toml +++ b/daily-wallpaper/plugin.toml @@ -1,13 +1,13 @@ id = "nzlov/daily-wallpaper" name = "Daily Wallpaper" -version = "1.0.0" +version = "1.1.0" plugin_api = 3 author = "nzlov" license = "MIT" -dependencies = [] -tags = ["wallpaper", "desktop"] +dependencies = ["xdg-open"] +tags = ["wallpaper", "desktop", "bar", "panel", "service"] icon = "photo" -description = "Fetches a new daily wallpaper from Bing or NASA." +description = "Fetches a new daily wallpaper from Bing or NASA, and shows its caption on the bar, in a panel, and on the desktop." [[setting]] key = "source" @@ -29,3 +29,39 @@ default = "" [[service]] id = "service" entry = "daily_wallpaper.luau" + +[[widget]] +id = "widget" +entry = "widget.luau" + + [[widget.setting]] + key = "glyph" + type = "glyph" + label_key = "settings.widget.glyph.label" + default = "photo" + + [[widget.setting]] + key = "show_text" + type = "bool" + label_key = "settings.widget.show_text.label" + default = true + + [[widget.setting]] + key = "max_chars" + type = "int" + label_key = "settings.widget.max_chars.label" + default = 28 + min = 8 + max = 80 + +[[panel]] +id = "panel" +entry = "panel.luau" +width = 420 +height = 320 +placement = "attached" +open_near_click = true + +[[desktop_widget]] +id = "desktop" +entry = "desktop_widget.luau" diff --git a/daily-wallpaper/translations/en.json b/daily-wallpaper/translations/en.json index b6e215f2..92ea385e 100644 --- a/daily-wallpaper/translations/en.json +++ b/daily-wallpaper/translations/en.json @@ -14,6 +14,31 @@ "bing": "Bing", "nasa": "NASA" } + }, + "widget": { + "glyph": { + "label": "Glyph" + }, + "show_text": { + "label": "Show caption text next to glyph" + }, + "max_chars": { + "label": "Max caption characters on the bar" + } } + }, + "widget": { + "title": "Daily Wallpaper", + "loading_short": "Loading…", + "tooltip_loading": "Fetching today's wallpaper caption…", + "tooltip_error": "Could not fetch today's wallpaper caption.", + "panel_launch_failed": "Could not open the Daily Wallpaper panel." + }, + "panel": { + "title": "Daily Wallpaper", + "loading": "Fetching today's wallpaper caption…", + "error": "Could not fetch today's wallpaper caption.", + "date": "{source} · {date}", + "learn_more": "Learn more" } } diff --git a/daily-wallpaper/widget.luau b/daily-wallpaper/widget.luau new file mode 100644 index 00000000..c236dc02 --- /dev/null +++ b/daily-wallpaper/widget.luau @@ -0,0 +1,52 @@ +--!nonstrict +-- Bar widget: a glyph (+ optional short caption text) reflecting today's +-- wallpaper. Pure view over daily_wallpaper.luau's "today"/"status" state. +-- Click opens the panel with the full caption, credit, and "learn more" link. + +local function tr(key, args) return noctalia.tr(key, args) end + +local glyph = noctalia.getConfig("glyph") or "photo" +local showText = noctalia.getConfig("show_text") +local maxChars = tonumber(noctalia.getConfig("max_chars")) or 28 + +local today = noctalia.state.get("today") +local status = noctalia.state.get("status") + +local function truncate(text) + if #text <= maxChars then + return text + end + return text:sub(1, maxChars - 1) .. "…" +end + +local function render() + barWidget.setGlyph(glyph) + + if type(today) == "table" and type(today.title) == "string" and today.title ~= "" then + barWidget.setText(showText and truncate(today.title) or "") + barWidget.setTooltip(today.title) + else + barWidget.setText(showText and tr("widget.loading_short") or "") + local msg = (type(status) == "table" and status.state == "error") + and (status.message or tr("widget.tooltip_error")) + or tr("widget.tooltip_loading") + barWidget.setTooltip(msg) + end +end + +noctalia.state.watch("today", function(value) + today = value + render() +end) +noctalia.state.watch("status", function(value) + status = value + render() +end) + +function onClick() + if not noctalia.runAsync("noctalia msg panel-toggle 'nzlov/daily-wallpaper:panel'") then + noctalia.notifyError(tr("widget.title"), tr("widget.panel_launch_failed")) + end +end + +render()