dlg_contentstore.lua 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603
  1. --Minetest
  2. --Copyright (C) 2018 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. local store = { packages = {}, packages_full = {} }
  18. local package_dialog = {}
  19. -- Screenshot
  20. local screenshot_dir = core.get_cache_path() .. DIR_DELIM .. "cdb"
  21. assert(core.create_dir(screenshot_dir))
  22. local screenshot_downloading = {}
  23. local screenshot_downloaded = {}
  24. -- Filter
  25. local search_string = ""
  26. local cur_page = 1
  27. local num_per_page = 5
  28. local filter_type = 1
  29. local filter_types_titles = {
  30. fgettext("All packages"),
  31. fgettext("Games"),
  32. fgettext("Mods"),
  33. fgettext("Texture packs"),
  34. }
  35. local filter_types_type = {
  36. nil,
  37. "game",
  38. "mod",
  39. "txp",
  40. }
  41. local function download_package(param)
  42. if core.download_file(param.package.url, param.filename) then
  43. return {
  44. package = param.package,
  45. filename = param.filename,
  46. successful = true,
  47. }
  48. else
  49. core.log("error", "downloading " .. dump(param.package.url) .. " failed")
  50. return {
  51. package = param.package,
  52. successful = false,
  53. }
  54. end
  55. end
  56. local function start_install(calling_dialog, package)
  57. local params = {
  58. package = package,
  59. filename = os.tempfolder() .. "_MODNAME_" .. package.name .. ".zip",
  60. }
  61. local function callback(result)
  62. if result.successful then
  63. local path, msg = pkgmgr.install(result.package.type,
  64. result.filename, result.package.name,
  65. result.package.path)
  66. if not path then
  67. gamedata.errormessage = msg
  68. else
  69. core.log("action", "Installed package to " .. path)
  70. local conf_path
  71. local name_is_title = false
  72. if result.package.type == "mod" then
  73. local actual_type = pkgmgr.get_folder_type(path)
  74. if actual_type.type == "modpack" then
  75. conf_path = path .. DIR_DELIM .. "modpack.conf"
  76. else
  77. conf_path = path .. DIR_DELIM .. "mod.conf"
  78. end
  79. elseif result.package.type == "game" then
  80. conf_path = path .. DIR_DELIM .. "game.conf"
  81. name_is_title = true
  82. elseif result.package.type == "txp" then
  83. conf_path = path .. DIR_DELIM .. "texture_pack.conf"
  84. end
  85. if conf_path then
  86. local conf = Settings(conf_path)
  87. if name_is_title then
  88. conf:set("name", result.package.title)
  89. else
  90. conf:set("title", result.package.title)
  91. conf:set("name", result.package.name)
  92. end
  93. if not conf:get("description") then
  94. conf:set("description", result.package.short_description)
  95. end
  96. conf:set("author", result.package.author)
  97. conf:set("release", result.package.release)
  98. conf:write()
  99. end
  100. end
  101. os.remove(result.filename)
  102. else
  103. gamedata.errormessage = fgettext("Failed to download $1", package.name)
  104. end
  105. if gamedata.errormessage == nil then
  106. core.button_handler({btn_hidden_close_download=result})
  107. else
  108. core.button_handler({btn_hidden_close_download={successful=false}})
  109. end
  110. end
  111. if not core.handle_async(download_package, params, callback) then
  112. core.log("error", "ERROR: async event failed")
  113. gamedata.errormessage = fgettext("Failed to download $1", package.name)
  114. end
  115. local new_dlg = dialog_create("store_downloading",
  116. function(data)
  117. return "size[7,2]label[0.25,0.75;" ..
  118. fgettext("Downloading and installing $1, please wait...", data.title) .. "]"
  119. end,
  120. function(this,fields)
  121. if fields["btn_hidden_close_download"] ~= nil then
  122. this:delete()
  123. return true
  124. end
  125. return false
  126. end,
  127. nil)
  128. new_dlg:set_parent(calling_dialog)
  129. new_dlg.data.title = package.title
  130. calling_dialog:hide()
  131. new_dlg:show()
  132. end
  133. local function get_screenshot(package)
  134. if not package.thumbnail then
  135. return defaulttexturedir .. "no_screenshot.png"
  136. elseif screenshot_downloading[package.thumbnail] then
  137. return defaulttexturedir .. "loading_screenshot.png"
  138. end
  139. -- Get tmp screenshot path
  140. local filepath = screenshot_dir .. DIR_DELIM ..
  141. package.type .. "-" .. package.author .. "-" .. package.name .. ".png"
  142. -- Return if already downloaded
  143. local file = io.open(filepath, "r")
  144. if file then
  145. file:close()
  146. return filepath
  147. end
  148. -- Show error if we've failed to download before
  149. if screenshot_downloaded[package.thumbnail] then
  150. return defaulttexturedir .. "error_screenshot.png"
  151. end
  152. -- Download
  153. local function download_screenshot(params)
  154. return core.download_file(params.url, params.dest)
  155. end
  156. local function callback(success)
  157. screenshot_downloading[package.thumbnail] = nil
  158. screenshot_downloaded[package.thumbnail] = true
  159. if not success then
  160. core.log("warning", "Screenshot download failed for some reason")
  161. end
  162. ui.update()
  163. end
  164. if core.handle_async(download_screenshot,
  165. { dest = filepath, url = package.thumbnail }, callback) then
  166. screenshot_downloading[package.thumbnail] = true
  167. else
  168. core.log("error", "ERROR: async event failed")
  169. return defaulttexturedir .. "error_screenshot.png"
  170. end
  171. return defaulttexturedir .. "loading_screenshot.png"
  172. end
  173. function package_dialog.get_formspec()
  174. local package = package_dialog.package
  175. store.update_paths()
  176. local formspec = {
  177. "size[9,4;true]",
  178. "image[0,1;4.5,3;", core.formspec_escape(get_screenshot(package)), ']',
  179. "label[3.8,1;",
  180. minetest.colorize(mt_color_green, core.formspec_escape(package.title)), "\n",
  181. minetest.colorize('#BFBFBF', "by " .. core.formspec_escape(package.author)), "]",
  182. "textarea[4,2;5.3,2;;;", core.formspec_escape(package.short_description), "]",
  183. "button[0,0;2,1;back;", fgettext("Back"), "]",
  184. }
  185. if not package.path then
  186. formspec[#formspec + 1] = "button[7,0;2,1;install;"
  187. formspec[#formspec + 1] = fgettext("Install")
  188. formspec[#formspec + 1] = "]"
  189. elseif package.installed_release < package.release then
  190. -- The install_ action also handles updating
  191. formspec[#formspec + 1] = "button[7,0;2,1;install;"
  192. formspec[#formspec + 1] = fgettext("Update")
  193. formspec[#formspec + 1] = "]"
  194. formspec[#formspec + 1] = "button[5,0;2,1;uninstall;"
  195. formspec[#formspec + 1] = fgettext("Uninstall")
  196. formspec[#formspec + 1] = "]"
  197. else
  198. formspec[#formspec + 1] = "button[7,0;2,1;uninstall;"
  199. formspec[#formspec + 1] = fgettext("Uninstall")
  200. formspec[#formspec + 1] = "]"
  201. end
  202. return table.concat(formspec, "")
  203. end
  204. function package_dialog.handle_submit(this, fields)
  205. if fields.back then
  206. this:delete()
  207. return true
  208. end
  209. if fields.install then
  210. start_install(this, package_dialog.package)
  211. return true
  212. end
  213. if fields.uninstall then
  214. local dlg_delmod = create_delete_content_dlg(package_dialog.package)
  215. dlg_delmod:set_parent(this)
  216. this:hide()
  217. dlg_delmod:show()
  218. return true
  219. end
  220. return false
  221. end
  222. function package_dialog.create(package)
  223. package_dialog.package = package
  224. return dialog_create("package_view",
  225. package_dialog.get_formspec,
  226. package_dialog.handle_submit,
  227. nil)
  228. end
  229. function store.load()
  230. local tmpdir = os.tempfolder()
  231. local target = tmpdir .. DIR_DELIM .. "packages.json"
  232. assert(core.create_dir(tmpdir))
  233. local base_url = core.settings:get("contentdb_url")
  234. local url = base_url ..
  235. "/api/packages/?type=mod&type=game&type=txp&protocol_version=" ..
  236. core.get_max_supp_proto()
  237. for _, item in pairs(core.settings:get("contentdb_flag_blacklist"):split(",")) do
  238. item = item:trim()
  239. if item ~= "" then
  240. url = url .. "&hide=" .. item
  241. end
  242. end
  243. core.download_file(url, target)
  244. local file = io.open(target, "r")
  245. if file then
  246. store.packages_full = core.parse_json(file:read("*all")) or {}
  247. file:close()
  248. for _, package in pairs(store.packages_full) do
  249. package.url = base_url .. "/packages/" ..
  250. package.author .. "/" .. package.name ..
  251. "/releases/" .. package.release .. "/download/"
  252. local name_len = #package.name
  253. if package.type == "game" and name_len > 5 and package.name:sub(name_len - 4) == "_game" then
  254. package.id = package.author:lower() .. "/" .. package.name:sub(1, name_len - 5)
  255. else
  256. package.id = package.author:lower() .. "/" .. package.name
  257. end
  258. end
  259. store.packages = store.packages_full
  260. store.loaded = true
  261. end
  262. core.delete_dir(tmpdir)
  263. end
  264. function store.update_paths()
  265. local mod_hash = {}
  266. pkgmgr.refresh_globals()
  267. for _, mod in pairs(pkgmgr.global_mods:get_list()) do
  268. if mod.author then
  269. mod_hash[mod.author:lower() .. "/" .. mod.name] = mod
  270. end
  271. end
  272. local game_hash = {}
  273. pkgmgr.update_gamelist()
  274. for _, game in pairs(pkgmgr.games) do
  275. if game.author ~= "" then
  276. game_hash[game.author:lower() .. "/" .. game.id] = game
  277. end
  278. end
  279. local txp_hash = {}
  280. for _, txp in pairs(pkgmgr.get_texture_packs()) do
  281. if txp.author then
  282. txp_hash[txp.author:lower() .. "/" .. txp.name] = txp
  283. end
  284. end
  285. for _, package in pairs(store.packages_full) do
  286. local content
  287. if package.type == "mod" then
  288. content = mod_hash[package.id]
  289. elseif package.type == "game" then
  290. content = game_hash[package.id]
  291. elseif package.type == "txp" then
  292. content = txp_hash[package.id]
  293. end
  294. if content then
  295. package.path = content.path
  296. package.installed_release = content.release or 0
  297. else
  298. package.path = nil
  299. end
  300. end
  301. end
  302. function store.filter_packages(query)
  303. if query == "" and filter_type == 1 then
  304. store.packages = store.packages_full
  305. return
  306. end
  307. local keywords = {}
  308. for word in query:lower():gmatch("%S+") do
  309. table.insert(keywords, word)
  310. end
  311. local function matches_keywords(package, keywords)
  312. for k = 1, #keywords do
  313. local keyword = keywords[k]
  314. if string.find(package.name:lower(), keyword, 1, true) or
  315. string.find(package.title:lower(), keyword, 1, true) or
  316. string.find(package.author:lower(), keyword, 1, true) or
  317. string.find(package.short_description:lower(), keyword, 1, true) then
  318. return true
  319. end
  320. end
  321. return false
  322. end
  323. store.packages = {}
  324. for _, package in pairs(store.packages_full) do
  325. if (query == "" or matches_keywords(package, keywords)) and
  326. (filter_type == 1 or package.type == filter_types_type[filter_type]) then
  327. store.packages[#store.packages + 1] = package
  328. end
  329. end
  330. end
  331. function store.get_formspec(dlgdata)
  332. store.update_paths()
  333. dlgdata.pagemax = math.max(math.ceil(#store.packages / num_per_page), 1)
  334. if cur_page > dlgdata.pagemax then
  335. cur_page = 1
  336. end
  337. local formspec
  338. if #store.packages_full > 0 then
  339. formspec = {
  340. "size[12,7;true]",
  341. "position[0.5,0.55]",
  342. "field[0.2,0.1;7.8,1;search_string;;",
  343. core.formspec_escape(search_string), "]",
  344. "field_close_on_enter[search_string;false]",
  345. "button[7.7,-0.2;2,1;search;",
  346. fgettext("Search"), "]",
  347. "dropdown[9.7,-0.1;2.4;type;",
  348. table.concat(filter_types_titles, ","),
  349. ";", filter_type, "]",
  350. -- "textlist[0,1;2.4,5.6;a;",
  351. -- table.concat(taglist, ","), "]",
  352. -- Page nav buttons
  353. "container[0,",
  354. num_per_page + 1.5, "]",
  355. "button[-0.1,0;3,1;back;",
  356. fgettext("Back to Main Menu"), "]",
  357. "button[7.1,0;1,1;pstart;<<]",
  358. "button[8.1,0;1,1;pback;<]",
  359. "label[9.2,0.2;",
  360. tonumber(cur_page), " / ",
  361. tonumber(dlgdata.pagemax), "]",
  362. "button[10.1,0;1,1;pnext;>]",
  363. "button[11.1,0;1,1;pend;>>]",
  364. "container_end[]",
  365. }
  366. if #store.packages == 0 then
  367. formspec[#formspec + 1] = "label[4,3;"
  368. formspec[#formspec + 1] = fgettext("No results")
  369. formspec[#formspec + 1] = "]"
  370. end
  371. else
  372. formspec = {
  373. "size[12,7;true]",
  374. "position[0.5,0.55]",
  375. "label[4,3;", fgettext("No packages could be retrieved"), "]",
  376. "button[-0.1,",
  377. num_per_page + 1.5,
  378. ";3,1;back;",
  379. fgettext("Back to Main Menu"), "]",
  380. }
  381. end
  382. local start_idx = (cur_page - 1) * num_per_page + 1
  383. for i=start_idx, math.min(#store.packages, start_idx+num_per_page-1) do
  384. local package = store.packages[i]
  385. formspec[#formspec + 1] = "container[0.5,"
  386. formspec[#formspec + 1] = (i - start_idx) * 1.1 + 1
  387. formspec[#formspec + 1] = "]"
  388. -- image
  389. formspec[#formspec + 1] = "image[-0.4,0;1.5,1;"
  390. formspec[#formspec + 1] = core.formspec_escape(get_screenshot(package))
  391. formspec[#formspec + 1] = "]"
  392. -- title
  393. formspec[#formspec + 1] = "label[1,-0.1;"
  394. formspec[#formspec + 1] = core.formspec_escape(
  395. minetest.colorize(mt_color_green, package.title) ..
  396. minetest.colorize("#BFBFBF", " by " .. package.author))
  397. formspec[#formspec + 1] = "]"
  398. -- description
  399. if package.path and package.installed_release < package.release then
  400. formspec[#formspec + 1] = "textarea[1.25,0.3;7.5,1;;;"
  401. else
  402. formspec[#formspec + 1] = "textarea[1.25,0.3;9,1;;;"
  403. end
  404. formspec[#formspec + 1] = core.formspec_escape(package.short_description)
  405. formspec[#formspec + 1] = "]"
  406. -- buttons
  407. if not package.path then
  408. formspec[#formspec + 1] = "button[9.9,0;1.5,1;install_"
  409. formspec[#formspec + 1] = tostring(i)
  410. formspec[#formspec + 1] = ";"
  411. formspec[#formspec + 1] = fgettext("Install")
  412. formspec[#formspec + 1] = "]"
  413. else
  414. if package.installed_release < package.release then
  415. -- The install_ action also handles updating
  416. formspec[#formspec + 1] = "button[8.4,0;1.5,1;install_"
  417. formspec[#formspec + 1] = tostring(i)
  418. formspec[#formspec + 1] = ";"
  419. formspec[#formspec + 1] = fgettext("Update")
  420. formspec[#formspec + 1] = "]"
  421. end
  422. formspec[#formspec + 1] = "button[9.9,0;1.5,1;uninstall_"
  423. formspec[#formspec + 1] = tostring(i)
  424. formspec[#formspec + 1] = ";"
  425. formspec[#formspec + 1] = fgettext("Uninstall")
  426. formspec[#formspec + 1] = "]"
  427. end
  428. --formspec[#formspec + 1] = "button[9.9,0;1.5,1;view_"
  429. --formspec[#formspec + 1] = tostring(i)
  430. --formspec[#formspec + 1] = ";"
  431. --formspec[#formspec + 1] = fgettext("View")
  432. --formspec[#formspec + 1] = "]"
  433. formspec[#formspec + 1] = "container_end[]"
  434. end
  435. return table.concat(formspec, "")
  436. end
  437. function store.handle_submit(this, fields)
  438. if fields.search or fields.key_enter_field == "search_string" then
  439. search_string = fields.search_string:trim()
  440. cur_page = 1
  441. store.filter_packages(search_string)
  442. return true
  443. end
  444. if fields.back then
  445. this:delete()
  446. return true
  447. end
  448. if fields.pstart then
  449. cur_page = 1
  450. return true
  451. end
  452. if fields.pend then
  453. cur_page = this.data.pagemax
  454. return true
  455. end
  456. if fields.pnext then
  457. cur_page = cur_page + 1
  458. if cur_page > this.data.pagemax then
  459. cur_page = 1
  460. end
  461. return true
  462. end
  463. if fields.pback then
  464. if cur_page == 1 then
  465. cur_page = this.data.pagemax
  466. else
  467. cur_page = cur_page - 1
  468. end
  469. return true
  470. end
  471. if fields.type then
  472. local new_type = table.indexof(filter_types_titles, fields.type)
  473. if new_type ~= filter_type then
  474. filter_type = new_type
  475. store.filter_packages(search_string)
  476. return true
  477. end
  478. end
  479. local start_idx = (cur_page - 1) * num_per_page + 1
  480. assert(start_idx ~= nil)
  481. for i=start_idx, math.min(#store.packages, start_idx+num_per_page-1) do
  482. local package = store.packages[i]
  483. assert(package)
  484. if fields["install_" .. i] then
  485. start_install(this, package)
  486. return true
  487. end
  488. if fields["uninstall_" .. i] then
  489. local dlg_delmod = create_delete_content_dlg(package)
  490. dlg_delmod:set_parent(this)
  491. this:hide()
  492. dlg_delmod:show()
  493. return true
  494. end
  495. if fields["view_" .. i] then
  496. local dlg = package_dialog.create(package)
  497. dlg:set_parent(this)
  498. this:hide()
  499. dlg:show()
  500. return true
  501. end
  502. end
  503. return false
  504. end
  505. function create_store_dlg(type)
  506. if not store.loaded or #store.packages_full == 0 then
  507. store.load()
  508. end
  509. search_string = ""
  510. cur_page = 1
  511. store.filter_packages(search_string)
  512. return dialog_create("store",
  513. store.get_formspec,
  514. store.handle_submit,
  515. nil)
  516. end