diff --git a/builtin/common/misc_helpers.lua b/builtin/common/misc_helpers.lua index 9c25b826fffd..2ad9b10afea8 100644 --- a/builtin/common/misc_helpers.lua +++ b/builtin/common/misc_helpers.lua @@ -235,6 +235,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 = {} diff --git a/builtin/mainmenu/content/contentdb.lua b/builtin/mainmenu/content/contentdb.lua index e0479cb4c265..4d59826dd6a3 100644 --- a/builtin/mainmenu/content/contentdb.lua +++ b/builtin/mainmenu/content/contentdb.lua @@ -182,6 +182,23 @@ function contentdb.get_package_by_id(id) end +function contentdb.calculate_package_id(type, author, name) + local id = author:lower() .. "/" + if (type == nil or type == "game") and #name > 5 and name:sub(#name - 4) == "_game" then + id = id .. name:sub(1, #name - 5) + else + id = id .. name + end + return id +end + + +function contentdb.get_package_by_info(author, name) + local id = contentdb.calculate_package_id(nil, author, name) + return contentdb.package_by_id[id] +end + + -- Create a coroutine from `fn` and provide results to `callback` when complete (dead). -- Returns a resumer function. local function make_callback_coroutine(fn, callback) @@ -415,15 +432,7 @@ 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! - 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) - else - package.id = package.id .. package.name - end - + package.id = params.calculate_package_id(package.type, package.author, package.name) package.url_part = core.urlencode(package.author) .. "/" .. core.urlencode(package.name) if package.aliases then @@ -443,7 +452,7 @@ end function contentdb.fetch_pkgs(callback) contentdb.loading = true - core.handle_async(fetch_pkgs, nil, function(result) + core.handle_async(fetch_pkgs, { calculate_package_id = contentdb.calculate_package_id }, function(result) if result then contentdb.load_ok = true contentdb.load_error = false @@ -581,3 +590,54 @@ function contentdb.filter_packages(query, by_type) end end end + + +function contentdb.get_full_package_info(package, callback) + assert(package) + if package.full_info then + callback(package.full_info) + return + end + + 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.urlencode(core.get_max_supp_proto()) .. + "&engine_version=" .. core.urlencode(version.string) .. + "&formspec_version=" .. core.urlencode(core.get_formspec_version()) .. + "&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 + + local function my_callback(value) + package.full_info = value + callback(value) + end + + if not core.handle_async(fetch, { package = package }, my_callback) then + core.log("error", "ERROR: async event failed") + callback(nil) + end +end diff --git a/builtin/mainmenu/content/dlg_contentdb.lua b/builtin/mainmenu/content/dlg_contentdb.lua index bcc89f7cdb9c..025430bfa353 100644 --- a/builtin/mainmenu/content/dlg_contentdb.lua +++ b/builtin/mainmenu/content/dlg_contentdb.lua @@ -46,48 +46,6 @@ local filter_types_type = { } -local function install_or_update_package(this, package) - local install_parent - if package.type == "mod" then - install_parent = core.get_modpath() - elseif package.type == "game" then - install_parent = core.get_gamepath() - elseif package.type == "txp" then - install_parent = core.get_texturepath() - else - error("Unknown package type: " .. package.type) - end - - if package.queued or package.downloading then - return - end - - local function on_confirm() - local dlg = create_install_dialog(package) - dlg:set_parent(this) - this:hide() - dlg:show() - - dlg:load_deps() - end - - 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: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:show() - else - on_confirm() - end -end - - -- Resolves the package specification stored in auto_install_spec into an actual package. -- May only be called after the package list has been loaded successfully. local function resolve_auto_install_spec() @@ -291,7 +249,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, 1)) formspec[#formspec + 1] = "]" -- title @@ -301,52 +259,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;;;" @@ -434,26 +357,13 @@ local function handle_submit(this, fields) 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 diff --git a/builtin/mainmenu/content/dlg_install.lua b/builtin/mainmenu/content/dlg_install.lua index 89819be2a719..3f43bd23cbb7 100644 --- a/builtin/mainmenu/content/dlg_install.lua +++ b/builtin/mainmenu/content/dlg_install.lua @@ -244,3 +244,45 @@ function create_install_dialog(package) return dlg end + + +function install_or_update_package(parent, package) + local install_parent + if package.type == "mod" then + install_parent = core.get_modpath() + elseif package.type == "game" then + install_parent = core.get_gamepath() + elseif package.type == "txp" then + install_parent = core.get_texturepath() + else + error("Unknown package type: " .. package.type) + end + + if package.queued or package.downloading then + return + end + + local function on_confirm() + local dlg = create_install_dialog(package) + dlg:set_parent(parent) + parent:hide() + dlg:show() + + dlg:load_deps() + end + + 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(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(parent) + parent:hide() + dlg:show() + else + on_confirm() + end +end diff --git a/builtin/mainmenu/content/dlg_package.lua b/builtin/mainmenu/content/dlg_package.lua new file mode 100644 index 000000000000..78bdf2e71f8a --- /dev/null +++ b/builtin/mainmenu/content/dlg_package.lua @@ -0,0 +1,325 @@ +--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, padding, text) + return table.concat({ + "formspec_version[6]", + "size[", size.x, ",", size.y, "]", + "padding[0,0]", + "bgcolor[;true]", + + "label[4,4.35;", text, "]", + "container[", padding.x, ",", size.y - 0.8 - padding.y, "]", + "button[0,0;2,0.8;back;", fgettext("Back"), "]", + "container_end[]", + }) +end + + +local function get_formspec(data) + -- Padding is increased on Android to account for notches + -- TODO: use Android API to determine size of cut outs + local window_padding = { x = PLATFORM == "Android" and 1 or 0.5, y = PLATFORM == "Android" and 0.25 or 0.5 } + local window = core.get_window_info() + local size = { x = window.max_formspec_size.x, y = window.max_formspec_size.y } + size.x = math.min(size.x, 20) + local W = size.x - window_padding.x * 2 + local H = size.y - window_padding.y * 2 + + 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() + return + end + + if info.forums then + info.forums = "https://forum.minetest.net/viewtopic.php?t=" .. info.forums + end + + assert(data.package.name == info.name) + data.info = info + ui.update() + end) + end + + -- get_full_package_info can return cached info immediately, so + -- check to see if that happened + if not data.info then + if data.loading_error then + return get_info_formspec(size, window_padding, fgettext("No packages could be retrieved")) + end + return get_info_formspec(size, window_padding, fgettext("Loading...")) + end + end + + -- 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 = H - 0.8 + + local formspec = { + "formspec_version[7]", + "size[", size.x, ",", size.y, "]", + "padding[0,0]", + "bgcolor[;true]", + + "container[", window_padding.x, ",", window_padding.y, "]", + + "button[0,", bottom_buttons_y, ";2,0.8;back;", fgettext("Back"), "]", + "button[", W - 3, ",", bottom_buttons_y, ";3,0.8;open_contentdb;", fgettext("ContentDB page"), "]", + + "style_type[label;font_size=+24;font=bold]", + "label[0,0.4;", core.formspec_escape(info.title), "]", + "style_type[label;font_size=;font=]", + + "label[0,1.2;", core.formspec_escape(info_line), "]", + } + + table.insert_all(formspec, { + "container[", W - 6, ",0]" + }) + + 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[]", + + "box[0,2.55;", W, ",", tab_body_height, ";#ffffff11]", + + "tabheader[0,2.55;", W, ",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 = "" .. core.hypertext_escape(info.short_description) .. "\n" + local winfo = core.get_window_info() + local fs_to_px = winfo.size.x / winfo.max_formspec_size.x + for i, ss in ipairs(info.screenshots) do + local path = get_screenshot(data.package, ss.url, 2) + hypertext = hypertext .. "" + if i ~= #info.screenshots then + hypertext = hypertext .. "" + end + end + hypertext = hypertext .. "\n" .. info.long_description.head + + local first = true + local function add_link_button(label, name) + if info[name] then + if not first then + hypertext = hypertext .. " | " + end + hypertext = hypertext .. "" .. core.hypertext_escape(label) .. "" + info.long_description.links["link_" .. name] = info[name] + first = false + end + end + + add_link_button(fgettext("Donate"), "donate_url") + add_link_button(fgettext("Website"), "website") + add_link_button(fgettext("Source"), "repo") + add_link_button(fgettext("Issue Tracker"), "issue_tracker") + add_link_button(fgettext("Translate"), "translation_url") + add_link_button(fgettext("Forum Topic"), "forums") + + hypertext = hypertext .. "\n\n" .. info.long_description.body + + hypertext = hypertext:gsub(""] = "\\>", - } - string = string:gsub("[\\<>]", hypertext_escapes) + string = core.hypertext_escape(string) string = string:gsub("%[.-%]", "%1") table.insert(dest, string) diff --git a/doc/lua_api.md b/doc/lua_api.md index 57c99ef9df48..2fb2950d3368 100644 --- a/doc/lua_api.md +++ b/doc/lua_api.md @@ -6581,6 +6581,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 characters "\", "<", 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: diff --git a/doc/menu_lua_api.md b/doc/menu_lua_api.md index be63af904966..c03c0501e096 100644 --- a/doc/menu_lua_api.md +++ b/doc/menu_lua_api.md @@ -57,7 +57,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. @@ -65,6 +68,8 @@ Functions * Android only. Shares file using the share popup * `core.get_version()` (possible in async calls) * returns current core version +* `core.get_formspec_version()` + * returns maximum supported formspec version diff --git a/src/script/lua_api/l_mainmenu.cpp b/src/script/lua_api/l_mainmenu.cpp index 65e69d7e4a3a..20faec9e01c7 100644 --- a/src/script/lua_api/l_mainmenu.cpp +++ b/src/script/lua_api/l_mainmenu.cpp @@ -41,6 +41,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,13 @@ int ModApiMainMenu::l_get_max_supp_proto(lua_State *L) return 1; } +/******************************************************************************/ +int ModApiMainMenu::l_get_formspec_version(lua_State *L) +{ + lua_pushinteger(L, FORMSPEC_API_VERSION); + return 1; +} + /******************************************************************************/ int ModApiMainMenu::l_open_url(lua_State *L) { @@ -1046,6 +1054,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) { @@ -1136,7 +1160,9 @@ void ModApiMainMenu::Initialize(lua_State *L, int top) API_FCT(get_active_irrlicht_device); API_FCT(get_min_supp_proto); API_FCT(get_max_supp_proto); + API_FCT(get_formspec_version); API_FCT(open_url); + API_FCT(open_url_dialog); API_FCT(open_dir); API_FCT(share_file); API_FCT(do_async_callback); @@ -1166,6 +1192,7 @@ void ModApiMainMenu::InitializeAsync(lua_State *L, int top) API_FCT(download_file); API_FCT(get_min_supp_proto); API_FCT(get_max_supp_proto); + API_FCT(get_formspec_version); API_FCT(get_language); API_FCT(gettext); } diff --git a/src/script/lua_api/l_mainmenu.h b/src/script/lua_api/l_mainmenu.h index 5535d21707cb..cb3e7f9ca53c 100644 --- a/src/script/lua_api/l_mainmenu.h +++ b/src/script/lua_api/l_mainmenu.h @@ -159,9 +159,13 @@ class ModApiMainMenu: public ModApiBase static int l_get_max_supp_proto(lua_State *L); + static int l_get_formspec_version(lua_State *L); + // 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);