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