RigsofRods
Soft-body Physics Simulation
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 
27 #include "GUI_RepositorySelector.h"
28 
29 #include "Application.h"
30 #include "GameContext.h"
31 #include "AppContext.h"
32 #include "Console.h"
33 #include "ContentManager.h"
34 #include "GUIManager.h"
35 #include "GUIUtils.h"
36 #include "Language.h"
37 #include "PlatformUtils.h"
38 #include "RoRVersion.h"
39 
40 #include <imgui.h>
41 #include <imgui_internal.h>
42 #include <rapidjson/document.h>
43 #include <vector>
44 #include <fmt/core.h>
45 #include <stdio.h>
46 #include <OgreFileSystemLayer.h>
47 
48 #ifdef USE_CURL
49 # include <curl/curl.h>
50 # include <curl/easy.h>
51 #endif //USE_CURL
52 
53 #if defined(_MSC_VER) && defined(GetObject) // This MS Windows macro from <wingdi.h> (Windows Kit 8.1) clashes with RapidJSON
54 # undef GetObject
55 #endif
56 
57 using namespace RoR;
58 using namespace GUI;
59 
60 #if defined(USE_CURL)
61 
62 static size_t CurlWriteFunc(void *ptr, size_t size, size_t nmemb, std::string* data)
63 {
64  data->append((char*)ptr, size * nmemb);
65  return size * nmemb;
66 }
67 
69 {
70  std::string filename;
71  double old_perc = 0;
72 };
73 
74 static size_t CurlProgressFunc(void* ptr, double filesize_B, double downloaded_B)
75 {
76  // Ensure that the file to be downloaded is not empty because that would cause a division by zero error later on
77  if (filesize_B <= 0.0)
78  {
79  return 0;
80  }
81 
83 
84  double perc = (downloaded_B / filesize_B) * 100;
85 
86  if (perc > context->old_perc)
87  {
89  m.payload = reinterpret_cast<void*>(new int(perc));
90  m.description = fmt::format("{} {}\n{}: {:.2f}{}\n{}: {:.2f}{}", "Downloading", context->filename, "File size", filesize_B/(1024 * 1024), "MB", "Downloaded", downloaded_B/(1024 * 1024), "MB");
92  }
93 
94  context->old_perc = perc;
95 
96  // If you don't return 0, the transfer will be aborted - see the documentation
97  return 0;
98 }
99 
100 static size_t CurlOgreDataStreamWriteFunc(char* data_ptr, size_t _unused, size_t data_length, void* userdata)
101 {
102  Ogre::DataStream* ogre_datastream = static_cast<Ogre::DataStream*>(userdata);
103  if (data_length > 0 && ogre_datastream->isWriteable())
104  {
105  return ogre_datastream->write((const void*)data_ptr, data_length);
106  }
107  else
108  {
109  return 0;
110  }
111 }
112 
113 std::vector<GUI::ResourceCategories> GetResourceCategories(std::string portal_url)
114 {
115  std::string repolist_url = portal_url + "/resource-categories";
116  std::string response_payload;
117  std::string response_header;
118  long response_code = 0;
119  std::string user_agent = fmt::format("{}/{}", "Rigs of Rods Client", ROR_VERSION_STRING);
120 
121  CURL *curl = curl_easy_init();
122  curl_easy_setopt(curl, CURLOPT_URL, repolist_url.c_str());
123  curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
124 #ifdef _WIN32
125  curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA);
126 #endif // _WIN32
127  curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "gzip");
128  curl_easy_setopt(curl, CURLOPT_USERAGENT, user_agent.c_str());
129  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteFunc);
130  curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_payload);
131  curl_easy_setopt(curl, CURLOPT_HEADERDATA, &response_header);
132 
133  CURLcode curl_result = curl_easy_perform(curl);
134  curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);
135 
136  curl_easy_cleanup(curl);
137  curl = nullptr;
138 
139  std::vector<GUI::ResourceCategories> cat;
140  if (curl_result != CURLE_OK || response_code != 200)
141  {
142  Ogre::LogManager::getSingleton().stream()
143  << "[RoR|Repository] Failed to retrieve category list;"
144  << " Error: '" << curl_easy_strerror(curl_result) << "'; HTTP status code: " << response_code;
145  return cat;
146  }
147 
148  rapidjson::Document j_data_doc;
149  j_data_doc.Parse(response_payload.c_str());
150 
151  rapidjson::Value& j_resp_body = j_data_doc["categories"];
152  size_t num_rows = j_resp_body.GetArray().Size();
153  cat.resize(num_rows);
154  for (size_t i = 0; i < num_rows; i++)
155  {
156  rapidjson::Value& j_row = j_resp_body[static_cast<rapidjson::SizeType>(i)];
157 
158  cat[i].title = j_row["title"].GetString();
159  cat[i].resource_category_id = j_row["resource_category_id"].GetInt();
160  cat[i].resource_count = j_row["resource_count"].GetInt();
161  cat[i].description = j_row["description"].GetString();
162  cat[i].display_order = j_row["display_order"].GetInt();
163  }
164 
165  return cat;
166 }
167 
168 void GetResources(std::string portal_url)
169 {
170  std::string repolist_url = portal_url + "/resources";
171  std::string response_payload;
172  std::string response_header;
173  long response_code = 0;
174  std::string user_agent = fmt::format("{}/{}", "Rigs of Rods Client", ROR_VERSION_STRING);
175 
176  CURL *curl = curl_easy_init();
177  curl_easy_setopt(curl, CURLOPT_URL, repolist_url.c_str());
178  curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
179 #ifdef _WIN32
180  curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA);
181 #endif // _WIN32
182  curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "gzip");
183  curl_easy_setopt(curl, CURLOPT_USERAGENT, user_agent.c_str());
184  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteFunc);
185  curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_payload);
186  curl_easy_setopt(curl, CURLOPT_HEADERDATA, &response_header);
187 
188  CURLcode curl_result = curl_easy_perform(curl);
189  curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);
190 
191  curl_easy_cleanup(curl);
192  curl = nullptr;
193 
194  if (curl_result != CURLE_OK || response_code != 200)
195  {
196  Ogre::LogManager::getSingleton().stream()
197  << "[RoR|Repository] Failed to retrieve repolist;"
198  << " Error: '"<< curl_easy_strerror(curl_result) << "'; HTTP status code: " << response_code;
199 
200  CurlFailInfo* failinfo = new CurlFailInfo();
201  failinfo->title = _LC("RepositorySelector", "Could not connect to server. Please check your connection.");
202  failinfo->curl_result = curl_result;
203  failinfo->http_response = response_code;
204 
207  return;
208  }
209 
210  rapidjson::Document j_data_doc;
211  j_data_doc.Parse(response_payload.c_str());
212  if (j_data_doc.HasParseError() || !j_data_doc.IsObject())
213  {
214  Ogre::LogManager::getSingleton().stream()
215  << "[RoR|Repository] Error parsing repolist JSON, code: " << j_data_doc.GetParseError();
217  Message(MSG_NET_REFRESH_REPOLIST_FAILURE, _LC("RepositorySelector", "Received malformed data. Please try again.")));
218  return;
219  }
220 
222 
223  std::vector<GUI::ResourceItem> resc;
224  rapidjson::Value& j_resp_body = j_data_doc["resources"];
225  size_t num_rows = j_resp_body.GetArray().Size();
226  resc.resize(num_rows);
227 
228  for (size_t i = 0; i < num_rows; i++)
229  {
230  rapidjson::Value& j_row = j_resp_body[static_cast<rapidjson::SizeType>(i)];
231 
232  resc[i].title = j_row["title"].GetString();
233  resc[i].tag_line = j_row["tag_line"].GetString();
234  resc[i].resource_id = j_row["resource_id"].GetInt();
235  resc[i].download_count = j_row["download_count"].GetInt();
236  resc[i].last_update = j_row["last_update"].GetInt();
237  resc[i].resource_category_id = j_row["resource_category_id"].GetInt();
238  resc[i].icon_url = j_row["icon_url"].GetString();
239  resc[i].rating_avg = j_row["rating_avg"].GetFloat();
240  resc[i].rating_count = j_row["rating_count"].GetInt();
241  resc[i].version = j_row["version"].GetString();
242  resc[i].authors = j_row["custom_fields"]["authors"].GetString();
243  resc[i].view_url = j_row["view_url"].GetString();
244  resc[i].resource_date = j_row["resource_date"].GetInt();
245  resc[i].view_count = j_row["view_count"].GetInt();
246  resc[i].preview_tex = Ogre::TexturePtr(); // null
247  }
248 
249  cdata_ptr->items = resc;
250  cdata_ptr->categories = GetResourceCategories(portal_url);
251 
253  Message(MSG_NET_REFRESH_REPOLIST_SUCCESS, (void*)cdata_ptr));
254 }
255 
256 void GetResourceFiles(std::string portal_url, int resource_id)
257 {
258  std::string response_payload;
259  std::string resource_url = portal_url + "/resources/" + std::to_string(resource_id);
260  std::string user_agent = fmt::format("{}/{}", "Rigs of Rods Client", ROR_VERSION_STRING);
261  long response_code = 0;
262 
263  CURL *curl = curl_easy_init();
264  curl_easy_setopt(curl, CURLOPT_URL, resource_url.c_str());
265  curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
266 #ifdef _WIN32
267  curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA);
268 #endif // _WIN32
269  curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "gzip");
270  curl_easy_setopt(curl, CURLOPT_USERAGENT, user_agent.c_str());
271  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteFunc);
272  curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_payload);
273 
274  CURLcode curl_result = curl_easy_perform(curl);
275  curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);
276 
277  curl_easy_cleanup(curl);
278  curl = nullptr;
279 
280  if (curl_result != CURLE_OK || response_code != 200)
281  {
282  Ogre::LogManager::getSingleton().stream()
283  << "[RoR|Repository] Failed to retrieve resource;"
284  << " Error: '" << curl_easy_strerror(curl_result) << "'; HTTP status code: " << response_code;
285 
286  // FIXME: we need a FAILURE message for MSG_NET_OPEN_RESOURCE_SUCCESS
287  }
288 
290 
291  rapidjson::Document j_data_doc;
292  j_data_doc.Parse(response_payload.c_str());
293 
294  std::vector<GUI::ResourceFiles> resc;
295  rapidjson::Value& j_resp_body = j_data_doc["resource"]["current_files"];
296  size_t num_rows = j_resp_body.GetArray().Size();
297  resc.resize(num_rows);
298 
299  for (size_t i = 0; i < num_rows; i++)
300  {
301  rapidjson::Value& j_row = j_resp_body[static_cast<rapidjson::SizeType>(i)];
302 
303  resc[i].id = j_row["id"].GetInt();
304  resc[i].filename = j_row["filename"].GetString();
305  resc[i].size = j_row["size"].GetInt();
306  }
307 
308  cdata_ptr->files = resc;
309 
311  Message(MSG_NET_OPEN_RESOURCE_SUCCESS, (void*)cdata_ptr));
312 }
313 
314 void DownloadResourceFile(int resource_id, std::string filename, int id)
315 {
317  int perc = 0;
318  m.payload = reinterpret_cast<void*>(new int(perc));
319  m.description = "Initialising...";
321 
322  std::string url = "https://forum.rigsofrods.org/resources/" + std::to_string(resource_id) + "/download?file=" + std::to_string(id);
323  std::string path = PathCombine(App::sys_user_dir->getStr(), "mods");
324  std::string file = PathCombine(path, filename);
325 
326  RepoProgressContext progress_context;
327  progress_context.filename = filename;
328  long response_code = 0;
329 
330  CURL *curl = curl_easy_init();
331  try // We write using Ogre::DataStream which throws exceptions
332  {
333  // smart pointer - closes stream automatically
334  Ogre::DataStreamPtr datastream = Ogre::ResourceGroupManager::getSingleton().createResource(file, RGN_REPO);
335 
336  curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
337  curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
338 #ifdef _WIN32
339  curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA);
340 #endif // _WIN32
341  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlOgreDataStreamWriteFunc);
342  curl_easy_setopt(curl, CURLOPT_WRITEDATA, datastream.get());
343  curl_easy_setopt(curl, CURLOPT_NOPROGRESS, NULL); // Disable Internal CURL progressmeter
344  curl_easy_setopt(curl, CURLOPT_PROGRESSDATA, &progress_context);
345  curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION, CurlProgressFunc); // Use our progress window
346 
347  CURLcode curl_result = curl_easy_perform(curl);
348  curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);
349 
350  if (curl_result != CURLE_OK || response_code != 200)
351  {
352  Ogre::LogManager::getSingleton().stream()
353  << "[RoR|Repository] Failed to download resource;"
354  << " Error: '" << curl_easy_strerror(curl_result) << "'; HTTP status code: " << response_code;
355 
356  // FIXME: we need a FAILURE message for MSG_GUI_DOWNLOAD_FINISHED
357  }
358  }
359  catch (Ogre::Exception& oex)
360  {
363  fmt::format("Repository UI: cannot download file '{}' - {}",
364  url, oex.getFullDescription()));
365  }
366  curl_easy_cleanup(curl);
367  curl = nullptr;
368 
371 }
372 #endif // defined(USE_CURL)
373 
375 {
376  Ogre::WorkQueue* wq = Ogre::Root::getSingleton().getWorkQueue();
377  m_ogre_workqueue_channel = wq->getChannel("RoR/RepoThumbnails");
378  wq->addRequestHandler(m_ogre_workqueue_channel, this);
379  wq->addResponseHandler(m_ogre_workqueue_channel, this);
380 
381  m_fallback_thumbnail = FetchIcon("ror.png");
382 }
383 
385 {}
386 
388 {
390 
391  ImGui::SetNextWindowSize(ImVec2((ImGui::GetIO().DisplaySize.x / 1.4), (ImGui::GetIO().DisplaySize.y / 1.2)), ImGuiCond_FirstUseEver);
392  ImGui::SetNextWindowPosCenter(ImGuiCond_Appearing);
393  ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse;
394  bool keep_open = true;
395  Ogre::TexturePtr tex1 = FetchIcon("arrow_rotate_anticlockwise.png");
396  Ogre::TexturePtr tex2 = FetchIcon("accept.png");
397  Ogre::TexturePtr tex3 = FetchIcon("star.png");
398  Ogre::TexturePtr tex4 = FetchIcon("arrow_left.png");
399 
400  ImGui::Begin(_LC("RepositorySelector", "Rigs of Rods Repository"), &keep_open, window_flags);
401 
402  if (m_resource_view && ImGui::ImageButton(reinterpret_cast<ImTextureID>(tex4->getHandle()), ImVec2(16, 16)))
403  {
404  m_resource_view = false;
405  }
406  else if (!m_resource_view && ImGui::ImageButton(reinterpret_cast<ImTextureID>(tex1->getHandle()), ImVec2(16, 16)))
407  {
408  this->Refresh();
409  }
410  ImGui::SameLine();
411 
412  if (m_draw)
413  {
414  // Deactivate in resource view
415  if (m_resource_view)
416  {
417  ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true);
418  ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.5f);
419  }
420 
421  // Category dropdown
422  ImGui::SetNextItemWidth(ImGui::GetWindowSize().x
423  - 16 // refresh button width
424  - 170 // search box width
425  - 2*80 // sort + view menu width
426  - 6*ImGui::GetStyle().ItemSpacing.x
427  - 2*ImGui::GetStyle().WindowPadding.x);
428 
429  // Calculate items of every shown category
430  int count = 0;
431  for (int i = 0; i < m_data.categories.size(); i++)
432  {
433  // Skip non mod categories
434  if (m_data.categories[i].resource_category_id >= 8 && m_data.categories[i].resource_category_id <= 13)
435  {
436  continue;
437  }
438  count += m_data.categories[i].resource_count;
439  }
440 
441  // Fill "All" category
442  if (m_current_category_id == 1)
443  {
444  m_current_category = "(" + std::to_string(count) + ") All";
446  }
447 
448  if (ImGui::BeginCombo("##repo-selector-cat", m_current_category.c_str()))
449  {
450  if (ImGui::Selectable(m_all_category_label.c_str(), m_current_category_id == 1))
451  {
454  }
455 
456  for (int i = 0; i < m_data.categories.size(); i++)
457  {
458  // Skip non mod categories
459  if (m_data.categories[i].resource_category_id >= 8 && m_data.categories[i].resource_category_id <= 13)
460  {
461  continue;
462  }
463 
464  m_current_category_label = "(" + std::to_string(m_data.categories[i].resource_count) + ") " + m_data.categories[i].title;
465  bool is_selected = (m_current_category_id == m_data.categories[i].resource_category_id);
466 
467  if (ImGui::Selectable(m_current_category_label.c_str(), is_selected))
468  {
470  m_current_category_id = m_data.categories[i].resource_category_id;
471  }
472  if (is_selected)
473  {
474  ImGui::SetItemDefaultFocus();
475  }
476  }
477  ImGui::EndCombo();
478  }
479 
480  // Search box
481  ImGui::SameLine();
482  ImGui::SetNextItemWidth(170);
483  float search_pos = ImGui::GetCursorPosX();
484  ImGui::InputText("##Search", m_search_input.GetBuffer(), m_search_input.GetCapacity());
485 
486  // Sort dropdown
487  ImGui::SameLine();
488  ImGui::SetNextItemWidth(80);
489 
490  if (ImGui::BeginCombo("##repo-selector-sort", _LC("RepositorySelector", "Sort")))
491  {
492  if (ImGui::Selectable(_LC("RepositorySelector", "Last Update"), m_current_sort == "Last Update"))
493  {
494  m_current_sort = "Last Update";
495  std::sort(m_data.items.begin(), m_data.items.end(), [](ResourceItem a, ResourceItem b) { return a.last_update > b.last_update; });
496  }
497  if (ImGui::Selectable(_LC("RepositorySelector", "Date Added"), m_current_sort == "Date Added"))
498  {
499  m_current_sort = "Date Added";
500  std::sort(m_data.items.begin(), m_data.items.end(), [](ResourceItem a, ResourceItem b) { return a.resource_date > b.resource_date; });
501  }
502  if (ImGui::Selectable(_LC("RepositorySelector", "Title"), m_current_sort == "Title"))
503  {
504  m_current_sort = "Title";
505  std::sort(m_data.items.begin(), m_data.items.end(), [](ResourceItem a, ResourceItem b) { return a.title < b.title; });
506  }
507  if (ImGui::Selectable(_LC("RepositorySelector", "Downloads"), m_current_sort == "Downloads"))
508  {
509  m_current_sort = "Downloads";
510  std::sort(m_data.items.begin(), m_data.items.end(), [](ResourceItem a, ResourceItem b) { return a.download_count > b.download_count; });
511  }
512  if (ImGui::Selectable(_LC("RepositorySelector", "Rating"), m_current_sort == "Rating"))
513  {
514  m_current_sort = "Rating";
515  std::sort(m_data.items.begin(), m_data.items.end(), [](ResourceItem a, ResourceItem b) { return a.rating_avg > b.rating_avg; });
516  }
517  if (ImGui::Selectable(_LC("RepositorySelector", "Rating Count"), m_current_sort == "Rating Count"))
518  {
519  m_current_sort = "Rating Count";
520  std::sort(m_data.items.begin(), m_data.items.end(), [](ResourceItem a, ResourceItem b) { return a.rating_count > b.rating_count; });
521  }
522  ImGui::EndCombo();
523  }
524 
525  // View mode dropdown
526  ImGui::SameLine();
527  ImGui::SetNextItemWidth(80);
528 
529  if (ImGui::BeginCombo("##repo-selector-view", _LC("RepositorySelector", "View")))
530  {
531  if (ImGui::Selectable(_LC("RepositorySelector", "List"), m_view_mode == "List"))
532  {
533  m_view_mode = "List";
534  }
535  if (ImGui::Selectable(_LC("RepositorySelector", "Compact"), m_view_mode == "Compact"))
536  {
537  m_view_mode = "Compact";
538  }
539  if (ImGui::Selectable(_LC("RepositorySelector", "Basic"), m_view_mode == "Basic"))
540  {
541  m_view_mode = "Basic";
542  }
543  ImGui::EndCombo();
544  }
545 
546  // Search box default text
547  if (m_search_input.IsEmpty())
548  {
549  ImGui::SameLine();
550  ImGui::SetCursorPosX(search_pos + ImGui::GetStyle().ItemSpacing.x);
551  ImGui::TextDisabled("%s", _LC("RepositorySelector", "Search Title, Author"));
552  }
553 
554  if (m_resource_view)
555  {
556  ImGui::PopItemFlag();
557  ImGui::PopStyleVar();
558  }
559 
560  const float table_height = ImGui::GetWindowHeight()
561  - ((2.f * ImGui::GetStyle().WindowPadding.y) + (3.f * ImGui::GetItemsLineHeightWithSpacing())
562  - ImGui::GetStyle().ItemSpacing.y);
563 
564  if (m_resource_view)
565  {
566  // Scroll area
567  ImGui::BeginChild("resource-view-scrolling", ImVec2(0.f, table_height), false);
568 
569  float text_pos = 140.f;
570 
571  ImGui::Text("%s", _LC("RepositorySelector", "Details:"));
572  ImGui::Separator();
573 
574  ImGui::TextDisabled("%s", _LC("RepositorySelector", "Title:"));
575  ImGui::SameLine();
576  ImGui::SetCursorPosX(text_pos);
577  ImGui::TextColored(theme.value_blue_text_color, "%s", m_selected_item.title.c_str());
578  ImGui::Separator();
579 
580  ImGui::TextDisabled("%s", _LC("RepositorySelector", "Resource ID:"));
581  ImGui::SameLine();
582  ImGui::SetCursorPosX(text_pos);
583  ImGui::TextColored(theme.value_blue_text_color, "%d", m_selected_item.resource_id);
584  ImGui::Separator();
585 
586  ImGui::TextDisabled("%s", _LC("RepositorySelector", "Category:"));
587  ImGui::SameLine();
588  ImGui::SetCursorPosX(text_pos);
589 
590  for (int i = 0; i < m_data.categories.size(); i++)
591  {
592  if (m_data.categories[i].resource_category_id == m_selected_item.resource_category_id)
593  {
594  ImGui::TextColored(theme.value_blue_text_color, "%s", m_data.categories[i].title.c_str());
595  }
596  }
597 
598  ImGui::Separator();
599 
600  ImGui::TextDisabled("%s", _LC("RepositorySelector", "Description:"));
601  ImGui::SameLine();
602  ImGui::SetCursorPosX(text_pos);
603  ImGui::TextColored(theme.value_blue_text_color, "%s", m_selected_item.tag_line.c_str());
604  ImGui::Separator();
605 
606  ImGui::TextDisabled("%s", _LC("RepositorySelector", "Downloads:"));
607  ImGui::SameLine();
608  ImGui::SetCursorPosX(text_pos);
609  ImGui::TextColored(theme.value_blue_text_color, "%d", m_selected_item.download_count);
610  ImGui::Separator();
611 
612  ImGui::TextDisabled("%s", _LC("RepositorySelector", "View Count:"));
613  ImGui::SameLine();
614  ImGui::SetCursorPosX(text_pos);
615  ImGui::TextColored(theme.value_blue_text_color, "%d", m_selected_item.view_count);
616  ImGui::Separator();
617 
618  ImGui::TextDisabled("%s", _LC("RepositorySelector", "Rating:"));
619  ImGui::SameLine();
620  ImGui::SetCursorPosX(text_pos);
621  ImGui::TextColored(theme.value_blue_text_color, "%f", m_selected_item.rating_avg);
622  ImGui::Separator();
623 
624  ImGui::TextDisabled("%s", _LC("RepositorySelector", "Rating Count:"));
625  ImGui::SameLine();
626  ImGui::SetCursorPosX(text_pos);
627  ImGui::TextColored(theme.value_blue_text_color, "%d", m_selected_item.rating_count);
628  ImGui::Separator();
629 
630  ImGui::TextDisabled(_LC("RepositorySelector", "Date Added:"));
631  ImGui::SameLine();
632  ImGui::SetCursorPosX(text_pos);
633  time_t a = (const time_t)m_selected_item.resource_date;
634  ImGui::TextColored(theme.value_blue_text_color, "%s", asctime(gmtime (&a)));
635  ImGui::Separator();
636 
637  ImGui::TextDisabled(_LC("RepositorySelector", "Last Update:"));
638  ImGui::SameLine();
639  ImGui::SetCursorPosX(text_pos);
640  time_t b = (const time_t)m_selected_item.last_update;
641  ImGui::TextColored(theme.value_blue_text_color, "%s", asctime(gmtime (&b)));
642  ImGui::Separator();
643 
644  ImGui::TextDisabled("%s", _LC("RepositorySelector", "Version:"));
645  ImGui::SameLine();
646  ImGui::SetCursorPosX(text_pos);
647  ImGui::TextColored(theme.value_blue_text_color, "%s", m_selected_item.version.c_str());
648  ImGui::Separator();
649 
650  ImGui::TextDisabled("%s", _LC("RepositorySelector", "Authors:"));
651  ImGui::SameLine();
652  ImGui::SetCursorPosX(text_pos);
653  ImGui::TextColored(theme.value_blue_text_color, "%s", m_selected_item.authors.c_str());
654  ImGui::Separator();
655 
656  ImGui::TextDisabled("%s", _LC("RepositorySelector", "View URL:"));
657  ImGui::SameLine();
658  ImGui::SetCursorPosX(text_pos);
659  ImGui::TextColored(theme.value_blue_text_color, "%s", m_selected_item.view_url.c_str());
660  ImGui::Separator();
661 
662  ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 20);
663  ImGui::Text("%s", _LC("RepositorySelector", "Files:"));
664 
665  // Spinner
666  if (m_data.files.empty() && m_repofiles_msg.empty())
667  {
668  ImGui::SameLine();
669  float spinner_size = 7.f;
670  LoadingIndicatorCircle("spinner", spinner_size, theme.value_blue_text_color, theme.value_blue_text_color, 10, 10);
671  }
672 
673  ImGui::Separator();
674 
675  if (!m_repofiles_msg.empty())
676  {
677  ImGui::TextDisabled("%s", m_repofiles_msg.c_str());
678  }
679 
680  // Check for duplicate files, remove the outdated one (lower id)
681  std::sort(m_data.files.begin(), m_data.files.end(), [](ResourceFiles a, ResourceFiles b) { return a.id > b.id; });
682  auto last = std::unique(m_data.files.begin(), m_data.files.end(), [](ResourceFiles a, ResourceFiles b) { return a.filename == b.filename; });
683  m_data.files.erase(last, m_data.files.end());
684 
685  for (int i = 0; i < m_data.files.size(); i++)
686  {
687  ImGui::PushID(i);
688 
689  ImGui::AlignTextToFramePadding();
690  float pos_y = ImGui::GetCursorPosY();
691 
692  ImGui::TextDisabled("%s", _LC("RepositorySelector", "Filename:"));
693  ImGui::SameLine();
694  ImGui::SetCursorPosX(text_pos);
695 
696  // File
697  std::string path = PathCombine(App::sys_user_dir->getStr(), "mods");
698  std::string file = PathCombine(path, m_data.files[i].filename);
699 
700  // Get created time
701  int file_time = 0;
702  if (FileExists(file))
703  {
704  file_time = GetFileLastModifiedTime(file);
705  }
706 
707  ImGui::TextColored(theme.value_blue_text_color, "%s", m_data.files[i].filename.c_str());
708 
709  if (FileExists(file) && ImGui::IsItemHovered())
710  {
711  ImGui::BeginTooltip();
712 
713  time_t c = (const time_t)file_time;
714  ImGui::TextDisabled("%s %s", "Installed on", asctime(gmtime (&c)));
715 
716  ImGui::EndTooltip();
717  }
718 
719  // File size
720  ImGui::SameLine();
721 
722  int size = m_data.files[i].size / 1024;
723  ImGui::TextDisabled("(%d %s)", size, "KB");
724 
725  // File exists show indicator
726  if (FileExists(file))
727  {
728  ImGui::SameLine();
729  ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 3.5f);
730  ImGui::Image(reinterpret_cast<ImTextureID>(tex2->getHandle()), ImVec2(16, 16));
731  }
732 
733  // Buttons
734  ImGui::SameLine();
735  ImGui::SetCursorPosX(ImGui::GetWindowSize().x - 220);
736  ImGui::SetCursorPosY(pos_y);
737 
738  std::string btn_label;
739  if (FileExists(file) && m_selected_item.last_update > file_time)
740  {
741  btn_label = fmt::format(_LC("RepositorySelector", "Update"));
742  }
743  else if (FileExists(file))
744  {
745  btn_label = fmt::format(_LC("RepositorySelector", "Reinstall"));
746  }
747  else
748  {
749  btn_label = fmt::format(_LC("RepositorySelector", "Install"));
750  }
751 
752  if (ImGui::Button(btn_label.c_str(), ImVec2(100, 0)))
753  {
754  this->Download(m_selected_item.resource_id, m_data.files[i].filename, m_data.files[i].id);
755  }
756 
757  ImGui::SameLine();
758 
759  if (FileExists(file) && ImGui::Button(_LC("RepositorySelector", "Remove"), ImVec2(100, 0)))
760  {
761  Ogre::ArchiveManager::getSingleton().unload(file);
762  Ogre::FileSystemLayer::removeFile(file);
763  m_update_cache = true;
764  }
765  else if (!FileExists(file))
766  {
767  ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true);
768  ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.5f);
769  ImGui::Button(_LC("RepositorySelector", "Remove"), ImVec2(100, 0));
770  ImGui::PopItemFlag();
771  ImGui::PopStyleVar();
772  }
773 
774  ImGui::Separator();
775 
776  ImGui::PopID();
777  }
778  ImGui::EndChild();
779  }
780  else
781  {
782  float col0_width = 0.40f * ImGui::GetWindowContentRegionWidth();
783  float col1_width = 0.15f * ImGui::GetWindowContentRegionWidth();
784  float col2_width = 0.20f * ImGui::GetWindowContentRegionWidth();
785  float col3_width = 0.10f * ImGui::GetWindowContentRegionWidth();
786 
787  if (m_view_mode == "Basic")
788  {
789  ImGui::Columns(5, "repo-selector-columns-basic-headers", false);
790  ImGui::SetColumnWidth(0, col0_width + ImGui::GetStyle().ItemSpacing.x);
791  ImGui::SetColumnWidth(1, col1_width);
792  ImGui::SetColumnWidth(2, col2_width);
793  ImGui::SetColumnWidth(3, col3_width);
794 
795  ImGui::TextDisabled("%s", _LC("RepositorySelector", "Title"));
796  ImGui::NextColumn();
797  ImGui::TextDisabled("%s", _LC("RepositorySelector", "Version"));
798  ImGui::NextColumn();
799  ImGui::TextDisabled("%s", _LC("RepositorySelector", "Last Update"));
800  ImGui::NextColumn();
801  ImGui::TextDisabled("%s", _LC("RepositorySelector", "Downloads"));
802  ImGui::NextColumn();
803  ImGui::TextDisabled("%s", _LC("RepositorySelector", "Rating"));
804  ImGui::Columns(1);
805  }
806 
807  // Scroll area
808  ImGui::BeginChild("scrolling", ImVec2(0.f, table_height), false);
809 
810  if (m_view_mode == "List")
811  {
812  ImGui::Columns(2, "repo-selector-columns");
813  ImGui::SetColumnWidth(0, 100.f);
814  ImGui::Separator();
815  }
816  else if (m_view_mode == "Basic")
817  {
818  ImGui::Columns(5, "repo-selector-columns-basic");
819  ImGui::SetColumnWidth(0, col0_width);
820  ImGui::SetColumnWidth(1, col1_width);
821  ImGui::SetColumnWidth(2, col2_width);
822  ImGui::SetColumnWidth(3, col3_width);
823  ImGui::Separator();
824  }
825 
826  // Draw table body
827  int num_drawn_items = 0;
828  for (int i = 0; i < m_data.items.size(); i++)
829  {
830  // Skip items from non mod categories
831  if (m_data.items[i].resource_category_id >= 8 && m_data.items[i].resource_category_id <= 13)
832  {
833  continue;
834  }
835 
836  if (m_data.items[i].resource_category_id == m_current_category_id || m_current_category_id == 1)
837  {
838  // Simple search filter: convert both title/author and input to lowercase, if input not found in the title/author continue
839  std::string title = m_data.items[i].title;
840  for(auto& c : title)
841  {
842  c = tolower(c);
843  }
844  std::string author = m_data.items[i].authors;
845  for(auto& c : author)
846  {
847  c = tolower(c);
848  }
849  std::string search = m_search_input.GetBuffer();
850  for(auto& c : search)
851  {
852  c = tolower(c);
853  }
854  if (title.find(search) == std::string::npos && author.find(search) == std::string::npos)
855  {
856  continue;
857  }
858 
859  ImGui::PushID(i);
860 
861  if (m_view_mode == "List")
862  {
863  // Thumbnail
864  ImGui::SetCursorPosX(ImGui::GetCursorPosX() - ImGui::GetStyle().ItemSpacing.x);
865  this->DrawThumbnail(i);
866 
867  float width = (ImGui::GetColumnWidth() + 90);
868  ImGui::NextColumn();
869 
870  // Columns already colored, just add a light background
871  ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.17f, 0.17f, 0.17f, 0.90f));
872  ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImGui::GetStyle().Colors[ImGuiCol_Header]);
873  ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0.22f, 0.22f, 0.22f, 0.90f));
874 
875  // Wrap a Selectable around the whole column
876  float orig_cursor_y = ImGui::GetCursorPosY();
877  std::string item_id = "##" + std::to_string(i);
878 
879  if (ImGui::Selectable(item_id.c_str(), m_selected_item.resource_id == m_data.items[i].resource_id, 0, ImVec2(0, 100)))
880  {
883  m_resource_view = true;
884  }
885 
886  ImGui::SetCursorPosY(orig_cursor_y);
887  ImGui::PopStyleColor(3);
888 
889  // Title, version
890  ImGui::Text("%s", m_data.items[i].title.c_str());
891  ImGui::SameLine();
892  ImGui::TextDisabled("%s", m_data.items[i].version.c_str());
893 
894  // Rating
895  for (int i = 1; i <= 5; i++)
896  {
897  ImGui::SameLine();
898  ImGui::SetCursorPosX(ImGui::GetColumnWidth() + 16 * i);
899  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));
900  }
901 
902  int rating = round(m_data.items[i].rating_avg);
903  for (int i = 1; i <= rating; i++)
904  {
905  ImGui::SameLine();
906  ImGui::SetCursorPosX(ImGui::GetColumnWidth() + 16 * i);
907  ImGui::Image(reinterpret_cast<ImTextureID>(tex3->getHandle()), ImVec2(16, 16));
908  }
909 
910  // Authors, rating count, last update, download count, description
911  ImGui::TextDisabled("%s:", _LC("RepositorySelector", "Authors"));
912  ImGui::SameLine();
913  ImGui::SetCursorPosX(width);
914  ImGui::TextColored(theme.value_blue_text_color, "%s", m_data.items[i].authors.c_str());
915 
916  ImGui::SameLine();
917  std::string rc = std::to_string(m_data.items[i].rating_count) + " ratings";
918  ImGui::SetCursorPosX(ImGui::GetColumnWidth() - (ImGui::CalcTextSize(rc.c_str()).x / 2) + 16 * 3.5);
919  ImGui::TextDisabled("%s", rc.c_str());
920 
921  ImGui::TextDisabled("%s:", _LC("RepositorySelector", "Last Update"));
922  ImGui::SameLine();
923  ImGui::SetCursorPosX(width);
924  time_t rawtime = (const time_t)m_data.items[i].last_update;
925  ImGui::TextColored(theme.value_blue_text_color, "%s", asctime(gmtime (&rawtime)));
926 
927  ImGui::TextDisabled("%s:", _LC("RepositorySelector", "Downloads"));
928  ImGui::SameLine();
929  ImGui::SetCursorPosX(width);
930  ImGui::TextColored(theme.value_blue_text_color, "%d", m_data.items[i].download_count);
931 
932  ImGui::TextDisabled("%s:", _LC("RepositorySelector", "Description"));
933  ImGui::SameLine();
934  ImGui::SetCursorPosX(width);
935  ImGui::TextColored(theme.value_blue_text_color, "%s", m_data.items[i].tag_line.c_str());
936 
937  ImGui::NextColumn();
938 
939  ImGui::Separator();
940  }
941  else if (m_view_mode == "Compact")
942  {
943  float orig_cursor_x = ImGui::GetCursorPos().x;
944 
945  // Calc box size: Draw 3 boxes per line, 2 for small resolutions
946  float box_width = (ImGui::GetIO().DisplaySize.x / 1.4) / 3;
947  if (ImGui::GetIO().DisplaySize.x <= 1280)
948  {
949  box_width = (ImGui::GetIO().DisplaySize.x / 1.4) / 2;
950  }
951 
952  // Skip to new line if at least 50% of the box can't fit on current line.
953  if (orig_cursor_x > ImGui::GetWindowContentRegionMax().x - (box_width * 0.5))
954  {
955  // Unless this is the 1st line... not much to do with such narrow window.
956  if (num_drawn_items != 0)
957  {
958  ImGui::NewLine();
959  }
960  }
961 
962  ImGui::BeginGroup();
963 
964  ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.70f, 0.70f, 0.70f, 0.90f));
965  ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImGui::GetStyle().Colors[ImGuiCol_Header]);
966  ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0.90f, 0.90f, 0.90f, 0.90f));
967 
968  // Wrap a Selectable around images + text
969  float orig_cursor_y = ImGui::GetCursorPosY();
970  std::string item_id = "##" + std::to_string(i);
971 
972  if (ImGui::Selectable(item_id.c_str(), m_selected_item.resource_id == m_data.items[i].resource_id, 0, ImVec2(box_width - ImGui::GetStyle().ItemSpacing.x, 100)))
973  {
976  m_resource_view = true;
977  }
978 
979  // Add a light background
980  ImVec2 p_min = ImGui::GetItemRectMin();
981  ImVec2 p_max = ImGui::GetItemRectMax();
982  ImGui::GetWindowDrawList()->AddRectFilled(p_min, p_max, ImColor(ImVec4(0.15f, 0.15f, 0.15f, 0.90f)));
983 
984  ImGui::SetCursorPosY(orig_cursor_y);
985  ImGui::PopStyleColor(3);
986 
987  // Thumbnail
988  this->DrawThumbnail(i);
989 
990  // Rating
991  float pos_y;
992  for (int i = 1; i <= 5; i++)
993  {
994  pos_y = ImGui::GetCursorPosY();
995  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));
996  if ( i < 5) { ImGui::SameLine(); }
997  }
998 
999  int rating = round(m_data.items[i].rating_avg);
1000  if (rating >= 1)
1001  {
1002  for (int i = 1; i <= rating; i++)
1003  {
1004  ImGui::SetCursorPosY(pos_y);
1005  ImGui::Image(reinterpret_cast<ImTextureID>(tex3->getHandle()), ImVec2(11, 11));
1006  if (i < rating) { ImGui::SameLine(); }
1007  }
1008  }
1009 
1010  // Move text top right of the image
1011  ImGui::SetCursorPosX(ImGui::GetCursorPos().x + 86);
1012  ImGui::SetCursorPosY(ImGui::GetCursorPos().y - 100);
1013 
1014  // Trim the title, can be long
1015  std::string tl = m_data.items[i].title;
1016  if (ImGui::CalcTextSize(tl.c_str()).x > box_width / 12)
1017  {
1018  tl.resize(box_width / 12);
1019  tl += "...";
1020  }
1021 
1022  // Title, version, last update, download count
1023  ImGui::Text("%s", tl.c_str());
1024 
1025  ImGui::SetCursorPosX(ImGui::GetCursorPos().x + 86);
1026  ImGui::TextColored(theme.value_blue_text_color, "%s %s", _LC("RepositorySelector", "Version"), m_data.items[i].version.c_str());
1027 
1028  ImGui::SetCursorPosX(ImGui::GetCursorPos().x + 86);
1029  time_t rawtime = (const time_t)m_data.items[i].last_update;
1030  ImGui::TextColored(theme.value_blue_text_color, "%s", asctime(gmtime (&rawtime)));
1031 
1032  ImGui::SetCursorPosX(ImGui::GetCursorPos().x + 86);
1033  ImGui::TextColored(theme.value_blue_text_color, "%s %d %s", _LC("RepositorySelector", "Downloaded"), m_data.items[i].download_count, _LC("RepositorySelector", "times"));
1034 
1035  // Add space for next item
1036  ImGui::SetCursorPosX(ImGui::GetCursorPos().x + box_width);
1037  ImGui::SetCursorPosY(ImGui::GetCursorPos().y + (1.5f * ImGui::GetStyle().WindowPadding.y));
1038 
1039  ImGui::EndGroup();
1040  ImGui::SameLine();
1041  }
1042  else if (m_view_mode == "Basic")
1043  {
1044  // Columns already colored, just add a light background
1045  ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.18f, 0.18f, 0.18f, 0.90f));
1046  ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImGui::GetStyle().Colors[ImGuiCol_Header]);
1047  ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0.22f, 0.22f, 0.22f, 0.90f));
1048 
1049  // Wrap a Selectable around the whole column
1050  std::string item_id = "##" + std::to_string(i);
1051 
1052  if (ImGui::Selectable(item_id.c_str(), m_selected_item.resource_id == m_data.items[i].resource_id, ImGuiSelectableFlags_SpanAllColumns))
1053  {
1056  m_resource_view = true;
1057  }
1058 
1059  ImGui::PopStyleColor(3);
1060 
1061  // Draw columns
1062  ImGui::SameLine();
1063  ImGui::SetCursorPosX(ImGui::GetCursorPosX() - 2*ImGui::GetStyle().ItemSpacing.x);
1064  ImGui::Text("%s", m_data.items[i].title.c_str());
1065 
1066  ImGui::NextColumn();
1067 
1068  ImGui::TextColored(theme.value_blue_text_color, "%s", m_data.items[i].version.c_str());
1069 
1070  ImGui::NextColumn();
1071 
1072  time_t rawtime = (const time_t)m_data.items[i].last_update;
1073  ImGui::TextColored(theme.value_blue_text_color, "%s", asctime(gmtime (&rawtime)));
1074 
1075  ImGui::NextColumn();
1076 
1077  ImGui::TextColored(theme.value_blue_text_color, "%d", m_data.items[i].download_count);
1078 
1079  ImGui::NextColumn();
1080 
1081  float pos_x = ImGui::GetCursorPosX();
1082 
1083  // Rating
1084  for (int i = 1; i <= 5; i++)
1085  {
1086  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));
1087  ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 16 * i);
1088  ImGui::SameLine();
1089  }
1090  ImGui::SetCursorPosX(pos_x);
1091 
1092  int rating = round(m_data.items[i].rating_avg);
1093  for (int i = 1; i <= rating; i++)
1094  {
1095  ImGui::Image(reinterpret_cast<ImTextureID>(tex3->getHandle()), ImVec2(16, 16));
1096  ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 16 * i);
1097  ImGui::SameLine();
1098  }
1099 
1100  ImGui::NextColumn();
1101 
1102  ImGui::Separator();
1103  }
1104  ImGui::PopID();
1105  num_drawn_items++;
1106  }
1107  }
1108  ImGui::EndChild();
1109  }
1110  }
1111 
1112  if (m_show_spinner)
1113  {
1114  float spinner_size = 27.f;
1115  ImGui::SetCursorPosX((ImGui::GetWindowSize().x / 2.f) - spinner_size);
1116  ImGui::SetCursorPosY((ImGui::GetWindowSize().y / 2.f) - spinner_size);
1117  LoadingIndicatorCircle("spinner", spinner_size, theme.value_blue_text_color, theme.value_blue_text_color, 10, 10);
1118  }
1119 
1120  if (m_repolist_msg != "")
1121  {
1122  const ImVec2 label_size = ImGui::CalcTextSize(m_repolist_msg.c_str());
1123  float y = (ImGui::GetWindowSize().y / 2.f) - (ImGui::GetTextLineHeight() / 2.f);
1124  ImGui::SetCursorPosX((ImGui::GetWindowSize().x / 2.f) - (label_size.x / 2.f));
1125  ImGui::SetCursorPosY(y);
1126  ImGui::TextColored(m_repolist_msg_color, "%s", m_repolist_msg.c_str());
1127  y += ImGui::GetTextLineHeightWithSpacing();
1128 
1129  if (m_repolist_curlmsg != "")
1130  {
1131  const ImVec2 detail_size = ImGui::CalcTextSize(m_repolist_curlmsg.c_str());
1132  ImGui::SetCursorPosX((ImGui::GetWindowSize().x / 2.f) - (detail_size.x / 2.f));
1133  ImGui::SetCursorPosY(y);
1134  ImGui::TextDisabled("%s", m_repolist_curlmsg.c_str());
1135  y += ImGui::GetTextLineHeight();
1136  }
1137 
1138  if (m_repolist_httpmsg != "")
1139  {
1140  const ImVec2 detail_size = ImGui::CalcTextSize(m_repolist_httpmsg.c_str());
1141  ImGui::SetCursorPosX((ImGui::GetWindowSize().x / 2.f) - (detail_size.x / 2.f));
1142  ImGui::SetCursorPosY(y);
1143  ImGui::TextDisabled("%s", m_repolist_httpmsg.c_str());
1144  }
1145  }
1146 
1147  ImGui::End();
1148  if (!keep_open)
1149  {
1150  this->SetVisible(false);
1151  }
1152 }
1153 
1155 {
1156 #if defined(USE_CURL)
1157  m_show_spinner = true;
1158  m_draw = false;
1159  m_data.items.clear();
1160  m_repolist_msg = "";
1161  std::packaged_task<void(std::string)> task(GetResources);
1162  std::thread(std::move(task), App::remote_query_url->getStr()).detach();
1163 #endif // defined(USE_CURL)
1164 }
1165 
1167 {
1168  m_show_spinner = false;
1169  m_data.categories = data->categories;
1170  m_data.items = data->items;
1171 
1172  // Sort
1173  std::sort(m_data.items.begin(), m_data.items.end(), [](ResourceItem a, ResourceItem b) { return a.last_update > b.last_update; });
1174 
1175  if (m_data.items.empty())
1176  {
1177  m_repolist_msg = _LC("RepositorySelector", "Sorry, the repository isn't available. Try again later.");
1179  }
1180  else
1181  {
1182  m_repolist_msg = "";
1183  m_draw = true;
1184  }
1185 }
1186 
1188 {
1189  m_data.files = data->files;
1190 
1191  if (m_data.files.empty())
1192  {
1193  m_repofiles_msg = _LC("RepositorySelector", "No files available :(");
1194  }
1195  else
1196  {
1197  m_repofiles_msg = "";
1198  }
1199 }
1200 
1202 {
1203 #if defined(USE_CURL)
1204  m_data.files.clear();
1205  m_repofiles_msg = "";
1206  std::packaged_task<void(std::string, int)> task(GetResourceFiles);
1207  std::thread(std::move(task), App::remote_query_url->getStr(), resource_id).detach();
1208 #endif // defined(USE_CURL)
1209 }
1210 
1211 void RepositorySelector::Download(int resource_id, std::string filename, int id)
1212 {
1213 #if defined(USE_CURL)
1214  m_update_cache = false;
1215  this->SetVisible(false);
1216  std::packaged_task<void(int, std::string, int)> task(DownloadResourceFile);
1217  std::thread(std::move(task), resource_id, filename, id).detach();
1218 #endif // defined(USE_CURL)
1219 }
1220 
1222 {
1223  m_update_cache = true;
1224 }
1225 
1227 {
1228  m_repolist_msg = failinfo->title;
1230  m_draw = false;
1231  m_show_spinner = false;
1232  if (failinfo->curl_result != CURLE_OK)
1233  m_repolist_curlmsg = curl_easy_strerror(failinfo->curl_result);
1234  if (failinfo->http_response != 0)
1235  m_repolist_httpmsg = fmt::format(_L("HTTP response code: {}"), failinfo->http_response);
1236 }
1237 
1239 {
1240  m_is_visible = visible;
1241  if (visible && m_data.items.size() == 0)
1242  {
1243  this->Refresh();
1244  }
1245  else if (!visible && (App::app_state->getEnum<AppState>() == AppState::MAIN_MENU))
1246  {
1248  if (m_update_cache)
1249  {
1250  m_update_cache = false;
1252  }
1253  }
1254 }
1255 
1256 // --------------------------------------------
1257 // Async thumbnail download via Ogre::WorkQueue
1258 // see https://wiki.ogre3d.org/How+to+use+the+WorkQueue
1259 
1260 void RepositorySelector::DrawThumbnail(int resource_item_idx)
1261 {
1262  // Runs on main thread when drawing GUI
1263  // Displays a thumbnail image if available, or shows a spinner and initiates async download.
1264  // -----------------------------------------------------------------------------------------
1265 
1266  GUIManager::GuiTheme const& theme = App::GetGuiManager()->GetTheme();
1267 
1268  ImVec2 image_size;
1269  if (m_view_mode == "List")
1270  {
1271  image_size = ImVec2(ImGui::GetColumnWidth() - ImGui::GetStyle().ItemSpacing.x, 96);
1272  }
1273  else
1274  {
1275  image_size = ImVec2(76, 86);
1276  }
1277 
1278  if (!m_data.items[resource_item_idx].preview_tex)
1279  {
1280  if (m_data.items[resource_item_idx].icon_url == "")
1281  {
1282  // No thumbnail defined - use a placeholder logo.
1283  m_data.items[resource_item_idx].preview_tex = m_fallback_thumbnail;
1284  }
1285  else
1286  {
1287  // Thumbnail defined - see if we want to initiate download.
1288  if (ImGui::IsRectVisible(image_size)
1289  && !m_data.items[resource_item_idx].thumbnail_dl_queued)
1290  {
1291  // Image is in visible screen area and not yet downloading.
1292  Ogre::Root::getSingleton().getWorkQueue()->addRequest(m_ogre_workqueue_channel, 1234, Ogre::Any(resource_item_idx));
1293  m_data.items[resource_item_idx].thumbnail_dl_queued = true;
1294  }
1295  }
1296  }
1297 
1298  if (m_data.items[resource_item_idx].preview_tex)
1299  {
1300  // Thumbnail downloaded or replaced by placeholder - draw it.
1301  ImGui::Image(
1302  reinterpret_cast<ImTextureID>(m_data.items[resource_item_idx].preview_tex->getHandle()),
1303  image_size);
1304  }
1305  else
1306  {
1307  // Thumbnail is downloading - draw spinner.
1308  if (m_view_mode == "List")
1309  {
1310  float spinner_size = ImGui::GetColumnWidth() / 4;
1311  ImGui::SetCursorPosX(((ImGui::GetColumnWidth() - ImGui::GetStyle().ItemSpacing.x) / 2.f) - spinner_size);
1312  ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 5*ImGui::GetStyle().ItemSpacing.y);
1313  LoadingIndicatorCircle("spinner", spinner_size, theme.value_blue_text_color, theme.value_blue_text_color, 10, 10);
1314  }
1315  else
1316  {
1317  float spinner_size = 25;
1318  ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 2*ImGui::GetStyle().ItemSpacing.x);
1319  ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 20);
1320  LoadingIndicatorCircle("spinner", spinner_size, theme.value_blue_text_color, theme.value_blue_text_color, 10, 10);
1321  ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 76 - (35 + spinner_size));
1322  }
1323  }
1324 }
1325 
1326 Ogre::WorkQueue::Response* RepositorySelector::handleRequest(const Ogre::WorkQueue::Request *req, const Ogre::WorkQueue *srcQ)
1327 {
1328  // This runs on background worker thread in Ogre::WorkQueue's thread pool.
1329  // Purpose: to fetch one thumbnail image using CURL.
1330  // -----------------------------------------------------------------------
1331 
1332  int item_idx = Ogre::any_cast<int>(req->getData());
1333  std::string filename = std::to_string(m_data.items[item_idx].resource_id) + ".png";
1334  std::string file = PathCombine(App::sys_thumbnails_dir->getStr(), filename);
1335  long response_code = 0;
1336 
1337  if (FileExists(file))
1338  {
1339  return OGRE_NEW Ogre::WorkQueue::Response(req, /*success:*/false, Ogre::Any(item_idx));
1340  }
1341  else
1342  {
1343  try // We write using Ogre::DataStream which throws exceptions
1344  {
1345  // smart pointer - closes stream automatically
1346  Ogre::DataStreamPtr datastream = Ogre::ResourceGroupManager::getSingleton().createResource(filename, RGN_REPO);
1347 
1348  curl_easy_setopt(curl_th, CURLOPT_URL, m_data.items[item_idx].icon_url.c_str());
1349  curl_easy_setopt(curl_th, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
1350 #ifdef _WIN32
1351  curl_easy_setopt(curl_th, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA);
1352 #endif // _WIN32
1353  curl_easy_setopt(curl_th, CURLOPT_WRITEFUNCTION, CurlOgreDataStreamWriteFunc);
1354  curl_easy_setopt(curl_th, CURLOPT_WRITEDATA, datastream.get());
1355  CURLcode curl_result = curl_easy_perform(curl_th);
1356 
1357  if (curl_result != CURLE_OK || response_code != 200)
1358  {
1359  Ogre::LogManager::getSingleton().stream()
1360  << "[RoR|Repository] Failed to download thumbnail;"
1361  << " Error: '" << curl_easy_strerror(curl_result) << "'; HTTP status code: " << response_code;
1362 
1363  return OGRE_NEW Ogre::WorkQueue::Response(req, /*success:*/false, Ogre::Any(item_idx));
1364  }
1365  else
1366  {
1367  return OGRE_NEW Ogre::WorkQueue::Response(req, /*success:*/true, Ogre::Any(item_idx));
1368  }
1369  }
1370  catch (Ogre::Exception& oex)
1371  {
1374  fmt::format("Repository UI: cannot download thumbnail '{}' - {}",
1375  m_data.items[item_idx].icon_url, oex.getFullDescription()));
1376 
1377  return OGRE_NEW Ogre::WorkQueue::Response(req, /*success:*/false, Ogre::Any(item_idx));
1378  }
1379  }
1380 }
1381 
1382 void RepositorySelector::handleResponse(const Ogre::WorkQueue::Response *req, const Ogre::WorkQueue *srcQ)
1383 {
1384  // This runs on main thread.
1385  // It's safe to load the texture and modify GUI data.
1386  // --------------------------------------------------
1387 
1388  int item_idx = Ogre::any_cast<int>(req->getData());
1389  std::string filename = std::to_string(m_data.items[item_idx].resource_id) + ".png";
1390  std::string file = PathCombine(App::sys_thumbnails_dir->getStr(), filename);
1391 
1392  if (FileExists(file)) // We have an image
1393  {
1394  try // Check if loads correctly (not null, not invalid etc...)
1395  {
1396  m_data.items[item_idx].preview_tex = FetchIcon(file.c_str());
1397  m_data.items[item_idx].preview_tex->load();
1398  }
1399  catch (...) // Doesn't load, fallback
1400  {
1401  m_data.items[item_idx].preview_tex = m_fallback_thumbnail;
1402  }
1403  }
1404 }
RoR::App::sys_user_dir
CVar * sys_user_dir
Definition: Application.cpp:163
GameContext.h
Game state manager and message-queue provider.
RoR::GUI::RepositorySelector::m_fallback_thumbnail
Ogre::TexturePtr m_fallback_thumbnail
Definition: GUI_RepositorySelector.h:128
y
float y
Definition: (ValueTypes) quaternion.h:6
RoR::GUI::RepositorySelector::Draw
void Draw()
Definition: GUI_RepositorySelector.cpp:387
RoR::FetchIcon
Ogre::TexturePtr FetchIcon(const char *name)
Definition: GUIUtils.cpp:344
RoR::GUI::ResourceItem::rating_count
int rating_count
Definition: GUI_RepositorySelector.h:64
RoR::GUI::ResourceItem::view_url
std::string view_url
Definition: GUI_RepositorySelector.h:65
RoR::Str::GetBuffer
char * GetBuffer()
Definition: Str.h:48
RoR::GUIManager::GuiTheme
Definition: GUIManager.h:68
RoR::CurlFailInfo::http_response
long http_response
Definition: Network.h:51
RoR::GUI::RepositorySelector::m_current_category
std::string m_current_category
Definition: GUI_RepositorySelector.h:117
RoR::App::GetGuiManager
GUIManager * GetGuiManager()
Definition: Application.cpp:269
RoR::GUIManager::GuiTheme::value_blue_text_color
ImVec4 value_blue_text_color
Definition: GUIManager.h:75
file
This is a raw Ogre binding for Imgui No project cmake file
Definition: README-OgreImGui.txt:3
RoR::GUI::RepositorySelector::RepositorySelector
RepositorySelector()
Definition: GUI_RepositorySelector.cpp:374
RoR::AppState::MAIN_MENU
@ MAIN_MENU
ContentManager.h
RoR::GUI::ResourceItem::authors
std::string authors
Definition: GUI_RepositorySelector.h:58
RoR::GUI::RepositorySelector::m_resource_view
bool m_resource_view
Definition: GUI_RepositorySelector.h:125
format
Truck file format(technical spec)
RoR::GUI::ResourceItem::tag_line
std::string tag_line
Definition: GUI_RepositorySelector.h:56
GUIUtils.h
RoR::MSG_GUI_DOWNLOAD_PROGRESS
@ MSG_GUI_DOWNLOAD_PROGRESS
Definition: Application.h:140
CurlProgressFunc
static size_t CurlProgressFunc(void *ptr, double filesize_B, double downloaded_B)
Definition: GUI_RepositorySelector.cpp:74
RoR::GUI::RepositorySelector::m_show_spinner
bool m_show_spinner
Definition: GUI_RepositorySelector.h:122
RoR::Str::GetCapacity
size_t GetCapacity() const
Definition: Str.h:49
RepoProgressContext::old_perc
double old_perc
Definition: GUI_RepositorySelector.cpp:71
AppContext.h
System integration layer; inspired by OgreBites::ApplicationContext.
Console.h
RoR::Console::putMessage
void putMessage(MessageArea area, MessageType type, std::string const &msg, std::string icon="")
Definition: Console.cpp:97
RoR::GUI::RepositorySelector::handleRequest
virtual Ogre::WorkQueue::Response * handleRequest(const Ogre::WorkQueue::Request *req, const Ogre::WorkQueue *srcQ) override
Ogre::WorkQueue API.
Definition: GUI_RepositorySelector.cpp:1326
RoR::GUI::RepositorySelector::handleResponse
virtual void handleResponse(const Ogre::WorkQueue::Response *req, const Ogre::WorkQueue *srcQ) override
Processes task results on main thread.
Definition: GUI_RepositorySelector.cpp:1382
RoR::GUI::RepositorySelector::m_is_visible
bool m_is_visible
Definition: GUI_RepositorySelector.h:113
RoR::GUI::RepositorySelector::DrawThumbnail
void DrawThumbnail(int resource_item_idx)
Definition: GUI_RepositorySelector.cpp:1260
RoR::GUI::ResourceItem::download_count
int download_count
Definition: GUI_RepositorySelector.h:60
RoR::GUI::RepositorySelector::UpdateFiles
void UpdateFiles(ResourcesCollection *data)
Definition: GUI_RepositorySelector.cpp:1187
RoR::CurlFailInfo
Definition: Network.h:47
Language.h
RoR::GUI::ResourceFiles
Definition: GUI_RepositorySelector.h:72
RoR::Console::CONSOLE_SYSTEM_ERROR
@ CONSOLE_SYSTEM_ERROR
Definition: Console.h:52
GUIManager.h
RoR::GUI::RepositorySelector::~RepositorySelector
~RepositorySelector()
Definition: GUI_RepositorySelector.cpp:384
RGN_REPO
#define RGN_REPO
Definition: Application.h:47
RoR::GUI::ResourceItem::resource_id
int resource_id
Definition: GUI_RepositorySelector.h:54
RoR::CurlFailInfo::title
std::string title
Definition: Network.h:49
RoR::GUI::RepositorySelector::OpenResource
void OpenResource(int resource_id)
Definition: GUI_RepositorySelector.cpp:1201
RoR::MSG_GUI_DOWNLOAD_FINISHED
@ MSG_GUI_DOWNLOAD_FINISHED
Definition: Application.h:141
GetResources
void GetResources(std::string portal_url)
Definition: GUI_RepositorySelector.cpp:168
RoR::GUI::ResourcesCollection::items
std::vector< ResourceItem > items
Definition: GUI_RepositorySelector.h:81
RoR::GUIManager::GetTheme
GuiTheme & GetTheme()
Definition: GUIManager.h:154
RoR::GUI::RepositorySelector::m_repolist_msg
std::string m_repolist_msg
Definition: GUI_RepositorySelector.h:135
RoR::GUI::RepositorySelector::m_ogre_workqueue_channel
Ogre::uint16 m_ogre_workqueue_channel
Definition: GUI_RepositorySelector.h:127
RoR::GUI::RepositorySelector::SetVisible
void SetVisible(bool visible)
Definition: GUI_RepositorySelector.cpp:1238
RoR::PathCombine
std::string PathCombine(std::string a, std::string b)
Definition: PlatformUtils.h:48
RoR::GUI::RepositorySelector::m_selected_item
ResourceItem m_selected_item
Definition: GUI_RepositorySelector.h:126
RoR::GUI::RepositorySelector::Refresh
void Refresh()
Definition: GUI_RepositorySelector.cpp:1154
RoR::GUI::ResourceItem::resource_date
int resource_date
Definition: GUI_RepositorySelector.h:66
RoR::GUI::ResourceItem::resource_category_id
int resource_category_id
Definition: GUI_RepositorySelector.h:62
RoR::GUI::RepositorySelector::m_repolist_httpmsg
std::string m_repolist_httpmsg
Displayed as dimmed text.
Definition: GUI_RepositorySelector.h:138
RoR::App::sys_thumbnails_dir
CVar * sys_thumbnails_dir
Definition: Application.cpp:166
RoR::GameContext::PushMessage
void PushMessage(Message m)
Doesn't guarantee order! Use ChainMessage() if order matters.
Definition: GameContext.cpp:65
RoR::App::app_state
CVar * app_state
Definition: Application.cpp:79
RepoProgressContext
Definition: GUI_RepositorySelector.cpp:68
RoR::GUI::RepositorySelector::DownloadFinished
void DownloadFinished()
Definition: GUI_RepositorySelector.cpp:1221
RoR::GUI::ResourcesCollection
Definition: GUI_RepositorySelector.h:79
PlatformUtils.h
Platform-specific utilities. We use narrow UTF-8 encoded strings as paths. Inspired by http://utf8eve...
RoR::MSG_NET_REFRESH_REPOLIST_FAILURE
@ MSG_NET_REFRESH_REPOLIST_FAILURE
Payload = RoR::CurlFailInfo* (owner)
Definition: Application.h:110
CurlOgreDataStreamWriteFunc
static size_t CurlOgreDataStreamWriteFunc(char *data_ptr, size_t _unused, size_t data_length, void *userdata)
Definition: GUI_RepositorySelector.cpp:100
RoR::GUI::RepositorySelector::Download
void Download(int resource_id, std::string filename, int id)
Definition: GUI_RepositorySelector.cpp:1211
RoR::GUI::RepositorySelector::m_all_category_label
std::string m_all_category_label
Definition: GUI_RepositorySelector.h:119
Application.h
Central state/object manager and communications hub.
RoR::App::GetConsole
Console * GetConsole()
Definition: Application.cpp:270
RoR::MSG_NET_OPEN_RESOURCE_SUCCESS
@ MSG_NET_OPEN_RESOURCE_SUCCESS
Payload = GUI::ResourcesCollection* (owner)
Definition: Application.h:109
RoR::Message::payload
void * payload
Definition: GameContext.h:59
RoR::App::GetGameContext
GameContext * GetGameContext()
Definition: Application.cpp:280
RoR::GUI::RepositorySelector::UpdateResources
void UpdateResources(ResourcesCollection *data)
Definition: GUI_RepositorySelector.cpp:1166
RoR::MSG_NET_REFRESH_REPOLIST_SUCCESS
@ MSG_NET_REFRESH_REPOLIST_SUCCESS
Payload = GUI::ResourcesCollection* (owner)
Definition: Application.h:108
RoR::GUIManager::GuiTheme::no_entries_text_color
ImVec4 no_entries_text_color
Definition: GUIManager.h:71
RoR::GUI::RepositorySelector::m_repolist_msg_color
ImVec4 m_repolist_msg_color
Definition: GUI_RepositorySelector.h:136
RoRVersion.h
RoR::GUI::GameMainMenu::SetVisible
void SetVisible(bool v)
Definition: GUI_GameMainMenu.h:43
tl
This is a raw Ogre binding for Imgui No project cmake no just four source because If you re familiar with integration should be pretty straightforward tl
Definition: README-OgreImGui.txt:8
ROR_VERSION_STRING
const char *const ROR_VERSION_STRING
RoR::Message::description
std::string description
Definition: GameContext.h:58
_LC
#define _LC(ctx, str)
Definition: Language.h:42
RoR::GUI::RepositorySelector::m_update_cache
bool m_update_cache
Definition: GUI_RepositorySelector.h:121
GUI_RepositorySelector.h
RoR::GUI::ResourcesCollection::categories
std::vector< ResourceCategories > categories
Definition: GUI_RepositorySelector.h:82
RoR::GUI::RepositorySelector::m_search_input
Str< 500 > m_search_input
Definition: GUI_RepositorySelector.h:116
RoR::GUI::RepositorySelector::m_current_category_id
int m_current_category_id
Definition: GUI_RepositorySelector.h:118
RoR::GUI::RepositorySelector::m_current_category_label
std::string m_current_category_label
Definition: GUI_RepositorySelector.h:120
RoR::GUI::RepositorySelector::m_data
ResourcesCollection m_data
Definition: GUI_RepositorySelector.h:115
RoR::GetFileLastModifiedTime
std::time_t GetFileLastModifiedTime(std::string const &path)
Definition: PlatformUtils.cpp:238
RoR::GUI::ResourceItem
Definition: GUI_RepositorySelector.h:52
GetResourceCategories
std::vector< GUI::ResourceCategories > GetResourceCategories(std::string portal_url)
Definition: GUI_RepositorySelector.cpp:113
RoR::Message
Unified game event system - all requests and state changes are reported using a message.
Definition: GameContext.h:51
RoR::GUI::RepositorySelector::m_view_mode
std::string m_view_mode
Definition: GUI_RepositorySelector.h:124
_L
#define _L
Definition: ErrorUtils.cpp:34
RoR::GUI::RepositorySelector::m_current_sort
std::string m_current_sort
Definition: GUI_RepositorySelector.h:123
DownloadResourceFile
void DownloadResourceFile(int resource_id, std::string filename, int id)
Definition: GUI_RepositorySelector.cpp:314
RoR::GUI::ResourceItem::title
std::string title
Definition: GUI_RepositorySelector.h:55
RoR::MSG_APP_MODCACHE_UPDATE_REQUESTED
@ MSG_APP_MODCACHE_UPDATE_REQUESTED
Definition: Application.h:90
CurlWriteFunc
static size_t CurlWriteFunc(void *ptr, size_t size, size_t nmemb, std::string *data)
Definition: GUI_RepositorySelector.cpp:62
RoR::GUI::RepositorySelector::m_draw
bool m_draw
Definition: GUI_RepositorySelector.h:114
RoR::GUI::ResourceItem::last_update
int last_update
Definition: GUI_RepositorySelector.h:61
RoR::CurlFailInfo::curl_result
CURLcode curl_result
Definition: Network.h:50
RoR::Console::CONSOLE_MSGTYPE_INFO
@ CONSOLE_MSGTYPE_INFO
Generic message.
Definition: Console.h:60
RoR::GUIManager::GuiTheme::error_text_color
ImVec4 error_text_color
Definition: GUIManager.h:72
RoR::GUI::ResourceItem::view_count
int view_count
Definition: GUI_RepositorySelector.h:67
RepoProgressContext::filename
std::string filename
Definition: GUI_RepositorySelector.cpp:70
RoR::GUI::ResourcesCollection::files
std::vector< ResourceFiles > files
Definition: GUI_RepositorySelector.h:83
RoR::GUI::RepositorySelector::m_repolist_curlmsg
std::string m_repolist_curlmsg
Displayed as dimmed text.
Definition: GUI_RepositorySelector.h:137
RoR::GUI::RepositorySelector::m_repofiles_msg
std::string m_repofiles_msg
Definition: GUI_RepositorySelector.h:134
RoR::GUI::RepositorySelector::ShowError
void ShowError(CurlFailInfo *failinfo)
Definition: GUI_RepositorySelector.cpp:1226
RoR::LoadingIndicatorCircle
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:132
RoR
Definition: AppContext.h:36
x
float x
Definition: (ValueTypes) quaternion.h:5
RoR::Str::IsEmpty
bool IsEmpty() const
Definition: Str.h:47
RoR::GUI::ResourceItem::rating_avg
float rating_avg
Definition: GUI_RepositorySelector.h:63
RoR::GUIManager::GameMainMenu
GUI::GameMainMenu GameMainMenu
Definition: GUIManager.h:104
RoR::GUI::RepositorySelector::curl_th
CURL * curl_th
Definition: GUI_RepositorySelector.h:130
RoR::FileExists
bool FileExists(const char *path)
Path must be UTF-8 encoded.
Definition: PlatformUtils.cpp:163
GetResourceFiles
void GetResourceFiles(std::string portal_url, int resource_id)
Definition: GUI_RepositorySelector.cpp:256
RoR::App::remote_query_url
CVar * remote_query_url
Definition: Application.cpp:130
RoR::GUI::ResourceItem::version
std::string version
Definition: GUI_RepositorySelector.h:59