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