diff --git a/docs/new-features.md b/docs/new-features.md index c3e6d30f..70d15d2a 100644 --- a/docs/new-features.md +++ b/docs/new-features.md @@ -458,6 +458,13 @@ automatically, so the tab opens on an "Add an index" prompt until you name one; paste an index URL or its `owner/repo` and it is remembered in `options.lua`. More than one index can be added, and the listings merge. +A feed author can publish per-mod release stats by adding three optional +fields to an entry -- `downloads` (total across every release), and +`first_release` / `last_release` (ISO days) -- which the listing shows in +the same gold line the MODS tab uses. The fields are additive: feeds that +carry them stay readable by every build that predates them, and feeds that +do not render exactly as before. + ## Soft reset (all versions) Holding A, B, START and SELECT together restarts the game the way flicking diff --git a/src/import/LauncherView.lua b/src/import/LauncherView.lua index 94506d86..3f92176b 100644 --- a/src/import/LauncherView.lua +++ b/src/import/LauncherView.lua @@ -1167,13 +1167,9 @@ local function buildModsPanel(imp, parent, m) -- counts, so a pre-downloads cache entry costs the line, not a wrong "0". local dlLine if info and info.downloads then - local formatted = ModUpdate.formatCount(info.downloads.total) - if info.dates then - dlLine = Strings("%s downloads across all releases - Released %s - Updated %s", - formatted, info.dates.first, info.dates.latest) - else - dlLine = Strings("%s downloads across all releases", formatted) - end + local d = info.dates + dlLine = ModUpdate.statsLine(info.downloads.total, + d and d.first, d and d.latest) end -- measure the body: name (with the badge beside it only when it fits), @@ -1441,9 +1437,17 @@ local function buildFindPanel(imp, parent, m) local btnH = math.ceil(textHeight(chipSize)) + 14 for _, entry in ipairs(rows) do local action, note = findActionFor(entry, installed[entry.id]) + -- Feed-published release stats (downloads, first/last release date) in + -- the same gold line the MODS tab uses; absent until a feed carries them. + local statsLine + if entry.downloads ~= nil or entry.first_release or entry.last_release then + statsLine = ModUpdate.statsLine(entry.downloads, + entry.first_release, entry.last_release) + end local bodyH = math.ceil(textHeight(titleSize)) + 4 + math.ceil(textHeight(smallSize)) + if statsLine then bodyH = bodyH + 4 + wrapHeight(smallSize, statsLine, bodyW) end if note then bodyH = bodyH + 4 + wrapHeight(smallSize, note, bodyW) end if entry.summary and entry.summary ~= "" then bodyH = bodyH + 4 + wrapHeight(smallSize, entry.summary, bodyW) @@ -1488,6 +1492,9 @@ local function buildFindPanel(imp, parent, m) end label(body, meta, smallSize, C("detail"), { width = "100%", textWrap = false, textOverflow = "ellipsis" }) + if statsLine then + label(body, statsLine, smallSize, C("gold"), { width = "100%" }) + end if note then label(body, note, smallSize, C("green"), { width = "100%" }) end if entry.summary and entry.summary ~= "" then label(body, entry.summary, smallSize, C("detail"), { width = "100%" }) diff --git a/src/mods/ModIndex.lua b/src/mods/ModIndex.lua index 481669cc..9bb71542 100644 --- a/src/mods/ModIndex.lua +++ b/src/mods/ModIndex.lua @@ -174,6 +174,13 @@ local function parseEntry(raw) conflicts = raw.conflicts, thumbnail = str(raw.thumbnail), description_url = str(raw.description_url), + -- Optional release stats a feed author can publish: total downloads + -- across all releases plus first/last release dates. Additive-only, + -- so a feed that carries them stays readable by every build that + -- predates them (and one that does not still renders fine here). + downloads = tonumber(raw.downloads), + first_release = str(raw.first_release), + last_release = str(raw.last_release), latest = parseLatest(raw.latest), update_check = str(raw.update_check) or "pending", } diff --git a/src/mods/ModUpdate.lua b/src/mods/ModUpdate.lua index ff872232..774e1a6c 100644 --- a/src/mods/ModUpdate.lua +++ b/src/mods/ModUpdate.lua @@ -7,6 +7,8 @@ local ModUpdate = {} ModUpdate.CACHE_TTL = 6 * 60 * 60 -- six hours +local Strings = require("src.core.Strings") + local function stripV(tag) return (tostring(tag):gsub("^[vV]", "")) end @@ -237,6 +239,24 @@ function ModUpdate.formatCount(n) return (s:gsub("^,", "")) end +-- The one-line stats string both launcher panels show: "1,234 downloads +-- across all releases - Released 2024-05-31 - Updated 2026-07-01". +-- Each part is optional; nil everywhere means nil, so a row with no data +-- shows no line rather than a wrong "0". +function ModUpdate.statsLine(total, first, latest) + local parts = {} + if total ~= nil then + parts[#parts + 1] = Strings("%s downloads across all releases", + ModUpdate.formatCount(total)) + end + if first or latest then + parts[#parts + 1] = Strings("Released %s - Updated %s", + first or "?", latest or "?") + end + if #parts == 0 then return nil end + return table.concat(parts, " - ") +end + -- ------- cache (options.modUpdateCache[repo]) local function cacheStore() diff --git a/tests/engine/mod_index_tests.lua b/tests/engine/mod_index_tests.lua index b4a6174f..82076453 100644 --- a/tests/engine/mod_index_tests.lua +++ b/tests/engine/mod_index_tests.lua @@ -110,6 +110,27 @@ do "the release asset URL survives parsing") eq(m.permissions[1], "engine_internals", "permissions are kept") eq(m.update_check, "ok", "update_check is kept") + check(m.downloads == nil and m.first_release == nil and m.last_release == nil, + "a feed without release stats parses them as absent") +end + +-- release stats a feed can publish: total downloads and first/last dates +-- ride along additively, so a feed carrying them stays readable by every +-- build that predates them +do + local withStats = {} + for k, v in pairs(NUZLOCKE) do withStats[k] = v end + withStats.downloads = 1234 + withStats.first_release = "2024-05-31" + withStats.last_release = "2026-07-01" + local index = ModIndex.parse(feed({ withStats })) + local m = index.mods[1] + eq(m.downloads, 1234, "total downloads are kept") + eq(m.first_release, "2024-05-31", "first release date is kept") + eq(m.last_release, "2026-07-01", "last release date is kept") + withStats.downloads = "9999" + m = ModIndex.parse(feed({ withStats })).mods[1] + eq(m.downloads, 9999, "numeric-string downloads are coerced") end -- schema_version is a contract, not a hint: an unknown one is refused rather diff --git a/tests/engine/mod_update_tests.lua b/tests/engine/mod_update_tests.lua index ae7c2c00..1e3bb9c3 100644 --- a/tests/engine/mod_update_tests.lua +++ b/tests/engine/mod_update_tests.lua @@ -183,6 +183,22 @@ eq(ModUpdate.formatCount("12345"), "12,345", "numeric strings are accepted") eq(ModUpdate.formatCount(nil), "0", "nil formats as zero") eq(ModUpdate.formatCount("garbage"), "0", "garbage formats as zero") +-- statsLine: the shared launcher row line, part by part +eq(ModUpdate.statsLine(1234567, "2024-05-31", "2026-07-01"), + "1,234,567 downloads across all releases - Released 2024-05-31 - Updated 2026-07-01", + "full stats line") +eq(ModUpdate.statsLine(82, nil, nil), + "82 downloads across all releases", + "downloads alone") +eq(ModUpdate.statsLine(nil, "2024-05-31", "2026-07-01"), + "Released 2024-05-31 - Updated 2026-07-01", + "dates alone") +eq(ModUpdate.statsLine(0, nil, nil), + "0 downloads across all releases", + "a real zero still shows") +check(ModUpdate.statsLine(nil, nil, nil) == nil, + "no data at all means no line") + -- cacheUsable: a cache entry from before downloads existed must not be -- trusted, everything current is do