Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 31 additions & 3 deletions daily-wallpaper/README.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -19,16 +24,39 @@ 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

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.
74 changes: 69 additions & 5 deletions daily-wallpaper/daily_wallpaper.luau
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 ""
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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[^"]*">([^<]*)</div>')
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
Expand Down Expand Up @@ -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
Expand Down
37 changes: 37 additions & 0 deletions daily-wallpaper/desktop_widget.luau
Original file line number Diff line number Diff line change
@@ -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()
75 changes: 75 additions & 0 deletions daily-wallpaper/panel.luau
Original file line number Diff line number Diff line change
@@ -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)
44 changes: 40 additions & 4 deletions daily-wallpaper/plugin.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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"
25 changes: 25 additions & 0 deletions daily-wallpaper/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
Loading
Loading