Add package update detection on Content tab (#13807)

This commit is contained in:
rubenwardy 2023-10-28 17:33:44 +01:00 committed by GitHub
parent ddce858c34
commit 4ee32c5441
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 329 additions and 54 deletions

@ -61,6 +61,7 @@ Zughy:
textures/base/pack/cdb_downloading.png
textures/base/pack/cdb_queued.png
textures/base/pack/cdb_update.png
textures/base/pack/cdb_update_cropped.png
textures/base/pack/cdb_viewonline.png
textures/base/pack/settings_btn.png
textures/base/pack/settings_info.png

@ -154,13 +154,17 @@ end
local function tab_header(self, size)
local toadd = ""
for i=1,#self.tablist,1 do
for i = 1, #self.tablist do
if toadd ~= "" then
toadd = toadd .. ","
end
toadd = toadd .. self.tablist[i].caption
local caption = self.tablist[i].caption
if type(caption) == "function" then
caption = caption(self)
end
toadd = toadd .. caption
end
return string.format("tabheader[%f,%f;%f,%f;%s;%s;%i;true;false]",
self.header_x, self.header_y, size.width, size.height, self.name, toadd, self.last_tab_index)

@ -74,15 +74,6 @@ local REASON_UPDATE = "update"
local REASON_DEPENDENCY = "dependency"
-- encodes for use as URL parameter or path component
local function urlencode(str)
return str:gsub("[^%a%d()._~-]", function(char)
return ("%%%02X"):format(char:byte())
end)
end
assert(urlencode("sample text?") == "sample%20text%3F")
local function get_download_url(package, reason)
local base_url = core.settings:get("contentdb_url")
local ret = base_url .. ("/packages/%s/releases/%d/download/"):format(
@ -202,6 +193,10 @@ local function start_install(package, reason)
end
local function queue_download(package, reason)
if package.queued or package.downloading then
return
end
local max_concurrent_downloads = tonumber(core.settings:get("contentdb_max_concurrent_downloads"))
if number_downloading < math.max(max_concurrent_downloads, 1) then
start_install(package, reason)
@ -222,7 +217,7 @@ local function get_raw_dependencies(package)
local url_fmt = "/api/packages/%s/dependencies/?only_hard=1&protocol_version=%s&engine_version=%s"
local version = core.get_version()
local base_url = core.settings:get("contentdb_url")
local url = base_url .. url_fmt:format(package.url_part, core.get_max_supp_proto(), urlencode(version.string))
local url = base_url .. url_fmt:format(package.url_part, core.get_max_supp_proto(), core.urlencode(version.string))
local response = http.fetch_sync({ url = url })
if not response.succeeded then
@ -547,6 +542,9 @@ local function install_or_update_package(this, package)
error("Unknown package type: " .. package.type)
end
if package.queued or package.downloading then
return
end
local function on_confirm()
local deps = get_raw_dependencies(package)
@ -630,17 +628,17 @@ local function get_screenshot(package)
return defaulttexturedir .. "loading_screenshot.png"
end
local function fetch_pkgs(param)
local function fetch_pkgs()
local version = core.get_version()
local base_url = core.settings:get("contentdb_url")
local url = base_url ..
"/api/packages/?type=mod&type=game&type=txp&protocol_version=" ..
core.get_max_supp_proto() .. "&engine_version=" .. param.urlencode(version.string)
core.get_max_supp_proto() .. "&engine_version=" .. core.urlencode(version.string)
for _, item in pairs(core.settings:get("contentdb_flag_blacklist"):split(",")) do
item = item:trim()
if item ~= "" then
url = url .. "&hide=" .. param.urlencode(item)
url = url .. "&hide=" .. core.urlencode(item)
end
end
@ -666,7 +664,7 @@ local function fetch_pkgs(param)
package.id = package.id .. package.name
end
package.url_part = param.urlencode(package.author) .. "/" .. param.urlencode(package.name)
package.url_part = core.urlencode(package.author) .. "/" .. core.urlencode(package.name)
if package.aliases then
for _, alias in ipairs(package.aliases) do
@ -701,7 +699,8 @@ local function resolve_auto_install_spec()
for _, pkg in ipairs(store.packages_full_unordered) do
if pkg.author == auto_install_spec.author and
pkg.name == auto_install_spec.name then
(pkg.name == auto_install_spec.name or
(pkg.type == "game" and pkg.name == auto_install_spec.name .. "_game")) then
resolved = pkg
break
end
@ -752,7 +751,7 @@ function store.load()
store.loading = true
core.handle_async(
fetch_pkgs,
{ urlencode = urlencode },
nil,
function(result)
if result then
store.load_ok = true

@ -0,0 +1,22 @@
--Minetest
--Copyright (C) 2023 rubenwardy
--
--This program is free software; you can redistribute it and/or modify
--it under the terms of the GNU Lesser General Public License as published by
--the Free Software Foundation; either version 2.1 of the License, or
--(at your option) any later version.
--
--This program is distributed in the hope that it will be useful,
--but WITHOUT ANY WARRANTY; without even the implied warranty of
--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
--GNU Lesser General Public License for more details.
--
--You should have received a copy of the GNU Lesser General Public License along
--with this program; if not, write to the Free Software Foundation, Inc.,
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
local path = core.get_mainmenu_path() .. DIR_DELIM .. "content"
dofile(path .. DIR_DELIM .. "pkgmgr.lua")
dofile(path .. DIR_DELIM .. "update_detector.lua")
dofile(path .. DIR_DELIM .. "dlg_contentstore.lua")

@ -177,6 +177,7 @@ function pkgmgr.get_mods(path, virtual_path, listing, modpack)
end
end
--------------------------------------------------------------------------------
function pkgmgr.get_texture_packs()
local txtpath = core.get_texturepath()
local txtpath_system = core.get_texturepath_share()
@ -195,6 +196,23 @@ function pkgmgr.get_texture_packs()
return retval
end
--------------------------------------------------------------------------------
function pkgmgr.get_all()
local result = {}
for _, mod in pairs(pkgmgr.global_mods:get_list()) do
result[#result + 1] = mod
end
for _, game in pairs(pkgmgr.games) do
result[#result + 1] = game
end
for _, txp in pairs(pkgmgr.get_texture_packs()) do
result[#result + 1] = txp
end
return result
end
--------------------------------------------------------------------------------
function pkgmgr.get_folder_type(path)
local testfile = io.open(path .. DIR_DELIM .. "init.lua","r")
@ -260,7 +278,10 @@ function pkgmgr.is_valid_modname(modpath)
end
--------------------------------------------------------------------------------
function pkgmgr.render_packagelist(render_list, use_technical_names, with_error)
--- @param render_list filterlist
--- @param use_technical_names boolean to show technical names instead of human-readable titles
--- @param with_icon table or nil, from virtual path to icon object
function pkgmgr.render_packagelist(render_list, use_technical_names, with_icon)
if not render_list then
if not pkgmgr.global_mods then
pkgmgr.refresh_globals()
@ -273,10 +294,10 @@ function pkgmgr.render_packagelist(render_list, use_technical_names, with_error)
for i, v in ipairs(list) do
local color = ""
local icon = 0
local error = with_error and with_error[v.virtual_path]
local function update_error(val)
if val and (not error or (error.type == "warning" and val.type == "error")) then
error = val
local icon_info = with_icon and with_icon[v.virtual_path or v.path]
local function update_icon_info(val)
if val and (not icon_info or (icon_info.type == "warning" and val.type == "error")) then
icon_info = val
end
end
@ -286,8 +307,8 @@ function pkgmgr.render_packagelist(render_list, use_technical_names, with_error)
for j = 1, #rawlist do
if rawlist[j].modpack == list[i].name then
if with_error then
update_error(with_error[rawlist[j].virtual_path])
if with_icon then
update_icon_info(with_icon[rawlist[j].virtual_path or rawlist[j].path])
end
if rawlist[j].enabled then
@ -303,10 +324,10 @@ function pkgmgr.render_packagelist(render_list, use_technical_names, with_error)
color = mt_color_blue
local rawlist = render_list:get_raw_list()
if v.type == "game" and with_error then
if v.type == "game" and with_icon then
for j = 1, #rawlist do
if rawlist[j].is_game_content then
update_error(with_error[rawlist[j].virtual_path])
update_icon_info(with_icon[rawlist[j].virtual_path or rawlist[j].path])
end
end
end
@ -315,13 +336,17 @@ function pkgmgr.render_packagelist(render_list, use_technical_names, with_error)
color = mt_color_green
end
if error then
if error.type == "warning" then
if icon_info then
if icon_info.type == "warning" then
color = mt_color_orange
icon = 2
else
elseif icon_info.type == "error" then
color = mt_color_red
icon = 3
elseif icon_info.type == "update" then
icon = 4
else
error("Unknown icon type " .. icon_info.type)
end
end
@ -332,7 +357,7 @@ function pkgmgr.render_packagelist(render_list, use_technical_names, with_error)
retval[#retval + 1] = "0"
end
if with_error then
if with_icon then
retval[#retval + 1] = icon
end

@ -57,7 +57,7 @@ local function reset()
end
setfenv(loadfile("builtin/common/misc_helpers.lua"), env)()
setfenv(loadfile("builtin/mainmenu/pkgmgr.lua"), env)()
setfenv(loadfile("builtin/mainmenu/content/pkgmgr.lua"), env)()
function env.pkgmgr.update_gamelist()
table.insert(calls, { "update_gamelist" })

@ -0,0 +1,144 @@
--Minetest
--Copyright (C) 2023 rubenwardy
--
--This program is free software; you can redistribute it and/or modify
--it under the terms of the GNU Lesser General Public License as published by
--the Free Software Foundation; either version 2.1 of the License, or
--(at your option) any later version.
--
--This program is distributed in the hope that it will be useful,
--but WITHOUT ANY WARRANTY; without even the implied warranty of
--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
--GNU Lesser General Public License for more details.
--
--You should have received a copy of the GNU Lesser General Public License along
--with this program; if not, write to the Free Software Foundation, Inc.,
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
update_detector = {}
if not core.get_http_api then
update_detector.get_all = function() return {} end
update_detector.get_count = function() return 0 end
return
end
local has_fetched = false
local latest_releases
local function fetch_latest_releases()
local version = core.get_version()
local base_url = core.settings:get("contentdb_url")
local url = base_url ..
"/api/updates/?type=mod&type=game&type=txp&protocol_version=" ..
core.get_max_supp_proto() .. "&engine_version=" .. core.urlencode(version.string)
local http = core.get_http_api()
local response = http.fetch_sync({ url = url })
if not response.succeeded then
return
end
return core.parse_json(response.data)
end
--- Get a table from package key (author/name) to latest release id
---
--- @param callback function that takes a single argument, table or nil
local function get_latest_releases(callback)
core.handle_async(fetch_latest_releases, nil, callback)
end
local function has_packages_from_cdb()
pkgmgr.refresh_globals()
pkgmgr.update_gamelist()
for _, content in pairs(pkgmgr.get_all()) do
if content.author and content.release > 0 then
return true
end
end
return false
end
--- @returns a new table with all keys lowercase
local function lowercase_keys(tab)
local ret = {}
for key, value in pairs(tab) do
ret[key:lower()] = value
end
return ret
end
local function fetch()
if has_fetched or not has_packages_from_cdb() then
return
end
has_fetched = true
get_latest_releases(function(releases)
if not releases then
has_fetched = false
return
end
latest_releases = lowercase_keys(releases)
if update_detector.get_count() > 0 then
local maintab = ui.find_by_name("maintab")
if not maintab.hidden then
ui.update()
end
end
end)
end
--- @returns a list of content with an update available
function update_detector.get_all()
if latest_releases == nil then
fetch()
return {}
end
pkgmgr.refresh_globals()
pkgmgr.update_gamelist()
local ret = {}
local all_content = pkgmgr.get_all()
for _, content in ipairs(all_content) do
if content.author and content.release > 0 then
-- The backend will account for aliases in `latest_releases`
local id = content.author:lower() .. "/"
if content.type == "game" then
id = id .. content.id
else
id = id .. content.name
end
local latest_release = latest_releases[id]
if not latest_release and content.type == "game" then
latest_release = latest_releases[id .. "_game"]
end
if latest_release and latest_release > content.release then
ret[#ret + 1] = content
end
end
end
return ret
end
--- @return number of packages with updates available
function update_detector.get_count()
return #update_detector.get_all()
end

@ -37,13 +37,12 @@ dofile(basepath .. "fstk" .. DIR_DELIM .. "tabview.lua")
dofile(basepath .. "fstk" .. DIR_DELIM .. "ui.lua")
dofile(menupath .. DIR_DELIM .. "async_event.lua")
dofile(menupath .. DIR_DELIM .. "common.lua")
dofile(menupath .. DIR_DELIM .. "pkgmgr.lua")
dofile(menupath .. DIR_DELIM .. "serverlistmgr.lua")
dofile(menupath .. DIR_DELIM .. "game_theme.lua")
dofile(menupath .. DIR_DELIM .. "content" .. DIR_DELIM .. "init.lua")
dofile(menupath .. DIR_DELIM .. "dlg_config_world.lua")
dofile(menupath .. DIR_DELIM .. "settings" .. DIR_DELIM .. "init.lua")
dofile(menupath .. DIR_DELIM .. "dlg_contentstore.lua")
dofile(menupath .. DIR_DELIM .. "dlg_create_world.lua")
dofile(menupath .. DIR_DELIM .. "dlg_delete_content.lua")
dofile(menupath .. DIR_DELIM .. "dlg_delete_world.lua")

@ -16,6 +16,16 @@
--with this program; if not, write to the Free Software Foundation, Inc.,
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
local function get_content_icons(packages_with_updates)
local ret = {}
for _, content in ipairs(packages_with_updates) do
ret[content.virtual_path or content.path] = { type = "update" }
end
return ret
end
local packages_raw, packages
local function update_packages()
@ -62,14 +72,28 @@ local function get_formspec(tabview, name, tabdata)
local use_technical_names = core.settings:get_bool("show_technical_names")
local packages_with_updates = update_detector.get_all()
local update_icons = get_content_icons(packages_with_updates)
local update_count = #packages_with_updates
local contentdb_label
if update_count == 0 then
contentdb_label = fgettext("Browse online content")
else
contentdb_label = fgettext("Browse online content [$1]", update_count)
end
local retval = {
"label[0.4,0.4;", fgettext("Installed Packages:"), "]",
"tablecolumns[color;tree;text]",
"tablecolumns[color;tree;image,align=inline,width=1.5",
",tooltip=", fgettext("Update available?"),
",0=", core.formspec_escape(defaulttexturedir .. "blank.png"),
",4=", core.formspec_escape(defaulttexturedir .. "cdb_update_cropped.png"),
";text]",
"table[0.4,0.8;6.3,4.8;pkglist;",
pkgmgr.render_packagelist(packages, use_technical_names),
pkgmgr.render_packagelist(packages, use_technical_names, update_icons),
";", tabdata.selected_pkg, "]",
"button[0.4,5.8;6.3,0.9;btn_contentdb;", fgettext("Browse online content"), "]"
"button[0.4,5.8;6.3,0.9;btn_contentdb;", contentdb_label, "]"
}
local selected_pkg
@ -104,15 +128,13 @@ local function get_formspec(tabview, name, tabdata)
core.colorize("#BFBFBF", selected_pkg.name)
end
table.insert_all(retval, {
"image[7.1,0.2;3,2;", core.formspec_escape(modscreenshot), "]",
"label[10.5,1;", core.formspec_escape(title_and_name), "]",
"box[7.1,2.4;8,3.1;#000]"
})
local desc_height = 3.2
if selected_pkg.is_modpack then
desc_height = 2.1
table.insert_all(retval, {
"button[11.1,5.8;4,0.9;btn_mod_mgr_rename_modpack;",
"button[7.1,4.7;8,0.9;btn_mod_mgr_rename_modpack;",
fgettext("Rename"), "]"
})
elseif selected_pkg.type == "mod" then
@ -136,25 +158,39 @@ local function get_formspec(tabview, name, tabdata)
end
end
elseif selected_pkg.type == "txp" then
desc_height = 2.1
if selected_pkg.enabled then
table.insert_all(retval, {
"button[11.1,5.8;4,0.9;btn_mod_mgr_disable_txp;",
"button[7.1,4.7;8,0.9;btn_mod_mgr_disable_txp;",
fgettext("Disable Texture Pack"), "]"
})
else
table.insert_all(retval, {
"button[11.1,5.8;4,0.9;btn_mod_mgr_use_txp;",
"button[7.1,4.7;8,0.9;btn_mod_mgr_use_txp;",
fgettext("Use Texture Pack"), "]"
})
end
end
table.insert_all(retval, {"textarea[7.1,2.4;8,3.1;;;", desc, "]"})
table.insert_all(retval, {
"image[7.1,0.2;3,2;", core.formspec_escape(modscreenshot), "]",
"label[10.5,1;", core.formspec_escape(title_and_name), "]",
"box[7.1,2.4;8,", tostring(desc_height), ";#000]",
"textarea[7.1,2.4;8,", tostring(desc_height), ";;;", desc, "]",
})
if core.may_modify_path(selected_pkg.path) then
table.insert_all(retval, {
"button[7.1,5.8;4,0.9;btn_mod_mgr_delete_mod;",
fgettext("Uninstall Package"), "]"
fgettext("Uninstall"), "]"
})
end
if update_icons[selected_pkg.virtual_path or selected_pkg.path] then
table.insert_all(retval, {
"button[11.1,5.8;4,0.9;btn_mod_mgr_update;",
fgettext("Update"), "]"
})
end
end
@ -216,6 +252,16 @@ local function handle_buttons(tabview, fields, tabname, tabdata)
return true
end
if fields.btn_mod_mgr_update then
local pkg = packages:get_list()[tabdata.selected_pkg]
local dlg = create_store_dlg(nil, { author = pkg.author, name = pkg.id or pkg.name })
dlg:set_parent(tabview)
tabview:hide()
dlg:show()
packages = nil
return true
end
if fields.btn_mod_mgr_use_txp or fields.btn_mod_mgr_disable_txp then
local txp_path = ""
if fields.btn_mod_mgr_use_txp then
@ -235,7 +281,14 @@ end
return {
name = "content",
caption = fgettext("Content"),
caption = function()
local update_count = update_detector.get_count()
if update_count == 0 then
return fgettext("Content")
else
return fgettext("Content [$1]", update_count)
end
end,
cbf_formspec = get_formspec,
cbf_button_handler = handle_buttons,
on_change = on_change

@ -672,6 +672,9 @@ Minetest namespace reference
* If a flag in this table is set to true, the feature is RESTRICTED.
* Possible flags: `load_client_mods`, `chat_messages`, `read_itemdefs`,
`read_nodedefs`, `lookup_nodes`, `read_playerinfo`
* `minetest.urlencode(str)`: Encodes non-unreserved URI characters by a
percent sign followed by two hex digits. See
[RFC 3986, section 2.3](https://datatracker.ietf.org/doc/html/rfc3986#section-2.3).
### Logging
* `minetest.debug(...)`
@ -1551,4 +1554,4 @@ Same as `image`, but does not accept a `position`; the position is instead deter
texture = "image.png",
-- ^ Uses texture (string)
}
```
```

@ -50,7 +50,7 @@ methods:
^ tab:
{
name = "tabname", -- name of tab to create
caption = "tab caption", -- text to show for tab header
caption = "tab caption", -- text to show for tab header. Either a string or a function: (tabview) -> string
cbf_button_handler = function(tabview, fields, tabname, tabdata), -- callback for button events
--TODO cbf_events = function(tabview, event, tabname), -- callback for events
cbf_formspec = function(tabview, name, tabdata), -- get formspec

@ -5406,6 +5406,9 @@ Utilities
use `colorspec_to_bytes` to generate raw RGBA values in a predictable way.
The resulting PNG image is always 32-bit. Palettes are not supported at the moment.
You may use this to procedurally generate textures during server init.
* `minetest.urlencode(str)`: Encodes non-unreserved URI characters by a
percent sign followed by two hex digits. See
[RFC 3986, section 2.3](https://datatracker.ietf.org/doc/html/rfc3986#section-2.3).
Logging
-------

@ -101,7 +101,7 @@ HTTP Requests
* `core.download_file(url, target)` (possible in async calls)
* `url` to download, and `target` to store to
* returns true/false
* `minetest.get_http_api()` (possible in async calls)
* `core.get_http_api()` (possible in async calls)
* returns `HTTPApiTable` containing http functions.
* The returned table contains the functions `fetch_sync`, `fetch_async` and
`fetch_async_get` described below.
@ -394,10 +394,13 @@ Helpers
* eg. `string.trim("\n \t\tfoo bar\t ")` == `"foo bar"`
* `core.is_yes(arg)` (possible in async calls)
* returns whether `arg` can be interpreted as yes
* `minetest.encode_base64(string)` (possible in async calls)
* `core.encode_base64(string)` (possible in async calls)
* Encodes a string in base64.
* `minetest.decode_base64(string)` (possible in async calls)
* `core.decode_base64(string)` (possible in async calls)
* Decodes a string encoded in base64.
* `core.urlencode(str)`: Encodes non-unreserved URI characters by a
percent sign followed by two hex digits. See
[RFC 3986, section 2.3](https://datatracker.ietf.org/doc/html/rfc3986#section-2.3).
Async

@ -651,6 +651,16 @@ int ModApiUtil::l_set_last_run_mod(lua_State *L)
return 0;
}
// urlencode(value)
int ModApiUtil::l_urlencode(lua_State *L)
{
NO_MAP_LOCK_REQUIRED;
const char *value = luaL_checkstring(L, 1);
lua_pushstring(L, urlencode(value).c_str());
return 1;
}
void ModApiUtil::Initialize(lua_State *L, int top)
{
API_FCT(log);
@ -697,6 +707,8 @@ void ModApiUtil::Initialize(lua_State *L, int top)
API_FCT(get_last_run_mod);
API_FCT(set_last_run_mod);
API_FCT(urlencode);
LuaSettings::create(L, g_settings, g_settings_path);
lua_setfield(L, top, "settings");
}
@ -723,6 +735,8 @@ void ModApiUtil::InitializeClient(lua_State *L, int top)
API_FCT(colorspec_to_colorstring);
API_FCT(colorspec_to_bytes);
API_FCT(urlencode);
LuaSettings::create(L, g_settings, g_settings_path);
lua_setfield(L, top, "settings");
}
@ -766,6 +780,8 @@ void ModApiUtil::InitializeAsync(lua_State *L, int top)
API_FCT(get_last_run_mod);
API_FCT(set_last_run_mod);
API_FCT(urlencode);
LuaSettings::create(L, g_settings, g_settings_path);
lua_setfield(L, top, "settings");
}

@ -128,6 +128,9 @@ private:
// set_last_run_mod(modname)
static int l_set_last_run_mod(lua_State *L);
// urlencode(value)
static int l_urlencode(lua_State *L);
public:
static void Initialize(lua_State *L, int top);
static void InitializeAsync(lua_State *L, int top);

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB