mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-05 17:03:12 -04:00
launcher auto index
This commit is contained in:
@@ -672,12 +672,10 @@ function SaveData.defaultOptions()
|
||||
-- GitHub release checks for mods with a manifest "github" field
|
||||
-- (src/mods/ModUpdate.lua). Keyed by owner/repo; TTL is six hours.
|
||||
modUpdateCache = {},
|
||||
-- Community mod indexes the player has chosen to browse
|
||||
-- (src/mods/ModIndex.lua), in the order they added them. Empty by
|
||||
-- default and never populated automatically: adding an index is how a
|
||||
-- player says they trust whoever publishes it, so the launcher asks
|
||||
-- rather than shipping one. Rows are { url, feed, base, fallback,
|
||||
-- label }.
|
||||
-- Player-added mod indexes, in their chosen order. ModIndex.sources()
|
||||
-- includes the permanent main index alongside these and reuses any
|
||||
-- main-index row saved by an older launcher. Rows are
|
||||
-- { url, feed, base, fallback, label }.
|
||||
modIndexes = {},
|
||||
-- Parsed index listings keyed by feed URL; TTL is 24 hours, matching how
|
||||
-- often the feeds themselves rebuild.
|
||||
|
||||
@@ -5005,10 +5005,11 @@ local function buildFilterModal(imp, m)
|
||||
action = function() imp._filterPopup = nil end })
|
||||
end
|
||||
|
||||
-- Index manager: every source with its Remove, plus Add and Refresh all.
|
||||
-- Index manager: built-in source and removable additions, plus Add and Refresh all.
|
||||
-- This replaces both the old always-visible source rows above the search
|
||||
-- field and the lone "Add index" header button.
|
||||
local function buildIndexesModal(imp, m)
|
||||
local ModIndex = require("src.mods.ModIndex")
|
||||
local sources = imp.findSources or {}
|
||||
local pad = math.floor(18 * m.s)
|
||||
local w = math.floor(520 * m.s)
|
||||
@@ -5031,17 +5032,24 @@ local function buildIndexesModal(imp, m)
|
||||
else
|
||||
for i, source in ipairs(sources) do
|
||||
local feed = source.feed
|
||||
local builtIn = ModIndex.isBuiltIn(feed)
|
||||
local actionLabel = builtIn and Strings("Built-in") or Strings("Remove")
|
||||
local id = "idx-rm-" .. tostring(feed)
|
||||
local cy = place(id, (i - 1) * (rowH + gap), rowH)
|
||||
local rmW = Kit.textWidth("small", Strings("Remove"))
|
||||
local rmW = Kit.textWidth("small", actionLabel)
|
||||
+ math.floor(20 * m.s)
|
||||
Kit.text("small", Kit.ellipsize("small", source.label or feed,
|
||||
rw - rmW - math.floor(12 * m.s)), x,
|
||||
cy + (rowH - Kit.textHeight("small")) / 2, PAL.detail)
|
||||
btn(imp, x + rw - rmW, cy, rmW, rowH,
|
||||
id, Strings("Remove"), {
|
||||
kind = "danger", font = "small",
|
||||
action = function() imp:_removeIndex(feed) end })
|
||||
if builtIn then
|
||||
Kit.text("small", actionLabel, x + rw - rmW,
|
||||
cy + (rowH - Kit.textHeight("small")) / 2, PAL.muted)
|
||||
else
|
||||
btn(imp, x + rw - rmW, cy, rmW, rowH,
|
||||
id, actionLabel, {
|
||||
kind = "danger", font = "small",
|
||||
action = function() imp:_removeIndex(feed) end })
|
||||
end
|
||||
end
|
||||
end
|
||||
btn(imp, x, place("idx-add", addY, m.btnH), rw, m.btnH, "idx-add",
|
||||
|
||||
@@ -8032,10 +8032,8 @@ end
|
||||
--
|
||||
-- The index is metadata only (src/mods/ModIndex.lua): it says where a mod's
|
||||
-- zip lives, and the install runs through exactly the same path "Import mod
|
||||
-- .zip" does. Nothing here is automatic -- no index ships with the launcher,
|
||||
-- and the tab stays an empty "Add an index" prompt until the player names one,
|
||||
-- because subscribing to somebody's list of mods is a trust decision and not a
|
||||
-- default.
|
||||
-- .zip" does. The main index is included by default; players can add other
|
||||
-- indexes alongside it. Installing a listed mod remains an explicit action.
|
||||
--
|
||||
-- Fetching is the same synchronous curl the update checks already use, cached
|
||||
-- in options for a day, so the first open of the tab costs one round trip and
|
||||
@@ -8556,9 +8554,7 @@ function RomImporter:_pumpFindStats()
|
||||
if next(pending) == nil then self._findStatsPending = nil end
|
||||
end
|
||||
|
||||
-- Open the "add an index" text prompt. Deliberately a typed URL rather than a
|
||||
-- picked-from-a-list affair: there is no blessed index, and presenting one
|
||||
-- would make the launcher's choice look like an endorsement.
|
||||
-- Open the text prompt for an additional index URL.
|
||||
function RomImporter:_promptAddIndex()
|
||||
self._indexPrompt = { text = "" }
|
||||
self:_armTextInput()
|
||||
|
||||
+26
-9
@@ -11,9 +11,8 @@
|
||||
-- no network) so the engine tier can table-drive it, and the fetch/cache half
|
||||
-- reaches for curl and options.lua.
|
||||
--
|
||||
-- Sources are never added automatically. options.modIndexes is a player-built
|
||||
-- list -- adding an index is a deliberate act of trusting whoever publishes it,
|
||||
-- so the launcher ships with none and asks.
|
||||
-- The main index is built in and cannot be removed. options.modIndexes keeps
|
||||
-- player-added sources, including main-index rows saved by older launchers.
|
||||
--
|
||||
-- schema_version is a hard gate, not a hint: a bumped feed may reuse a field
|
||||
-- name for something else, so an unknown version is refused outright rather
|
||||
@@ -637,18 +636,34 @@ local function loadOptions()
|
||||
return require("src.core.SaveData").loadOptions()
|
||||
end
|
||||
|
||||
-- The player's index list, normalised. Rows are { url, feed, base, fallback,
|
||||
local BUILTIN = ModIndex.resolveSource("bryanthaboi/gen1recomp-mod-index")
|
||||
BUILTIN.url = "bryanthaboi/gen1recomp-mod-index"
|
||||
|
||||
function ModIndex.isBuiltIn(feed)
|
||||
return feed == BUILTIN.feed
|
||||
end
|
||||
|
||||
-- The player's index list plus the built-in source. Rows are { url, feed, base, fallback,
|
||||
-- label }; `url` is what they typed, kept so the row reads back the way they
|
||||
-- entered it.
|
||||
-- entered it. Keep an existing main-index row in place to preserve source
|
||||
-- precedence and its cache; otherwise append it without rewriting options.
|
||||
function ModIndex.sources()
|
||||
local ok, opts = pcall(loadOptions)
|
||||
if not ok or type(opts) ~= "table" then return {} end
|
||||
local out = {}
|
||||
for _, row in ipairs(opts.modIndexes or {}) do
|
||||
local saved = ok and type(opts) == "table" and opts.modIndexes or {}
|
||||
local out, hasBuiltin = {}, false
|
||||
for _, row in ipairs(type(saved) == "table" and saved or {}) do
|
||||
if type(row) == "table" and type(row.feed) == "string" then
|
||||
out[#out + 1] = row
|
||||
if not ModIndex.isBuiltIn(row.feed) or not hasBuiltin then
|
||||
out[#out + 1] = row
|
||||
end
|
||||
if ModIndex.isBuiltIn(row.feed) then hasBuiltin = true end
|
||||
end
|
||||
end
|
||||
if not hasBuiltin then
|
||||
local row = {}
|
||||
for k, v in pairs(BUILTIN) do row[k] = v end
|
||||
out[#out + 1] = row
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
@@ -657,6 +672,7 @@ end
|
||||
function ModIndex.addSource(input)
|
||||
local source, err = ModIndex.resolveSource(input)
|
||||
if not source then return nil, err end
|
||||
if ModIndex.isBuiltIn(source.feed) then return nil, "that index is already added" end
|
||||
local ok, result, addErr = pcall(function()
|
||||
local SaveData = require("src.core.SaveData")
|
||||
local opts = loadOptions()
|
||||
@@ -680,6 +696,7 @@ end
|
||||
-- outlives the index it came from.
|
||||
function ModIndex.removeSource(feed)
|
||||
if type(feed) ~= "string" or feed == "" then return nil, "missing index" end
|
||||
if ModIndex.isBuiltIn(feed) then return nil, "the built-in index cannot be removed" end
|
||||
local ok, result = pcall(function()
|
||||
local SaveData = require("src.core.SaveData")
|
||||
local opts = loadOptions()
|
||||
|
||||
@@ -1229,6 +1229,7 @@ check(find.findBase == nil, "with no base-game filter armed")
|
||||
find.tab = "find"
|
||||
find.modScope = nil
|
||||
find.findLoaded = true
|
||||
find._findFetch = nil
|
||||
find.findSources = { { feed = "https://example.test/data/index.json",
|
||||
base = "https://example.test/",
|
||||
label = "example/index" } }
|
||||
|
||||
@@ -8,6 +8,7 @@ local View = require("src.import.LauncherView")
|
||||
local Transition = require("src.ui.kit.Transition")
|
||||
local Importer = require("src.import.RomImporter")
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
local ModIndex = require("src.mods.ModIndex")
|
||||
|
||||
local width, height = 853, 480
|
||||
love.graphics.getDimensions = function() return width, height end
|
||||
@@ -23,6 +24,7 @@ local function fixture(tab)
|
||||
local imp = Importer.new(function() end, { launcher = true, onEditSave = function() end })
|
||||
for _, v in ipairs(GameVersion.ORDER) do imp.ready[v] = true end
|
||||
imp._ensureSlots, imp._ensureMods = function() end, function() end
|
||||
imp._ensureFind = function() end
|
||||
imp._refreshMods = function() end
|
||||
imp._resetModOrder = function(self) self.didReset = true end
|
||||
imp.mods = {}
|
||||
@@ -44,6 +46,7 @@ local MODALS = {
|
||||
open = function(imp)
|
||||
imp.findSources = {}
|
||||
for i = 1, 6 do imp.findSources[i] = { feed = "https://x/" .. i, label = "Index " .. i } end
|
||||
imp.findSources[1] = ModIndex.resolveSource("bryanthaboi/gen1recomp-mod-index")
|
||||
imp._indexManage = true
|
||||
end },
|
||||
}
|
||||
@@ -85,7 +88,9 @@ for _, size in ipairs({ { 853, 480 }, { 780, 360 }, { 1280, 720 } }) do
|
||||
local state = imp._modalScroll and imp._modalScroll[spec.key]
|
||||
T.check(state ~= nil and state.rect ~= nil, tag .. " records a scroll rect")
|
||||
local closeSeen, escaped = false, {}
|
||||
local removes = 0
|
||||
for _, r in ipairs(rects) do
|
||||
if r.label == "Remove" then removes = removes + 1 end
|
||||
if r.clip then
|
||||
if not inside(r.clip, width, height) then escaped[#escaped + 1] = r.label .. " clip" end
|
||||
if state and not within(r.clip, state.rect) then escaped[#escaped + 1] = r.label .. " clip outside body" end
|
||||
@@ -98,6 +103,9 @@ for _, size in ipairs({ { 853, 480 }, { 780, 360 }, { 1280, 720 } }) do
|
||||
end
|
||||
end
|
||||
T.check(closeSeen, tag .. " draws a pinned Close")
|
||||
if spec.key == "_indexManage" then
|
||||
T.eq(removes, 5, tag .. " only offers Remove for the five custom indexes")
|
||||
end
|
||||
T.check(#escaped == 0, tag .. " keeps every control on screen or in the scroll clip: "
|
||||
.. table.concat(escaped, ", "))
|
||||
if state and state.rect then
|
||||
|
||||
@@ -108,6 +108,7 @@ local function launcher(secondStatus, feedCarts)
|
||||
findIndex = { mods = {}, carts = feedCarts or {} },
|
||||
}, RomImporter)
|
||||
ri._refreshMods = function() end
|
||||
ri._refreshFindSources = function(self) self.findSources = {} end
|
||||
return ri
|
||||
end
|
||||
|
||||
|
||||
@@ -48,6 +48,66 @@ do
|
||||
check(bad == nil and err ~= nil, "nil input soft-fails")
|
||||
end
|
||||
|
||||
-- ------- permanent main source, including options saved before it was built in
|
||||
|
||||
do
|
||||
local oldSaveData = package.loaded["src.core.SaveData"]
|
||||
local opts, writes = {}, 0
|
||||
package.loaded["src.core.SaveData"] = {
|
||||
loadOptions = function() return opts end,
|
||||
saveOptions = function(saved) opts = saved; writes = writes + 1; return saved end,
|
||||
}
|
||||
local main = ModIndex.resolveSource("bryanthaboi/gen1recomp-mod-index")
|
||||
local custom = ModIndex.resolveSource("other/community-index")
|
||||
local cached = { checkedAt = 123, mods = { { id = "existing" } } }
|
||||
|
||||
local sources = ModIndex.sources()
|
||||
eq(#sources, 1, "fresh options include the main index")
|
||||
eq(sources[1].feed, main.feed, "the default uses the main feed")
|
||||
eq(sources[1].fallback, main.fallback, "the default includes the raw fallback")
|
||||
sources[1].feed = "changed by caller"
|
||||
eq(ModIndex.sources()[1].feed, main.feed, "callers cannot alter the built-in definition")
|
||||
eq(writes, 0, "reading sources does not rewrite options")
|
||||
|
||||
for _, url in ipairs({ "bryanthaboi/gen1recomp-mod-index",
|
||||
"https://github.com/bryanthaboi/gen1recomp-mod-index",
|
||||
main.base, main.base:sub(1, -2), main.feed }) do
|
||||
local savedMain = ModIndex.resolveSource(url)
|
||||
savedMain.url = url
|
||||
opts = { modIndexes = { custom, savedMain, main },
|
||||
modIndexCache = { [main.feed] = cached } }
|
||||
sources = ModIndex.sources()
|
||||
eq(#sources, 2, "an already-added main index is listed once: " .. url)
|
||||
eq(sources[1], custom, "the existing source precedence is preserved")
|
||||
eq(sources[2], savedMain, "the first saved main-index row is reused")
|
||||
eq(sources[2].url, url, "the player's original URL is preserved")
|
||||
eq(ModIndex.readCache(main.feed), cached, "the existing main-index cache survives")
|
||||
local added, addErr = ModIndex.addSource(url)
|
||||
check(added == nil and addErr ~= nil, "the built-in source cannot be added again")
|
||||
local removed, removeErr = ModIndex.removeSource(main.feed)
|
||||
check(removed == nil and removeErr ~= nil, "an older main-index row cannot be removed")
|
||||
eq(opts.modIndexCache[main.feed], cached, "blocked removal keeps its cache")
|
||||
end
|
||||
eq(writes, 0, "duplicate additions and blocked removals do not write options")
|
||||
|
||||
opts = { modIndexes = { custom }, modIndexCache = { [custom.feed] = cached } }
|
||||
sources = ModIndex.sources()
|
||||
eq(#sources, 2, "existing custom-only options gain the default")
|
||||
eq(sources[1], custom, "adding the default preserves custom-source precedence")
|
||||
eq(sources[2].feed, main.feed, "the missing main source is appended")
|
||||
check(ModIndex.isBuiltIn(main.feed), "the main index is protected")
|
||||
check(not ModIndex.isBuiltIn(custom.feed), "a custom index remains removable")
|
||||
check(ModIndex.removeSource(custom.feed), "a custom source can still be removed")
|
||||
eq(opts.modIndexCache[custom.feed], nil, "custom-source removal clears its cache")
|
||||
eq(#ModIndex.sources(), 1, "removing the last custom source leaves the main index")
|
||||
check(ModIndex.addSource("other/community-index") ~= nil, "custom sources can still be added")
|
||||
eq(#ModIndex.sources(), 2, "the added custom source appears beside the main index")
|
||||
|
||||
package.loaded["src.core.SaveData"].loadOptions = function() error("unavailable options") end
|
||||
eq(ModIndex.sources()[1].feed, main.feed, "the main index remains available if options cannot load")
|
||||
package.loaded["src.core.SaveData"] = oldSaveData
|
||||
end
|
||||
|
||||
do
|
||||
local base = "https://bryanthaboi.github.io/gen1recomp-mod-index/"
|
||||
eq(ModIndex.joinUrl(base, "data/mods/bryanthaboi@nuzlocke/thumbnail.png"),
|
||||
@@ -351,6 +411,20 @@ end
|
||||
|
||||
-- ------- search / filter
|
||||
|
||||
do
|
||||
local index = ModIndex.parse(feed({
|
||||
{ id = "untamed_advanced", title = "Untamed Advanced",
|
||||
games = { "firered", "leafgreen", "emerald" }, categories = { "GAMEPLAY" } },
|
||||
}))
|
||||
for _, game in ipairs({ "gen3", "firered", "leafgreen", "emerald" }) do
|
||||
local rows = ModIndex.filter(index.mods, { game = game, query = "untamed" })
|
||||
eq(#rows, 1, "Untamed Advanced appears when filtering by " .. game)
|
||||
eq(rows[1] and rows[1].id, "untamed_advanced", "the filtered listing is Untamed Advanced")
|
||||
end
|
||||
eq(#ModIndex.filter(index.mods, { game = "gen2", query = "untamed" }), 0,
|
||||
"Untamed Advanced remains excluded from unsupported generations")
|
||||
end
|
||||
|
||||
do
|
||||
local mods = {
|
||||
{ id = "nuzlocke", title = "Nuzlocke", author = "bryanthaboi",
|
||||
@@ -613,4 +687,4 @@ do
|
||||
"a cartless feed offers no base games")
|
||||
end
|
||||
|
||||
print("ok mod_index_tests")
|
||||
T.finish("mod index")
|
||||
|
||||
Reference in New Issue
Block a user