end
end
+local function get_raw_dependencies(package)
+ if package.raw_deps then
+ return package.raw_deps
+ end
+
+ local url_fmt = "/api/packages/%s/dependencies/?only_hard=1&protocol_version=%s&engine_version=%s"
+ local version = core.get_version()
+ local base_url = core.settings:get("contentdb_url")
+ local url = base_url .. url_fmt:format(package.id, core.get_max_supp_proto(), version.string)
+
+ local response = http.fetch_sync({ url = url })
+ if not response.succeeded then
+ return
+ end
+
+ local data = core.parse_json(response.data) or {}
+
+ local content_lookup = {}
+ for _, pkg in pairs(store.packages_full) do
+ content_lookup[pkg.id] = pkg
+ end
+
+ for id, raw_deps in pairs(data) do
+ local package2 = content_lookup[id:lower()]
+ if package2 and not package2.raw_deps then
+ package2.raw_deps = raw_deps
+
+ for _, dep in pairs(raw_deps) do
+ local packages = {}
+ for i=1, #dep.packages do
+ packages[#packages + 1] = content_lookup[dep.packages[i]:lower()]
+ end
+ dep.packages = packages
+ end
+ end
+ end
+
+ return package.raw_deps
+end
+
+local function has_hard_deps(raw_deps)
+ for i=1, #raw_deps do
+ if not raw_deps[i].is_optional then
+ return true
+ end
+ end
+
+ return false
+end
+
+-- Recursively resolve dependencies, given the installed mods
+local function resolve_dependencies_2(raw_deps, installed_mods, out)
+ local function resolve_dep(dep)
+ -- Check whether it's already installed
+ if installed_mods[dep.name] then
+ return {
+ is_optional = dep.is_optional,
+ name = dep.name,
+ installed = true,
+ }
+ end
+
+ -- Find exact name matches
+ local fallback
+ for _, package in pairs(dep.packages) do
+ if package.type ~= "game" then
+ if package.name == dep.name then
+ return {
+ is_optional = dep.is_optional,
+ name = dep.name,
+ installed = false,
+ package = package,
+ }
+ elseif not fallback then
+ fallback = package
+ end
+ end
+ end
+
+ -- Otherwise, find the first mod that fulfils it
+ if fallback then
+ return {
+ is_optional = dep.is_optional,
+ name = dep.name,
+ installed = false,
+ package = fallback,
+ }
+ end
+
+ return {
+ is_optional = dep.is_optional,
+ name = dep.name,
+ installed = false,
+ }
+ end
+
+ for _, dep in pairs(raw_deps) do
+ if not dep.is_optional and not out[dep.name] then
+ local result = resolve_dep(dep)
+ out[dep.name] = result
+ if result and result.package and not result.installed then
+ local raw_deps2 = get_raw_dependencies(result.package)
+ if raw_deps2 then
+ resolve_dependencies_2(raw_deps2, installed_mods, out)
+ end
+ end
+ end
+ end
+
+ return true
+end
+
+-- Resolve dependencies for a package, calls the recursive version.
+local function resolve_dependencies(raw_deps, game)
+ assert(game)
+
+ local installed_mods = {}
+
+ local mods = {}
+ pkgmgr.get_game_mods(game, mods)
+ for _, mod in pairs(mods) do
+ installed_mods[mod.name] = true
+ end
+
+ for _, mod in pairs(pkgmgr.global_mods:get_list()) do
+ installed_mods[mod.name] = true
+ end
+
+ local out = {}
+ if not resolve_dependencies_2(raw_deps, installed_mods, out) then
+ return nil
+ end
+
+ local retval = {}
+ for _, dep in pairs(out) do
+ retval[#retval + 1] = dep
+ end
+
+ table.sort(retval, function(a, b)
+ return a.name < b.name
+ end)
+
+ return retval
+end
+
+local install_dialog = {}
+function install_dialog.get_formspec()
+ local package = install_dialog.package
+ local raw_deps = install_dialog.raw_deps
+ local will_install_deps = install_dialog.will_install_deps
+
+ local selected_game_idx = 1
+ local selected_gameid = core.settings:get("menu_last_game")
+ local games = table.copy(pkgmgr.games)
+ for i=1, #games do
+ if selected_gameid and games[i].id == selected_gameid then
+ selected_game_idx = i
+ end
+
+ games[i] = minetest.formspec_escape(games[i].name)
+ end
+
+ local selected_game = pkgmgr.games[selected_game_idx]
+ local deps_to_install = 0
+ local deps_not_found = 0
+
+ install_dialog.dependencies = resolve_dependencies(raw_deps, selected_game)
+ local formatted_deps = {}
+ for _, dep in pairs(install_dialog.dependencies) do
+ formatted_deps[#formatted_deps + 1] = "#fff"
+ formatted_deps[#formatted_deps + 1] = minetest.formspec_escape(dep.name)
+ if dep.installed then
+ formatted_deps[#formatted_deps + 1] = "#ccf"
+ formatted_deps[#formatted_deps + 1] = fgettext("Already installed")
+ elseif dep.package then
+ formatted_deps[#formatted_deps + 1] = "#cfc"
+ formatted_deps[#formatted_deps + 1] = fgettext("$1 by $2", dep.package.title, dep.package.author)
+ deps_to_install = deps_to_install + 1
+ else
+ formatted_deps[#formatted_deps + 1] = "#f00"
+ formatted_deps[#formatted_deps + 1] = fgettext("Not found")
+ deps_not_found = deps_not_found + 1
+ end
+ end
+
+ local message_bg = "#3333"
+ local message
+ if will_install_deps then
+ message = fgettext("$1 and $2 dependencies will be installed.", package.title, deps_to_install)
+ else
+ message = fgettext("$1 will be installed, and $2 dependencies will be skipped.", package.title, deps_to_install)
+ end
+ if deps_not_found > 0 then
+ message = fgettext("$1 required dependencies could not be found.", deps_not_found) ..
+ " " .. fgettext("Please check that the base game is correct.", deps_not_found) ..
+ "\n" .. message
+ message_bg = mt_color_orange
+ end
+
+ local formspec = {
+ "formspec_version[3]",
+ "size[7,7.85]",
+ "style[title;border=false]",
+ "box[0,0;7,0.5;#3333]",
+ "button[0,0;7,0.5;title;", fgettext("Install $1", package.title) , "]",
+
+ "container[0.375,0.70]",
+
+ "label[0,0.25;", fgettext("Base Game:"), "]",
+ "dropdown[2,0;4.25,0.5;gameid;", table.concat(games, ","), ";", selected_game_idx, "]",
+
+ "label[0,0.8;", fgettext("Dependencies:"), "]",
+
+ "tablecolumns[color;text;color;text]",
+ "table[0,1.1;6.25,3;packages;", table.concat(formatted_deps, ","), "]",
+
+ "container_end[]",
+
+ "checkbox[0.375,5.1;will_install_deps;",
+ fgettext("Install missing dependencies"), ";",
+ will_install_deps and "true" or "false", "]",
+
+ "box[0,5.4;7,1.2;", message_bg, "]",
+ "textarea[0.375,5.5;6.25,1;;;", message, "]",
+
+ "container[1.375,6.85]",
+ "button[0,0;2,0.8;install_all;", fgettext("Install"), "]",
+ "button[2.25,0;2,0.8;cancel;", fgettext("Cancel"), "]",
+ "container_end[]",
+ }
+
+ return table.concat(formspec, "")
+end
+
+function install_dialog.handle_submit(this, fields)
+ if fields.cancel then
+ this:delete()
+ return true
+ end
+
+ if fields.will_install_deps ~= nil then
+ install_dialog.will_install_deps = minetest.is_yes(fields.will_install_deps)
+ return true
+ end
+
+ if fields.install_all then
+ queue_download(install_dialog.package)
+
+ if install_dialog.will_install_deps then
+ for _, dep in pairs(install_dialog.dependencies) do
+ if not dep.is_optional and not dep.installed and dep.package then
+ queue_download(dep.package)
+ end
+ end
+ end
+
+ this:delete()
+ return true
+ end
+
+ if fields.gameid then
+ for _, game in pairs(pkgmgr.games) do
+ if game.name == fields.gameid then
+ core.settings:set("menu_last_game", game.id)
+ break
+ end
+ end
+ return true
+ end
+
+ return false
+end
+
+function install_dialog.create(package, raw_deps)
+ install_dialog.dependencies = nil
+ install_dialog.package = package
+ install_dialog.raw_deps = raw_deps
+ install_dialog.will_install_deps = true
+ return dialog_create("install_dialog",
+ install_dialog.get_formspec,
+ install_dialog.handle_submit,
+ nil)
+end
+
+
+local confirm_overwrite = {}
+function confirm_overwrite.get_formspec()
+ local package = confirm_overwrite.package
+
+ return "size[11.5,4.5,true]" ..
+ "label[2,2;" ..
+ fgettext("\"$1\" already exists. Would you like to overwrite it?", package.name) .. "]"..
+ "style[install;bgcolor=red]" ..
+ "button[3.25,3.5;2.5,0.5;install;" .. fgettext("Overwrite") .. "]" ..
+ "button[5.75,3.5;2.5,0.5;cancel;" .. fgettext("Cancel") .. "]"
+end
+
+function confirm_overwrite.handle_submit(this, fields)
+ if fields.cancel then
+ this:delete()
+ return true
+ end
+
+ if fields.install then
+ this:delete()
+ confirm_overwrite.callback()
+ return true
+ end
+
+ return false
+end
+
+function confirm_overwrite.create(package, callback)
+ assert(type(package) == "table")
+ assert(type(callback) == "function")
+
+ confirm_overwrite.package = package
+ confirm_overwrite.callback = callback
+ return dialog_create("confirm_overwrite",
+ confirm_overwrite.get_formspec,
+ confirm_overwrite.handle_submit,
+ nil)
+end
+
+
local function get_file_extension(path)
local parts = path:split(".")
return parts[#parts]
local W = 15.75
local H = 9.5
-
local formspec
if #store.packages_full > 0 then
formspec = {
"size[15.75,9.5]",
"position[0.5,0.55]",
- "style[status;border=false]",
+ "style[status,downloading,queued;border=false]",
"container[0.375,0.375]",
"field[0,0;7.225,0.8;search_string;;", core.formspec_escape(search_string), "]",
"field_close_on_enter[search_string;false]",
- "button[7.225,0;2,0.8;search;", fgettext("Search"), "]",
+ "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.6,0;2.4,0.8;type;", table.concat(filter_types_titles, ","), ";", filter_type, "]",
"container_end[]",
}
if number_downloading > 0 then
- formspec[#formspec + 1] = "button[12.75,0.375;2.625,0.8;status;"
+ formspec[#formspec + 1] = "button[12.75,0.375;2.625,0.8;downloading;"
if #download_queue > 0 then
formspec[#formspec + 1] = fgettext("$1 downloading,\n$2 queued", number_downloading, #download_queue)
else
}
end
+ -- download/queued tooltips always have the same message
+ local tooltip_colors = ";#dff6f5;#302c2e]"
+ formspec[#formspec + 1] = "tooltip[downloading;" .. fgettext("Downloading...") .. tooltip_colors
+ formspec[#formspec + 1] = "tooltip[queued;" .. fgettext("Queued") .. tooltip_colors
+
local start_idx = (cur_page - 1) * num_per_page + 1
for i=start_idx, math.min(#store.packages, start_idx+num_per_page-1) do
local package = store.packages[i]
+ local container_y = (i - start_idx) * 1.375 + (2*0.375 + 0.8)
formspec[#formspec + 1] = "container[0.375,"
- formspec[#formspec + 1] = (i - start_idx) * 1.375 + (2*0.375 + 0.8)
+ formspec[#formspec + 1] = container_y
formspec[#formspec + 1] = "]"
-- image
formspec[#formspec + 1] = "]"
-- buttons
- local description_width = W - 0.375*5 - 1 - 2*1.5
+ local left_base = "image_button[-1.55,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] = "button[-3.5,0;2,0.8;status;"
- formspec[#formspec + 1] = fgettext("Downloading...")
- formspec[#formspec + 1] = "]"
+ 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] = "button[-3.5,0;2,0.8;status;"
- formspec[#formspec + 1] = fgettext("Queued")
- formspec[#formspec + 1] = "]"
+ formspec[#formspec + 1] = left_base
+ formspec[#formspec + 1] = core.formspec_escape(defaulttexturedir)
+ formspec[#formspec + 1] = "cdb_queued.png;queued]"
elseif not package.path then
- formspec[#formspec + 1] = "button[-3,0;1.5,0.8;install_"
- formspec[#formspec + 1] = tostring(i)
- formspec[#formspec + 1] = ";"
- formspec[#formspec + 1] = fgettext("Install")
- formspec[#formspec + 1] = "]"
+ local elem_name = "install_" .. i .. ";"
+ formspec[#formspec + 1] = "style[" .. elem_name .. "bgcolor=#71aa34]"
+ formspec[#formspec + 1] = left_base .. "cdb_add.png;" .. elem_name .. "]"
+ formspec[#formspec + 1] = "tooltip[" .. elem_name .. fgettext("Install") .. tooltip_colors
else
if package.installed_release < package.release then
- description_width = description_width - 1.5
-- The install_ action also handles updating
- formspec[#formspec + 1] = "button[-4.5,0;1.5,0.8;install_"
- formspec[#formspec + 1] = tostring(i)
- formspec[#formspec + 1] = ";"
- formspec[#formspec + 1] = fgettext("Update")
- formspec[#formspec + 1] = "]"
- end
+ local elem_name = "install_" .. i .. ";"
+ formspec[#formspec + 1] = "style[" .. elem_name .. "bgcolor=#28ccdf]"
+ formspec[#formspec + 1] = left_base .. "cdb_update.png;" .. elem_name .. "]"
+ formspec[#formspec + 1] = "tooltip[" .. elem_name .. fgettext("Update") .. tooltip_colors
+ else
- formspec[#formspec + 1] = "button[-3,0;1.5,0.8;uninstall_"
- formspec[#formspec + 1] = tostring(i)
- formspec[#formspec + 1] = ";"
- formspec[#formspec + 1] = fgettext("Uninstall")
- formspec[#formspec + 1] = "]"
+ local elem_name = "uninstall_" .. i .. ";"
+ formspec[#formspec + 1] = "style[" .. elem_name .. "bgcolor=#a93b3b]"
+ formspec[#formspec + 1] = left_base .. "cdb_clear.png;" .. elem_name .. "]"
+ formspec[#formspec + 1] = "tooltip[" .. elem_name .. fgettext("Uninstall") .. tooltip_colors
+ end
end
- formspec[#formspec + 1] = "button[-1.5,0;1.5,0.8;view_"
- formspec[#formspec + 1] = tostring(i)
- formspec[#formspec + 1] = ";"
- formspec[#formspec + 1] = fgettext("View")
- formspec[#formspec + 1] = "]"
+ 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
+ local description_width = W - 0.375*5 - 0.85 - 2*0.7
formspec[#formspec + 1] = "textarea[1.855,0.3;"
formspec[#formspec + 1] = tostring(description_width)
formspec[#formspec + 1] = ",0.8;;;"
return true
end
+ if fields.clear then
+ search_string = ""
+ cur_page = 1
+ store.filter_packages("")
+ return true
+ end
+
if fields.back then
this:delete()
return true
assert(package)
if fields["install_" .. i] then
- queue_download(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
+
+
+ local function on_confirm()
+ local deps = get_raw_dependencies(package)
+ if deps and has_hard_deps(deps) then
+ local dlg = install_dialog.create(package, deps)
+ dlg:set_parent(this)
+ this:hide()
+ dlg:show()
+ else
+ queue_download(package)
+ end
+ end
+
+ if not package.path and core.is_dir(install_parent .. DIR_DELIM .. package.name) then
+ local dlg = confirm_overwrite.create(package, on_confirm)
+ dlg:set_parent(this)
+ this:hide()
+ dlg:show()
+ else
+ on_confirm()
+ end
+
return true
end
if fields["uninstall_" .. i] then
- local dlg_delmod = create_delete_content_dlg(package)
- dlg_delmod:set_parent(this)
+ local dlg = create_delete_content_dlg(package)
+ dlg:set_parent(this)
this:hide()
- dlg_delmod:show()
+ dlg:show()
return true
end