Rigs of Rods 2023.09
Soft-body Physics Simulation
All Data Structures Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Modules Pages
Loading...
Searching...
No Matches
GUI_RepositorySelector.cpp
Go to the documentation of this file.
1/*
2 This source file is part of Rigs of Rods
3 Copyright 2005-2012 Pierre-Michel Ricordel
4 Copyright 2007-2012 Thomas Fischer
5 Copyright 2013-2021 Petr Ohlidal
6
7 For more information, see http://www.rigsofrods.org/
8
9 Rigs of Rods is free software: you can redistribute it and/or modify
10 it under the terms of the GNU General Public License version 3, as
11 published by the Free Software Foundation.
12
13 Rigs of Rods is distributed in the hope that it will be useful,
14 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 GNU General Public License for more details.
17
18 You should have received a copy of the GNU General Public License
19 along with Rigs of Rods. If not, see <http://www.gnu.org/licenses/>.
20*/
21
26
28
29#include "Application.h"
30#include "BBDocument.h"
31#include "GameContext.h"
32#include "AppContext.h"
33#include "Console.h"
34#include "ContentManager.h"
35#include "GUIManager.h"
36#include "GUIUtils.h"
37#include "Language.h"
38#include "PlatformUtils.h"
39#include "RoRVersion.h"
40
41#include <imgui.h>
42#include <imgui_internal.h>
43#include <rapidjson/document.h>
44#include <vector>
45#include <fmt/core.h>
46#include <stdio.h>
47#include <OgreFileSystemLayer.h>
48
49#ifdef USE_CURL
50# include <curl/curl.h>
51# include <curl/easy.h>
52#endif //USE_CURL
53
54#if defined(_MSC_VER) && defined(GetObject) // This MS Windows macro from <wingdi.h> (Windows Kit 8.1) clashes with RapidJSON
55# undef GetObject
56#endif
57
58using namespace RoR;
59using namespace GUI;
60using namespace bbcpp; // See 'BBDocument.h'
61
62#if defined(USE_CURL)
63
64static size_t CurlWriteFunc(void *ptr, size_t size, size_t nmemb, std::string* data)
65{
66 data->append((char*)ptr, size * nmemb);
67 return size * nmemb;
68}
69
76
77static size_t CurlProgressFunc(void* ptr, double filesize_B, double downloaded_B)
78{
79 // Ensure that the file to be downloaded is not empty because that would cause a division by zero error later on
80 if (filesize_B <= 0.0)
81 {
82 return 0;
83 }
84
86
87 double perc = (downloaded_B / filesize_B) * 100;
88
89 if (perc > context->old_perc)
90 {
92 m.payload = reinterpret_cast<void*>(new int(perc));
93 m.description = fmt::format("{} {}\n{}: {:.2f}{}\n{}: {:.2f}{}", "Downloading", context->filename, "File size", filesize_B/(1024 * 1024), "MB", "Downloaded", downloaded_B/(1024 * 1024), "MB");
95 }
96
97 context->old_perc = perc;
98
99 // If you don't return 0, the transfer will be aborted - see the documentation
100 return 0;
101}
102
103static size_t CurlOgreDataStreamWriteFunc(char* data_ptr, size_t _unused, size_t data_length, void* userdata)
104{
105 Ogre::DataStream* ogre_datastream = static_cast<Ogre::DataStream*>(userdata);
106 if (data_length > 0 && ogre_datastream->isWriteable())
107 {
108 return ogre_datastream->write((const void*)data_ptr, data_length);
109 }
110 else
111 {
112 return 0;
113 }
114}
115
116std::vector<GUI::ResourceCategories> GetResourceCategories(std::string portal_url)
117{
118 std::string repolist_url = portal_url + "/resource-categories";
119 std::string response_payload;
120 std::string response_header;
121 long response_code = 0;
122 std::string user_agent = fmt::format("{}/{}", "Rigs of Rods Client", ROR_VERSION_STRING);
123
124 // The CURL* handle is not multithreaded, see https://curl.se/libcurl/c/threadsafe.html
125 // For simplicity we avoid any reuse during OGRE14 migration.
126 CURL *curl = curl_easy_init();
127 curl_easy_setopt(curl, CURLOPT_URL, repolist_url.c_str());
128 curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
129#ifdef _WIN32
130 curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA);
131#endif // _WIN32
132 curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "gzip");
133 curl_easy_setopt(curl, CURLOPT_USERAGENT, user_agent.c_str());
134 curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteFunc);
135 curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_payload);
136 curl_easy_setopt(curl, CURLOPT_HEADERDATA, &response_header);
137
138 CURLcode curl_result = curl_easy_perform(curl);
139 curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);
140
141 curl_easy_cleanup(curl);
142 curl = nullptr;
143
144 std::vector<GUI::ResourceCategories> cat;
145 if (curl_result != CURLE_OK || response_code != 200)
146 {
147 Ogre::LogManager::getSingleton().stream()
148 << "[RoR|Repository] Failed to retrieve category list;"
149 << " Error: '" << curl_easy_strerror(curl_result) << "'; HTTP status code: " << response_code;
150 return cat;
151 }
152
153 rapidjson::Document j_data_doc;
154 j_data_doc.Parse(response_payload.c_str());
155
156 rapidjson::Value& j_resp_body = j_data_doc["categories"];
157 size_t num_rows = j_resp_body.GetArray().Size();
158 cat.resize(num_rows);
159 for (size_t i = 0; i < num_rows; i++)
160 {
161 rapidjson::Value& j_row = j_resp_body[static_cast<rapidjson::SizeType>(i)];
162
163 cat[i].title = j_row["title"].GetString();
164 cat[i].resource_category_id = j_row["resource_category_id"].GetInt();
165 cat[i].resource_count = j_row["resource_count"].GetInt();
166 cat[i].description = j_row["description"].GetString();
167 cat[i].display_order = j_row["display_order"].GetInt();
168 }
169
170 return cat;
171}
172
173void GetResources(std::string portal_url)
174{
175 std::string repolist_url = portal_url + "/resources";
176 std::string response_payload;
177 std::string response_header;
178 long response_code = 0;
179 std::string user_agent = fmt::format("{}/{}", "Rigs of Rods Client", ROR_VERSION_STRING);
180
181 CURL *curl = curl_easy_init();
182 curl_easy_setopt(curl, CURLOPT_URL, repolist_url.c_str());
183 curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
184#ifdef _WIN32
185 curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA);
186#endif // _WIN32
187 curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "gzip");
188 curl_easy_setopt(curl, CURLOPT_USERAGENT, user_agent.c_str());
189 curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteFunc);
190 curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_payload);
191 curl_easy_setopt(curl, CURLOPT_HEADERDATA, &response_header);
192
193 CURLcode curl_result = curl_easy_perform(curl);
194 curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);
195
196 curl_easy_cleanup(curl);
197 curl = nullptr;
198
199 if (curl_result != CURLE_OK || response_code != 200)
200 {
201 Ogre::LogManager::getSingleton().stream()
202 << "[RoR|Repository] Failed to retrieve repolist;"
203 << " Error: '"<< curl_easy_strerror(curl_result) << "'; HTTP status code: " << response_code;
204
205 CurlFailInfo* failinfo = new CurlFailInfo();
206 failinfo->title = _LC("RepositorySelector", "Could not connect to server. Please check your connection.");
207 failinfo->curl_result = curl_result;
208 failinfo->http_response = response_code;
209
212 return;
213 }
214
215 rapidjson::Document j_data_doc;
216 j_data_doc.Parse(response_payload.c_str());
217 if (j_data_doc.HasParseError() || !j_data_doc.IsObject())
218 {
219 Ogre::LogManager::getSingleton().stream()
220 << "[RoR|Repository] Error parsing repolist JSON, code: " << j_data_doc.GetParseError();
222 Message(MSG_NET_REFRESH_REPOLIST_FAILURE, _LC("RepositorySelector", "Received malformed data. Please try again.")));
223 return;
224 }
225
227
228 std::vector<GUI::ResourceItem> resc;
229 rapidjson::Value& j_resp_body = j_data_doc["resources"];
230 size_t num_rows = j_resp_body.GetArray().Size();
231 resc.resize(num_rows);
232
233 for (size_t i = 0; i < num_rows; i++)
234 {
235 rapidjson::Value& j_row = j_resp_body[static_cast<rapidjson::SizeType>(i)];
236
237 resc[i].title = j_row["title"].GetString();
238 resc[i].tag_line = j_row["tag_line"].GetString();
239 resc[i].resource_id = j_row["resource_id"].GetInt();
240 resc[i].download_count = j_row["download_count"].GetInt();
241 resc[i].last_update = j_row["last_update"].GetInt();
242 resc[i].resource_category_id = j_row["resource_category_id"].GetInt();
243 resc[i].icon_url = j_row["icon_url"].GetString();
244 resc[i].rating_avg = j_row["rating_avg"].GetFloat();
245 resc[i].rating_count = j_row["rating_count"].GetInt();
246 resc[i].version = j_row["version"].GetString();
247 resc[i].authors = j_row["custom_fields"]["authors"].GetString();
248 resc[i].view_url = j_row["view_url"].GetString();
249 resc[i].resource_date = j_row["resource_date"].GetInt();
250 resc[i].view_count = j_row["view_count"].GetInt();
251 resc[i].preview_tex = Ogre::TexturePtr(); // null
252 // NOTE: description is stripped here for bandwidth reasons - fetched separately from individual resources.
253 }
254
255 cdata_ptr->items = resc;
256 cdata_ptr->categories = GetResourceCategories(portal_url);
257
259 Message(MSG_NET_REFRESH_REPOLIST_SUCCESS, (void*)cdata_ptr));
260}
261
262void GetResourceFiles(std::string portal_url, int resource_id)
263{
264 std::string response_payload;
265 std::string resource_url = portal_url + "/resources/" + std::to_string(resource_id);
266 std::string user_agent = fmt::format("{}/{}", "Rigs of Rods Client", ROR_VERSION_STRING);
267 long response_code = 0;
268
269 CURL *curl = curl_easy_init();
270 curl_easy_setopt(curl, CURLOPT_URL, resource_url.c_str());
271 curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
272#ifdef _WIN32
273 curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA);
274#endif // _WIN32
275 curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "gzip");
276 curl_easy_setopt(curl, CURLOPT_USERAGENT, user_agent.c_str());
277 curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteFunc);
278 curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_payload);
279
280 CURLcode curl_result = curl_easy_perform(curl);
281 curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);
282
283 curl_easy_cleanup(curl);
284 curl = nullptr;
285
286 if (curl_result != CURLE_OK || response_code != 200)
287 {
288 Ogre::LogManager::getSingleton().stream()
289 << "[RoR|Repository] Failed to retrieve resource;"
290 << " Error: '" << curl_easy_strerror(curl_result) << "'; HTTP status code: " << response_code;
291
292 // FIXME: we need a FAILURE message for MSG_NET_OPEN_RESOURCE_SUCCESS
293 }
294
296
297 rapidjson::Document j_data_doc;
298 j_data_doc.Parse(response_payload.c_str());
299
300 std::vector<GUI::ResourceFiles> resc;
301 rapidjson::Value& j_resp_body = j_data_doc["resource"]["current_files"];
302 size_t num_rows = j_resp_body.GetArray().Size();
303 resc.resize(num_rows);
304
305 for (size_t i = 0; i < num_rows; i++)
306 {
307 rapidjson::Value& j_row = j_resp_body[static_cast<rapidjson::SizeType>(i)];
308
309 resc[i].id = j_row["id"].GetInt();
310 resc[i].filename = j_row["filename"].GetString();
311 resc[i].size = j_row["size"].GetInt();
312 }
313
314 cdata_ptr->files = resc;
315
316 // Also pass on the description (via a dummy item)
317 ResourceItem item;
319 item.description->load(j_data_doc["resource"]["description"].GetString());
320 item.resource_id = j_data_doc["resource"]["resource_id"].GetInt();
321 cdata_ptr->items.push_back(item);
322
324 Message(MSG_NET_OPEN_RESOURCE_SUCCESS, (void*)cdata_ptr));
325}
326
328{
330 int perc = 0;
331 m.payload = reinterpret_cast<void*>(new int(perc));
332 m.description = "Initialising...";
334
335 std::string url = "https://forum.rigsofrods.org/resources/" + std::to_string(request.rfir_resource_id) + "/download?file=" + std::to_string(request.rfir_repofile_id);
336 std::string part_filepath = request.rfir_filepath + ".part";
337
338 RepoProgressContext progress_context;
339 progress_context.filename = request.rfir_filename;
340 progress_context.install_request_id = request.rfir_install_request_id;
341 long response_code = 0;
342
343 CURL *curl = curl_easy_init();
344 try // We write using Ogre::DataStream which throws exceptions
345 {
346 // smart pointer - closes stream automatically
347 Ogre::DataStreamPtr datastream = Ogre::ResourceGroupManager::getSingleton().createResource(part_filepath, RGN_CACHE);
348
349 curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
350 curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
351#ifdef _WIN32
352 curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA);
353#endif // _WIN32
354 curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlOgreDataStreamWriteFunc);
355 curl_easy_setopt(curl, CURLOPT_WRITEDATA, datastream.get());
356 curl_easy_setopt(curl, CURLOPT_NOPROGRESS, NULL); // Disable Internal CURL progressmeter
357 curl_easy_setopt(curl, CURLOPT_PROGRESSDATA, &progress_context);
358 curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION, CurlProgressFunc); // Use our progress window
359
360 CURLcode curl_result = curl_easy_perform(curl);
361 curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);
362
363 if (curl_result != CURLE_OK || response_code != 200)
364 {
365 Ogre::LogManager::getSingleton().stream()
366 << "[RoR|Repository] Failed to download resource;"
367 << " Error: '" << curl_easy_strerror(curl_result) << "'; HTTP status code: " << response_code;
368
370 }
371 }
372 catch (Ogre::Exception& oex)
373 {
376 fmt::format("Repository UI: cannot download file '{}' - {}",
377 url, oex.getFullDescription()));
378 }
379 curl_easy_cleanup(curl);
380 curl = nullptr;
381
383}
384#endif // defined(USE_CURL)
385
387{
388 Ogre::WorkQueue* wq = Ogre::Root::getSingleton().getWorkQueue();
389 m_ogre_workqueue_channel = wq->getChannel("RoR/RepoThumbnails");
391 m_fallback_thumbnail = FetchIcon("ror.png");
392}
393
396
398{
400
401 ImGui::SetNextWindowSize(ImVec2((ImGui::GetIO().DisplaySize.x / 1.4), (ImGui::GetIO().DisplaySize.y / 1.2)), ImGuiCond_FirstUseEver);
402 ImGui::SetNextWindowPosCenter(ImGuiCond_Appearing);
403 ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse;
404 bool keep_open = true;
405 Ogre::TexturePtr tex1 = FetchIcon("arrow_rotate_anticlockwise.png");
406 Ogre::TexturePtr tex2 = FetchIcon("accept.png");
407 Ogre::TexturePtr tex3 = FetchIcon("star.png");
408 Ogre::TexturePtr tex4 = FetchIcon("arrow_left.png");
409
410 ImGui::Begin(_LC("RepositorySelector", "Rigs of Rods Repository"), &keep_open, window_flags);
411
413 && ImGui::ImageButton(reinterpret_cast<ImTextureID>(tex4->getHandle()), ImVec2(16, 16)))
414 {
416 {
418 }
419 else
420 {
422 }
423 }
425 && ImGui::ImageButton(reinterpret_cast<ImTextureID>(tex1->getHandle()), ImVec2(16, 16)))
426 {
427 this->Refresh();
428 }
429 ImGui::SameLine();
430
431 if (m_draw)
432 {
433 // Deactivate in resource view
435 {
436 ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true);
437 ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.5f);
438 }
439
440 // Category dropdown
441 ImGui::SetNextItemWidth(ImGui::GetWindowSize().x
442 - 16 // refresh button width
443 - 170 // search box width
444 - 2*80 // sort + view menu width
445 - 6*ImGui::GetStyle().ItemSpacing.x
446 - 2*ImGui::GetStyle().WindowPadding.x);
447
448 // Calculate items of every shown category
449 int count = 0;
450 for (int i = 0; i < m_data.categories.size(); i++)
451 {
452 // Skip non mod categories
453 if (m_data.categories[i].resource_category_id >= 8 && m_data.categories[i].resource_category_id <= 13)
454 {
455 continue;
456 }
457 count += m_data.categories[i].resource_count;
458 }
459
460 // Fill "All" category
461 if (m_current_category_id == 1)
462 {
463 m_current_category = "(" + std::to_string(count) + ") All";
465 }
466
467 if (ImGui::BeginCombo("##repo-selector-cat", m_current_category.c_str()))
468 {
469 if (ImGui::Selectable(m_all_category_label.c_str(), m_current_category_id == 1))
470 {
473 }
474
475 for (int i = 0; i < m_data.categories.size(); i++)
476 {
477 // Skip non mod categories
478 if (m_data.categories[i].resource_category_id >= 8 && m_data.categories[i].resource_category_id <= 13)
479 {
480 continue;
481 }
482
483 m_current_category_label = "(" + std::to_string(m_data.categories[i].resource_count) + ") " + m_data.categories[i].title;
484 bool is_selected = (m_current_category_id == m_data.categories[i].resource_category_id);
485
486 if (ImGui::Selectable(m_current_category_label.c_str(), is_selected))
487 {
489 m_current_category_id = m_data.categories[i].resource_category_id;
490 }
491 if (is_selected)
492 {
493 ImGui::SetItemDefaultFocus();
494 }
495 }
496 ImGui::EndCombo();
497 }
498
499 // Search box
500 ImGui::SameLine();
501 float searchbox_x = ImGui::GetCursorPosX();
502 ImGui::SetNextItemWidth(170);
503 float search_pos = ImGui::GetCursorPosX();
504 ImGui::InputText("##Search", m_search_input.GetBuffer(), m_search_input.GetCapacity());
505
506 // Sort dropdown
507 ImGui::SameLine();
508 ImGui::SetNextItemWidth(80);
509
510 if (ImGui::BeginCombo("##repo-selector-sort", _LC("RepositorySelector", "Sort")))
511 {
512 if (ImGui::Selectable(_LC("RepositorySelector", "Last Update"), m_current_sort == "Last Update"))
513 {
514 m_current_sort = "Last Update";
515 std::sort(m_data.items.begin(), m_data.items.end(), [](ResourceItem a, ResourceItem b) { return a.last_update > b.last_update; });
516 }
517 if (ImGui::Selectable(_LC("RepositorySelector", "Date Added"), m_current_sort == "Date Added"))
518 {
519 m_current_sort = "Date Added";
520 std::sort(m_data.items.begin(), m_data.items.end(), [](ResourceItem a, ResourceItem b) { return a.resource_date > b.resource_date; });
521 }
522 if (ImGui::Selectable(_LC("RepositorySelector", "Title"), m_current_sort == "Title"))
523 {
524 m_current_sort = "Title";
525 std::sort(m_data.items.begin(), m_data.items.end(), [](ResourceItem a, ResourceItem b) { return a.title < b.title; });
526 }
527 if (ImGui::Selectable(_LC("RepositorySelector", "Downloads"), m_current_sort == "Downloads"))
528 {
529 m_current_sort = "Downloads";
530 std::sort(m_data.items.begin(), m_data.items.end(), [](ResourceItem a, ResourceItem b) { return a.download_count > b.download_count; });
531 }
532 if (ImGui::Selectable(_LC("RepositorySelector", "Rating"), m_current_sort == "Rating"))
533 {
534 m_current_sort = "Rating";
535 std::sort(m_data.items.begin(), m_data.items.end(), [](ResourceItem a, ResourceItem b) { return a.rating_avg > b.rating_avg; });
536 }
537 if (ImGui::Selectable(_LC("RepositorySelector", "Rating Count"), m_current_sort == "Rating Count"))
538 {
539 m_current_sort = "Rating Count";
540 std::sort(m_data.items.begin(), m_data.items.end(), [](ResourceItem a, ResourceItem b) { return a.rating_count > b.rating_count; });
541 }
542 ImGui::EndCombo();
543 }
544
545 // View mode dropdown
546 ImGui::SameLine();
547 ImGui::SetNextItemWidth(80);
548
549 if (ImGui::BeginCombo("##repo-selector-view", _LC("RepositorySelector", "View")))
550 {
551 if (ImGui::Selectable(_LC("RepositorySelector", "List"), m_view_mode == "List"))
552 {
553 m_view_mode = "List";
554 }
555 if (ImGui::Selectable(_LC("RepositorySelector", "Compact"), m_view_mode == "Compact"))
556 {
557 m_view_mode = "Compact";
558 }
559 if (ImGui::Selectable(_LC("RepositorySelector", "Basic"), m_view_mode == "Basic"))
560 {
561 m_view_mode = "Basic";
562 }
563 ImGui::EndCombo();
564 }
565
566 // Search box default text
568 {
569 ImGui::SameLine();
570 ImGui::SetCursorPosX(search_pos + ImGui::GetStyle().ItemSpacing.x);
571 ImGui::TextDisabled("%s", _LC("RepositorySelector", "Search Title, Author"));
572 }
573
575 {
576 ImGui::PopItemFlag();
577 ImGui::PopStyleVar();
578 }
579
580 const float table_height = ImGui::GetWindowHeight()
581 - ((2.f * ImGui::GetStyle().WindowPadding.y) + (3.f * ImGui::GetItemsLineHeightWithSpacing())
582 - ImGui::GetStyle().ItemSpacing.y);
583
585 {
587 {
588 this->DrawGalleryView();
590 }
591 else
592 {
593 this->DrawResourceView(searchbox_x);
595 }
596 }
597 else
598 {
599
600 float col0_width = 0.40f * ImGui::GetWindowContentRegionWidth();
601 float col1_width = 0.15f * ImGui::GetWindowContentRegionWidth();
602 float col2_width = 0.20f * ImGui::GetWindowContentRegionWidth();
603 float col3_width = 0.10f * ImGui::GetWindowContentRegionWidth();
604
605 if (m_view_mode == "Basic")
606 {
607 ImGui::Columns(5, "repo-selector-columns-basic-headers", false);
608 ImGui::SetColumnWidth(0, col0_width + ImGui::GetStyle().ItemSpacing.x);
609 ImGui::SetColumnWidth(1, col1_width);
610 ImGui::SetColumnWidth(2, col2_width);
611 ImGui::SetColumnWidth(3, col3_width);
612
613 ImGui::TextDisabled("%s", _LC("RepositorySelector", "Title"));
614 ImGui::NextColumn();
615 ImGui::TextDisabled("%s", _LC("RepositorySelector", "Version"));
616 ImGui::NextColumn();
617 ImGui::TextDisabled("%s", _LC("RepositorySelector", "Last Update"));
618 ImGui::NextColumn();
619 ImGui::TextDisabled("%s", _LC("RepositorySelector", "Downloads"));
620 ImGui::NextColumn();
621 ImGui::TextDisabled("%s", _LC("RepositorySelector", "Rating"));
622 ImGui::Columns(1);
623 }
624
625 // Scroll area
626 ImGui::BeginChild("scrolling", ImVec2(0.f, table_height), false);
627
628 if (m_view_mode == "List")
629 {
630 ImGui::Columns(2, "repo-selector-columns");
631 ImGui::SetColumnWidth(0, 100.f);
632 ImGui::Separator();
633 }
634 else if (m_view_mode == "Basic")
635 {
636 ImGui::Columns(5, "repo-selector-columns-basic");
637 ImGui::SetColumnWidth(0, col0_width);
638 ImGui::SetColumnWidth(1, col1_width);
639 ImGui::SetColumnWidth(2, col2_width);
640 ImGui::SetColumnWidth(3, col3_width);
641 ImGui::Separator();
642 }
643
644 // Draw table body
645 int num_drawn_items = 0;
646 for (int i = 0; i < m_data.items.size(); i++)
647 {
648 // Skip items from non mod categories
649 if (m_data.items[i].resource_category_id >= 8 && m_data.items[i].resource_category_id <= 13)
650 {
651 continue;
652 }
653
654 if (m_data.items[i].resource_category_id == m_current_category_id || m_current_category_id == 1)
655 {
656 // Simple search filter: convert both title/author and input to lowercase, if input not found in the title/author continue
657 std::string title = m_data.items[i].title;
658 for (auto& c : title)
659 {
660 c = tolower(c);
661 }
662 std::string author = m_data.items[i].authors;
663 for (auto& c : author)
664 {
665 c = tolower(c);
666 }
667 std::string search = m_search_input.GetBuffer();
668 for (auto& c : search)
669 {
670 c = tolower(c);
671 }
672 if (title.find(search) == std::string::npos && author.find(search) == std::string::npos)
673 {
674 continue;
675 }
676
677 ImGui::PushID(i);
678
679 if (m_view_mode == "List")
680 {
681 // Thumbnail
682 ImGui::SetCursorPosX(ImGui::GetCursorPosX() - ImGui::GetStyle().ItemSpacing.x);
683 const ImVec2 thumb_size = ImVec2(ImGui::GetColumnWidth() - ImGui::GetStyle().ItemSpacing.x, 96);
684 const float spinner_size = ImGui::GetColumnWidth() / 4;
685 const float spinner_cursor_x(((ImGui::GetColumnWidth() - ImGui::GetStyle().ItemSpacing.x) / 2.f) - spinner_size);
686 const float spinner_cursor_y(ImGui::GetCursorPosY() + 5 * ImGui::GetStyle().ItemSpacing.y);
687 this->DrawThumbnail(i, thumb_size, spinner_size, ImVec2(spinner_cursor_x, spinner_cursor_y));
688
689 float width = (ImGui::GetColumnWidth() + 90);
690 ImGui::NextColumn();
691
692 // Columns already colored, just add a light background
693 ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.17f, 0.17f, 0.17f, 0.90f));
694 ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImGui::GetStyle().Colors[ImGuiCol_Header]);
695 ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0.22f, 0.22f, 0.22f, 0.90f));
696
697 // Wrap a Selectable around the whole column
698 float orig_cursor_y = ImGui::GetCursorPosY();
699 std::string item_id = "##" + std::to_string(i);
700
701 if (ImGui::Selectable(item_id.c_str(), /*selected:*/false, 0, ImVec2(0, 100)))
702 {
704 this->OpenResource(m_data.items[i].resource_id);
705 }
706
707 ImGui::SetCursorPosY(orig_cursor_y);
708 ImGui::PopStyleColor(3);
709
710 // Title, version
711 ImGui::Text("%s", m_data.items[i].title.c_str());
712 ImGui::SameLine();
713 ImGui::TextDisabled("%s", m_data.items[i].version.c_str());
714
715 // Rating
716 for (int i = 1; i <= 5; i++)
717 {
718 ImGui::SameLine();
719 ImGui::SetCursorPosX(ImGui::GetColumnWidth() + 16 * i);
720 ImGui::Image(reinterpret_cast<ImTextureID>(tex3->getHandle()), ImVec2(16, 16), ImVec2(0.f, 0.f), ImVec2(1.f, 1.f), ImVec4(1.f, 1.f, 1.f, 0.2f));
721 }
722
723 int rating = round(m_data.items[i].rating_avg);
724 for (int i = 1; i <= rating; i++)
725 {
726 ImGui::SameLine();
727 ImGui::SetCursorPosX(ImGui::GetColumnWidth() + 16 * i);
728 ImGui::Image(reinterpret_cast<ImTextureID>(tex3->getHandle()), ImVec2(16, 16));
729 }
730
731 // Authors, rating count, last update, download count, description
732 ImGui::TextDisabled("%s:", _LC("RepositorySelector", "Authors"));
733 ImGui::SameLine();
734 ImGui::SetCursorPosX(width);
735 ImGui::TextColored(theme.value_blue_text_color, "%s", m_data.items[i].authors.c_str());
736
737 ImGui::SameLine();
738 std::string rc = std::to_string(m_data.items[i].rating_count) + " ratings";
739 ImGui::SetCursorPosX(ImGui::GetColumnWidth() - (ImGui::CalcTextSize(rc.c_str()).x / 2) + 16 * 3.5);
740 ImGui::TextDisabled("%s", rc.c_str());
741
742 ImGui::TextDisabled("%s:", _LC("RepositorySelector", "Last Update"));
743 ImGui::SameLine();
744 ImGui::SetCursorPosX(width);
745 time_t rawtime = (const time_t)m_data.items[i].last_update;
746 ImGui::TextColored(theme.value_blue_text_color, "%s", asctime(gmtime(&rawtime)));
747
748 ImGui::TextDisabled("%s:", _LC("RepositorySelector", "Downloads"));
749 ImGui::SameLine();
750 ImGui::SetCursorPosX(width);
751 ImGui::TextColored(theme.value_blue_text_color, "%d", m_data.items[i].download_count);
752
753 ImGui::TextDisabled("%s:", _LC("RepositorySelector", "Description"));
754 ImGui::SameLine();
755 ImGui::SetCursorPosX(width);
756 ImGui::TextColored(theme.value_blue_text_color, "%s", m_data.items[i].tag_line.c_str());
757
758 ImGui::NextColumn();
759
760 ImGui::Separator();
761 }
762 else if (m_view_mode == "Compact")
763 {
764 float orig_cursor_x = ImGui::GetCursorPos().x;
765
766 // Calc box size: Draw 3 boxes per line, 2 for small resolutions
767 float box_width = (ImGui::GetIO().DisplaySize.x / 1.4) / 3;
768 if (ImGui::GetIO().DisplaySize.x <= 1280)
769 {
770 box_width = (ImGui::GetIO().DisplaySize.x / 1.4) / 2;
771 }
772
773 // Skip to new line if at least 50% of the box can't fit on current line.
774 if (orig_cursor_x > ImGui::GetWindowContentRegionMax().x - (box_width * 0.5))
775 {
776 // Unless this is the 1st line... not much to do with such narrow window.
777 if (num_drawn_items != 0)
778 {
779 ImGui::NewLine();
780 }
781 }
782
783 ImGui::BeginGroup();
784
785 ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.70f, 0.70f, 0.70f, 0.90f));
786 ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImGui::GetStyle().Colors[ImGuiCol_Header]);
787 ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0.90f, 0.90f, 0.90f, 0.90f));
788
789 // Wrap a Selectable around images + text
790 float orig_cursor_y = ImGui::GetCursorPosY();
791 std::string item_id = "##" + std::to_string(i);
792
793 if (ImGui::Selectable(item_id.c_str(), /*selected:*/false, 0, ImVec2(box_width - ImGui::GetStyle().ItemSpacing.x, 100)))
794 {
796 this->OpenResource(m_data.items[i].resource_id);
797 }
798
799 // Add a light background
800 ImVec2 p_min = ImGui::GetItemRectMin();
801 ImVec2 p_max = ImGui::GetItemRectMax();
802 ImGui::GetWindowDrawList()->AddRectFilled(p_min, p_max, ImColor(ImVec4(0.15f, 0.15f, 0.15f, 0.90f)));
803
804 ImGui::SetCursorPosY(orig_cursor_y);
805 ImGui::PopStyleColor(3);
806
807 // Thumbnail
808 const ImVec2 thumbnail_size(76, 86);
809 const float spinner_size = 25;
810 const float spinner_cursor_x(ImGui::GetCursorPosX() + 2 * ImGui::GetStyle().ItemSpacing.x);
811 const float spinner_cursor_y(ImGui::GetCursorPosY() + 20);
812 this->DrawThumbnail(i, thumbnail_size, spinner_size, ImVec2(spinner_cursor_x, spinner_cursor_y));
813 if (!m_data.items[i].preview_tex)
814 {
815 ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 76 - (35 + spinner_size)); //adjustment after spinner
816 }
817
818 // Rating
819 float pos_y;
820 for (int i = 1; i <= 5; i++)
821 {
822 pos_y = ImGui::GetCursorPosY();
823 ImGui::Image(reinterpret_cast<ImTextureID>(tex3->getHandle()), ImVec2(11, 11), ImVec2(0.f, 0.f), ImVec2(1.f, 1.f), ImVec4(1.f, 1.f, 1.f, 0.2f));
824 if (i < 5) { ImGui::SameLine(); }
825 }
826
827 int rating = round(m_data.items[i].rating_avg);
828 if (rating >= 1)
829 {
830 for (int i = 1; i <= rating; i++)
831 {
832 ImGui::SetCursorPosY(pos_y);
833 ImGui::Image(reinterpret_cast<ImTextureID>(tex3->getHandle()), ImVec2(11, 11));
834 if (i < rating) { ImGui::SameLine(); }
835 }
836 }
837
838 // Move text top right of the image
839 ImGui::SetCursorPosX(ImGui::GetCursorPos().x + 86);
840 ImGui::SetCursorPosY(ImGui::GetCursorPos().y - 100);
841
842 // Trim the title, can be long
843 std::string tl = m_data.items[i].title;
844 if (ImGui::CalcTextSize(tl.c_str()).x > box_width / 12)
845 {
846 tl.resize(box_width / 12);
847 tl += "...";
848 }
849
850 // Title, version, last update, download count
851 ImGui::Text("%s", tl.c_str());
852
853 ImGui::SetCursorPosX(ImGui::GetCursorPos().x + 86);
854 ImGui::TextColored(theme.value_blue_text_color, "%s %s", _LC("RepositorySelector", "Version"), m_data.items[i].version.c_str());
855
856 ImGui::SetCursorPosX(ImGui::GetCursorPos().x + 86);
857 time_t rawtime = (const time_t)m_data.items[i].last_update;
858 ImGui::TextColored(theme.value_blue_text_color, "%s", asctime(gmtime(&rawtime)));
859
860 ImGui::SetCursorPosX(ImGui::GetCursorPos().x + 86);
861 ImGui::TextColored(theme.value_blue_text_color, "%s %d %s", _LC("RepositorySelector", "Downloaded"), m_data.items[i].download_count, _LC("RepositorySelector", "times"));
862
863 // Add space for next item
864 ImGui::SetCursorPosX(ImGui::GetCursorPos().x + box_width);
865 ImGui::SetCursorPosY(ImGui::GetCursorPos().y + (1.5f * ImGui::GetStyle().WindowPadding.y));
866
867 ImGui::EndGroup();
868 ImGui::SameLine();
869 }
870 else if (m_view_mode == "Basic")
871 {
872 // Columns already colored, just add a light background
873 ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.18f, 0.18f, 0.18f, 0.90f));
874 ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImGui::GetStyle().Colors[ImGuiCol_Header]);
875 ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0.22f, 0.22f, 0.22f, 0.90f));
876
877 // Wrap a Selectable around the whole column
878 std::string item_id = "##" + std::to_string(i);
879
880 if (ImGui::Selectable(item_id.c_str(), /*selected:*/false, ImGuiSelectableFlags_SpanAllColumns))
881 {
883 this->OpenResource(m_data.items[i].resource_id);
884 }
885
886 ImGui::PopStyleColor(3);
887
888 // Draw columns
889 ImGui::SameLine();
890 ImGui::SetCursorPosX(ImGui::GetCursorPosX() - 2 * ImGui::GetStyle().ItemSpacing.x);
891 ImGui::Text("%s", m_data.items[i].title.c_str());
892
893 ImGui::NextColumn();
894
895 ImGui::TextColored(theme.value_blue_text_color, "%s", m_data.items[i].version.c_str());
896
897 ImGui::NextColumn();
898
899 time_t rawtime = (const time_t)m_data.items[i].last_update;
900 ImGui::TextColored(theme.value_blue_text_color, "%s", asctime(gmtime(&rawtime)));
901
902 ImGui::NextColumn();
903
904 ImGui::TextColored(theme.value_blue_text_color, "%d", m_data.items[i].download_count);
905
906 ImGui::NextColumn();
907
908 float pos_x = ImGui::GetCursorPosX();
909
910 // Rating
911 for (int i = 1; i <= 5; i++)
912 {
913 ImGui::Image(reinterpret_cast<ImTextureID>(tex3->getHandle()), ImVec2(16, 16), ImVec2(0.f, 0.f), ImVec2(1.f, 1.f), ImVec4(1.f, 1.f, 1.f, 0.2f));
914 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 16 * i);
915 ImGui::SameLine();
916 }
917 ImGui::SetCursorPosX(pos_x);
918
919 int rating = round(m_data.items[i].rating_avg);
920 for (int i = 1; i <= rating; i++)
921 {
922 ImGui::Image(reinterpret_cast<ImTextureID>(tex3->getHandle()), ImVec2(16, 16));
923 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 16 * i);
924 ImGui::SameLine();
925 }
926
927 ImGui::NextColumn();
928
929 ImGui::Separator();
930 }
931 ImGui::PopID();
932 num_drawn_items++;
933 }
934 }
935 ImGui::EndChild();
936
938 }
939 }
940
941 if (m_show_spinner)
942 {
943 float spinner_size = 27.f;
944 ImGui::SetCursorPosX((ImGui::GetWindowSize().x / 2.f) - spinner_size);
945 ImGui::SetCursorPosY((ImGui::GetWindowSize().y / 2.f) - spinner_size);
946 LoadingIndicatorCircle("spinner", spinner_size, theme.value_blue_text_color, theme.value_blue_text_color, 10, 10);
947 }
948
949 if (m_repolist_msg != "")
950 {
951 const ImVec2 label_size = ImGui::CalcTextSize(m_repolist_msg.c_str());
952 float y = (ImGui::GetWindowSize().y / 2.f) - (ImGui::GetTextLineHeight() / 2.f);
953 ImGui::SetCursorPosX((ImGui::GetWindowSize().x / 2.f) - (label_size.x / 2.f));
954 ImGui::SetCursorPosY(y);
955 ImGui::TextColored(m_repolist_msg_color, "%s", m_repolist_msg.c_str());
956 y += ImGui::GetTextLineHeightWithSpacing();
957
958 if (m_repolist_curlmsg != "")
959 {
960 const ImVec2 detail_size = ImGui::CalcTextSize(m_repolist_curlmsg.c_str());
961 ImGui::SetCursorPosX((ImGui::GetWindowSize().x / 2.f) - (detail_size.x / 2.f));
962 ImGui::SetCursorPosY(y);
963 ImGui::TextDisabled("%s", m_repolist_curlmsg.c_str());
964 y += ImGui::GetTextLineHeight();
965 }
966
967 if (m_repolist_httpmsg != "")
968 {
969 const ImVec2 detail_size = ImGui::CalcTextSize(m_repolist_httpmsg.c_str());
970 ImGui::SetCursorPosX((ImGui::GetWindowSize().x / 2.f) - (detail_size.x / 2.f));
971 ImGui::SetCursorPosY(y);
972 ImGui::TextDisabled("%s", m_repolist_httpmsg.c_str());
973 }
974 }
975
976 ImGui::End();
977 if (!keep_open)
978 {
979 this->SetVisible(false);
980 }
981}
982
984{
985 // show number of queued downloads, total filesize in MB and current download progressbar
986 // --------------------------------------------------------------------------------------
988
989 if (m_queued_install_requests.size() == 0)
990 {
991 ImGui::TextDisabled("%s", _LC("RepositorySelector", "No downloads queued."));
992 return;
993 }
994 // Calculate total size of queued downloads
995 size_t total_bytes = 0;
996 for (auto& req : m_queued_install_requests)
997 {
998 total_bytes += req.rfir_filesize_bytes;
999 }
1000 std::string download_count_text = fmt::format("{} {}", _LC("RepositorySelector", "Num. queued downloads:"), m_queued_install_requests.size());
1001 std::string download_size_text = fmt::format("{} {:.2f} MB", _LC("RepositorySelector", "Total Size:"), total_bytes / (1024.0 * 1024.0));
1002
1003 ImGui::Text("%s (%s)", download_count_text.c_str(), download_size_text.c_str());
1004}
1005
1007{
1008 // Gallery mode - just draw the pic and be done with it.
1010 if (itor != m_repo_attachments.end())
1011 {
1012 Ogre::TexturePtr& tex = itor->second;
1013 ImVec2 img_size(tex->getWidth(), tex->getHeight());
1014 float scale_ratio = 1.f;
1015 // Shrink to fit
1016 if (img_size.x > ImGui::GetContentRegionAvail().x)
1017 {
1018 scale_ratio = ImGui::GetContentRegionAvail().x / img_size.x;
1019 if ((img_size.y * scale_ratio) > ImGui::GetContentRegionAvail().y)
1020 {
1021 scale_ratio = ImGui::GetContentRegionAvail().y / img_size.y;
1022 }
1023 }
1024 ImGui::Image(reinterpret_cast<ImTextureID>(tex->getHandle()), img_size * scale_ratio);
1025 // Left-licking the image will close the gallery mode again
1026 if (ImGui::IsItemHovered(0))
1027 {
1028 ImGui::SetMouseCursor(7);// Hand cursor
1029 if (ImGui::IsMouseClicked(0)) // Left button
1030 {
1032 }
1033 }
1034 }
1035 else
1036 {
1037 m_gallery_mode_attachment_id = -1; // Image not found - close gallery mode.
1038 }
1039}
1040
1042{
1044 Ogre::TexturePtr tex2 = FetchIcon("accept.png");
1045 Ogre::TexturePtr tex3 = FetchIcon("star.png");
1046 Ogre::TexturePtr tex4 = FetchIcon("arrow_left.png");
1048
1049 const float INFOBAR_HEIGHT = 100.f;
1050 const float INFOBAR_SPACING_LEFTSIDE = 2.f;
1051
1052 // --- top info bar, left side ---
1053
1054 // Black background
1055 ImVec2 leftmost_cursor = ImGui::GetCursorPos();
1056 float left_pane_width = searchbox_x - (leftmost_cursor.x + ImGui::GetStyle().ItemSpacing.x);
1057 ImVec2 backdrop_size = ImVec2(left_pane_width, INFOBAR_HEIGHT + ImGui::GetStyle().WindowPadding.y * 2);
1058 ImGui::GetWindowDrawList()->AddRectFilled(ImGui::GetCursorScreenPos(), ImGui::GetCursorScreenPos() + backdrop_size, ImColor(0.f, 0.f, 0.f, 0.5f), /*rounding:*/5.f);
1059 ImGui::SetCursorPos(ImGui::GetCursorPos() + ImGui::GetStyle().WindowPadding);
1060
1061 // The thumbnail again (like on web repo)
1062 ImVec2 thumbnail_cursor = ImGui::GetCursorPos();
1063 const float spinner_size = INFOBAR_HEIGHT / 4;
1064 const ImVec2 spinner_cursor = thumbnail_cursor + ImVec2(INFOBAR_HEIGHT/5, INFOBAR_HEIGHT/5);
1065 this->DrawThumbnail(m_resourceview_item_arraypos, ImVec2(INFOBAR_HEIGHT, INFOBAR_HEIGHT), spinner_size, spinner_cursor);
1066
1067 // Title + version (like on web repo)
1068 ImGui::SameLine();
1069 ImVec2 newline_cursor = thumbnail_cursor + ImVec2(INFOBAR_HEIGHT + ImGui::GetStyle().WindowPadding.x*2, 0.f);
1070 ImGui::SetCursorPos(newline_cursor);
1071 ImGui::TextColored(RESOURCE_TITLE_COLOR, "%s", m_data.items[m_resourceview_item_arraypos].title.c_str());
1072 ImGui::SameLine();
1073
1074 // Far right - "view in browser" hyperlink.
1075 std::string browser_text = _LC("RepositorySelector", "View in web browser");
1076 ImGui::SetCursorPosX(searchbox_x - (ImGui::CalcTextSize(browser_text.c_str()).x + ImGui::GetStyle().ItemSpacing.x + ImGui::GetStyle().WindowPadding.x));
1077 ImHyperlink(selected_item.view_url, browser_text);
1078
1079 // One line below - the tagline (with [..] tooltip button if oversize)
1080 newline_cursor += ImVec2(0.f, ImGui::GetTextLineHeight() + INFOBAR_SPACING_LEFTSIDE);
1081 ImGui::SetCursorPos(newline_cursor);
1082 const ImVec2 tagline_btnsize = ImGui::CalcTextSize("[...]") + ImGui::GetStyle().ItemSpacing * 2;
1083 const ImVec2 tagline_size = ImGui::CalcTextSize(selected_item.tag_line.c_str());
1084 const ImVec2 tagline_clipmin = ImGui::GetCursorScreenPos();
1085 const ImVec2 tagline_clipmax((ImGui::GetWindowPos().x + searchbox_x) - (tagline_btnsize.x + ImGui::GetStyle().FramePadding.x), tagline_clipmin.y + ImGui::GetTextLineHeight());
1086 ImGui::PushClipRect(tagline_clipmin, tagline_clipmax, /* intersect_with_current_cliprect:*/ false);
1087 ImGui::Text("%s", selected_item.tag_line.c_str());
1088 ImGui::PopClipRect();
1089 if (tagline_size.x > tagline_clipmax.x - tagline_clipmin.x)
1090 {
1091 // tagline is oversize - draw [...] tooltip (same line, far right)
1092 ImGui::SetCursorPos(ImVec2(searchbox_x - (tagline_btnsize.x + ImGui::GetStyle().FramePadding.x), newline_cursor.y));
1093 ImGui::TextDisabled("[...]");
1094 if (ImGui::IsItemHovered())
1095 {
1096 ImGui::BeginTooltip();
1097 ImGui::Text("%s", selected_item.tag_line.c_str()); // no clipping this time
1098 ImGui::EndTooltip();
1099 }
1100 }
1101
1102 // One line below (bigger gap) - the version and num downloads
1103 newline_cursor += ImVec2(0.f, ImGui::GetTextLineHeight() + INFOBAR_SPACING_LEFTSIDE * 4);
1104 ImGui::SetCursorPos(newline_cursor);
1105 ImGui::TextDisabled("%s", _LC("RepositorySelector", "Version:"));
1106 ImGui::SameLine();
1107 ImGui::TextColored(theme.value_blue_text_color, "%s", selected_item.version.c_str());
1108 ImGui::SameLine();
1109 ImGui::TextDisabled("(%s", _LC("RepositorySelector", "Downloads:"));
1110 ImGui::SameLine();
1111 ImGui::TextColored(theme.value_blue_text_color, "%d", selected_item.download_count);
1112 ImGui::SameLine();
1113 ImGui::SetCursorPosX(ImGui::GetCursorPosX() - ImGui::GetStyle().ItemSpacing.x); // Don't put gap between the blue value and gray parentheses
1114 ImGui::TextDisabled(")");
1115
1116 // One line below - the rating and rating count
1117 newline_cursor += ImVec2(0.f, ImGui::GetTextLineHeight() + INFOBAR_SPACING_LEFTSIDE);
1118 ImGui::SetCursorPos(newline_cursor);
1119 ImGui::TextDisabled("%s", _LC("RepositorySelector", "Rating:"));
1120 ImGui::SameLine();
1121 ImGui::TextColored(theme.value_blue_text_color, "%.1f", selected_item.rating_avg);
1122 ImGui::SameLine();
1123 // (Rating stars)
1124 ImVec2 stars_cursor = ImGui::GetCursorPos();
1125 int rating = round(selected_item.rating_avg);
1126 for (int i = 1; i <= 5; i++)
1127 {
1128 ImGui::SetCursorPosX(stars_cursor.x + 16 * (i-1));
1129 ImVec4 tint_color = (i <= rating) ? ImVec4(1, 1, 1, 1) : ImVec4(1.f, 1.f, 1.f, 0.2f);
1130 ImGui::Image(reinterpret_cast<ImTextureID>(tex3->getHandle()), ImVec2(16, 16), ImVec2(0.f, 0.f), ImVec2(1.f, 1.f), tint_color);
1131 ImGui::SameLine();
1132 }
1133 ImGui::SetCursorPosX(stars_cursor.x + 16 * 5 + ImGui::GetStyle().ItemSpacing.x);
1134 ImGui::TextDisabled("(%s", _LC("RepositorySelector", "Rating Count:"));
1135 ImGui::SameLine();
1136 ImGui::TextColored(theme.value_blue_text_color, "%d", selected_item.rating_count);
1137 ImGui::SameLine();
1138 ImGui::SetCursorPosX(ImGui::GetCursorPosX() - ImGui::GetStyle().ItemSpacing.x); // Don't put gap between the blue value and gray parentheses
1139 ImGui::TextDisabled(")");
1140
1141 // One line below - authors
1142 newline_cursor += ImVec2(0.f, ImGui::GetTextLineHeight() + INFOBAR_SPACING_LEFTSIDE);
1143 ImGui::SetCursorPos(newline_cursor);
1144 ImGui::TextDisabled("%s", _LC("RepositorySelector", "Authors:"));
1145 ImGui::SameLine();
1146 ImGui::TextColored(theme.value_blue_text_color, "%s", selected_item.authors.c_str());
1147
1148 // --- Right column ---
1149
1150 ImGui::SetCursorPos(ImVec2(searchbox_x, leftmost_cursor.y));
1152
1153 // --- content area ---
1154 ImGui::SetCursorPos(leftmost_cursor + ImVec2(0.f, backdrop_size.y + ImGui::GetStyle().ItemSpacing.y));
1155 const float table_height = ImGui::GetWindowHeight()
1156 - ((2.f * ImGui::GetStyle().WindowPadding.y) + (3.f * ImGui::GetItemsLineHeightWithSpacing() + backdrop_size.y + ImGui::GetStyle().ItemSpacing.y));
1157
1158 // Scroll area
1159 // Make child windows use padding - only works when border is visible, so set it to transparent
1160 // see https://github.com/ocornut/imgui/issues/462
1161 ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.f, 0.f, 0.f, 0.f));
1162 const ImVec2 child_screenpos = ImGui::GetCursorScreenPos();
1163 const ImVec2 child_size(left_pane_width, table_height);
1164 ImGui::BeginChild("resource-view-scrolling", child_size, /*border:*/true);
1165 ImGui::PopStyleColor();
1166
1167 if (!m_repofiles_msg.empty())
1168 {
1169 // Downloading failed
1170 ImGui::TextDisabled("%s", m_repofiles_msg.c_str());
1171 }
1172 else if (m_data.files.empty())
1173 {
1174 // Downloading in progress - show spinner (centered)
1175 float spinner_radius = 25.f;
1176 ImGui::SetCursorPos(ImGui::GetCursorPos() + ImVec2(ImGui::GetContentRegionAvailWidth() / 2 - spinner_radius, 200.f));
1177 LoadingIndicatorCircle("spinner", spinner_radius, theme.value_blue_text_color, theme.value_blue_text_color, 10, 10);
1178 }
1179 else
1180 {
1181 // Files + description downloaded OK
1182 this->DrawResourceDescriptionBBCode(selected_item, child_screenpos, child_size);
1183 }
1184
1185 ImGui::EndChild();
1186}
1187
1189{
1190 if (m_data.files.size() == 0)
1191 {
1192 return; // Nothing to draw yet
1193 }
1194
1196
1197 const float table_height = ImGui::GetWindowHeight()
1198 - ((2.f * ImGui::GetStyle().WindowPadding.y) + (3.f * ImGui::GetItemsLineHeightWithSpacing())
1199 - ImGui::GetStyle().ItemSpacing.y);
1200
1201 ImVec2 rightcol_size = ImVec2(ImGui::GetContentRegionAvailWidth(), table_height);
1202 // Make child windows use padding - only works when border is visible, so set it to transparent
1203 // see https://github.com/ocornut/imgui/issues/462
1204 ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.f, 0.f, 0.f, 0.f));
1205 ImGui::BeginChild("resource-view-files", rightcol_size, /*border:*/true);
1206 ImGui::PopStyleColor();
1207
1209 Ogre::TexturePtr tex2 = FetchIcon("accept.png");
1210 ImGui::Text("%s", _LC("RepositorySelector", "Files:"));
1211
1212 // Check for duplicate files, remove the outdated one (lower id)
1213 std::sort(m_data.files.begin(), m_data.files.end(), [](ResourceFiles a, ResourceFiles b) { return a.id > b.id; });
1214 auto last = std::unique(m_data.files.begin(), m_data.files.end(), [](ResourceFiles a, ResourceFiles b) { return a.filename == b.filename; });
1215 m_data.files.erase(last, m_data.files.end());
1216
1217 for (int i = 0; i < m_data.files.size(); i++)
1218 {
1219 ImGui::PushID(i);
1220
1221 ImGui::AlignTextToFramePadding();
1222
1223 // File - construct download path
1224 std::string path = PathCombine(App::sys_user_dir->getStr(), "mods");
1225 std::string file = PathCombine(path, m_data.files[i].filename);
1226 // Determine installation status
1227 std::string installed_file;
1228 const bool is_installed = this->CheckRepoFileIsInstalled(m_data.files[i], /*[out]*/ installed_file);
1229 if (is_installed)
1230 {
1231 file = installed_file;
1232 }
1233
1234 // Get created time
1235 int file_time = 0;
1236 if (is_installed)
1237 {
1238 file_time = GetFileLastModifiedTime(file);
1239 }
1240
1241 // Filename and size on separate line
1242 ImGui::TextColored(theme.value_blue_text_color, "%s", m_data.files[i].filename.c_str());
1243
1244 if (is_installed && ImGui::IsItemHovered())
1245 {
1246 ImGui::BeginTooltip();
1247
1248 time_t c = (const time_t)file_time;
1249 ImGui::TextDisabled("%s %s", "Installed on", asctime(gmtime(&c)));
1250
1251 ImGui::EndTooltip();
1252 }
1253
1254 // File size
1255 ImGui::SameLine();
1256
1257 int size = m_data.files[i].size / 1024;
1258 ImGui::TextDisabled("(%d %s)", size, "KB");
1259
1260 // File exists show indicator
1261 if (is_installed)
1262 {
1263 ImGui::SameLine();
1264 ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 3.5f);
1265 ImGui::Image(reinterpret_cast<ImTextureID>(tex2->getHandle()), ImVec2(16, 16));
1266 }
1267
1268 // Buttons (new line)
1269
1270 if (!FileExists(file + ".part")) // Hide buttons if a download is in progress for this file
1271 {
1272 std::string btn_label;
1273 ImVec4 btn_color = ImGui::GetStyle().Colors[ImGuiCol_Button];
1274 ImVec4 text_color = ImGui::GetStyle().Colors[ImGuiCol_Text];
1275 if (is_installed && selected_item.last_update > file_time)
1276 {
1277 btn_label = fmt::format(_LC("RepositorySelector", "Update"));
1278 }
1279 else if (is_installed)
1280 {
1281 btn_label = fmt::format(_LC("RepositorySelector", "Reinstall"));
1282 }
1283 else
1284 {
1285 btn_label = fmt::format(_LC("RepositorySelector", "Install"));
1286 btn_color = RESOURCE_INSTALL_BTN_COLOR;
1287 text_color = ImVec4(0.1, 0.1, 0.1, 1.0);
1288 }
1289
1290 ImGui::PushStyleColor(ImGuiCol_Button, btn_color);
1291 ImGui::PushStyleColor(ImGuiCol_Text, text_color);
1292 if (ImGui::Button(btn_label.c_str(), ImVec2(100, 0)))
1293 {
1294 this->RequestInstallRepoFile(selected_item.resource_id, i, file);
1295 }
1296 ImGui::PopStyleColor(2); // Button, Text
1297 }
1298 else
1299 {
1300 ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true);
1301 ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.5f);
1302 ImGui::Button(_LC("RepositorySelector", "Install"), ImVec2(100, 0));
1303 ImGui::PopItemFlag();
1304 ImGui::PopStyleVar();
1305 }
1306
1307 if (is_installed)
1308 {
1309 ImGui::SameLine();
1310 if (ImGui::Button(_LC("RepositorySelector", "Remove"), ImVec2(100, 0)))
1311 {
1312 // Request modcache to remove this file - this will also despawn any actors using it.
1314 }
1315 }
1316 else
1317 {
1318 ImGui::SameLine();
1319 ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true);
1320 ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.5f);
1321 ImGui::Button(_LC("RepositorySelector", "Remove"), ImVec2(100, 0));
1322 ImGui::PopItemFlag();
1323 ImGui::PopStyleVar();
1324 }
1325
1326
1327 ImGui::PopID(); // i
1328 }
1329
1330 ImGui::Separator();
1331 ImGui::NewLine();
1332
1333 // Right side: The detail text
1334 ImGui::Text("%s", _LC("RepositorySelector", "Details:"));
1335
1336 // Right side, next line
1337 ImGui::TextDisabled("%s", _LC("RepositorySelector", "Resource ID:"));
1338 ImGui::SameLine();
1339 ImGui::TextColored(theme.value_blue_text_color, "%d", selected_item.resource_id);
1340
1341 // Right side, next line
1342 ImGui::TextDisabled("%s", _LC("RepositorySelector", "View Count:"));
1343 ImGui::SameLine();
1344 ImGui::TextColored(theme.value_blue_text_color, "%d", selected_item.view_count);
1345
1346 // Right side, next line
1347 ImGui::TextDisabled(_LC("RepositorySelector", "Date Added:"));
1348 ImGui::SameLine();
1349 time_t a = (const time_t)selected_item.resource_date;
1350 ImGui::TextColored(theme.value_blue_text_color, "%s", asctime(gmtime(&a)));
1351
1352 // Right side, next line
1353 ImGui::TextDisabled(_LC("RepositorySelector", "Last Update:"));
1354 ImGui::SameLine();
1355 time_t b = (const time_t)selected_item.last_update;
1356 ImGui::TextColored(theme.value_blue_text_color, "%s", asctime(gmtime(&b)));
1357
1358 ImGui::EndChild();
1359}
1360
1362{
1363#if defined(USE_CURL)
1364 m_show_spinner = true;
1365 m_draw = false;
1366 m_data.items.clear();
1367 m_repolist_msg = "";
1368 std::packaged_task<void(std::string)> task(GetResources);
1369 std::thread(std::move(task), App::remote_query_url->getStr()).detach();
1370#endif // defined(USE_CURL)
1371}
1372
1374{
1375 m_show_spinner = false;
1377 m_data.items = data->items;
1378
1379 // Sort
1380 std::sort(m_data.items.begin(), m_data.items.end(), [](ResourceItem a, ResourceItem b) { return a.last_update > b.last_update; });
1381
1382 if (m_data.items.empty())
1383 {
1384 m_repolist_msg = _LC("RepositorySelector", "Sorry, the repository isn't available. Try again later.");
1386 }
1387 else
1388 {
1389 m_repolist_msg = "";
1390 m_draw = true;
1391 }
1392}
1393
1395{
1396 // Assign data to currently viewed resource item.
1397 m_data.files = data->files;
1398
1399 if (m_data.files.empty())
1400 {
1401 m_repofiles_msg = _LC("RepositorySelector", "No files available :(");
1402 }
1403 else
1404 {
1405 m_repofiles_msg = "";
1406 }
1407
1408 // Fill in item's description (stripped from resource list for bandwidth reasons).
1409 for (auto& item : m_data.items)
1410 {
1411 if (item.resource_id == data->items[0].resource_id)
1412 {
1413 item.description = data->items[0].description;
1414 break;
1415 }
1416 }
1417
1418 // Finally, initiate download of attachments (images included in description).
1419 // To do that we must locate [ATTACH] BBCode tags in the description.
1420 this->DownloadBBCodeAttachmentsRecursive(*data->items[0].description);
1421}
1422
1423bool DecodeBBAttachment(bbcpp::BBNodePtr node, int& attachment_id, std::string& attachment_ext)
1424{
1425 const auto element = node->downCast<BBElementPtr>();
1426 if (!element
1427 || element->getElementType() == bbcpp::BBElement::CLOSING
1428 || element->getChildren().size() == 0)
1429 {
1430 return false;
1431 }
1432 const auto textnode = element->getChildren().front()->downCast<BBTextPtr>();
1433 if (!textnode)
1434 {
1435 return false;
1436 }
1437
1438 try // `std::stoi()` throws exception on bad input
1439 {
1440 attachment_id = std::stoi(textnode->getText());
1441
1442 // Our XenForo always stores original filename in 'alt' attribute.
1443 std::string basename; // dummy
1444 Ogre::StringUtil::splitBaseFilename(element->findParameter("alt"), basename, attachment_ext);
1445 return true;
1446 }
1447 catch (...)
1448 {
1449 return false;
1450 }
1451}
1452
1454{
1455 for (const auto node : parent.getChildren())
1456 {
1457 if (node->getNodeType() == BBNode::NodeType::ELEMENT
1458 && node->getNodeName() == "ATTACH")
1459 {
1460 int attachment_id = 0;
1461 std::string attachment_ext = "";
1462 if (DecodeBBAttachment(node, /*[out]*/ attachment_id, /*[out]*/ attachment_ext))
1463 {
1464 this->DownloadAttachment(attachment_id, attachment_ext);
1465 }
1466 // Don't log any error - the node may not be applicable (end tag)
1467 // and parsing will also fail upon render, so the image won't be missed.
1468 }
1470 }
1471}
1472
1473void RepositorySelector::DownloadAttachment(int attachment_id, std::string const& attachment_ext)
1474{
1475 // Check if file is already downloaded
1476 const std::string filename = fmt::format("{}.{}", attachment_id, attachment_ext);
1477 const std::string filepath = PathCombine(App::sys_repo_attachments_dir->getStr(), filename);
1478 if (FileExists(filepath))
1479 {
1480 // Load image to memory
1481 Ogre::TexturePtr tex;
1482 try // Check if loads correctly (not null, not invalid etc...)
1483 {
1484 tex = Ogre::TextureManager::getSingleton().load(filename, RGN_REPO_ATTACHMENTS);
1485 }
1486 catch (...) // Doesn't load, fallback
1487 {
1489 }
1490 m_repo_attachments[attachment_id] = tex;
1491 }
1492 else
1493 {
1494 // Request the async download
1496 request->attachment_id = attachment_id;
1497 request->attachment_ext = attachment_ext;
1498
1499 Ogre::Root::getSingleton().getWorkQueue()->addRequest(m_ogre_workqueue_channel, 1234, Ogre::Any(request));
1500 }
1501}
1502
1504{
1505#if defined(USE_CURL)
1506 m_data.files.clear();
1507 m_repofiles_msg = "";
1508 std::packaged_task<void(std::string, int)> task(GetResourceFiles);
1509 std::thread(std::move(task), App::remote_query_url->getStr(), resource_id).detach();
1510#endif // defined(USE_CURL)
1511}
1512
1513void RepositorySelector::RequestInstallRepoFile(int resource_id, int datafile_pos, std::string filepath)
1514{
1517 request->rfir_resource_id = resource_id;
1518 request->rfir_repofile_id = m_data.files[datafile_pos].id;
1519 request->rfir_filename = m_data.files[datafile_pos].filename;
1520 request->rfir_filesize_bytes = m_data.files[datafile_pos].size;
1521 request->rfir_filepath = filepath;
1523}
1524
1530
1532{
1534 {
1536#if defined(USE_CURL)
1537 std::packaged_task<void(RepoFileInstallRequest)> task(DownloadResourceFile);
1538 std::thread(std::move(task), next_request).detach();
1539#endif // defined(USE_CURL)
1541 }
1542 else
1543 {
1545 }
1546}
1547
1549{
1551 {
1553 && m_active_install_request_id == m_queued_install_requests.front().rfir_install_request_id);
1554
1555 // Install the new bundle
1557 {
1558 ROR_ASSERT(FileExists(request->rfir_filepath + ".part"));
1559 if (FileExists(request->rfir_filepath))
1560 {
1561 Ogre::FileSystemLayer::removeFile(request->rfir_filepath);
1562 }
1563 Ogre::FileSystemLayer::renameFile(request->rfir_filepath + ".part", request->rfir_filepath);
1565
1566 // Update the installation status
1567 auto itor = std::find_if(m_data.files.begin(), m_data.files.end(),
1568 [request](ResourceFiles const& file) { return file.id == request->rfir_repofile_id; });
1569 ROR_ASSERT(itor != m_data.files.end());
1570 if (itor != m_data.files.end())
1571 {
1572 itor->cached_install_status = ResFileInstallStatus::RFIS_INSTALLED;
1573 itor->cached_install_path = request->rfir_filepath;
1574 }
1575 }
1576
1577 // remove finished request from queue
1581 }
1582}
1583
1584void RepositorySelector::NotifyRepoFileUninstalled(std::string const& filename)
1585{
1586 // Update the installation status
1587 auto itor = std::find_if(m_data.files.begin(), m_data.files.end(),
1588 [filename](ResourceFiles const& file) { return file.filename == filename; });
1589 ROR_ASSERT(itor != m_data.files.end());
1590 if (itor != m_data.files.end())
1591 {
1592 itor->cached_install_status = ResFileInstallStatus::RFIS_NOT_INSTALLED;
1593 itor->cached_install_path = "";
1594 }
1595}
1596
1598{
1599 m_repolist_msg = failinfo->title;
1601 m_draw = false;
1602 m_show_spinner = false;
1603 if (failinfo->curl_result != CURLE_OK)
1604 m_repolist_curlmsg = curl_easy_strerror(failinfo->curl_result);
1605 if (failinfo->http_response != 0)
1606 m_repolist_httpmsg = fmt::format(_L("HTTP response code: {}"), failinfo->http_response);
1607}
1608
1610{
1611 m_is_visible = visible;
1612 if (visible && m_data.items.size() == 0)
1613 {
1614 this->Refresh();
1615 }
1616 else if (!visible)
1617 {
1618 // Invocable both from pause-menu (paused) and top-menubar (unpaused)
1619 if (App::app_state->getEnum<AppState>() == AppState::MAIN_MENU ||
1620 App::sim_state->getEnum<SimState>() == SimState::PAUSED)
1621 {
1623 }
1624 }
1625}
1626
1627// Internal helper used by `DrawResourceDescriptionBBCode()`
1628// Adopted from 'bbcpp' library's utility code. See 'BBDocument.h'.
1630{
1631 // Because we simulate text effect with just color, we need rules.
1632 bool m_italic_text = false;
1633 bool m_bold_text = false; // Wins over italic
1634 bool m_underline_text = false; // Wins over bold
1635
1636 void HandleBBText(const BBTextPtr& textnode)
1637 {
1638 ImVec4 color = ImGui::GetStyle().Colors[ImGuiCol_Text];
1639 if (m_italic_text)
1640 {
1641 color = ImVec4(0.205f, 0.789f, 0.820f, 1.f);
1642 }
1643 if (m_bold_text) // wins over italic
1644 {
1645 color = ImVec4(0.860f, 0.740f, 0.0602f, 1.f);
1646 }
1647 if (m_underline_text) // wins over bold
1648 {
1649 color = ImVec4(0.00f, 0.930f, 0.480f, 1.f);
1650 }
1651 std::string text = textnode->getText();
1652 if (text != "")
1653 {
1654 m_feeder.AddMultiline(ImColor(color), m_wrap_width, text.c_str(), text.c_str() + text.length());
1655 }
1656 }
1657
1658 bool HandleBBElement(const BBElementPtr& element)
1659 {
1660 bool recurse_children = true;
1661 if (element->getNodeName() == "B")
1662 {
1663 m_bold_text = element->getElementType() != BBElement::CLOSING;
1664 }
1665 else if (element->getNodeName() == "I")
1666 {
1667 m_italic_text = element->getElementType() != BBElement::CLOSING;
1668 }
1669 else if (element->getNodeName() == "U")
1670 {
1671 m_underline_text = element->getElementType() != BBElement::CLOSING;
1672 }
1673 else if (element->getNodeName() == "ATTACH")
1674 {
1675 recurse_children = false; // the only child is the image ID text
1676
1677 int attachment_id = 0;
1678 std::string attachment_ext = "";
1679 if (DecodeBBAttachment(element, /*[out]*/ attachment_id, /*[out]*/ attachment_ext))
1680 {
1682 }
1683 else
1684 {
1685 ImGui::TextColored(ImVec4(1, 0, 0, 1), "<bad image>");
1686 }
1687 }
1688 else if (element->getNodeName() == "*")
1689 {
1690 // We can't just call `ImGui::BulletText()` in the middle of using the feeder.
1691 // Adapted from `ImGui::BuletTextV()` ...
1692 ImU32 text_col = ImColor(ImGui::GetStyle().Colors[ImGuiCol_Text]);
1693 ImVec2 center = m_feeder.cursor + ImVec2(ImGui::GetStyle().FramePadding.x + ImGui::GetFontSize() * 0.5f, ImGui::GetTextLineHeight() * 0.5f);
1694 ImGui::RenderBullet(ImGui::GetWindowDrawList(), center, text_col);
1695 m_feeder.cursor += ImVec2(ImGui::GetStyle().FramePadding.x * 2 + ImGui::GetFontSize(), 0.f);
1696 }
1697 return recurse_children;
1698
1699 /* for reference:
1700 if (element->getElementType() == BBElement::PARAMETER)
1701 {
1702 std::stringstream ss;
1703 ss << element->getParameters();
1704 ImGui::TextDisabled(ss.str().c_str());
1705 }
1706 */
1707 }
1708
1709public:
1710 BBCodeDrawingContext(ImTextFeeder& feeder, float wrap_w, ImVec2 panel_screenpos, ImVec2 panel_size)
1711 : m_feeder(feeder), m_wrap_width(wrap_w), bb_panel_screenpos(panel_screenpos), bb_panel_size(panel_size)
1712 {}
1715 ImVec2 bb_panel_screenpos = ImVec2(0, 0);
1716 ImVec2 bb_panel_size = ImVec2(0, 0);
1718 {
1719 for (const auto node : parent.getChildren())
1720 {
1721 switch (node->getNodeType())
1722 {
1723 default:
1724 this->DrawBBCodeChildrenRecursive(*node);
1725 break;
1726
1727 case BBNode::NodeType::ELEMENT:
1728 if (this->HandleBBElement(node->downCast<BBElementPtr>()))
1729 {
1730 this->DrawBBCodeChildrenRecursive(*node);
1731 }
1732 break;
1733
1734 case BBNode::NodeType::TEXT:
1735 this->HandleBBText(node->downCast<BBTextPtr>());
1736 break;
1737 }
1738 }
1739 }
1740
1741};
1742
1743void RepositorySelector::DrawResourceDescriptionBBCode(const ResourceItem& item, ImVec2 panel_screenpos, ImVec2 panel_size)
1744{
1745 // Decomposes BBCode into DearIMGUI function calls.
1746 // ------------------------------------------------
1747
1748 if (!item.description)
1749 return; // Not loaded yet.
1750
1751 ImVec2 text_pos = ImGui::GetCursorScreenPos();
1752 ImTextFeeder feeder(ImGui::GetWindowDrawList(), text_pos);
1753 BBCodeDrawingContext bb_ctx(feeder, ImGui::GetWindowContentRegionWidth(), panel_screenpos, panel_size);
1755 feeder.NextLine(); // Account correctly for last line height - there may be images on it.
1756
1757 // From `ImGui::TextEx()` ...
1758 ImRect bb(text_pos, text_pos + feeder.size);
1759 ImGui::ItemSize(feeder.size);
1760 ImGui::ItemAdd(bb, 0);
1761}
1762
1763// -------------------------------------------------------
1764// Async thumbnail/attachment download via Ogre::WorkQueue
1765// NOTE: The API changed in OGRE 14.0
1766// see https://github.com/OGRECave/ogre/blob/master/Docs/14-Notes.md#task-based-workqueue
1767
1768void RepositorySelector::DrawThumbnail(ResourceItemArrayPos_t resource_arraypos, ImVec2 image_size, float spinner_size, ImVec2 spinner_cursor)
1769{
1770 // Runs on main thread when drawing GUI
1771 // Displays a thumbnail image if available, or shows a spinner and initiates async download.
1772 // -----------------------------------------------------------------------------------------
1773
1775
1776 if (!m_data.items[resource_arraypos].preview_tex)
1777 {
1778 if (m_data.items[resource_arraypos].icon_url == "")
1779 {
1780 // No thumbnail defined - use a placeholder logo.
1781 m_data.items[resource_arraypos].preview_tex = m_fallback_thumbnail;
1782 }
1783 else
1784 {
1785 // Thumbnail defined - see if we want to initiate download.
1786 if (ImGui::IsRectVisible(image_size)
1787 && !m_data.items[resource_arraypos].thumbnail_dl_queued)
1788 {
1789 // Image is in visible screen area and not yet downloading.
1791 request->thumb_resourceitem_idx = resource_arraypos;
1792 request->thumb_resource_id = m_data.items[request->thumb_resourceitem_idx].resource_id;
1793 request->thumb_url = m_data.items[request->thumb_resourceitem_idx].icon_url;
1794
1795 Ogre::Root::getSingleton().getWorkQueue()->addRequest(m_ogre_workqueue_channel, 1234, Ogre::Any(request));
1796 m_data.items[resource_arraypos].thumbnail_dl_queued = true;
1797 }
1798 }
1799 }
1800
1801 if (m_data.items[resource_arraypos].preview_tex)
1802 {
1803 // Thumbnail downloaded or replaced by placeholder - draw it.
1804 ImGui::Image(
1805 reinterpret_cast<ImTextureID>(m_data.items[resource_arraypos].preview_tex->getHandle()),
1806 image_size);
1807 }
1808 else
1809 {
1810 // Thumbnail is downloading - draw spinner.
1811 ImGui::SetCursorPos(spinner_cursor);
1812 LoadingIndicatorCircle("spinner", spinner_size, theme.value_blue_text_color, theme.value_blue_text_color, 10, 10);
1813 }
1814}
1815
1817{
1818 // Runs on main thread when drawing GUI
1819 // Displays a thumbnail image if already downloaded, or shows a spinner if not yet.
1820 // Note that downloading attachments is initiated in `UpdateResourceFilesAndDescription()`
1821 // -----------------------------------------------------------------------------------------
1822
1824
1825 auto itor = m_repo_attachments.find(attachment_id);
1826 if (itor != m_repo_attachments.end())
1827 {
1828 // Attachment image is already downloaded - draw it.
1829 Ogre::TexturePtr& tex = itor->second;
1830 // Scale down and maintain ratio.
1831 float img_scale = ATTACH_MAX_WIDTH / tex->getWidth();
1832 if (tex->getHeight() * img_scale > ATTACH_MAX_HEIGHT)
1833 {
1834 img_scale = ATTACH_MAX_HEIGHT / tex->getHeight();
1835 }
1836 ImVec2 img_size(tex->getWidth() * img_scale, tex->getHeight() * img_scale);
1837 // Update feeder to account the image
1838 ImVec2 img_min;
1839 context->m_feeder.AddRectWrapped(img_size, ImGui::GetStyle().ItemSpacing, context->m_wrap_width, /* [out] */ img_min);
1840 const ImVec2 img_max = img_min + img_size;
1841 // Draw image directly via ImGui Drawlist
1842 context->m_feeder.drawlist->AddImage(
1843 reinterpret_cast<ImTextureID>(tex->getHandle()), img_min, img_max);
1844 // Handle mouse hover and click
1845 if (ImGui::GetMousePos().x > img_min.x && ImGui::GetMousePos().y > img_min.y
1846 && ImGui::GetMousePos().x < img_max.x && ImGui::GetMousePos().y < img_max.y
1847 // remember the window is scrollable - only recognize hover in visible region!
1848 && ImGui::GetMousePos().y > context->bb_panel_screenpos.y
1849 && ImGui::GetMousePos().y < context->bb_panel_screenpos.y + context->bb_panel_size.y)
1850 {
1851 ImGui::SetMouseCursor(7);//Hand cursor
1852 if (ImGui::IsMouseClicked(0))
1853 {
1854 m_gallery_mode_attachment_id = attachment_id;
1855 }
1856 }
1857 }
1858 else
1859 {
1860 // Attachment image is not downloaded yet - draw spinner
1861 ImVec2 spinnerbox_size = (ATTACH_SPINNER_PADDING + ImVec2(ATTACH_SPINNER_RADIUS, ATTACH_SPINNER_RADIUS)) * 2.f;
1862 // Update feeder to account the spinner
1863 ImVec2 spinnerbox_min;
1864 context->m_feeder.AddRectWrapped(spinnerbox_size, ImGui::GetStyle().ItemSpacing, context->m_wrap_width, /* [out] */ spinnerbox_min);
1865 // Draw the spinner body
1866 ImVec2 backup_screenpos = ImGui::GetCursorScreenPos();
1867 ImGui::SetCursorScreenPos(spinnerbox_min + ATTACH_SPINNER_PADDING);
1869 ImGui::SetCursorScreenPos(backup_screenpos);
1870 }
1871}
1872
1874{
1875 // This runs on background worker thread in Ogre::WorkQueue's thread pool.
1876 // Purpose: to fetch one thumbnail image using CURL.
1877 // -----------------------------------------------------------------------
1878
1879 ROR_ASSERT(request->thumb_resourceitem_idx != -1 || request->attachment_id != -1);
1880 std::string filename, filepath, rg_name, url;
1881 if (request->thumb_resourceitem_idx != -1)
1882 {
1883 filename = std::to_string(request->thumb_resource_id) + ".png";
1884 filepath = PathCombine(App::sys_thumbnails_dir->getStr(), filename);
1885 rg_name = RGN_THUMBNAILS;
1886 url = request->thumb_url;
1887
1888 }
1889 else if (request->attachment_id != -1)
1890 {
1891 filename = fmt::format("{}.{}", request->attachment_id, request->attachment_ext);
1892 filepath = PathCombine(App::sys_repo_attachments_dir->getStr(), filename);
1893 rg_name = RGN_REPO_ATTACHMENTS;
1894 url = "https://forum.rigsofrods.org/attachments/" + std::to_string(request->attachment_id);
1895 }
1896 else
1897 {
1898 // Invalid request, return empty response.
1899 LOG("[RoR|RepoUI] Invalid (empty) download request - ignoring it");
1900 return;
1901 }
1902 long response_code = 0;
1903
1904 if (FileExists(filepath))
1905 {
1907 return;
1908 }
1909 else
1910 {
1911 try // We write using Ogre::DataStream which throws exceptions
1912 {
1913 // smart pointer - closes stream automatically
1914 Ogre::DataStreamPtr datastream = Ogre::ResourceGroupManager::getSingleton().createResource(filename, rg_name);
1915
1916 // The CURL* handle is not multithreaded, see https://curl.se/libcurl/c/threadsafe.html
1917 // For simplicity we avoid any reuse during OGRE14 migration.
1918 CURL *curl_th = curl_easy_init();
1919 curl_easy_setopt(curl_th, CURLOPT_URL, url.c_str());
1920 curl_easy_setopt(curl_th, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
1921 curl_easy_setopt(curl_th, CURLOPT_FOLLOWLOCATION, 1L); // Necessary for attachment images
1922#ifdef _WIN32
1923 curl_easy_setopt(curl_th, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA);
1924#endif // _WIN32
1925 curl_easy_setopt(curl_th, CURLOPT_WRITEFUNCTION, CurlOgreDataStreamWriteFunc);
1926 curl_easy_setopt(curl_th, CURLOPT_WRITEDATA, datastream.get());
1927 CURLcode curl_result = curl_easy_perform(curl_th);
1928 curl_easy_cleanup(curl_th);
1929
1930 // If CURL follows a redirect then it returns 0 for HTTP response code.
1931 if (curl_result != CURLE_OK || (response_code != 0 && response_code != 200))
1932 {
1933 Ogre::LogManager::getSingleton().stream()
1934 << "[RoR|Repository] Failed to download image;"
1935 << " URL: '" << url << "',"
1936 << " Error: '" << curl_easy_strerror(curl_result) << "',"
1937 << " HTTP status code: " << response_code;
1938 }
1939 else
1940 {
1942 return;
1943 }
1944 }
1945 catch (Ogre::Exception& oex)
1946 {
1949 fmt::format("Repository UI: cannot download image '{}' - {}",
1950 url, oex.getDescription()));
1951 }
1952
1953 // Remove any incomplete download before reporting failure
1954 if (FileExists(filepath))
1955 {
1956 Ogre::FileSystemLayer::removeFile(filepath);
1957 }
1959 }
1960}
1961
1963{
1964 // This runs on main thread.
1965 // It's safe to load the texture and modify GUI data.
1966 // --------------------------------------------------
1967
1968 std::string filename, filepath, rg_name;
1969 if (request->thumb_resourceitem_idx != -1)
1970 {
1971 filename = std::to_string(m_data.items[request->thumb_resourceitem_idx].resource_id) + ".png";
1972 filepath = PathCombine(App::sys_thumbnails_dir->getStr(), filename);
1973 rg_name = RGN_THUMBNAILS;
1974 }
1975 else if (request->attachment_id != -1)
1976 {
1977 filename = fmt::format("{}.{}", request->attachment_id, request->attachment_ext);
1978 filepath = PathCombine(App::sys_repo_attachments_dir->getStr(), filename);
1979 rg_name = RGN_REPO_ATTACHMENTS;
1980 }
1981 else
1982 {
1983 // Invalid request, nothing to do.
1984 LOG("[RoR|RepoUI] Invalid (empty) download response - ignoring it");
1985 return;
1986 }
1987
1988 if (FileExists(filepath)) // We have an image
1989 {
1990 Ogre::TexturePtr tex;
1991 try // Check if loads correctly (not null, not invalid etc...)
1992 {
1993 tex = Ogre::TextureManager::getSingleton().load(filename, rg_name);
1994 }
1995 catch (...) // Doesn't load, fallback
1996 {
1998 }
1999
2000 if (request->thumb_resourceitem_idx != -1)
2001 {
2002 // Thumbnail for resource item
2003 m_data.items[request->thumb_resourceitem_idx].preview_tex = tex;
2004 }
2005 else if (request->attachment_id != -1)
2006 {
2007 // Attachment image to display in description
2008 m_repo_attachments[request->attachment_id] = tex;
2009 }
2010 }
2011}
2012
2013// This will be removed after OGRE14 migration is complete
2014Ogre::WorkQueue::Response* RepoImageRequestHandler::handleRequest(const Ogre::WorkQueue::Request* req, const Ogre::WorkQueue* srcQ)
2015{
2016 RepoImageDownloadRequest* request = Ogre::any_cast<RepoImageDownloadRequest*>(req->getData());
2018 return nullptr; // Because we use `MSG_NET_DOWNLOAD_REPOIMAGE_*` message to notify main thread, we don't need OGRE's response system here.
2019}
2020
2021bool RepositorySelector::CheckRepoFileIsInstalled(ResourceFiles& resfile, std::string& out_filepath)
2022{
2024 {
2025 if (App::GetCacheSystem()->IsRepoFileInstalled(resfile.filename, resfile.cached_install_path))
2026 {
2028 }
2029 else
2030 {
2032 }
2033 }
2034
2035 out_filepath = resfile.cached_install_path;
2037}
System integration layer; inspired by OgreBites::ApplicationContext.
Central state/object manager and communications hub.
#define ROR_ASSERT(_EXPR)
Definition Application.h:40
#define RGN_REPO_ATTACHMENTS
Definition Application.h:48
#define RGN_THUMBNAILS
Definition Application.h:47
void LOG(const char *msg)
Legacy alias - formerly a macro.
#define RGN_CACHE
Definition Application.h:46
#define _L
static size_t CurlWriteFunc(void *ptr, size_t size, size_t nmemb, std::string *data)
void DownloadResourceFile(RepoFileInstallRequest request)
static size_t CurlProgressFunc(void *ptr, double filesize_B, double downloaded_B)
std::vector< GUI::ResourceCategories > GetResourceCategories(std::string portal_url)
void GetResources(std::string portal_url)
void GetResourceFiles(std::string portal_url, int resource_id)
static size_t CurlOgreDataStreamWriteFunc(char *data_ptr, size_t _unused, size_t data_length, void *userdata)
bool DecodeBBAttachment(bbcpp::BBNodePtr node, int &attachment_id, std::string &attachment_ext)
Game state manager and message-queue provider.
#define _LC(ctx, str)
Definition Language.h:38
Platform-specific utilities. We use narrow UTF-8 encoded strings as paths. Inspired by http://utf8eve...
const char *const ROR_VERSION_STRING
void ParseSingleZip(Ogre::String path)
@ CONSOLE_MSGTYPE_INFO
Generic message.
Definition Console.h:60
void putMessage(MessageArea area, MessageType type, std::string const &msg, std::string icon="")
Definition Console.cpp:103
@ CONSOLE_SYSTEM_ERROR
Definition Console.h:52
bool HandleBBElement(const BBElementPtr &element)
void HandleBBText(const BBTextPtr &textnode)
BBCodeDrawingContext(ImTextFeeder &feeder, float wrap_w, ImVec2 panel_screenpos, ImVec2 panel_size)
void DrawBBCodeChildrenRecursive(const BBNode &parent)
RepoAttachmentsMap m_repo_attachments
Fully loaded images in memory.
void InstallDownloadedRepoFile(MsgType result, RepoFileInstallRequest *request)
void DrawAttachment(BBCodeDrawingContext *context, int attachment_id)
void DrawResourceDescriptionBBCode(const ResourceItem &item, ImVec2 panel_screenpos, ImVec2 panel_size)
bool CheckRepoFileIsInstalled(ResourceFiles &resfile, std::string &out_filepath)
void RequestInstallRepoFile(int resource_id, int datafile_pos, std::string filepath)
RepoFileInstallRequestID_t GetNextInstallRequestId()
void NotifyRepoFileUninstalled(std::string const &filename)
void QueueInstallRepoFile(RepoFileInstallRequest *request)
void DrawThumbnail(ResourceItemArrayPos_t resource_arraypos, ImVec2 image_size, float spinner_size, ImVec2 spinner_cursor)
std::string m_repolist_curlmsg
Displayed as dimmed text.
ResourceItemArrayPos_t m_resourceview_item_arraypos
RepoImageRequestHandler m_repo_image_request_handler
RepoFileInstallRequestID_t m_active_install_request_id
std::string m_repolist_httpmsg
Displayed as dimmed text.
void ShowError(CurlFailInfo *failinfo)
void DownloadBBCodeAttachmentsRecursive(const bbcpp::BBNode &parent)
void DownloadAttachment(int attachment_id, std::string const &attachment_ext)
void UpdateResources(ResourcesCollection *data)
void LoadDownloadedImage(RepoImageDownloadRequest *request)
To be run on main thread.
void UpdateResourceFilesAndDescription(ResourcesCollection *data)
static void DownloadImage(RepoImageDownloadRequest *request)
To be run on background via Ogre WorkQueue.
std::vector< RepoFileInstallRequest > m_queued_install_requests
GUI::RepositorySelector RepositorySelector
Definition GUIManager.h:130
GUI::GameMainMenu GameMainMenu
Definition GUIManager.h:117
GuiTheme & GetTheme()
Definition GUIManager.h:168
void PushMessage(Message m)
Doesn't guarantee order! Use ChainMessage() if order matters.
char * GetBuffer()
Definition Str.h:48
bool IsEmpty() const
Definition Str.h:47
size_t GetCapacity() const
Definition Str.h:49
static BBDocumentPtr create()
Definition BBDocument.h:560
const BBNodeList & getChildren() const
Definition BBDocument.h:131
std::time_t GetFileLastModifiedTime(std::string const &path)
std::string PathCombine(std::string a, std::string b)
bool FileExists(const char *path)
Path must be UTF-8 encoded.
MsgType
Global gameplay message loop, see struct Message in GameContext.h.
Definition Application.h:76
@ MSG_NET_DOWNLOAD_REPOFILE_REQUESTED
Payload = RoR::RepoFileInstallRequest* (owner)
@ MSG_NET_DOWNLOAD_REPOFILE_SUCCESS
Payload = RoR::RepoFileInstallRequest* (owner)
@ MSG_EDI_DELETE_BUNDLE_REQUESTED
Description = filename.
@ MSG_NET_DOWNLOAD_REPOIMAGE_FAILURE
Payload = RoR::RepoImageDownloadRequest* (owner)
@ MSG_NET_DOWNLOAD_REPOIMAGE_SUCCESS
Payload = RoR::RepoImageDownloadRequest* (owner)
@ MSG_NET_OPEN_RESOURCE_SUCCESS
Payload = GUI::ResourcesCollection* (owner)
@ MSG_NET_DOWNLOAD_REPOFILE_PROGRESS
Payload = int* (owner)
@ MSG_NET_REFRESH_REPOLIST_FAILURE
Payload = RoR::CurlFailInfo* (owner)
@ MSG_NET_REFRESH_REPOLIST_SUCCESS
Payload = GUI::ResourcesCollection* (owner)
@ MSG_NET_DOWNLOAD_REPOFILE_FAILURE
Payload = RoR::RepoFileInstallRequest* (owner)
CVar * sys_repo_attachments_dir
CVar * sim_state
CVar * remote_query_url
GUIManager * GetGuiManager()
GameContext * GetGameContext()
CVar * app_state
CVar * sys_user_dir
Console * GetConsole()
CacheSystem * GetCacheSystem()
CVar * sys_thumbnails_dir
const int RESOURCEITEMARRAYPOS_INVALID
static const RepoFileInstallRequestID_t REPOFILEINSTALLREQUESTID_INVALID
Invalid ID for repository item installation request.
int RepoFileInstallRequestID_t
Unique sequentially generated ID of a repository item installation request; use GUI::RepositorySelect...
void ImHyperlink(std::string url, std::string caption="", bool tooltip=true)
Full-featured hypertext with tooltip showing full URL.
Definition GUIUtils.cpp:612
Ogre::TexturePtr FetchIcon(const char *name)
Definition GUIUtils.cpp:372
void LoadingIndicatorCircle(const char *label, const float indicator_radius, const ImVec4 &main_color, const ImVec4 &backdrop_color, const int circle_count, const float speed)
Draws animated loading spinner.
Definition GUIUtils.cpp:158
std::shared_ptr< BBElement > BBElementPtr
Definition BBDocument.h:60
std::shared_ptr< BBNode > BBNodePtr
Definition BBDocument.h:58
std::shared_ptr< BBText > BBTextPtr
Definition BBDocument.h:59
RepoFileInstallRequestID_t install_request_id
CURLcode curl_result
Definition Network.h:49
std::string title
Definition Network.h:48
ResFileInstallStatus cached_install_status
std::string cached_install_path
Valid if cached_install_status == RFIS_INSTALLED
bbcpp::BBDocumentPtr description
std::vector< ResourceItem > items
std::vector< ResourceCategories > categories
std::vector< ResourceFiles > files
Helper for drawing multiline wrapped & colored text.
Definition GUIUtils.h:28
ImVec2 size
Accumulated text size.
Definition GUIUtils.h:44
void AddRectWrapped(ImVec2 size, ImVec2 spacing, float wrap_width, ImVec2 &out_rect_min)
Reserves space for i.e. an image. wrap_width=-1.f disables wrapping.
Definition GUIUtils.cpp:124
ImDrawList * drawlist
Definition GUIUtils.h:41
void AddMultiline(ImU32 color, float wrap_width, const char *text, const char *text_end)
Wraps substrings separated by blanks. wrap_width=-1.f disables wrapping.
Definition GUIUtils.cpp:78
ImVec2 cursor
Next draw position, screen space.
Definition GUIUtils.h:42
Unified game event system - all requests and state changes are reported using a message.
Definition GameContext.h:52
std::string description
Definition GameContext.h:58
void * payload
Definition GameContext.h:59
Payload for MSG_NET_INSTALL_REPOFILE_REQUEST message - also used for update (overwrites existing)
RepoFileInstallRequestID_t rfir_install_request_id
Ogre::WorkQueue::Response * handleRequest(const Ogre::WorkQueue::Request *req, const Ogre::WorkQueue *srcQ) override