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