ContentDB redesign: Add package dialog

Co-authored-by: Gregor Parzefall <gregor.parzefall@posteo.de>
This commit is contained in:
rubenwardy 2024-03-31 19:24:27 +01:00
parent 05cbd84ae0
commit 78aab8c95d
12 changed files with 529 additions and 130 deletions

@ -235,6 +235,16 @@ function core.formspec_escape(text)
end 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) function core.wrap_text(text, max_length, as_table)
local result = {} local result = {}
local line = {} local line = {}

@ -182,6 +182,23 @@ function contentdb.get_package_by_id(id)
end 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). -- Create a coroutine from `fn` and provide results to `callback` when complete (dead).
-- Returns a resumer function. -- Returns a resumer function.
local function make_callback_coroutine(fn, callback) local function make_callback_coroutine(fn, callback)
@ -415,15 +432,7 @@ local function fetch_pkgs(params)
local aliases = {} local aliases = {}
for _, package in pairs(packages) do for _, package in pairs(packages) do
local name_len = #package.name package.id = params.calculate_package_id(package.type, package.author, 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.url_part = core.urlencode(package.author) .. "/" .. core.urlencode(package.name) package.url_part = core.urlencode(package.author) .. "/" .. core.urlencode(package.name)
if package.aliases then if package.aliases then
@ -443,7 +452,7 @@ end
function contentdb.fetch_pkgs(callback) function contentdb.fetch_pkgs(callback)
contentdb.loading = true 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 if result then
contentdb.load_ok = true contentdb.load_ok = true
contentdb.load_error = false contentdb.load_error = false
@ -581,3 +590,54 @@ function contentdb.filter_packages(query, by_type)
end end
end 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

@ -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. -- 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. -- May only be called after the package list has been loaded successfully.
local function resolve_auto_install_spec() local function resolve_auto_install_spec()
@ -291,7 +249,7 @@ local function get_formspec(dlgdata)
-- image -- image
formspec[#formspec + 1] = "image[0,0;1.5,1;" 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] = "]" formspec[#formspec + 1] = "]"
-- title -- title
@ -301,52 +259,17 @@ local function get_formspec(dlgdata)
core.colorize("#BFBFBF", " by " .. package.author)) core.colorize("#BFBFBF", " by " .. package.author))
formspec[#formspec + 1] = "]" formspec[#formspec + 1] = "]"
-- buttons -- button
local description_width = W - 2.625 - 2 * 0.7 - 2 * 0.15 formspec[#formspec + 1] = "button["
formspec[#formspec + 1] = W-0.375*2-2
local second_base = "image_button[-1.55,0;0.7,0.7;" .. core.formspec_escape(defaulttexturedir) formspec[#formspec + 1] = ",0.1;2,0.7;view_"
local third_base = "image_button[-2.4,0;0.7,0.7;" .. core.formspec_escape(defaulttexturedir) formspec[#formspec + 1] = i
formspec[#formspec + 1] = "container[" formspec[#formspec + 1] = ";"
formspec[#formspec + 1] = W - 0.375*2 formspec[#formspec + 1] = fgettext("View")
formspec[#formspec + 1] = ",0.1]" formspec[#formspec + 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[]"
-- description -- description
local description_width = W - 2.625 - 2 * 0.7 - 2 * 0.15
formspec[#formspec + 1] = "textarea[1.855,0.3;" formspec[#formspec + 1] = "textarea[1.855,0.3;"
formspec[#formspec + 1] = tostring(description_width) formspec[#formspec + 1] = tostring(description_width)
formspec[#formspec + 1] = ",0.8;;;" formspec[#formspec + 1] = ",0.8;;;"
@ -434,26 +357,13 @@ local function handle_submit(this, fields)
local package = contentdb.packages[i] local package = contentdb.packages[i]
assert(package) assert(package)
if fields["install_" .. i] then if fields["view_" .. i] then
install_or_update_package(this, package) local dlg = create_package_dialog(package)
return true
end
if fields["uninstall_" .. i] then
local dlg = create_delete_content_dlg(package)
dlg:set_parent(this) dlg:set_parent(this)
this:hide() this:hide()
dlg:show() dlg:show()
return true return true
end 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 end
return false return false

@ -244,3 +244,45 @@ function create_install_dialog(package)
return dlg return dlg
end 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

@ -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 = "<big><b>" .. core.hypertext_escape(info.short_description) .. "</b></big>\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 .. "<action name=\"ss_" .. i .. "\"><img name=\"" ..
core.hypertext_escape(path) .. "\" width=" .. (3 * fs_to_px) ..
" height=" .. (2 * fs_to_px) .. "></action>"
if i ~= #info.screenshots then
hypertext = hypertext .. "<img name=\"blank.png\" width=" .. (0.25 * fs_to_px) ..
" height=" .. (2.25 * fs_to_px).. ">"
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 .. "<action name=link_" .. name .. ">" .. core.hypertext_escape(label) .. "</action>"
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("<img name=\"?blank.png\"? ",
"<img name=\"" .. core.hypertext_escape(defaulttexturedir) .. "blank.png\" ")
table.insert_all(formspec, {
"hypertext[0,0;", W, ",", tab_body_height - 0.375,
";desc;", core.formspec_escape(hypertext), "]",
})
elseif current_tab == 2 then
local hypertext = info.info_hypertext.head .. info.info_hypertext.body
table.insert_all(formspec, {
"hypertext[0,0;", W, ",", tab_body_height - 0.375,
";info;", core.formspec_escape(hypertext), "]",
})
else
error("Unknown tab " .. current_tab)
end
formspec[#formspec + 1] = "container_end[]"
formspec[#formspec + 1] = "container_end[]"
return table.concat(formspec)
end
local function handle_hypertext_event(this, event, hypertext_object)
if not (event and event:sub(1, 7) == "action:") then
return
end
for i, ss in ipairs(this.data.info.screenshots) do
if event == "action:ss_" .. i then
core.open_url(ss.url)
return true
end
end
local base_url = core.settings:get("contentdb_url"):gsub("(%W)", "%%%1")
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.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
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 .. "screenshots.lua")
dofile(path .. DIR_DELIM .. "dlg_install.lua") dofile(path .. DIR_DELIM .. "dlg_install.lua")
dofile(path .. DIR_DELIM .. "dlg_overwrite.lua") dofile(path .. DIR_DELIM .. "dlg_overwrite.lua")
dofile(path .. DIR_DELIM .. "dlg_package.lua")
dofile(path .. DIR_DELIM .. "dlg_contentdb.lua") dofile(path .. DIR_DELIM .. "dlg_contentdb.lua")

@ -23,23 +23,40 @@ local screenshot_downloading = {}
local screenshot_downloaded = {} local screenshot_downloaded = {}
local function get_filename(path)
local parts = path:split("/")
return parts[#parts]
end
local function get_file_extension(path) local function get_file_extension(path)
local parts = path:split(".") local parts = path:split(".")
return parts[#parts] return parts[#parts]
end end
function get_screenshot(package) function get_screenshot(package, screenshot_url, level)
if not package.thumbnail then if not screenshot_url then
return defaulttexturedir .. "no_screenshot.png" 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"
end
-- Set thumbnail level
screenshot_url = screenshot_url:gsub("/thumbnails/[0-9]+/", "/thumbnails/" .. level .. "/")
screenshot_url = screenshot_url:gsub("/uploads/", "/thumbnails/" .. level .. "/")
if screenshot_downloading[screenshot_url] then
return defaulttexturedir .. "loading_screenshot.png" return defaulttexturedir .. "loading_screenshot.png"
end end
-- Get tmp screenshot path
local ext = get_file_extension(package.thumbnail)
local filepath = screenshot_dir .. DIR_DELIM .. 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, get_filename(screenshot_url))
-- Return if already downloaded -- Return if already downloaded
local file = io.open(filepath, "r") local file = io.open(filepath, "r")
@ -49,7 +66,7 @@ function get_screenshot(package)
end end
-- Show error if we've failed to download before -- 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" return defaulttexturedir .. "error_screenshot.png"
end end
@ -59,16 +76,16 @@ function get_screenshot(package)
return core.download_file(params.url, params.dest) return core.download_file(params.url, params.dest)
end end
local function callback(success) local function callback(success)
screenshot_downloading[package.thumbnail] = nil screenshot_downloading[screenshot_url] = nil
screenshot_downloaded[package.thumbnail] = true screenshot_downloaded[screenshot_url] = true
if not success then if not success then
core.log("warning", "Screenshot download failed for some reason") core.log("warning", "Screenshot download failed for some reason")
end end
ui.update() ui.update()
end end
if core.handle_async(download_screenshot, if core.handle_async(download_screenshot,
{ dest = filepath, url = package.thumbnail }, callback) then { dest = filepath, url = screenshot_url }, callback) then
screenshot_downloading[package.thumbnail] = true screenshot_downloading[screenshot_url] = true
else else
core.log("error", "ERROR: async event failed") core.log("error", "ERROR: async event failed")
return defaulttexturedir .. "error_screenshot.png" return defaulttexturedir .. "error_screenshot.png"

@ -19,12 +19,7 @@
local function prepare_credits(dest, source) local function prepare_credits(dest, source)
local string = table.concat(source, "\n") .. "\n" local string = table.concat(source, "\n") .. "\n"
local hypertext_escapes = { string = core.hypertext_escape(string)
["\\"] = "\\\\",
["<"] = "\\<",
[">"] = "\\>",
}
string = string:gsub("[\\<>]", hypertext_escapes)
string = string:gsub("%[.-%]", "<gray>%1</gray>") string = string:gsub("%[.-%]", "<gray>%1</gray>")
table.insert(dest, string) table.insert(dest, string)

@ -6581,6 +6581,9 @@ Formspec
* `minetest.formspec_escape(string)`: returns a string * `minetest.formspec_escape(string)`: returns a string
* escapes the characters "[", "]", "\", "," and ";", which cannot be used * escapes the characters "[", "]", "\", "," and ";", which cannot be used
in formspecs. 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 * `minetest.explode_table_event(string)`: returns a table
* returns e.g. `{type="CHG", row=1, column=2}` * returns e.g. `{type="CHG", row=1, column=2}`
* `type` is one of: * `type` is one of:

@ -57,7 +57,10 @@ Functions
* returns the maximum supported network protocol version * returns the maximum supported network protocol version
* `core.open_url(url)` * `core.open_url(url)`
* opens the URL in a web browser, returns false on failure. * 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)` * `core.open_dir(path)`
* opens the path in the system file browser/explorer, returns false on failure. * opens the path in the system file browser/explorer, returns false on failure.
* Must be an existing directory. * Must be an existing directory.
@ -65,6 +68,8 @@ Functions
* Android only. Shares file using the share popup * Android only. Shares file using the share popup
* `core.get_version()` (possible in async calls) * `core.get_version()` (possible in async calls)
* returns current core version * returns current core version
* `core.get_formspec_version()`
* returns maximum supported formspec version

@ -41,6 +41,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "content/mod_configuration.h" #include "content/mod_configuration.h"
#include "threading/mutex_auto_lock.h" #include "threading/mutex_auto_lock.h"
#include "common/c_converter.h" #include "common/c_converter.h"
#include "gui/guiOpenURL.h"
/******************************************************************************/ /******************************************************************************/
std::string ModApiMainMenu::getTextData(lua_State *L, const std::string &name) 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; 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) int ModApiMainMenu::l_open_url(lua_State *L)
{ {
@ -1046,6 +1054,22 @@ int ModApiMainMenu::l_open_url(lua_State *L)
return 1; 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) 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_active_irrlicht_device);
API_FCT(get_min_supp_proto); API_FCT(get_min_supp_proto);
API_FCT(get_max_supp_proto); API_FCT(get_max_supp_proto);
API_FCT(get_formspec_version);
API_FCT(open_url); API_FCT(open_url);
API_FCT(open_url_dialog);
API_FCT(open_dir); API_FCT(open_dir);
API_FCT(share_file); API_FCT(share_file);
API_FCT(do_async_callback); API_FCT(do_async_callback);
@ -1166,6 +1192,7 @@ void ModApiMainMenu::InitializeAsync(lua_State *L, int top)
API_FCT(download_file); API_FCT(download_file);
API_FCT(get_min_supp_proto); API_FCT(get_min_supp_proto);
API_FCT(get_max_supp_proto); API_FCT(get_max_supp_proto);
API_FCT(get_formspec_version);
API_FCT(get_language); API_FCT(get_language);
API_FCT(gettext); API_FCT(gettext);
} }

@ -159,9 +159,13 @@ private:
static int l_get_max_supp_proto(lua_State *L); static int l_get_max_supp_proto(lua_State *L);
static int l_get_formspec_version(lua_State *L);
// other // other
static int l_open_url(lua_State *L); 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_open_dir(lua_State *L);
static int l_share_file(lua_State *L); static int l_share_file(lua_State *L);