mirror of
https://github.com/minetest/minetest.git
synced 2024-06-30 21:20:36 +02:00
Compare commits
11 Commits
000c2dff3a
...
0a569a288f
Author | SHA1 | Date | |
---|---|---|---|
|
0a569a288f | ||
|
9a1501ae89 | ||
|
514e106414 | ||
|
4c001bd248 | ||
|
92d89bcf2c | ||
|
70b63dcb14 | ||
|
e81f3857dd | ||
|
c1bc340b78 | ||
|
18ef9b0f50 | ||
|
9d890c992a | ||
|
6ade0ee2ad |
@ -57,12 +57,10 @@ srifqi:
|
||||
textures/base/pack/minimap_btn.png
|
||||
|
||||
Zughy:
|
||||
textures/base/pack/cdb_add.png
|
||||
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
|
||||
textures/base/pack/settings_reset.png
|
||||
@ -79,7 +77,6 @@ kilbith:
|
||||
textures/base/pack/progress_bar_bg.png
|
||||
|
||||
SmallJoker:
|
||||
textures/base/pack/cdb_clear.png
|
||||
textures/base/pack/server_favorite_delete.png (based on server_favorite.png)
|
||||
|
||||
DS:
|
||||
|
@ -265,6 +265,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 = {}
|
||||
|
@ -182,6 +182,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 {}
|
||||
@ -377,8 +393,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)
|
||||
@ -543,3 +559,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
|
||||
|
@ -23,30 +23,29 @@ if not core.get_http_api then
|
||||
return
|
||||
end
|
||||
|
||||
local color_backdrop = "#000c"
|
||||
local color_text_white = "#fff"
|
||||
local color_text_blue = "#22e0f6"
|
||||
local color_text_green = mt_color_green
|
||||
|
||||
-- Filter
|
||||
local search_string = ""
|
||||
local cur_page = 1
|
||||
local num_per_page = 5
|
||||
local filter_type = 1
|
||||
local filter_types_titles = {
|
||||
fgettext("All packages"),
|
||||
fgettext("Games"),
|
||||
fgettext("Mods"),
|
||||
fgettext("Texture packs"),
|
||||
}
|
||||
local filter_type
|
||||
|
||||
-- Automatic package installation
|
||||
local auto_install_spec = nil
|
||||
|
||||
local filter_types_type = {
|
||||
nil,
|
||||
"game",
|
||||
"mod",
|
||||
"txp",
|
||||
|
||||
local filter_type_names = {
|
||||
{ "type_all", nil },
|
||||
{ "type_game", "game" },
|
||||
{ "type_mod", "mod" },
|
||||
{ "type_txp", "txp" },
|
||||
}
|
||||
|
||||
|
||||
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 +65,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 +82,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()
|
||||
@ -154,7 +153,7 @@ end
|
||||
local function sort_and_filter_pkgs()
|
||||
contentdb.update_paths()
|
||||
contentdb.sort_packages()
|
||||
contentdb.filter_packages(search_string, filter_types_type[filter_type])
|
||||
contentdb.filter_packages(search_string, filter_type)
|
||||
|
||||
local auto_install_pkg = resolve_auto_install_spec()
|
||||
if auto_install_pkg then
|
||||
@ -185,72 +184,130 @@ local function load()
|
||||
end
|
||||
|
||||
|
||||
local function get_info_formspec(text)
|
||||
local H = 9.5
|
||||
local function get_info_formspec(size, safezone_left, text)
|
||||
return table.concat({
|
||||
"formspec_version[6]",
|
||||
"size[15.75,9.5]",
|
||||
core.settings:get_bool("enable_touch") and "padding[0.01,0.01]" or "position[0.5,0.55]",
|
||||
"size[", size.x, ",", size.y, "]",
|
||||
"padding[-0.01,-0.01]",
|
||||
|
||||
"label[4,4.35;", text, "]",
|
||||
"container[0,", H - 0.8 - 0.375, "]",
|
||||
"button[0.375,0;5,0.8;back;", fgettext("Back to Main Menu"), "]",
|
||||
"label[", safezone_left + 3.625, ",4.35;", text, "]",
|
||||
"container[0,", size.y - 0.8 - 0.375, "]",
|
||||
"button[", safezone_left, ",0;4,0.8;back;", fgettext("Back"), "]",
|
||||
"container_end[]",
|
||||
})
|
||||
end
|
||||
|
||||
|
||||
-- Determines how to fit `num_per_page` into `size` space
|
||||
local function fit_cells(num_per_page, size)
|
||||
local cell_spacing = 0.25
|
||||
local desired_size = 4.5
|
||||
local row_cells = math.min(5, math.floor(size.x / desired_size))
|
||||
local cell_w, cell_h
|
||||
-- Fit cells into the available height
|
||||
while true do
|
||||
cell_w = (size.x - (row_cells-1)*cell_spacing) / row_cells
|
||||
cell_h = cell_w * 2 / 3
|
||||
|
||||
local required_height = math.ceil(num_per_page / row_cells) * (cell_h + cell_spacing) - cell_spacing
|
||||
-- Add 0.1 to be more lenient
|
||||
if required_height <= size.y + 0.1 then
|
||||
break
|
||||
end
|
||||
|
||||
row_cells = row_cells + 1
|
||||
end
|
||||
|
||||
return cell_spacing, row_cells, cell_w, cell_h
|
||||
end
|
||||
|
||||
|
||||
local function get_formspec(dlgdata)
|
||||
local safezone_left = PLATFORM == "Android" and 1 or 0.375
|
||||
local window = core.get_window_info()
|
||||
local size = { x = window.max_formspec_size.x, y = window.max_formspec_size.y }
|
||||
|
||||
if contentdb.loading then
|
||||
return get_info_formspec(fgettext("Loading..."))
|
||||
return get_info_formspec(size, safezone_left, fgettext("Loading..."))
|
||||
end
|
||||
if contentdb.load_error then
|
||||
return get_info_formspec(fgettext("No packages could be retrieved"))
|
||||
return get_info_formspec(size, safezone_left, fgettext("No packages could be retrieved"))
|
||||
end
|
||||
assert(contentdb.load_ok)
|
||||
|
||||
contentdb.update_paths()
|
||||
|
||||
local num_per_page = dlgdata.num_per_page
|
||||
dlgdata.pagemax = math.max(math.ceil(#contentdb.packages / num_per_page), 1)
|
||||
if cur_page > dlgdata.pagemax then
|
||||
cur_page = 1
|
||||
end
|
||||
|
||||
local W = 15.75
|
||||
local H = 9.5
|
||||
local W = size.x - safezone_left
|
||||
local H = size.y
|
||||
|
||||
local category_x = 0
|
||||
local number_category_buttons = 4
|
||||
local max_button_w = (W - 0.375 - 0.25 - 7) / number_category_buttons
|
||||
local category_button_w = math.min(max_button_w, 3)
|
||||
local function make_category_button(name, label, selected)
|
||||
category_x = category_x + 1
|
||||
local color = selected and mt_color_green or ""
|
||||
return ("style[%s;bgcolor=%s]button[%f,0;%f,0.8;%s;%s]"):format(name, color,
|
||||
(category_x - 1) * category_button_w, category_button_w, name, label)
|
||||
end
|
||||
|
||||
|
||||
local selected_type = filter_type
|
||||
|
||||
local search_box_width = W - 0.375 - 0.25 - 2*0.8
|
||||
- number_category_buttons * category_button_w
|
||||
local formspec = {
|
||||
"formspec_version[6]",
|
||||
"size[15.75,9.5]",
|
||||
core.settings:get_bool("enable_touch") and "padding[0.01,0.01]" or "position[0.5,0.55]",
|
||||
"formspec_version[7]",
|
||||
"size[", size.x, ",", size.y, "]",
|
||||
"padding[-0.01,-0.01]",
|
||||
|
||||
"style[status,downloading,queued;border=false]",
|
||||
"container[", safezone_left, ",0]",
|
||||
|
||||
"container[0.375,0.375]",
|
||||
"field[0,0;7.225,0.8;search_string;;", core.formspec_escape(search_string), "]",
|
||||
-- Top-left: categories
|
||||
"container[0,0.375]",
|
||||
make_category_button("type_all", fgettext("All"), selected_type == nil),
|
||||
make_category_button("type_game", fgettext("Games"), selected_type == "game"),
|
||||
make_category_button("type_mod", fgettext("Mods"), selected_type == "mod"),
|
||||
make_category_button("type_txp", fgettext("Texture Packs"), selected_type == "txp"),
|
||||
"container_end[]",
|
||||
|
||||
-- Top-right: Search
|
||||
"container[", W - 0.375 - search_box_width - 0.8*2, ",0.375]",
|
||||
"field[0,0;", search_box_width, ",0.8;search_string;;", core.formspec_escape(search_string), "]",
|
||||
"field_enter_after_edit[search_string;true]",
|
||||
"image_button[7.3,0;0.8,0.8;", core.formspec_escape(defaulttexturedir .. "search.png"), ";search;]",
|
||||
"image_button[8.125,0;0.8,0.8;", core.formspec_escape(defaulttexturedir .. "clear.png"), ";clear;]",
|
||||
"dropdown[9.175,0;2.7875,0.8;type;", table.concat(filter_types_titles, ","), ";", filter_type, "]",
|
||||
"image_button[", search_box_width, ",0;0.8,0.8;",
|
||||
core.formspec_escape(defaulttexturedir .. "search.png"), ";search;]",
|
||||
"image_button[", search_box_width + 0.8, ",0;0.8,0.8;",
|
||||
core.formspec_escape(defaulttexturedir .. "clear.png"), ";clear;]",
|
||||
"container_end[]",
|
||||
|
||||
-- Page nav buttons
|
||||
-- Bottom strip start
|
||||
"container[0,", H - 0.8 - 0.375, "]",
|
||||
"button[0.375,0;5,0.8;back;", fgettext("Back to Main Menu"), "]",
|
||||
"button[0,0;4,0.8;back;", fgettext("Back to Main Menu"), "]",
|
||||
|
||||
"container[", W - 0.375 - 0.8*4 - 2, ",0]",
|
||||
"image_button[0,0;0.8,0.8;", core.formspec_escape(defaulttexturedir), "start_icon.png;pstart;]",
|
||||
"image_button[0.8,0;0.8,0.8;", core.formspec_escape(defaulttexturedir), "prev_icon.png;pback;]",
|
||||
-- Bottom-center: Page nav buttons
|
||||
"container[", (W - 1*4 - 2) / 2, ",0]",
|
||||
"image_button[0,0;1,0.8;", core.formspec_escape(defaulttexturedir), "start_icon.png;pstart;]",
|
||||
"image_button[1,0;1,0.8;", core.formspec_escape(defaulttexturedir), "prev_icon.png;pback;]",
|
||||
"style[pagenum;border=false]",
|
||||
"button[1.6,0;2,0.8;pagenum;", tonumber(cur_page), " / ", tonumber(dlgdata.pagemax), "]",
|
||||
"image_button[3.6,0;0.8,0.8;", core.formspec_escape(defaulttexturedir), "next_icon.png;pnext;]",
|
||||
"image_button[4.4,0;0.8,0.8;", core.formspec_escape(defaulttexturedir), "end_icon.png;pend;]",
|
||||
"container_end[]",
|
||||
"button[2,0;2,0.8;pagenum;", tonumber(cur_page), " / ", tonumber(dlgdata.pagemax), "]",
|
||||
"image_button[4,0;1,0.8;", core.formspec_escape(defaulttexturedir), "next_icon.png;pnext;]",
|
||||
"image_button[5,0;1,0.8;", core.formspec_escape(defaulttexturedir), "end_icon.png;pend;]",
|
||||
"container_end[]", -- page nav end
|
||||
|
||||
"container_end[]",
|
||||
-- Bottom-right: updating
|
||||
"container[", W - 0.375 - 3, ",0]",
|
||||
"style[status,downloading,queued;border=false]",
|
||||
}
|
||||
|
||||
if contentdb.number_downloading > 0 then
|
||||
formspec[#formspec + 1] = "button[12.5875,0.375;2.7875,0.8;downloading;"
|
||||
formspec[#formspec + 1] = "button[0,0;3,0.8;downloading;"
|
||||
if #contentdb.download_queue > 0 then
|
||||
formspec[#formspec + 1] = fgettext("$1 downloading,\n$2 queued",
|
||||
contentdb.number_downloading, #contentdb.download_queue)
|
||||
@ -269,16 +326,19 @@ local function get_formspec(dlgdata)
|
||||
end
|
||||
|
||||
if num_avail_updates == 0 then
|
||||
formspec[#formspec + 1] = "button[12.5875,0.375;2.7875,0.8;status;"
|
||||
formspec[#formspec + 1] = "button[0,0;3,0.8;status;"
|
||||
formspec[#formspec + 1] = fgettext("No updates")
|
||||
formspec[#formspec + 1] = "]"
|
||||
else
|
||||
formspec[#formspec + 1] = "button[12.5875,0.375;2.7875,0.8;update_all;"
|
||||
formspec[#formspec + 1] = "button[0,0;3,0.8;update_all;"
|
||||
formspec[#formspec + 1] = fgettext("Update All [$1]", num_avail_updates)
|
||||
formspec[#formspec + 1] = "]"
|
||||
end
|
||||
end
|
||||
|
||||
formspec[#formspec + 1] = "container_end[]" -- updating end
|
||||
formspec[#formspec + 1] = "container_end[]" -- bottom strip end
|
||||
|
||||
if #contentdb.packages == 0 then
|
||||
formspec[#formspec + 1] = "label[4,4.75;"
|
||||
formspec[#formspec + 1] = fgettext("No results")
|
||||
@ -290,81 +350,91 @@ local function get_formspec(dlgdata)
|
||||
formspec[#formspec + 1] = "tooltip[downloading;" .. fgettext("Downloading...") .. tooltip_colors
|
||||
formspec[#formspec + 1] = "tooltip[queued;" .. fgettext("Queued") .. tooltip_colors
|
||||
|
||||
formspec[#formspec + 1] = "container[0,1.425]"
|
||||
|
||||
local cell_spacing, row_cells, cell_w, cell_h = fit_cells(num_per_page, {
|
||||
x = W - 0.375,
|
||||
y = H - 1.425 - 0.25 - 0.8 - 0.375
|
||||
})
|
||||
|
||||
local start_idx = (cur_page - 1) * num_per_page + 1
|
||||
for i=start_idx, math.min(#contentdb.packages, start_idx+num_per_page-1) do
|
||||
local package = contentdb.packages[i]
|
||||
local container_y = (i - start_idx) * 1.375 + (2*0.375 + 0.8)
|
||||
formspec[#formspec + 1] = "container[0.375,"
|
||||
formspec[#formspec + 1] = container_y
|
||||
formspec[#formspec + 1] = "]"
|
||||
|
||||
-- image
|
||||
formspec[#formspec + 1] = "image[0,0;1.5,1;"
|
||||
formspec[#formspec + 1] = core.formspec_escape(get_screenshot(package))
|
||||
formspec[#formspec + 1] = "]"
|
||||
|
||||
-- title
|
||||
formspec[#formspec + 1] = "label[1.875,0.1;"
|
||||
formspec[#formspec + 1] = core.formspec_escape(
|
||||
core.colorize(mt_color_green, package.title) ..
|
||||
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
|
||||
local textcolor = color_text_white
|
||||
if package.path then
|
||||
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
|
||||
textcolor = color_text_blue
|
||||
else
|
||||
textcolor = color_text_green
|
||||
end
|
||||
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
|
||||
table.insert_all(formspec, {
|
||||
"container[",
|
||||
(cell_w + cell_spacing) * ((i - start_idx) % row_cells),
|
||||
",",
|
||||
(cell_h + cell_spacing) * math.floor((i - start_idx) / row_cells),
|
||||
"]",
|
||||
|
||||
-- image,
|
||||
"image_button[0,0;", cell_w, ",", cell_h, ";",
|
||||
core.formspec_escape(get_screenshot(package, package.thumbnail, 2)),
|
||||
";view_", i, ";;;false]",
|
||||
|
||||
--"style[title_", i, ";border=false]",
|
||||
-- The 0.01 here fixes a single line of image pixels appearing below the box
|
||||
"box[0,", cell_h - 0.5 + 0.01, ";", cell_w, ",0.5;", color_backdrop, "]",
|
||||
|
||||
"style_type[button;font_size=*1.1;border=false]",
|
||||
"button[0.25,", cell_h - 0.5, ";", cell_w - 0.5, ",0.5;title_", i ,";",
|
||||
core.formspec_escape(core.colorize(textcolor, package.title)), "]",
|
||||
"style_type[button;font_size=;border=;textcolor=]",
|
||||
})
|
||||
|
||||
if package.featured then
|
||||
table.insert_all(formspec, {
|
||||
"tooltip[0,0;0.8,0.8;", fgettext("Featured"), "]",
|
||||
"image[0.2,0.2;0.4,0.4;", defaulttexturedir, "server_favorite.png]",
|
||||
})
|
||||
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[]"
|
||||
table.insert_all(formspec, {
|
||||
"container[", cell_w - 0.5,",", cell_h - 0.5, "]",
|
||||
})
|
||||
|
||||
-- description
|
||||
formspec[#formspec + 1] = "textarea[1.855,0.3;"
|
||||
formspec[#formspec + 1] = tostring(description_width)
|
||||
formspec[#formspec + 1] = ",0.8;;;"
|
||||
formspec[#formspec + 1] = core.formspec_escape(package.short_description)
|
||||
formspec[#formspec + 1] = "]"
|
||||
if package.downloading then
|
||||
table.insert_all(formspec, {
|
||||
"animated_image[0,0;0.5,0.5;downloading;", defaulttexturedir, "cdb_downloading.png;3;400;;]",
|
||||
})
|
||||
elseif package.queued then
|
||||
table.insert_all(formspec, {
|
||||
"image[0,0;0.5,0.5;", defaulttexturedir, "cdb_queued.png]",
|
||||
})
|
||||
elseif package.path then
|
||||
if package.installed_release < package.release then
|
||||
table.insert_all(formspec, {
|
||||
"image[0,0;0.5,0.5;", defaulttexturedir, "cdb_update.png]",
|
||||
})
|
||||
else
|
||||
table.insert_all(formspec, {
|
||||
"image[0.1,0.1;0.3,0.3;", defaulttexturedir, "checkbox_64.png]",
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
formspec[#formspec + 1] = "container_end[]"
|
||||
local tooltip = package.short_description
|
||||
|
||||
table.insert_all(formspec, {
|
||||
"container_end[]",
|
||||
"tooltip[0,0;", cell_w, ",", cell_h, ";", core.formspec_escape(tooltip), "]",
|
||||
"container_end[]",
|
||||
})
|
||||
end
|
||||
|
||||
formspec[#formspec + 1] = "container_end[]"
|
||||
formspec[#formspec + 1] = "container_end[]"
|
||||
|
||||
return table.concat(formspec)
|
||||
end
|
||||
|
||||
@ -373,14 +443,14 @@ local function handle_submit(this, fields)
|
||||
if fields.search or fields.key_enter_field == "search_string" then
|
||||
search_string = fields.search_string:trim()
|
||||
cur_page = 1
|
||||
contentdb.filter_packages(search_string, filter_types_type[filter_type])
|
||||
contentdb.filter_packages(search_string, filter_type)
|
||||
return true
|
||||
end
|
||||
|
||||
if fields.clear then
|
||||
search_string = ""
|
||||
cur_page = 1
|
||||
contentdb.filter_packages("", filter_types_type[filter_type])
|
||||
contentdb.filter_packages("", filter_type)
|
||||
return true
|
||||
end
|
||||
|
||||
@ -416,12 +486,11 @@ local function handle_submit(this, fields)
|
||||
return true
|
||||
end
|
||||
|
||||
if fields.type then
|
||||
local new_type = table.indexof(filter_types_titles, fields.type)
|
||||
if new_type ~= filter_type then
|
||||
filter_type = new_type
|
||||
for _, pair in ipairs(filter_type_names) do
|
||||
if fields[pair[1]] then
|
||||
filter_type = pair[2]
|
||||
cur_page = 1
|
||||
contentdb.filter_packages(search_string, filter_types_type[filter_type])
|
||||
contentdb.filter_packages(search_string, filter_type)
|
||||
return true
|
||||
end
|
||||
end
|
||||
@ -437,32 +506,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] or fields["title_" .. i] or fields["author_" .. 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
|
||||
@ -494,17 +551,7 @@ end
|
||||
function create_contentdb_dlg(type, install_spec)
|
||||
search_string = ""
|
||||
cur_page = 1
|
||||
if type then
|
||||
-- table.indexof does not work on tables that contain `nil`
|
||||
for i, v in pairs(filter_types_type) do
|
||||
if v == type then
|
||||
filter_type = i
|
||||
break
|
||||
end
|
||||
end
|
||||
else
|
||||
filter_type = 1
|
||||
end
|
||||
filter_type = type
|
||||
|
||||
-- Keep the old auto_install_spec if the caller doesn't specify one.
|
||||
if install_spec then
|
||||
@ -513,8 +560,10 @@ function create_contentdb_dlg(type, install_spec)
|
||||
|
||||
load()
|
||||
|
||||
return dialog_create("contentdb",
|
||||
local dlg = dialog_create("contentdb",
|
||||
get_formspec,
|
||||
handle_submit,
|
||||
handle_events)
|
||||
dlg.data.num_per_page = core.settings:get_bool("enable_touch") and 8 or 15
|
||||
return dlg
|
||||
end
|
||||
|
316
builtin/mainmenu/content/dlg_package.lua
Normal file
316
builtin/mainmenu/content/dlg_package.lua
Normal file
@ -0,0 +1,316 @@
|
||||
--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
|
||||
|
||||
|
||||
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()
|
||||
return
|
||||
end
|
||||
|
||||
if info.forums then
|
||||
info.forums = "https://forum.minetest.net/viewtopic.php?t=" .. info.forums
|
||||
end
|
||||
|
||||
if 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), "]",
|
||||
}
|
||||
|
||||
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]",
|
||||
})
|
||||
|
||||
local safezone_right = PLATFORM == "Android" and 0.375 or 0
|
||||
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.375,0;",
|
||||
size.x - 2*0.375 - safezone_right, ",",
|
||||
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.375,0;", size.x - 2*0.375 - safezone_right, ",", 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
|
||||
|
||||
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
|
||||
|
||||
-- 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.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 .. "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"
|
||||
|
@ -3902,6 +3902,7 @@ Operators
|
||||
---------
|
||||
|
||||
Operators can be used if all of the involved vectors have metatables:
|
||||
|
||||
* `v1 == v2`:
|
||||
* Returns whether `v1` and `v2` are identical.
|
||||
* `-v`:
|
||||
@ -6460,6 +6461,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:
|
||||
|
@ -48,7 +48,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.
|
||||
|
@ -200,7 +200,7 @@ class IGUIButton : public IGUIElement
|
||||
\param loop: True if the animation should loop, false if not
|
||||
\param scale: True if the sprite should scale to button size, false if not */
|
||||
virtual void setSprite(EGUI_BUTTON_STATE state, s32 index,
|
||||
video::SColor color = video::SColor(255, 255, 255, 255), bool loop = false, bool scale = false) = 0;
|
||||
video::SColor color = video::SColor(255, 255, 255, 255), bool loop = false) = 0;
|
||||
|
||||
//! Get the sprite-index for the given state or -1 when no sprite is set
|
||||
virtual s32 getSpriteIndex(EGUI_BUTTON_STATE state) const = 0;
|
||||
@ -211,9 +211,6 @@ class IGUIButton : public IGUIElement
|
||||
//! Returns if the sprite in the given state does loop
|
||||
virtual bool getSpriteLoop(EGUI_BUTTON_STATE state) const = 0;
|
||||
|
||||
//! Returns if the sprite in the given state is scaled
|
||||
virtual bool getSpriteScale(EGUI_BUTTON_STATE state) const = 0;
|
||||
|
||||
//! Sets if the button should behave like a push button.
|
||||
/** Which means it can be in two states: Normal or Pressed. With a click on the button,
|
||||
the user can change the state of the button. */
|
||||
|
@ -374,6 +374,12 @@ const c8 *const GUISkinFontNames[EGDF_COUNT + 1] = {
|
||||
class IGUISkin : virtual public IReferenceCounted
|
||||
{
|
||||
public:
|
||||
//! returns display density scaling factor
|
||||
virtual float getScale() const = 0;
|
||||
|
||||
//! sets display density scaling factor
|
||||
virtual void setScale(float scale) = 0;
|
||||
|
||||
//! returns default color
|
||||
virtual video::SColor getColor(EGUI_DEFAULT_COLOR color) const = 0;
|
||||
|
||||
|
@ -75,12 +75,11 @@ void CGUIButton::setSpriteBank(IGUISpriteBank *sprites)
|
||||
SpriteBank = sprites;
|
||||
}
|
||||
|
||||
void CGUIButton::setSprite(EGUI_BUTTON_STATE state, s32 index, video::SColor color, bool loop, bool scale)
|
||||
void CGUIButton::setSprite(EGUI_BUTTON_STATE state, s32 index, video::SColor color, bool loop)
|
||||
{
|
||||
ButtonSprites[(u32)state].Index = index;
|
||||
ButtonSprites[(u32)state].Color = color;
|
||||
ButtonSprites[(u32)state].Loop = loop;
|
||||
ButtonSprites[(u32)state].Scale = scale;
|
||||
}
|
||||
|
||||
//! Get the sprite-index for the given state or -1 when no sprite is set
|
||||
@ -101,12 +100,6 @@ bool CGUIButton::getSpriteLoop(EGUI_BUTTON_STATE state) const
|
||||
return ButtonSprites[(u32)state].Loop;
|
||||
}
|
||||
|
||||
//! Returns if the sprite in the given state is scaled
|
||||
bool CGUIButton::getSpriteScale(EGUI_BUTTON_STATE state) const
|
||||
{
|
||||
return ButtonSprites[(u32)state].Scale;
|
||||
}
|
||||
|
||||
//! called if an event happened.
|
||||
bool CGUIButton::OnEvent(const SEvent &event)
|
||||
{
|
||||
@ -294,19 +287,26 @@ void CGUIButton::draw()
|
||||
void CGUIButton::drawSprite(EGUI_BUTTON_STATE state, u32 startTime, const core::position2di ¢er)
|
||||
{
|
||||
u32 stateIdx = (u32)state;
|
||||
s32 spriteIdx = ButtonSprites[stateIdx].Index;
|
||||
if (spriteIdx == -1)
|
||||
return;
|
||||
|
||||
if (ButtonSprites[stateIdx].Index != -1) {
|
||||
if (ButtonSprites[stateIdx].Scale) {
|
||||
const video::SColor colors[] = {ButtonSprites[stateIdx].Color, ButtonSprites[stateIdx].Color, ButtonSprites[stateIdx].Color, ButtonSprites[stateIdx].Color};
|
||||
SpriteBank->draw2DSprite(ButtonSprites[stateIdx].Index, AbsoluteRect,
|
||||
&AbsoluteClippingRect, colors,
|
||||
os::Timer::getTime() - startTime, ButtonSprites[stateIdx].Loop);
|
||||
} else {
|
||||
SpriteBank->draw2DSprite(ButtonSprites[stateIdx].Index, center,
|
||||
&AbsoluteClippingRect, ButtonSprites[stateIdx].Color, startTime, os::Timer::getTime(),
|
||||
ButtonSprites[stateIdx].Loop, true);
|
||||
}
|
||||
}
|
||||
u32 rectIdx = SpriteBank->getSprites()[spriteIdx].Frames[0].rectNumber;
|
||||
core::rect<s32> srcRect = SpriteBank->getPositions()[rectIdx];
|
||||
|
||||
IGUISkin *skin = Environment->getSkin();
|
||||
s32 scale = std::max(std::floor(skin->getScale()), 1.0f);
|
||||
core::rect<s32> rect(center, srcRect.getSize() * scale);
|
||||
rect -= rect.getSize() / 2;
|
||||
|
||||
const video::SColor colors[] = {
|
||||
ButtonSprites[stateIdx].Color,
|
||||
ButtonSprites[stateIdx].Color,
|
||||
ButtonSprites[stateIdx].Color,
|
||||
ButtonSprites[stateIdx].Color,
|
||||
};
|
||||
SpriteBank->draw2DSprite(spriteIdx, rect, &AbsoluteClippingRect, colors,
|
||||
os::Timer::getTime() - startTime, ButtonSprites[stateIdx].Loop);
|
||||
}
|
||||
|
||||
EGUI_BUTTON_IMAGE_STATE CGUIButton::getImageState(bool pressed) const
|
||||
|
@ -92,7 +92,7 @@ class CGUIButton : public IGUIButton
|
||||
*/
|
||||
virtual void setSprite(EGUI_BUTTON_STATE state, s32 index,
|
||||
video::SColor color = video::SColor(255, 255, 255, 255),
|
||||
bool loop = false, bool scale = false) override;
|
||||
bool loop = false) override;
|
||||
|
||||
//! Get the sprite-index for the given state or -1 when no sprite is set
|
||||
s32 getSpriteIndex(EGUI_BUTTON_STATE state) const override;
|
||||
@ -103,9 +103,6 @@ class CGUIButton : public IGUIButton
|
||||
//! Returns if the sprite in the given state does loop
|
||||
bool getSpriteLoop(EGUI_BUTTON_STATE state) const override;
|
||||
|
||||
//! Returns if the sprite in the given state is scaled
|
||||
bool getSpriteScale(EGUI_BUTTON_STATE state) const override;
|
||||
|
||||
//! Sets if the button should behave like a push button. Which means it
|
||||
//! can be in two states: Normal or Pressed. With a click on the button,
|
||||
//! the user can change the state of the button.
|
||||
@ -158,19 +155,18 @@ class CGUIButton : public IGUIButton
|
||||
struct ButtonSprite
|
||||
{
|
||||
ButtonSprite() :
|
||||
Index(-1), Loop(false), Scale(false)
|
||||
Index(-1), Loop(false)
|
||||
{
|
||||
}
|
||||
|
||||
bool operator==(const ButtonSprite &other) const
|
||||
{
|
||||
return Index == other.Index && Color == other.Color && Loop == other.Loop && Scale == other.Scale;
|
||||
return Index == other.Index && Color == other.Color && Loop == other.Loop;
|
||||
}
|
||||
|
||||
s32 Index;
|
||||
video::SColor Color;
|
||||
bool Loop;
|
||||
bool Scale;
|
||||
};
|
||||
|
||||
ButtonSprite ButtonSprites[EGBS_COUNT];
|
||||
|
@ -24,6 +24,12 @@ class CGUISkin : public IGUISkin
|
||||
//! destructor
|
||||
virtual ~CGUISkin();
|
||||
|
||||
//! returns display density scaling factor
|
||||
virtual float getScale() const override { return Scale; }
|
||||
|
||||
//! sets display density scaling factor
|
||||
virtual void setScale(float scale) override { Scale = scale; }
|
||||
|
||||
//! returns default color
|
||||
video::SColor getColor(EGUI_DEFAULT_COLOR color) const override;
|
||||
|
||||
@ -210,6 +216,7 @@ class CGUISkin : public IGUISkin
|
||||
EGUI_SKIN_TYPE getType() const override;
|
||||
|
||||
private:
|
||||
float Scale = 1.0f;
|
||||
video::SColor Colors[EGDC_COUNT];
|
||||
s32 Sizes[EGDS_COUNT];
|
||||
u32 Icons[EGDI_COUNT];
|
||||
|
@ -129,9 +129,9 @@ EM_BOOL CIrrDeviceSDL::MouseLeaveCallback(int eventType, const EmscriptenMouseEv
|
||||
}
|
||||
#endif
|
||||
|
||||
bool CIrrDeviceSDL::keyIsKnownSpecial(EKEY_CODE key)
|
||||
bool CIrrDeviceSDL::keyIsKnownSpecial(EKEY_CODE irrlichtKey)
|
||||
{
|
||||
switch (key) {
|
||||
switch (irrlichtKey) {
|
||||
// keys which are known to have safe special character interpretation
|
||||
// could need changes over time (removals and additions!)
|
||||
case KEY_RETURN:
|
||||
@ -189,24 +189,68 @@ bool CIrrDeviceSDL::keyIsKnownSpecial(EKEY_CODE key)
|
||||
}
|
||||
}
|
||||
|
||||
int CIrrDeviceSDL::findCharToPassToIrrlicht(int assumedChar, EKEY_CODE key)
|
||||
int CIrrDeviceSDL::findCharToPassToIrrlicht(uint32_t sdlKey, EKEY_CODE irrlichtKey, bool numlock)
|
||||
{
|
||||
switch (irrlichtKey) {
|
||||
// special cases that always return a char regardless of how the SDL keycode
|
||||
// looks
|
||||
switch (key) {
|
||||
case KEY_RETURN:
|
||||
case KEY_ESCAPE:
|
||||
return (int)key;
|
||||
return (int)irrlichtKey;
|
||||
|
||||
// This is necessary for keys on the numpad because they don't use the same
|
||||
// keycodes as their non-numpad versions (whose keycodes correspond to chars),
|
||||
// but have their own SDL keycodes and their own Irrlicht keycodes (which
|
||||
// don't correspond to chars).
|
||||
case KEY_MULTIPLY:
|
||||
return '*';
|
||||
case KEY_ADD:
|
||||
return '+';
|
||||
case KEY_SUBTRACT:
|
||||
return '-';
|
||||
case KEY_DIVIDE:
|
||||
return '/';
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (numlock) {
|
||||
// Number keys on the numpad are also affected, but we only want them
|
||||
// to produce number chars when numlock is enabled.
|
||||
switch (irrlichtKey) {
|
||||
case KEY_NUMPAD0:
|
||||
return '0';
|
||||
case KEY_NUMPAD1:
|
||||
return '1';
|
||||
case KEY_NUMPAD2:
|
||||
return '2';
|
||||
case KEY_NUMPAD3:
|
||||
return '3';
|
||||
case KEY_NUMPAD4:
|
||||
return '4';
|
||||
case KEY_NUMPAD5:
|
||||
return '5';
|
||||
case KEY_NUMPAD6:
|
||||
return '6';
|
||||
case KEY_NUMPAD7:
|
||||
return '7';
|
||||
case KEY_NUMPAD8:
|
||||
return '8';
|
||||
case KEY_NUMPAD9:
|
||||
return '9';
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// SDL in-place ORs values with no character representation with 1<<30
|
||||
// https://wiki.libsdl.org/SDL2/SDLKeycodeLookup
|
||||
if (assumedChar & (1 << 30))
|
||||
// This also affects the numpad keys btw.
|
||||
if (sdlKey & (1 << 30))
|
||||
return 0;
|
||||
|
||||
switch (key) {
|
||||
switch (irrlichtKey) {
|
||||
case KEY_PRIOR:
|
||||
case KEY_NEXT:
|
||||
case KEY_HOME:
|
||||
@ -218,7 +262,7 @@ int CIrrDeviceSDL::findCharToPassToIrrlicht(int assumedChar, EKEY_CODE key)
|
||||
case KEY_NUMLOCK:
|
||||
return 0;
|
||||
default:
|
||||
return assumedChar;
|
||||
return sdlKey;
|
||||
}
|
||||
}
|
||||
|
||||
@ -825,7 +869,8 @@ bool CIrrDeviceSDL::run()
|
||||
irrevent.KeyInput.PressedDown = (SDL_event.type == SDL_KEYDOWN);
|
||||
irrevent.KeyInput.Shift = (SDL_event.key.keysym.mod & KMOD_SHIFT) != 0;
|
||||
irrevent.KeyInput.Control = (SDL_event.key.keysym.mod & KMOD_CTRL) != 0;
|
||||
irrevent.KeyInput.Char = findCharToPassToIrrlicht(mp.SDLKey, key);
|
||||
irrevent.KeyInput.Char = findCharToPassToIrrlicht(mp.SDLKey, key,
|
||||
(SDL_event.key.keysym.mod & KMOD_NUM) != 0);
|
||||
postEventFromUser(irrevent);
|
||||
} break;
|
||||
|
||||
|
@ -273,10 +273,10 @@ class CIrrDeviceSDL : public CIrrDeviceStub
|
||||
|
||||
#endif
|
||||
// Check if a key is a known special character with no side effects on text boxes.
|
||||
static bool keyIsKnownSpecial(EKEY_CODE key);
|
||||
static bool keyIsKnownSpecial(EKEY_CODE irrlichtKey);
|
||||
|
||||
// Return the Char that should be sent to Irrlicht for the given key (either the one passed in or 0).
|
||||
static int findCharToPassToIrrlicht(int assumedChar, EKEY_CODE key);
|
||||
static int findCharToPassToIrrlicht(uint32_t sdlKey, EKEY_CODE irrlichtKey, bool numlock);
|
||||
|
||||
// Check if a text box is in focus. Enable or disable SDL_TEXTINPUT events only if in focus.
|
||||
void resetReceiveTextInputEvents();
|
||||
|
@ -352,6 +352,7 @@ void ClientLauncher::config_guienv()
|
||||
|
||||
float density = rangelim(g_settings->getFloat("gui_scaling"), 0.5f, 20) *
|
||||
RenderingEngine::getDisplayDensity();
|
||||
skin->setScale(density);
|
||||
skin->setSize(gui::EGDS_CHECK_BOX_WIDTH, (s32)(17.0f * density));
|
||||
skin->setSize(gui::EGDS_SCROLLBAR_SIZE, (s32)(21.0f * density));
|
||||
skin->setSize(gui::EGDS_WINDOW_BUTTON_WIDTH, (s32)(15.0f * density));
|
||||
|
@ -89,12 +89,11 @@ void GUIButton::setSpriteBank(IGUISpriteBank* sprites)
|
||||
SpriteBank = sprites;
|
||||
}
|
||||
|
||||
void GUIButton::setSprite(EGUI_BUTTON_STATE state, s32 index, video::SColor color, bool loop, bool scale)
|
||||
void GUIButton::setSprite(EGUI_BUTTON_STATE state, s32 index, video::SColor color, bool loop)
|
||||
{
|
||||
ButtonSprites[(u32)state].Index = index;
|
||||
ButtonSprites[(u32)state].Color = color;
|
||||
ButtonSprites[(u32)state].Loop = loop;
|
||||
ButtonSprites[(u32)state].Scale = scale;
|
||||
}
|
||||
|
||||
//! Get the sprite-index for the given state or -1 when no sprite is set
|
||||
@ -115,12 +114,6 @@ bool GUIButton::getSpriteLoop(EGUI_BUTTON_STATE state) const
|
||||
return ButtonSprites[(u32)state].Loop;
|
||||
}
|
||||
|
||||
//! Returns if the sprite in the given state is scaled
|
||||
bool GUIButton::getSpriteScale(EGUI_BUTTON_STATE state) const
|
||||
{
|
||||
return ButtonSprites[(u32)state].Scale;
|
||||
}
|
||||
|
||||
//! called if an event happened.
|
||||
bool GUIButton::OnEvent(const SEvent& event)
|
||||
{
|
||||
@ -354,23 +347,26 @@ void GUIButton::draw()
|
||||
void GUIButton::drawSprite(EGUI_BUTTON_STATE state, u32 startTime, const core::position2di& center)
|
||||
{
|
||||
u32 stateIdx = (u32)state;
|
||||
s32 spriteIdx = ButtonSprites[stateIdx].Index;
|
||||
if (spriteIdx == -1)
|
||||
return;
|
||||
|
||||
if (ButtonSprites[stateIdx].Index != -1)
|
||||
{
|
||||
if ( ButtonSprites[stateIdx].Scale )
|
||||
{
|
||||
const video::SColor colors[] = {ButtonSprites[stateIdx].Color,ButtonSprites[stateIdx].Color,ButtonSprites[stateIdx].Color,ButtonSprites[stateIdx].Color};
|
||||
SpriteBank->draw2DSprite(ButtonSprites[stateIdx].Index, AbsoluteRect.UpperLeftCorner,
|
||||
&AbsoluteClippingRect, colors[0], // FIXME: remove [0]
|
||||
porting::getTimeMs()-startTime, ButtonSprites[stateIdx].Loop);
|
||||
}
|
||||
else
|
||||
{
|
||||
SpriteBank->draw2DSprite(ButtonSprites[stateIdx].Index, center,
|
||||
&AbsoluteClippingRect, ButtonSprites[stateIdx].Color, startTime, porting::getTimeMs(),
|
||||
ButtonSprites[stateIdx].Loop, true);
|
||||
}
|
||||
}
|
||||
u32 rectIdx = SpriteBank->getSprites()[spriteIdx].Frames[0].rectNumber;
|
||||
core::rect<s32> srcRect = SpriteBank->getPositions()[rectIdx];
|
||||
|
||||
IGUISkin *skin = Environment->getSkin();
|
||||
s32 scale = std::max(std::floor(skin->getScale()), 1.0f);
|
||||
core::rect<s32> rect(center, srcRect.getSize() * scale);
|
||||
rect -= rect.getSize() / 2;
|
||||
|
||||
const video::SColor colors[] = {
|
||||
ButtonSprites[stateIdx].Color,
|
||||
ButtonSprites[stateIdx].Color,
|
||||
ButtonSprites[stateIdx].Color,
|
||||
ButtonSprites[stateIdx].Color,
|
||||
};
|
||||
SpriteBank->draw2DSprite(spriteIdx, rect, &AbsoluteClippingRect, colors,
|
||||
porting::getTimeMs() - startTime, ButtonSprites[stateIdx].Loop);
|
||||
}
|
||||
|
||||
EGUI_BUTTON_IMAGE_STATE GUIButton::getImageState(bool pressed) const
|
||||
|
@ -92,7 +92,7 @@ class GUIButton : public gui::IGUIButton
|
||||
*/
|
||||
virtual void setSprite(gui::EGUI_BUTTON_STATE state, s32 index,
|
||||
video::SColor color=video::SColor(255,255,255,255),
|
||||
bool loop=false, bool scale=false) override;
|
||||
bool loop=false) override;
|
||||
|
||||
//! Get the sprite-index for the given state or -1 when no sprite is set
|
||||
virtual s32 getSpriteIndex(gui::EGUI_BUTTON_STATE state) const override;
|
||||
@ -103,9 +103,6 @@ class GUIButton : public gui::IGUIButton
|
||||
//! Returns if the sprite in the given state does loop
|
||||
virtual bool getSpriteLoop(gui::EGUI_BUTTON_STATE state) const override;
|
||||
|
||||
//! Returns if the sprite in the given state is scaled
|
||||
virtual bool getSpriteScale(gui::EGUI_BUTTON_STATE state) const override;
|
||||
|
||||
//! Sets if the button should behave like a push button. Which means it
|
||||
//! can be in two states: Normal or Pressed. With a click on the button,
|
||||
//! the user can change the state of the button.
|
||||
@ -230,13 +227,12 @@ class GUIButton : public gui::IGUIButton
|
||||
{
|
||||
bool operator==(const ButtonSprite &other) const
|
||||
{
|
||||
return Index == other.Index && Color == other.Color && Loop == other.Loop && Scale == other.Scale;
|
||||
return Index == other.Index && Color == other.Color && Loop == other.Loop;
|
||||
}
|
||||
|
||||
s32 Index = -1;
|
||||
video::SColor Color;
|
||||
bool Loop = false;
|
||||
bool Scale = false;
|
||||
};
|
||||
|
||||
ButtonSprite ButtonSprites[gui::EGBS_COUNT];
|
||||
|
@ -27,6 +27,12 @@ namespace gui
|
||||
//! destructor
|
||||
virtual ~GUISkin();
|
||||
|
||||
//! returns display density scaling factor
|
||||
virtual float getScale() const { return Scale; }
|
||||
|
||||
//! sets display density scaling factor
|
||||
virtual void setScale(float scale) { Scale = scale; }
|
||||
|
||||
//! returns default color
|
||||
virtual video::SColor getColor(EGUI_DEFAULT_COLOR color) const;
|
||||
|
||||
@ -292,6 +298,7 @@ namespace gui
|
||||
|
||||
private:
|
||||
|
||||
float Scale = 1.0f;
|
||||
video::SColor Colors[EGDC_COUNT];
|
||||
s32 Sizes[EGDS_COUNT];
|
||||
u32 Icons[EGDI_COUNT];
|
||||
|
@ -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);
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 147 B |
Binary file not shown.
Before Width: | Height: | Size: 150 B |
Binary file not shown.
Before Width: | Height: | Size: 191 B |
Loading…
Reference in New Issue
Block a user