ContentDB redesign: Add package dialog

This commit is contained in:
rubenwardy 2024-03-31 19:24:27 +01:00
parent 3017b0213b
commit 6ade0ee2ad
10 changed files with 501 additions and 82 deletions

@ -262,6 +262,16 @@ function core.formspec_escape(text)
end
local hypertext_escapes = {
["\\"] = "\\\\",
["<"] = "\\<",
[">"] = "\\>",
}
function core.hypertext_escape(text)
return text and text:gsub("[\\<>]", hypertext_escapes)
end
function core.wrap_text(text, max_length, as_table)
local result = {}
local line = {}

@ -179,6 +179,22 @@ function contentdb.get_package_by_id(id)
end
function contentdb.get_package_by_info(author, name)
local id = author:lower() .. "/" .. name
local package = contentdb.package_by_id[id]
if package then
return package
end
local name_len = #name
if name_len > 5 and name:sub(name_len - 4) == "_game" then
id = author:lower() .. "/" .. name:sub(1, name_len - 5)
return contentdb.package_by_id[id]
end
return nil
end
local function get_raw_dependencies(package)
if package.type ~= "mod" then
return {}
@ -374,8 +390,8 @@ local function fetch_pkgs(params)
local aliases = {}
for _, package in pairs(packages) do
local name_len = #package.name
-- This must match what contentdb.update_paths() does!
local name_len = #package.name
package.id = package.author:lower() .. "/"
if package.type == "game" and name_len > 5 and package.name:sub(name_len - 4) == "_game" then
package.id = package.id .. package.name:sub(1, name_len - 5)
@ -540,3 +556,45 @@ function contentdb.filter_packages(query, by_type)
end
end
end
function contentdb.get_full_package_info(package, callback)
assert(package)
local function fetch(params)
local version = core.get_version()
local base_url = core.settings:get("contentdb_url")
local languages
local current_language = core.get_language()
if current_language ~= "" then
languages = { current_language, "en;q=0.8" }
else
languages = { "en" }
end
local url = base_url ..
"/api/packages/" .. params.package.url_part .. "/for-client/?" ..
"protocol_version=" .. core.get_max_supp_proto() ..
"&engine_version=" .. core.urlencode(version.string) ..
"&formspec_version=" .. core.urlencode(7) ..
"&include_images=false"
local http = core.get_http_api()
local response = http.fetch_sync({
url = url,
extra_headers = {
"Accept-Language: " .. table.concat(languages, ", ")
},
})
if not response.succeeded then
return nil
end
return core.parse_json(response.data)
end
if not core.handle_async(fetch, { package = package }, callback) then
core.log("error", "ERROR: async event failed")
callback(nil)
end
end

@ -46,7 +46,7 @@ local filter_types_type = {
}
local function install_or_update_package(this, package)
function install_or_update_package(parent, package)
local install_parent
if package.type == "mod" then
install_parent = core.get_modpath()
@ -66,14 +66,14 @@ local function install_or_update_package(this, package)
local has_hard_deps = contentdb.has_hard_deps(package)
if has_hard_deps then
local dlg = create_install_dialog(package)
dlg:set_parent(this)
this:hide()
dlg:set_parent(parent)
parent:hide()
dlg:show()
elseif has_hard_deps == nil then
local dlg = messagebox("error_checking_deps",
fgettext("Error getting dependencies for package"))
dlg:set_parent(this)
this:hide()
dlg:set_parent(parent)
parent:hide()
dlg:show()
else
contentdb.queue_download(package, package.path and contentdb.REASON_UPDATE or contentdb.REASON_NEW)
@ -83,13 +83,13 @@ local function install_or_update_package(this, package)
if package.type == "mod" and #pkgmgr.games == 0 then
local dlg = messagebox("install_game",
fgettext("You need to install a game before you can install a mod"))
dlg:set_parent(this)
this:hide()
dlg:set_parent(parent)
parent:hide()
dlg:show()
elseif not package.path and core.is_dir(install_parent .. DIR_DELIM .. package.name) then
local dlg = create_confirm_overwrite(package, on_confirm)
dlg:set_parent(this)
this:hide()
dlg:set_parent(parent)
parent:hide()
dlg:show()
else
on_confirm()
@ -300,7 +300,7 @@ local function get_formspec(dlgdata)
-- image
formspec[#formspec + 1] = "image[0,0;1.5,1;"
formspec[#formspec + 1] = core.formspec_escape(get_screenshot(package))
formspec[#formspec + 1] = core.formspec_escape(get_screenshot(package, package.thumbnail))
formspec[#formspec + 1] = "]"
-- title
@ -310,52 +310,17 @@ local function get_formspec(dlgdata)
core.colorize("#BFBFBF", " by " .. package.author))
formspec[#formspec + 1] = "]"
-- buttons
local description_width = W - 2.625 - 2 * 0.7 - 2 * 0.15
local second_base = "image_button[-1.55,0;0.7,0.7;" .. core.formspec_escape(defaulttexturedir)
local third_base = "image_button[-2.4,0;0.7,0.7;" .. core.formspec_escape(defaulttexturedir)
formspec[#formspec + 1] = "container["
formspec[#formspec + 1] = W - 0.375*2
formspec[#formspec + 1] = ",0.1]"
if package.downloading then
formspec[#formspec + 1] = "animated_image[-1.7,-0.15;1,1;downloading;"
formspec[#formspec + 1] = core.formspec_escape(defaulttexturedir)
formspec[#formspec + 1] = "cdb_downloading.png;3;400;]"
elseif package.queued then
formspec[#formspec + 1] = second_base
formspec[#formspec + 1] = "cdb_queued.png;queued;]"
elseif not package.path then
local elem_name = "install_" .. i .. ";"
formspec[#formspec + 1] = "style[" .. elem_name .. "bgcolor=#71aa34]"
formspec[#formspec + 1] = second_base .. "cdb_add.png;" .. elem_name .. "]"
formspec[#formspec + 1] = "tooltip[" .. elem_name .. fgettext("Install") .. tooltip_colors
else
if package.installed_release < package.release then
-- The install_ action also handles updating
local elem_name = "install_" .. i .. ";"
formspec[#formspec + 1] = "style[" .. elem_name .. "bgcolor=#28ccdf]"
formspec[#formspec + 1] = third_base .. "cdb_update.png;" .. elem_name .. "]"
formspec[#formspec + 1] = "tooltip[" .. elem_name .. fgettext("Update") .. tooltip_colors
description_width = description_width - 0.7 - 0.15
end
local elem_name = "uninstall_" .. i .. ";"
formspec[#formspec + 1] = "style[" .. elem_name .. "bgcolor=#a93b3b]"
formspec[#formspec + 1] = second_base .. "cdb_clear.png;" .. elem_name .. "]"
formspec[#formspec + 1] = "tooltip[" .. elem_name .. fgettext("Uninstall") .. tooltip_colors
end
local web_elem_name = "view_" .. i .. ";"
formspec[#formspec + 1] = "image_button[-0.7,0;0.7,0.7;" ..
core.formspec_escape(defaulttexturedir) .. "cdb_viewonline.png;" .. web_elem_name .. "]"
formspec[#formspec + 1] = "tooltip[" .. web_elem_name ..
fgettext("View more information in a web browser") .. tooltip_colors
formspec[#formspec + 1] = "container_end[]"
-- button
formspec[#formspec + 1] = "button["
formspec[#formspec + 1] = W-0.375*2-2
formspec[#formspec + 1] = ",0.1;2,0.7;view_"
formspec[#formspec + 1] = i
formspec[#formspec + 1] = ";"
formspec[#formspec + 1] = fgettext("View")
formspec[#formspec + 1] = "]"
-- description
local description_width = W - 2.625 - 2 * 0.7 - 2 * 0.15
formspec[#formspec + 1] = "textarea[1.855,0.3;"
formspec[#formspec + 1] = tostring(description_width)
formspec[#formspec + 1] = ",0.8;;;"
@ -437,32 +402,20 @@ local function handle_submit(this, fields)
return true
end
local num_per_page = this.data.num_per_page
local start_idx = (cur_page - 1) * num_per_page + 1
assert(start_idx ~= nil)
for i=start_idx, math.min(#contentdb.packages, start_idx+num_per_page-1) do
local package = contentdb.packages[i]
assert(package)
if fields["install_" .. i] then
install_or_update_package(this, package)
return true
end
if fields["uninstall_" .. i] then
local dlg = create_delete_content_dlg(package)
if fields["view_" .. i] then
local dlg = create_package_dialog(package)
dlg:set_parent(this)
this:hide()
dlg:show()
return true
end
if fields["view_" .. i] then
local url = ("%s/packages/%s?protocol_version=%d"):format(
core.settings:get("contentdb_url"), package.url_part,
core.get_max_supp_proto())
core.open_url(url)
return true
end
end
return false

@ -0,0 +1,351 @@
--Minetest
--Copyright (C) 2018-24 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 function get_info_formspec(size, text)
return table.concat({
"formspec_version[6]",
"size[", size.x, ",", size.y, "]",
"padding[-0.01,-0.01]",
"label[4,4.35;", text, "]",
"container[0,", size.y - 0.8 - 0.375, "]",
"button[0.375,0;2,0.8;back;", fgettext("Back"), "]",
"container_end[]",
})
end
--- Creates a scrollbaroptions for a scroll_container
--
-- @param visible_l the length of the scroll_container and scrollbar
-- @param total_l length of the scrollable area
-- @param scroll_factor as passed to scroll_container
local function make_scrollbaroptions_for_scroll_container(visible_l, total_l, scroll_factor)
assert(total_l >= visible_l)
local max = total_l - visible_l
local thumb_size = (visible_l / total_l) * max
return ("scrollbaroptions[min=0;max=%f;thumbsize=%f]"):format(max / scroll_factor, thumb_size / scroll_factor)
end
local function get_formspec(data)
local window = core.get_window_info()
local size = { x = window.max_formspec_size.x, y = window.max_formspec_size.y }
if not data.info then
if not data.loading and not data.loading_error then
data.loading = true
contentdb.get_full_package_info(data.package, function(info)
data.loading = false
if info == nil then
data.loading_error = true
ui.update()
elseif data.package.name == info.name then
data.info = info
ui.update()
end
end)
end
if data.loading_error then
return get_info_formspec(size, fgettext("No packages could be retrieved"))
else
return get_info_formspec(size, fgettext("Loading..."))
end
else
-- Check installation status
contentdb.update_paths()
local info = data.info
local info_line =
fgettext("by $1 — $2 downloads — +$3 / $4 / -$5",
info.author, info.downloads,
info.reviews.positive, info.reviews.neutral, info.reviews.negative)
local bottom_buttons_y = size.y - 0.8 - 0.375
local formspec = {
"formspec_version[7]",
"size[", size.x, ",", size.y, "]",
"padding[-0.01,-0.01]",
"bgcolor[#0000]",
"box[0,0;", size.x, ",", size.y, ";#0000008C]",
"button[0.375,", bottom_buttons_y, ";2,0.8;back;", fgettext("Back"), "]",
"button[", size.x - 3.375, ",", bottom_buttons_y, ";3,0.8;open_contentdb;", fgettext("ContentDB page"), "]",
"style_type[label;font_size=+24;font=bold]",
"label[0.375,0.7;", core.formspec_escape(info.title), "]",
"style_type[label;font_size=;font=]",
"label[0.375,1.4;", core.formspec_escape(info_line), "]",
}
local x = size.x - 3.375
local function add_link_button(label, name)
if info[name] then
x = x - 3.25
table.insert_all(formspec, {
"button[", x, ",", bottom_buttons_y, ";3,0.8;open_", name, ";", label, "]",
})
end
end
add_link_button(fgettext("Translate"), "translation_url")
add_link_button(fgettext("Issue Tracker"), "issue_tracker")
add_link_button(fgettext("Forums"), "forums")
add_link_button(fgettext("Source"), "repo")
add_link_button(fgettext("Website"), "website")
table.insert_all(formspec, {
"container[", size.x - 6.375, ",0.375]"
})
local left_button_rect = "0,0;2.875,1"
local right_button_rect = "3.125,0;2.875,1"
if data.package.downloading then
formspec[#formspec + 1] = "animated_image[5,0;1,1;downloading;"
formspec[#formspec + 1] = core.formspec_escape(defaulttexturedir)
formspec[#formspec + 1] = "cdb_downloading.png;3;400;]"
elseif data.package.queued then
formspec[#formspec + 1] = "style[queued;border=false]"
formspec[#formspec + 1] = "image_button[5,0;1,1;" .. core.formspec_escape(defaulttexturedir)
formspec[#formspec + 1] = "cdb_queued.png;queued;]"
elseif not data.package.path then
formspec[#formspec + 1] = "style[install;bgcolor=green]"
formspec[#formspec + 1] = "button["
formspec[#formspec + 1] = right_button_rect
formspec[#formspec + 1] =";install;"
formspec[#formspec + 1] = fgettext("Install [$1]", info.download_size)
formspec[#formspec + 1] = "]"
else
if data.package.installed_release < data.package.release then
-- The install_ action also handles updating
formspec[#formspec + 1] = "style[install;bgcolor=#28ccdf]"
formspec[#formspec + 1] = "button["
formspec[#formspec + 1] = left_button_rect
formspec[#formspec + 1] = ";install;"
formspec[#formspec + 1] = fgettext("Update")
formspec[#formspec + 1] = "]"
end
formspec[#formspec + 1] = "style[uninstall;bgcolor=#a93b3b]"
formspec[#formspec + 1] = "button["
formspec[#formspec + 1] = right_button_rect
formspec[#formspec + 1] = ";uninstall;"
formspec[#formspec + 1] = fgettext("Uninstall")
formspec[#formspec + 1] = "]"
end
local current_tab = data.current_tab or 1
local tab_titles = {
fgettext("Description"),
fgettext("Information"),
}
local tab_body_height = bottom_buttons_y - 2.8
table.insert_all(formspec, {
"container_end[]",
"tabheader[0.375,2.55;", size.x - 0.375*2, ",0.8;tabs;",
table.concat(tab_titles, ","), ";", current_tab, ";true;true]",
"container[0,2.8]",
})
if current_tab == 1 then
-- Screenshots and description
local hypertext = info.long_description.head ..
"<big><b>" .. core.hypertext_escape(info.short_description) .. "</b></big>\n\n" ..
info.long_description.body
hypertext = hypertext:gsub("<img name=blank.png ",
"<img name=" .. core.hypertext_escape(defaulttexturedir) .. "blank.png ")
local screenshots_w = #info.screenshots*3.25 + 0.375 - 0.25
local scroll_factor = 0.1
local needs_scrollbar = screenshots_w > size.x
local hypertext_y = (#info.screenshots > 0 and 2.25 or 0) + (needs_scrollbar and 0.5 or 0)
table.insert_all(formspec, {
"hypertext[0.375,", hypertext_y, ";",
size.x - 2*0.375, ",",
tab_body_height - hypertext_y - 0.375,
";desc;", core.formspec_escape(hypertext), "]",
"scroll_container[0,0;", size.x, ",2.25;images_sb;horizontal;", scroll_factor, "]",
})
for i, ss in ipairs(info.screenshots) do
local path = get_screenshot(data.package, ss.url, 2)
table.insert_all(formspec, {
"image_button[", (i-1)*3.25 + 0.375, ",0;3,2;",
core.formspec_escape(path), ";ss_", i, ";;false;false]",
})
end
formspec[#formspec + 1] = "scroll_container_end[]"
if needs_scrollbar then
table.insert_all(formspec, {
make_scrollbaroptions_for_scroll_container(size.x, screenshots_w, scroll_factor),
"scrollbar[0,2.25;", size.x, ",0.25;horizontal;images_sb;0]",
})
end
elseif current_tab == 2 then
local hypertext = info.info_hypertext.head .. info.info_hypertext.body
table.insert_all(formspec, {
"hypertext[0.375,0;", size.x - 2*0.375, ",", tab_body_height - 0.375,
";info;", core.formspec_escape(hypertext), "]",
})
else
error("Unknown tab " .. current_tab)
end
formspec[#formspec + 1] = "container_end[]"
return table.concat(formspec)
end
end
local function handle_hypertext_event(this, event, hypertext_object)
if not (event and event:sub(1, 7) == "action:") then
return
end
-- TODO: escape base_url
local base_url = core.settings:get("contentdb_url")
for key, url in pairs(hypertext_object.links) do
if event == "action:" .. key then
local author, name = url:match("^" .. base_url .. "/?packages/([A-Za-z0-9 _-]+)/([a-z0-9_]+)/?$")
if author and name then
local package2 = contentdb.get_package_by_info(author, name)
if package2 then
local dlg = create_package_dialog(package2)
dlg:set_parent(this)
this:hide()
dlg:show()
return true
end
end
core.open_url_dialog(url)
return true
end
end
end
local function handle_submit(this, fields)
local info = this.data.info
local package = this.data.package
if fields.back then
this:delete()
return true
end
if not info then
return false
end
if fields.open_contentdb then
local url = ("%s/packages/%s/?protocol_version=%d"):format(
core.settings:get("contentdb_url"), package.url_part,
core.get_max_supp_proto())
core.open_url(url)
return true
end
if fields.open_translation_url then
core.open_url_dialog(info.translation_url)
return true
end
if fields.open_issue_tracker then
core.open_url_dialog(info.issue_tracker)
return true
end
if fields.open_forums then
core.open_url("https://forum.minetest.net/viewtopic.php?t=" .. info.forums)
return true
end
if fields.open_repo then
core.open_url_dialog(info.repo)
return true
end
if fields.open_website then
core.open_url_dialog(info.website)
return true
end
if fields.install then
install_or_update_package(this, package)
return true
end
if fields.uninstall then
local dlg = create_delete_content_dlg(package)
dlg:set_parent(this)
this:hide()
dlg:show()
return true
end
if fields.tabs then
this.data.current_tab = tonumber(fields.tabs)
return true
end
for i, ss in ipairs(info.screenshots) do
if fields["ss_" .. i] then
core.open_url(ss.url)
return true
end
end
if handle_hypertext_event(this, fields.desc, info.long_description) or
handle_hypertext_event(this, fields.info, info.info_hypertext) then
return true
end
end
function create_package_dialog(package)
assert(package)
local dlg = dialog_create("package_dialog_" .. package.id,
get_formspec,
handle_submit)
local data = dlg.data
data.package = package
data.info = nil
data.loading = false
data.loading_error = nil
data.current_tab = 1
return dlg
end

@ -23,4 +23,5 @@ dofile(path .. DIR_DELIM .. "update_detector.lua")
dofile(path .. DIR_DELIM .. "screenshots.lua")
dofile(path .. DIR_DELIM .. "dlg_install.lua")
dofile(path .. DIR_DELIM .. "dlg_overwrite.lua")
dofile(path .. DIR_DELIM .. "dlg_package.lua")
dofile(path .. DIR_DELIM .. "dlg_contentdb.lua")

@ -23,23 +23,43 @@ local screenshot_downloading = {}
local screenshot_downloaded = {}
local function get_filename(path)
local parts = path:split("/")
return parts[#parts]
end
local function get_file_extension(path)
local parts = path:split(".")
return parts[#parts]
end
function get_screenshot(package)
if not package.thumbnail then
function get_screenshot(package, screenshot_url, level)
if not screenshot_url then
return defaulttexturedir .. "no_screenshot.png"
elseif screenshot_downloading[package.thumbnail] then
end
-- Minetest only supports png and jpg
local ext = get_file_extension(screenshot_url)
if ext ~= "png" and ext ~= "jpg" then
screenshot_url = screenshot_url:sub(0, -#ext - 1) .. "png"
level = level or 4
end
-- Set thumbnail level
if level then
screenshot_url = screenshot_url:gsub("/thumbnails/[0-9]+/", "/thumbnails/" .. level .. "/")
screenshot_url = screenshot_url:gsub("/uploads/", "/thumbnails/" .. level .. "/")
end
if screenshot_downloading[screenshot_url] then
return defaulttexturedir .. "loading_screenshot.png"
end
-- Get tmp screenshot path
local ext = get_file_extension(package.thumbnail)
local filepath = screenshot_dir .. DIR_DELIM ..
("%s-%s-%s.%s"):format(package.type, package.author, package.name, ext)
("%s-%s-%s-l%d-%s"):format(package.type, package.author, package.name,
level or 1, get_filename(screenshot_url))
-- Return if already downloaded
local file = io.open(filepath, "r")
@ -49,7 +69,7 @@ function get_screenshot(package)
end
-- Show error if we've failed to download before
if screenshot_downloaded[package.thumbnail] then
if screenshot_downloaded[screenshot_url] then
return defaulttexturedir .. "error_screenshot.png"
end
@ -59,16 +79,16 @@ function get_screenshot(package)
return core.download_file(params.url, params.dest)
end
local function callback(success)
screenshot_downloading[package.thumbnail] = nil
screenshot_downloaded[package.thumbnail] = true
screenshot_downloading[screenshot_url] = nil
screenshot_downloaded[screenshot_url] = true
if not success then
core.log("warning", "Screenshot download failed for some reason")
end
ui.update()
end
if core.handle_async(download_screenshot,
{ dest = filepath, url = package.thumbnail }, callback) then
screenshot_downloading[package.thumbnail] = true
{ dest = filepath, url = screenshot_url }, callback) then
screenshot_downloading[screenshot_url] = true
else
core.log("error", "ERROR: async event failed")
return defaulttexturedir .. "error_screenshot.png"

@ -6443,6 +6443,9 @@ Formspec
* `minetest.formspec_escape(string)`: returns a string
* escapes the characters "[", "]", "\", "," and ";", which cannot be used
in formspecs.
* `minetest.hypertext_escape(string)`: returns a string
* escapes the charecters "\", "<", and ">" to show text in a hypertext element.
* not safe for use with tag attributes.
* `minetest.explode_table_event(string)`: returns a table
* returns e.g. `{type="CHG", row=1, column=2}`
* `type` is one of:

@ -47,7 +47,10 @@ Functions
* returns the maximum supported network protocol version
* `core.open_url(url)`
* opens the URL in a web browser, returns false on failure.
* Must begin with http:// or https://
* `url` must begin with http:// or https://
* `core.open_url_dialog(url)`
* shows a dialog to allow the user to choose whether to open a URL.
* `url` must begin with http:// or https://
* `core.open_dir(path)`
* opens the path in the system file browser/explorer, returns false on failure.
* Must be an existing directory.

@ -40,6 +40,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "content/mod_configuration.h"
#include "threading/mutex_auto_lock.h"
#include "common/c_converter.h"
#include "gui/guiOpenURL.h"
/******************************************************************************/
std::string ModApiMainMenu::getTextData(lua_State *L, const std::string &name)
@ -1038,6 +1039,22 @@ int ModApiMainMenu::l_open_url(lua_State *L)
return 1;
}
/******************************************************************************/
int ModApiMainMenu::l_open_url_dialog(lua_State *L)
{
GUIEngine* engine = getGuiEngine(L);
sanity_check(engine != NULL);
std::string url = luaL_checkstring(L, 1);
GUIOpenURLMenu* openURLMenu =
new GUIOpenURLMenu(engine->m_rendering_engine->get_gui_env(),
engine->m_parent, -1, engine->m_menumanager,
engine->m_texture_source.get(), url);
openURLMenu->drop();
return 1;
}
/******************************************************************************/
int ModApiMainMenu::l_open_dir(lua_State *L)
{
@ -1129,6 +1146,7 @@ void ModApiMainMenu::Initialize(lua_State *L, int top)
API_FCT(get_min_supp_proto);
API_FCT(get_max_supp_proto);
API_FCT(open_url);
API_FCT(open_url_dialog);
API_FCT(open_dir);
API_FCT(share_file);
API_FCT(do_async_callback);

@ -162,6 +162,8 @@ class ModApiMainMenu: public ModApiBase
// other
static int l_open_url(lua_State *L);
static int l_open_url_dialog(lua_State *L);
static int l_open_dir(lua_State *L);
static int l_share_file(lua_State *L);