dlg_contentstore.lua 29 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061
  1. --Minetest
  2. --Copyright (C) 2018-20 rubenwardy
  3. --
  4. --This program is free software; you can redistribute it and/or modify
  5. --it under the terms of the GNU Lesser General Public License as published by
  6. --the Free Software Foundation; either version 2.1 of the License, or
  7. --(at your option) any later version.
  8. --
  9. --This program is distributed in the hope that it will be useful,
  10. --but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. --MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. --GNU Lesser General Public License for more details.
  13. --
  14. --You should have received a copy of the GNU Lesser General Public License along
  15. --with this program; if not, write to the Free Software Foundation, Inc.,
  16. --51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  17. if not core.get_http_api then
  18. function create_store_dlg()
  19. return messagebox("store",
  20. fgettext("ContentDB is not available when Minetest was compiled without cURL"))
  21. end
  22. return
  23. end
  24. -- Unordered preserves the original order of the ContentDB API,
  25. -- before the package list is ordered based on installed state.
  26. local store = { packages = {}, packages_full = {}, packages_full_unordered = {}, aliases = {} }
  27. local http = core.get_http_api()
  28. -- Screenshot
  29. local screenshot_dir = core.get_cache_path() .. DIR_DELIM .. "cdb"
  30. assert(core.create_dir(screenshot_dir))
  31. local screenshot_downloading = {}
  32. local screenshot_downloaded = {}
  33. -- Filter
  34. local search_string = ""
  35. local cur_page = 1
  36. local num_per_page = 5
  37. local filter_type = 1
  38. local filter_types_titles = {
  39. fgettext("All packages"),
  40. fgettext("Games"),
  41. fgettext("Mods"),
  42. fgettext("Texture packs"),
  43. }
  44. local number_downloading = 0
  45. local download_queue = {}
  46. local filter_types_type = {
  47. nil,
  48. "game",
  49. "mod",
  50. "txp",
  51. }
  52. local REASON_NEW = "new"
  53. local REASON_UPDATE = "update"
  54. local REASON_DEPENDENCY = "dependency"
  55. -- encodes for use as URL parameter or path component
  56. local function urlencode(str)
  57. return str:gsub("[^%a%d()._~-]", function(char)
  58. return string.format("%%%02X", string.byte(char))
  59. end)
  60. end
  61. assert(urlencode("sample text?") == "sample%20text%3F")
  62. local function get_download_url(package, reason)
  63. local base_url = core.settings:get("contentdb_url")
  64. local ret = base_url .. ("/packages/%s/releases/%d/download/"):format(
  65. package.url_part, package.release)
  66. if reason then
  67. ret = ret .. "?reason=" .. reason
  68. end
  69. return ret
  70. end
  71. local function download_and_extract(param)
  72. local package = param.package
  73. local filename = core.get_temp_path(true)
  74. if filename == "" or not core.download_file(param.url, filename) then
  75. core.log("error", "Downloading " .. dump(param.url) .. " failed")
  76. return {
  77. msg = fgettext("Failed to download \"$1\"", package.title)
  78. }
  79. end
  80. local tempfolder = core.get_temp_path()
  81. if tempfolder ~= "" then
  82. tempfolder = tempfolder .. DIR_DELIM .. "MT_" .. math.random(1, 1024000)
  83. if not core.extract_zip(filename, tempfolder) then
  84. tempfolder = nil
  85. end
  86. else
  87. tempfolder = nil
  88. end
  89. os.remove(filename)
  90. if not tempfolder then
  91. return {
  92. msg = fgettext("Failed to extract \"$1\" (unsupported file type or broken archive)", package.title),
  93. }
  94. end
  95. return {
  96. path = tempfolder
  97. }
  98. end
  99. local function start_install(package, reason)
  100. local params = {
  101. package = package,
  102. url = get_download_url(package, reason),
  103. }
  104. number_downloading = number_downloading + 1
  105. local function callback(result)
  106. if result.msg then
  107. gamedata.errormessage = result.msg
  108. else
  109. local path, msg = pkgmgr.install_dir(package.type, result.path, package.name, package.path)
  110. core.delete_dir(result.path)
  111. if not path then
  112. gamedata.errormessage = fgettext("Error installing \"$1\": $2", package.title, msg)
  113. else
  114. core.log("action", "Installed package to " .. path)
  115. local conf_path
  116. local name_is_title = false
  117. if package.type == "mod" then
  118. local actual_type = pkgmgr.get_folder_type(path)
  119. if actual_type.type == "modpack" then
  120. conf_path = path .. DIR_DELIM .. "modpack.conf"
  121. else
  122. conf_path = path .. DIR_DELIM .. "mod.conf"
  123. end
  124. elseif package.type == "game" then
  125. conf_path = path .. DIR_DELIM .. "game.conf"
  126. name_is_title = true
  127. elseif package.type == "txp" then
  128. conf_path = path .. DIR_DELIM .. "texture_pack.conf"
  129. end
  130. if conf_path then
  131. local conf = Settings(conf_path)
  132. conf:set("title", package.title)
  133. if not name_is_title then
  134. conf:set("name", package.name)
  135. end
  136. if not conf:get("description") then
  137. conf:set("description", package.short_description)
  138. end
  139. conf:set("author", package.author)
  140. conf:set("release", package.release)
  141. conf:write()
  142. end
  143. end
  144. end
  145. package.downloading = false
  146. number_downloading = number_downloading - 1
  147. local next = download_queue[1]
  148. if next then
  149. table.remove(download_queue, 1)
  150. start_install(next.package, next.reason)
  151. end
  152. ui.update()
  153. end
  154. package.queued = false
  155. package.downloading = true
  156. if not core.handle_async(download_and_extract, params, callback) then
  157. core.log("error", "ERROR: async event failed")
  158. gamedata.errormessage = fgettext("Failed to download $1", package.name)
  159. return
  160. end
  161. end
  162. local function queue_download(package, reason)
  163. local max_concurrent_downloads = tonumber(core.settings:get("contentdb_max_concurrent_downloads"))
  164. if number_downloading < math.max(max_concurrent_downloads, 1) then
  165. start_install(package, reason)
  166. else
  167. table.insert(download_queue, { package = package, reason = reason })
  168. package.queued = true
  169. end
  170. end
  171. local function get_raw_dependencies(package)
  172. if package.raw_deps then
  173. return package.raw_deps
  174. end
  175. local url_fmt = "/api/packages/%s/dependencies/?only_hard=1&protocol_version=%s&engine_version=%s"
  176. local version = core.get_version()
  177. local base_url = core.settings:get("contentdb_url")
  178. local url = base_url .. url_fmt:format(package.url_part, core.get_max_supp_proto(), urlencode(version.string))
  179. local response = http.fetch_sync({ url = url })
  180. if not response.succeeded then
  181. return
  182. end
  183. local data = core.parse_json(response.data) or {}
  184. local content_lookup = {}
  185. for _, pkg in pairs(store.packages_full) do
  186. content_lookup[pkg.id] = pkg
  187. end
  188. for id, raw_deps in pairs(data) do
  189. local package2 = content_lookup[id:lower()]
  190. if package2 and not package2.raw_deps then
  191. package2.raw_deps = raw_deps
  192. for _, dep in pairs(raw_deps) do
  193. local packages = {}
  194. for i=1, #dep.packages do
  195. packages[#packages + 1] = content_lookup[dep.packages[i]:lower()]
  196. end
  197. dep.packages = packages
  198. end
  199. end
  200. end
  201. return package.raw_deps
  202. end
  203. local function has_hard_deps(raw_deps)
  204. for i=1, #raw_deps do
  205. if not raw_deps[i].is_optional then
  206. return true
  207. end
  208. end
  209. return false
  210. end
  211. -- Recursively resolve dependencies, given the installed mods
  212. local function resolve_dependencies_2(raw_deps, installed_mods, out)
  213. local function resolve_dep(dep)
  214. -- Check whether it's already installed
  215. if installed_mods[dep.name] then
  216. return {
  217. is_optional = dep.is_optional,
  218. name = dep.name,
  219. installed = true,
  220. }
  221. end
  222. -- Find exact name matches
  223. local fallback
  224. for _, package in pairs(dep.packages) do
  225. if package.type ~= "game" then
  226. if package.name == dep.name then
  227. return {
  228. is_optional = dep.is_optional,
  229. name = dep.name,
  230. installed = false,
  231. package = package,
  232. }
  233. elseif not fallback then
  234. fallback = package
  235. end
  236. end
  237. end
  238. -- Otherwise, find the first mod that fulfills it
  239. if fallback then
  240. return {
  241. is_optional = dep.is_optional,
  242. name = dep.name,
  243. installed = false,
  244. package = fallback,
  245. }
  246. end
  247. return {
  248. is_optional = dep.is_optional,
  249. name = dep.name,
  250. installed = false,
  251. }
  252. end
  253. for _, dep in pairs(raw_deps) do
  254. if not dep.is_optional and not out[dep.name] then
  255. local result = resolve_dep(dep)
  256. out[dep.name] = result
  257. if result and result.package and not result.installed then
  258. local raw_deps2 = get_raw_dependencies(result.package)
  259. if raw_deps2 then
  260. resolve_dependencies_2(raw_deps2, installed_mods, out)
  261. end
  262. end
  263. end
  264. end
  265. return true
  266. end
  267. -- Resolve dependencies for a package, calls the recursive version.
  268. local function resolve_dependencies(raw_deps, game)
  269. assert(game)
  270. local installed_mods = {}
  271. local mods = {}
  272. pkgmgr.get_game_mods(game, mods)
  273. for _, mod in pairs(mods) do
  274. installed_mods[mod.name] = true
  275. end
  276. for _, mod in pairs(pkgmgr.global_mods:get_list()) do
  277. installed_mods[mod.name] = true
  278. end
  279. local out = {}
  280. if not resolve_dependencies_2(raw_deps, installed_mods, out) then
  281. return nil
  282. end
  283. local retval = {}
  284. for _, dep in pairs(out) do
  285. retval[#retval + 1] = dep
  286. end
  287. table.sort(retval, function(a, b)
  288. return a.name < b.name
  289. end)
  290. return retval
  291. end
  292. local install_dialog = {}
  293. function install_dialog.get_formspec()
  294. local selected_game, selected_game_idx = pkgmgr.find_by_gameid(core.settings:get("menu_last_game"))
  295. if not selected_game_idx then
  296. selected_game_idx = 1
  297. selected_game = pkgmgr.games[1]
  298. end
  299. local game_list = {}
  300. for i, game in ipairs(pkgmgr.games) do
  301. game_list[i] = core.formspec_escape(game.title)
  302. end
  303. local package = install_dialog.package
  304. local raw_deps = install_dialog.raw_deps
  305. local will_install_deps = install_dialog.will_install_deps
  306. local deps_to_install = 0
  307. local deps_not_found = 0
  308. install_dialog.dependencies = resolve_dependencies(raw_deps, selected_game)
  309. local formatted_deps = {}
  310. for _, dep in pairs(install_dialog.dependencies) do
  311. formatted_deps[#formatted_deps + 1] = "#fff"
  312. formatted_deps[#formatted_deps + 1] = core.formspec_escape(dep.name)
  313. if dep.installed then
  314. formatted_deps[#formatted_deps + 1] = "#ccf"
  315. formatted_deps[#formatted_deps + 1] = fgettext("Already installed")
  316. elseif dep.package then
  317. formatted_deps[#formatted_deps + 1] = "#cfc"
  318. formatted_deps[#formatted_deps + 1] = fgettext("$1 by $2", dep.package.title, dep.package.author)
  319. deps_to_install = deps_to_install + 1
  320. else
  321. formatted_deps[#formatted_deps + 1] = "#f00"
  322. formatted_deps[#formatted_deps + 1] = fgettext("Not found")
  323. deps_not_found = deps_not_found + 1
  324. end
  325. end
  326. local message_bg = "#3333"
  327. local message
  328. if will_install_deps then
  329. message = fgettext("$1 and $2 dependencies will be installed.", package.title, deps_to_install)
  330. else
  331. message = fgettext("$1 will be installed, and $2 dependencies will be skipped.", package.title, deps_to_install)
  332. end
  333. if deps_not_found > 0 then
  334. message = fgettext("$1 required dependencies could not be found.", deps_not_found) ..
  335. " " .. fgettext("Please check that the base game is correct.", deps_not_found) ..
  336. "\n" .. message
  337. message_bg = mt_color_orange
  338. end
  339. local formspec = {
  340. "formspec_version[3]",
  341. "size[7,7.85]",
  342. "style[title;border=false]",
  343. "box[0,0;7,0.5;#3333]",
  344. "button[0,0;7,0.5;title;", fgettext("Install $1", package.title) , "]",
  345. "container[0.375,0.70]",
  346. "label[0,0.25;", fgettext("Base Game:"), "]",
  347. "dropdown[2,0;4.25,0.5;selected_game;", table.concat(game_list, ","), ";", selected_game_idx, "]",
  348. "label[0,0.8;", fgettext("Dependencies:"), "]",
  349. "tablecolumns[color;text;color;text]",
  350. "table[0,1.1;6.25,3;packages;", table.concat(formatted_deps, ","), "]",
  351. "container_end[]",
  352. "checkbox[0.375,5.1;will_install_deps;",
  353. fgettext("Install missing dependencies"), ";",
  354. will_install_deps and "true" or "false", "]",
  355. "box[0,5.4;7,1.2;", message_bg, "]",
  356. "textarea[0.375,5.5;6.25,1;;;", message, "]",
  357. "container[1.375,6.85]",
  358. "button[0,0;2,0.8;install_all;", fgettext("Install"), "]",
  359. "button[2.25,0;2,0.8;cancel;", fgettext("Cancel"), "]",
  360. "container_end[]",
  361. }
  362. return table.concat(formspec, "")
  363. end
  364. function install_dialog.handle_submit(this, fields)
  365. if fields.cancel then
  366. this:delete()
  367. return true
  368. end
  369. if fields.will_install_deps ~= nil then
  370. install_dialog.will_install_deps = core.is_yes(fields.will_install_deps)
  371. return true
  372. end
  373. if fields.install_all then
  374. queue_download(install_dialog.package, REASON_NEW)
  375. if install_dialog.will_install_deps then
  376. for _, dep in pairs(install_dialog.dependencies) do
  377. if not dep.is_optional and not dep.installed and dep.package then
  378. queue_download(dep.package, REASON_DEPENDENCY)
  379. end
  380. end
  381. end
  382. this:delete()
  383. return true
  384. end
  385. if fields.selected_game then
  386. for _, game in pairs(pkgmgr.games) do
  387. if game.title == fields.selected_game then
  388. core.settings:set("menu_last_game", game.id)
  389. break
  390. end
  391. end
  392. return true
  393. end
  394. return false
  395. end
  396. function install_dialog.create(package, raw_deps)
  397. install_dialog.dependencies = nil
  398. install_dialog.package = package
  399. install_dialog.raw_deps = raw_deps
  400. install_dialog.will_install_deps = true
  401. return dialog_create("install_dialog",
  402. install_dialog.get_formspec,
  403. install_dialog.handle_submit,
  404. nil)
  405. end
  406. local confirm_overwrite = {}
  407. function confirm_overwrite.get_formspec()
  408. local package = confirm_overwrite.package
  409. return confirmation_formspec(
  410. fgettext("\"$1\" already exists. Would you like to overwrite it?", package.name),
  411. 'install', fgettext("Overwrite"),
  412. 'cancel', fgettext("Cancel"))
  413. end
  414. function confirm_overwrite.handle_submit(this, fields)
  415. if fields.cancel then
  416. this:delete()
  417. return true
  418. end
  419. if fields.install then
  420. this:delete()
  421. confirm_overwrite.callback()
  422. return true
  423. end
  424. return false
  425. end
  426. function confirm_overwrite.create(package, callback)
  427. assert(type(package) == "table")
  428. assert(type(callback) == "function")
  429. confirm_overwrite.package = package
  430. confirm_overwrite.callback = callback
  431. return dialog_create("confirm_overwrite",
  432. confirm_overwrite.get_formspec,
  433. confirm_overwrite.handle_submit,
  434. nil)
  435. end
  436. local function get_file_extension(path)
  437. local parts = path:split(".")
  438. return parts[#parts]
  439. end
  440. local function get_screenshot(package)
  441. if not package.thumbnail then
  442. return defaulttexturedir .. "no_screenshot.png"
  443. elseif screenshot_downloading[package.thumbnail] then
  444. return defaulttexturedir .. "loading_screenshot.png"
  445. end
  446. -- Get tmp screenshot path
  447. local ext = get_file_extension(package.thumbnail)
  448. local filepath = screenshot_dir .. DIR_DELIM ..
  449. ("%s-%s-%s.%s"):format(package.type, package.author, package.name, ext)
  450. -- Return if already downloaded
  451. local file = io.open(filepath, "r")
  452. if file then
  453. file:close()
  454. return filepath
  455. end
  456. -- Show error if we've failed to download before
  457. if screenshot_downloaded[package.thumbnail] then
  458. return defaulttexturedir .. "error_screenshot.png"
  459. end
  460. -- Download
  461. local function download_screenshot(params)
  462. return core.download_file(params.url, params.dest)
  463. end
  464. local function callback(success)
  465. screenshot_downloading[package.thumbnail] = nil
  466. screenshot_downloaded[package.thumbnail] = true
  467. if not success then
  468. core.log("warning", "Screenshot download failed for some reason")
  469. end
  470. ui.update()
  471. end
  472. if core.handle_async(download_screenshot,
  473. { dest = filepath, url = package.thumbnail }, callback) then
  474. screenshot_downloading[package.thumbnail] = true
  475. else
  476. core.log("error", "ERROR: async event failed")
  477. return defaulttexturedir .. "error_screenshot.png"
  478. end
  479. return defaulttexturedir .. "loading_screenshot.png"
  480. end
  481. function store.load()
  482. local version = core.get_version()
  483. local base_url = core.settings:get("contentdb_url")
  484. local url = base_url ..
  485. "/api/packages/?type=mod&type=game&type=txp&protocol_version=" ..
  486. core.get_max_supp_proto() .. "&engine_version=" .. urlencode(version.string)
  487. for _, item in pairs(core.settings:get("contentdb_flag_blacklist"):split(",")) do
  488. item = item:trim()
  489. if item ~= "" then
  490. url = url .. "&hide=" .. urlencode(item)
  491. end
  492. end
  493. local response = http.fetch_sync({ url = url })
  494. if not response.succeeded then
  495. return
  496. end
  497. store.packages_full = core.parse_json(response.data) or {}
  498. store.aliases = {}
  499. for _, package in pairs(store.packages_full) do
  500. local name_len = #package.name
  501. -- This must match what store.update_paths() does!
  502. package.id = package.author:lower() .. "/"
  503. if package.type == "game" and name_len > 5 and package.name:sub(name_len - 4) == "_game" then
  504. package.id = package.id .. package.name:sub(1, name_len - 5)
  505. else
  506. package.id = package.id .. package.name
  507. end
  508. package.url_part = urlencode(package.author) .. "/" .. urlencode(package.name)
  509. if package.aliases then
  510. for _, alias in ipairs(package.aliases) do
  511. -- We currently don't support name changing
  512. local suffix = "/" .. package.name
  513. if alias:sub(-#suffix) == suffix then
  514. store.aliases[alias:lower()] = package.id
  515. end
  516. end
  517. end
  518. end
  519. store.packages_full_unordered = store.packages_full
  520. store.packages = store.packages_full
  521. store.loaded = true
  522. end
  523. function store.update_paths()
  524. local mod_hash = {}
  525. pkgmgr.refresh_globals()
  526. for _, mod in pairs(pkgmgr.global_mods:get_list()) do
  527. if mod.author and mod.release > 0 then
  528. local id = mod.author:lower() .. "/" .. mod.name
  529. mod_hash[store.aliases[id] or id] = mod
  530. end
  531. end
  532. local game_hash = {}
  533. pkgmgr.update_gamelist()
  534. for _, game in pairs(pkgmgr.games) do
  535. if game.author ~= "" and game.release > 0 then
  536. local id = game.author:lower() .. "/" .. game.id
  537. game_hash[store.aliases[id] or id] = game
  538. end
  539. end
  540. local txp_hash = {}
  541. for _, txp in pairs(pkgmgr.get_texture_packs()) do
  542. if txp.author and txp.release > 0 then
  543. local id = txp.author:lower() .. "/" .. txp.name
  544. txp_hash[store.aliases[id] or id] = txp
  545. end
  546. end
  547. for _, package in pairs(store.packages_full) do
  548. local content
  549. if package.type == "mod" then
  550. content = mod_hash[package.id]
  551. elseif package.type == "game" then
  552. content = game_hash[package.id]
  553. elseif package.type == "txp" then
  554. content = txp_hash[package.id]
  555. end
  556. if content then
  557. package.path = content.path
  558. package.installed_release = content.release or 0
  559. else
  560. package.path = nil
  561. end
  562. end
  563. end
  564. function store.sort_packages()
  565. local ret = {}
  566. -- Add installed content
  567. for i=1, #store.packages_full_unordered do
  568. local package = store.packages_full_unordered[i]
  569. if package.path then
  570. ret[#ret + 1] = package
  571. end
  572. end
  573. -- Sort installed content by title
  574. table.sort(ret, function(a, b)
  575. return a.title < b.title
  576. end)
  577. -- Add uninstalled content
  578. for i=1, #store.packages_full_unordered do
  579. local package = store.packages_full_unordered[i]
  580. if not package.path then
  581. ret[#ret + 1] = package
  582. end
  583. end
  584. store.packages_full = ret
  585. end
  586. function store.filter_packages(query)
  587. if query == "" and filter_type == 1 then
  588. store.packages = store.packages_full
  589. return
  590. end
  591. local keywords = {}
  592. for word in query:lower():gmatch("%S+") do
  593. table.insert(keywords, word)
  594. end
  595. local function matches_keywords(package)
  596. for k = 1, #keywords do
  597. local keyword = keywords[k]
  598. if string.find(package.name:lower(), keyword, 1, true) or
  599. string.find(package.title:lower(), keyword, 1, true) or
  600. string.find(package.author:lower(), keyword, 1, true) or
  601. string.find(package.short_description:lower(), keyword, 1, true) then
  602. return true
  603. end
  604. end
  605. return false
  606. end
  607. store.packages = {}
  608. for _, package in pairs(store.packages_full) do
  609. if (query == "" or matches_keywords(package)) and
  610. (filter_type == 1 or package.type == filter_types_type[filter_type]) then
  611. store.packages[#store.packages + 1] = package
  612. end
  613. end
  614. end
  615. function store.get_formspec(dlgdata)
  616. store.update_paths()
  617. dlgdata.pagemax = math.max(math.ceil(#store.packages / num_per_page), 1)
  618. if cur_page > dlgdata.pagemax then
  619. cur_page = 1
  620. end
  621. local W = 15.75
  622. local H = 9.5
  623. local formspec
  624. if #store.packages_full > 0 then
  625. formspec = {
  626. "formspec_version[3]",
  627. "size[15.75,9.5]",
  628. "position[0.5,0.55]",
  629. "style[status,downloading,queued;border=false]",
  630. "container[0.375,0.375]",
  631. "field[0,0;7.225,0.8;search_string;;", core.formspec_escape(search_string), "]",
  632. "field_close_on_enter[search_string;false]",
  633. "image_button[7.3,0;0.8,0.8;", core.formspec_escape(defaulttexturedir .. "search.png"), ";search;]",
  634. "image_button[8.125,0;0.8,0.8;", core.formspec_escape(defaulttexturedir .. "clear.png"), ";clear;]",
  635. "dropdown[9.6,0;2.4,0.8;type;", table.concat(filter_types_titles, ","), ";", filter_type, "]",
  636. "container_end[]",
  637. -- Page nav buttons
  638. "container[0,", H - 0.8 - 0.375, "]",
  639. "button[0.375,0;4,0.8;back;", fgettext("Back to Main Menu"), "]",
  640. "container[", W - 0.375 - 0.8*4 - 2, ",0]",
  641. "image_button[0,0;0.8,0.8;", core.formspec_escape(defaulttexturedir), "start_icon.png;pstart;]",
  642. "image_button[0.8,0;0.8,0.8;", core.formspec_escape(defaulttexturedir), "prev_icon.png;pback;]",
  643. "style[pagenum;border=false]",
  644. "button[1.6,0;2,0.8;pagenum;", tonumber(cur_page), " / ", tonumber(dlgdata.pagemax), "]",
  645. "image_button[3.6,0;0.8,0.8;", core.formspec_escape(defaulttexturedir), "next_icon.png;pnext;]",
  646. "image_button[4.4,0;0.8,0.8;", core.formspec_escape(defaulttexturedir), "end_icon.png;pend;]",
  647. "container_end[]",
  648. "container_end[]",
  649. }
  650. if number_downloading > 0 then
  651. formspec[#formspec + 1] = "button[12.75,0.375;2.625,0.8;downloading;"
  652. if #download_queue > 0 then
  653. formspec[#formspec + 1] = fgettext("$1 downloading,\n$2 queued", number_downloading, #download_queue)
  654. else
  655. formspec[#formspec + 1] = fgettext("$1 downloading...", number_downloading)
  656. end
  657. formspec[#formspec + 1] = "]"
  658. else
  659. local num_avail_updates = 0
  660. for i=1, #store.packages_full do
  661. local package = store.packages_full[i]
  662. if package.path and package.installed_release < package.release and
  663. not (package.downloading or package.queued) then
  664. num_avail_updates = num_avail_updates + 1
  665. end
  666. end
  667. if num_avail_updates == 0 then
  668. formspec[#formspec + 1] = "button[12.75,0.375;2.625,0.8;status;"
  669. formspec[#formspec + 1] = fgettext("No updates")
  670. formspec[#formspec + 1] = "]"
  671. else
  672. formspec[#formspec + 1] = "button[12.75,0.375;2.625,0.8;update_all;"
  673. formspec[#formspec + 1] = fgettext("Update All [$1]", num_avail_updates)
  674. formspec[#formspec + 1] = "]"
  675. end
  676. end
  677. if #store.packages == 0 then
  678. formspec[#formspec + 1] = "label[4,3;"
  679. formspec[#formspec + 1] = fgettext("No results")
  680. formspec[#formspec + 1] = "]"
  681. end
  682. else
  683. formspec = {
  684. "size[12,7]",
  685. "position[0.5,0.55]",
  686. "label[4,3;", fgettext("No packages could be retrieved"), "]",
  687. "container[0,", H - 0.8 - 0.375, "]",
  688. "button[0,0;4,0.8;back;", fgettext("Back to Main Menu"), "]",
  689. "container_end[]",
  690. }
  691. end
  692. -- download/queued tooltips always have the same message
  693. local tooltip_colors = ";#dff6f5;#302c2e]"
  694. formspec[#formspec + 1] = "tooltip[downloading;" .. fgettext("Downloading...") .. tooltip_colors
  695. formspec[#formspec + 1] = "tooltip[queued;" .. fgettext("Queued") .. tooltip_colors
  696. local start_idx = (cur_page - 1) * num_per_page + 1
  697. for i=start_idx, math.min(#store.packages, start_idx+num_per_page-1) do
  698. local package = store.packages[i]
  699. local container_y = (i - start_idx) * 1.375 + (2*0.375 + 0.8)
  700. formspec[#formspec + 1] = "container[0.375,"
  701. formspec[#formspec + 1] = container_y
  702. formspec[#formspec + 1] = "]"
  703. -- image
  704. formspec[#formspec + 1] = "image[0,0;1.5,1;"
  705. formspec[#formspec + 1] = core.formspec_escape(get_screenshot(package))
  706. formspec[#formspec + 1] = "]"
  707. -- title
  708. formspec[#formspec + 1] = "label[1.875,0.1;"
  709. formspec[#formspec + 1] = core.formspec_escape(
  710. core.colorize(mt_color_green, package.title) ..
  711. core.colorize("#BFBFBF", " by " .. package.author))
  712. formspec[#formspec + 1] = "]"
  713. -- buttons
  714. local left_base = "image_button[-1.55,0;0.7,0.7;" .. core.formspec_escape(defaulttexturedir)
  715. formspec[#formspec + 1] = "container["
  716. formspec[#formspec + 1] = W - 0.375*2
  717. formspec[#formspec + 1] = ",0.1]"
  718. if package.downloading then
  719. formspec[#formspec + 1] = "animated_image[-1.7,-0.15;1,1;downloading;"
  720. formspec[#formspec + 1] = core.formspec_escape(defaulttexturedir)
  721. formspec[#formspec + 1] = "cdb_downloading.png;3;400;]"
  722. elseif package.queued then
  723. formspec[#formspec + 1] = left_base
  724. formspec[#formspec + 1] = "cdb_queued.png;queued;]"
  725. elseif not package.path then
  726. local elem_name = "install_" .. i .. ";"
  727. formspec[#formspec + 1] = "style[" .. elem_name .. "bgcolor=#71aa34]"
  728. formspec[#formspec + 1] = left_base .. "cdb_add.png;" .. elem_name .. "]"
  729. formspec[#formspec + 1] = "tooltip[" .. elem_name .. fgettext("Install") .. tooltip_colors
  730. else
  731. if package.installed_release < package.release then
  732. -- The install_ action also handles updating
  733. local elem_name = "install_" .. i .. ";"
  734. formspec[#formspec + 1] = "style[" .. elem_name .. "bgcolor=#28ccdf]"
  735. formspec[#formspec + 1] = left_base .. "cdb_update.png;" .. elem_name .. "]"
  736. formspec[#formspec + 1] = "tooltip[" .. elem_name .. fgettext("Update") .. tooltip_colors
  737. else
  738. local elem_name = "uninstall_" .. i .. ";"
  739. formspec[#formspec + 1] = "style[" .. elem_name .. "bgcolor=#a93b3b]"
  740. formspec[#formspec + 1] = left_base .. "cdb_clear.png;" .. elem_name .. "]"
  741. formspec[#formspec + 1] = "tooltip[" .. elem_name .. fgettext("Uninstall") .. tooltip_colors
  742. end
  743. end
  744. local web_elem_name = "view_" .. i .. ";"
  745. formspec[#formspec + 1] = "image_button[-0.7,0;0.7,0.7;" ..
  746. core.formspec_escape(defaulttexturedir) .. "cdb_viewonline.png;" .. web_elem_name .. "]"
  747. formspec[#formspec + 1] = "tooltip[" .. web_elem_name ..
  748. fgettext("View more information in a web browser") .. tooltip_colors
  749. formspec[#formspec + 1] = "container_end[]"
  750. -- description
  751. local description_width = W - 0.375*5 - 0.85 - 2*0.7
  752. formspec[#formspec + 1] = "textarea[1.855,0.3;"
  753. formspec[#formspec + 1] = tostring(description_width)
  754. formspec[#formspec + 1] = ",0.8;;;"
  755. formspec[#formspec + 1] = core.formspec_escape(package.short_description)
  756. formspec[#formspec + 1] = "]"
  757. formspec[#formspec + 1] = "container_end[]"
  758. end
  759. return table.concat(formspec, "")
  760. end
  761. function store.handle_submit(this, fields)
  762. if fields.search or fields.key_enter_field == "search_string" then
  763. search_string = fields.search_string:trim()
  764. cur_page = 1
  765. store.filter_packages(search_string)
  766. return true
  767. end
  768. if fields.clear then
  769. search_string = ""
  770. cur_page = 1
  771. store.filter_packages("")
  772. return true
  773. end
  774. if fields.back then
  775. this:delete()
  776. return true
  777. end
  778. if fields.pstart then
  779. cur_page = 1
  780. return true
  781. end
  782. if fields.pend then
  783. cur_page = this.data.pagemax
  784. return true
  785. end
  786. if fields.pnext then
  787. cur_page = cur_page + 1
  788. if cur_page > this.data.pagemax then
  789. cur_page = 1
  790. end
  791. return true
  792. end
  793. if fields.pback then
  794. if cur_page == 1 then
  795. cur_page = this.data.pagemax
  796. else
  797. cur_page = cur_page - 1
  798. end
  799. return true
  800. end
  801. if fields.type then
  802. local new_type = table.indexof(filter_types_titles, fields.type)
  803. if new_type ~= filter_type then
  804. filter_type = new_type
  805. store.filter_packages(search_string)
  806. return true
  807. end
  808. end
  809. if fields.update_all then
  810. for i=1, #store.packages_full do
  811. local package = store.packages_full[i]
  812. if package.path and package.installed_release < package.release and
  813. not (package.downloading or package.queued) then
  814. queue_download(package, REASON_UPDATE)
  815. end
  816. end
  817. return true
  818. end
  819. local start_idx = (cur_page - 1) * num_per_page + 1
  820. assert(start_idx ~= nil)
  821. for i=start_idx, math.min(#store.packages, start_idx+num_per_page-1) do
  822. local package = store.packages[i]
  823. assert(package)
  824. if fields["install_" .. i] then
  825. local install_parent
  826. if package.type == "mod" then
  827. install_parent = core.get_modpath()
  828. elseif package.type == "game" then
  829. install_parent = core.get_gamepath()
  830. elseif package.type == "txp" then
  831. install_parent = core.get_texturepath()
  832. else
  833. error("Unknown package type: " .. package.type)
  834. end
  835. local function on_confirm()
  836. local deps = get_raw_dependencies(package)
  837. if deps and has_hard_deps(deps) then
  838. local dlg = install_dialog.create(package, deps)
  839. dlg:set_parent(this)
  840. this:hide()
  841. dlg:show()
  842. else
  843. queue_download(package, package.path and REASON_UPDATE or REASON_NEW)
  844. end
  845. end
  846. if not package.path and core.is_dir(install_parent .. DIR_DELIM .. package.name) then
  847. local dlg = confirm_overwrite.create(package, on_confirm)
  848. dlg:set_parent(this)
  849. this:hide()
  850. dlg:show()
  851. else
  852. on_confirm()
  853. end
  854. return true
  855. end
  856. if fields["uninstall_" .. i] then
  857. local dlg = create_delete_content_dlg(package)
  858. dlg:set_parent(this)
  859. this:hide()
  860. dlg:show()
  861. return true
  862. end
  863. if fields["view_" .. i] then
  864. local url = ("%s/packages/%s?protocol_version=%d"):format(
  865. core.settings:get("contentdb_url"), package.url_part,
  866. core.get_max_supp_proto())
  867. core.open_url(url)
  868. return true
  869. end
  870. end
  871. return false
  872. end
  873. function create_store_dlg(type)
  874. if not store.loaded or #store.packages_full == 0 then
  875. store.load()
  876. end
  877. store.update_paths()
  878. store.sort_packages()
  879. search_string = ""
  880. cur_page = 1
  881. if type then
  882. -- table.indexof does not work on tables that contain `nil`
  883. for i, v in pairs(filter_types_type) do
  884. if v == type then
  885. filter_type = i
  886. break
  887. end
  888. end
  889. end
  890. store.filter_packages(search_string)
  891. return dialog_create("store",
  892. store.get_formspec,
  893. store.handle_submit,
  894. nil)
  895. end