mirror of
https://github.com/Vita3K/Vita3K.git
synced 2026-07-11 01:34:23 +02:00
gui: Add Vita3K Network (V3KN) and cloud save manager.
- Add V3KN account management dialog wtih create/login/logout/delete account, change avatar/panel/password/online id/server, quota refresh. - Add show server error/success popup inside V3KN account management dialog. - Add Cloud screen to LiveArea with cloud button for upload/download savedata on V3KN - Add "Please wait" and "Connecting… Please wait." dialogs for network operations (upload/download, auth) - Add overwrite confirmation dialog and "Copying" progress dialogs for cloud/local saves. - Draw Cloud icons. util/net_utils: Some refactor and protocol improvements. - Add Upload file func with multipart/form-data support - Add keep-alive support for (download/upload)file. - Refactor get_web_response() to support POST bodies and return server-side errors - Refactor download_file() for stability and clearer error handling - Clean up duplicated logic and unify request handling.
This commit is contained in:
@@ -125,6 +125,8 @@ depending on its size and your hardware.</delete_app_description>
|
||||
<an_error_occurred>An error occurred.
|
||||
Error code: {}</an_error_occurred>
|
||||
<cancel>Cancel</cancel>
|
||||
<connecting_please_wait>Connecting...
|
||||
Please wait.</connecting_please_wait>
|
||||
<close>Close</close>
|
||||
<could_not_load>Could not load the file.</could_not_load>
|
||||
<could_not_save>Could not save the file.</could_not_save>
|
||||
@@ -140,6 +142,10 @@ Error code: {}</an_error_occurred>
|
||||
<select>Select</select>
|
||||
<submit>Submit</submit>
|
||||
<yes>Yes</yes>
|
||||
<could_not_connect_server>Could not connect to the server.</could_not_connect_server>
|
||||
<must_sign_in_vita3k_network>You must sign in to Vita3K Network to use this feature.</must_sign_in_vita3k_network>
|
||||
<could_not_connect_internet>Could not connect to the Internet.</could_not_connect_internet>
|
||||
<vita3k_network_connection_lost>The connection to Vita3K Network has been lost.</vita3k_network_connection_lost>
|
||||
<wday>
|
||||
<day>Sunday</day>
|
||||
<day>Monday</day>
|
||||
@@ -558,6 +564,27 @@ Check vita3k.log to see console output for details.
|
||||
3. If you want to install or boot Vitamin, it is not supported.</load_app_failed>
|
||||
</message>
|
||||
|
||||
<online_storage>
|
||||
<save_data_auto_upload>Saved Data Auto-Upload</save_data_auto_upload>
|
||||
<copying>Copying...</copying>
|
||||
<download>Download</download>
|
||||
<eligible>Eligible</eligible>
|
||||
<ineligible>Ineligible</ineligible>
|
||||
<location>Location</location>
|
||||
<memory_card>Memory Card</memory_card>
|
||||
<minutes_left>{} Minutes Left</minutes_left>
|
||||
<online_storage>Online Storage</online_storage>
|
||||
<overwrite_saved_data>Do you want to overwrite the saved data?</overwrite_saved_data>
|
||||
<ps_vita_system>PS Vita System</ps_vita_system>
|
||||
<ps_tv_system>PS TV System</ps_tv_system>
|
||||
<seconds_left>{} Seconds Left</seconds_left>
|
||||
<size>Size</size>
|
||||
<trophy_earning>Trophy Earning</trophy_earning>
|
||||
<updated>Updated</updated>
|
||||
<upload>Upload</upload>
|
||||
<uploaded>Uploaded</uploaded>
|
||||
</online_storage>
|
||||
|
||||
<overlay name="Overlay">
|
||||
<gamepad_overlay>Gamepad Overlay</gamepad_overlay>
|
||||
<enable_gamepad_overlay>Show gamepad overlay ingame</enable_gamepad_overlay>
|
||||
|
||||
@@ -93,6 +93,8 @@
|
||||
<an_error_occurred>Une erreur est survenue.
|
||||
Code d'erreur: {}</an_error_occurred>
|
||||
<cancel>Annuler</cancel>
|
||||
<connecting_please_wait>Connexion en cours...
|
||||
Merci de patienter.</connecting_please_wait>
|
||||
<close>Fermer</close>
|
||||
<could_not_load>Le chargement du fichier a échoué.</could_not_load>
|
||||
<could_not_save>La sauvegarde du fichier a échoué.</could_not_save>
|
||||
@@ -106,6 +108,10 @@ Code d'erreur: {}</an_error_occurred>
|
||||
<select_all>Sélectionner tout</select_all>
|
||||
<select>Sélectionner</select>
|
||||
<yes>Oui</yes>
|
||||
<could_not_connect_server>La connexion au serveur a échoué.</could_not_connect_server>
|
||||
<must_sign_in_vita3k_network>Vous devez vous connecter à Vita3K Network pour utiliser cette fonctionnalité.</must_sign_in_vita3k_network>
|
||||
<could_not_connect_internet>La connexion à Internet a échoué.</could_not_connect_internet>
|
||||
<vita3k_network_connection_lost>La connexion à Vita3K Network a été interrompue.</vita3k_network_connection_lost>
|
||||
<wday>
|
||||
<day>dimanche</day>
|
||||
<day>lundi</day>
|
||||
@@ -372,6 +378,27 @@ Appuyez sur 'Autoriser l'accès' pour continuer.</storage_file_permissions>
|
||||
</help>
|
||||
</live_area>
|
||||
|
||||
<online_storage>
|
||||
<save_data_auto_upload>Téléchargement en amont automatique des données sauvegardées</save_data_auto_upload>
|
||||
<copying>Copie en cours...</copying>
|
||||
<download>Télécharger</download>
|
||||
<eligible>Autorisé</eligible>
|
||||
<ineligible>Non autorisé</ineligible>
|
||||
<location>Emplacement</location>
|
||||
<memory_card>Carte mémoire</memory_card>
|
||||
<minutes_left>{} minutes restantes</minutes_left>
|
||||
<online_storage>Stockage en ligne</online_storage>
|
||||
<overwrite_saved_data>Écraser les données sauvegardées ?</overwrite_saved_data>
|
||||
<ps_vita_system>Système PS Vita</ps_vita_system>
|
||||
<ps_tv_system>Système PS TV</ps_tv_system>
|
||||
<seconds_left>{} secondes restantes</seconds_left>
|
||||
<size>Taille</size>
|
||||
<trophy_earning>Obtention de trophées</trophy_earning>
|
||||
<updated>Mis à jour</updated>
|
||||
<upload>Téléchargement en amont</upload>
|
||||
<uploaded>Téléchargée en amont</uploaded>
|
||||
</online_storage>
|
||||
|
||||
<settings name="Paramètres">
|
||||
<sound_display name="Son et affichage">
|
||||
<system_music>Musique système</system_music>
|
||||
|
||||
@@ -130,6 +130,7 @@ add_subdirectory(touch)
|
||||
add_subdirectory(util)
|
||||
add_subdirectory(gdbstub)
|
||||
add_subdirectory(packages)
|
||||
add_subdirectory(v3kn)
|
||||
add_subdirectory(vkutil)
|
||||
|
||||
if(ANDROID)
|
||||
|
||||
@@ -5,4 +5,4 @@ add_library(
|
||||
|
||||
target_include_directories(emuenv INTERFACE include)
|
||||
target_link_libraries(emuenv PUBLIC mem)
|
||||
target_link_libraries(emuenv PRIVATE audio camera config ctrl dialog display ime io kernel motion net ngs nids np regmgr renderer touch gdbstub packages http)
|
||||
target_link_libraries(emuenv PRIVATE audio camera config ctrl dialog display ime io kernel motion net ngs nids np regmgr renderer touch gdbstub packages http v3kn)
|
||||
|
||||
@@ -55,6 +55,7 @@ struct DialogState;
|
||||
struct Ime;
|
||||
struct License;
|
||||
struct RegMgrState;
|
||||
struct V3KNState;
|
||||
struct SfoFile;
|
||||
struct GDBState;
|
||||
struct HTTPState;
|
||||
@@ -101,6 +102,7 @@ private:
|
||||
std::unique_ptr<Ime> _ime;
|
||||
std::unique_ptr<License> _license;
|
||||
std::unique_ptr<RegMgrState> _regmgr;
|
||||
std::unique_ptr<V3KNState> _v3kn;
|
||||
std::unique_ptr<SfoFile> _sfo_handle;
|
||||
std::unique_ptr<GDBState> _gdb;
|
||||
std::unique_ptr<HTTPState> _http;
|
||||
@@ -167,6 +169,7 @@ public:
|
||||
Ime &ime;
|
||||
License &license;
|
||||
RegMgrState ®mgr;
|
||||
V3KNState &v3kn;
|
||||
SfoFile &sfo_handle;
|
||||
NIDSet missing_nids;
|
||||
float system_dpi_scale = 1.f;
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
#include <regmgr/state.h>
|
||||
#include <renderer/state.h>
|
||||
#include <touch/state.h>
|
||||
#include <v3kn/state.h>
|
||||
|
||||
#include <gdbstub/state.h>
|
||||
|
||||
@@ -85,6 +86,8 @@ EmuEnvState::EmuEnvState()
|
||||
, license(*_license)
|
||||
, _regmgr(new RegMgrState)
|
||||
, regmgr(*_regmgr)
|
||||
, _v3kn(new V3KNState)
|
||||
, v3kn(*_v3kn)
|
||||
, _sfo_handle(new SfoFile)
|
||||
, sfo_handle(*_sfo_handle)
|
||||
, _gdb(new GDBState)
|
||||
|
||||
@@ -26,6 +26,7 @@ set(GUI_SOURCES
|
||||
src/main_menubar.cpp
|
||||
src/manual.cpp
|
||||
src/mutexes_dialog.cpp
|
||||
src/online_storage.cpp
|
||||
src/perf_overlay.cpp
|
||||
src/pkg_install_dialog.cpp
|
||||
src/private.h
|
||||
@@ -44,6 +45,7 @@ set(GUI_SOURCES
|
||||
src/disassembly_dialog.cpp
|
||||
src/trophy_unlocked.cpp
|
||||
src/about_dialog.cpp
|
||||
src/v3kn_dialog.cpp
|
||||
src/vita3k_update.cpp
|
||||
src/welcome_dialog.cpp
|
||||
)
|
||||
@@ -59,7 +61,7 @@ endif()
|
||||
add_library(gui STATIC ${GUI_SOURCES})
|
||||
|
||||
target_include_directories(gui PUBLIC include ${CMAKE_SOURCE_DIR}/vita3k)
|
||||
target_link_libraries(gui PUBLIC app bgm_player compat config dialog emuenv ime imgui lang regmgr np)
|
||||
target_link_libraries(gui PUBLIC app bgm_player compat config dialog emuenv ime imgui lang regmgr np v3kn)
|
||||
target_link_libraries(gui PRIVATE audio camera cppcommon ctrl kernel miniz motion psvpfsparser pugixml::pugixml stb renderer packages SDL3::SDL3 touch vkutil host::dialog concurrentqueue)
|
||||
# Link macOS frameworks for Objective-C++ code
|
||||
if(APPLE)
|
||||
|
||||
@@ -120,10 +120,13 @@ struct VitaAreaState {
|
||||
bool information_bar = false;
|
||||
bool live_area_screen = false;
|
||||
bool manual = false;
|
||||
bool online_storage = false;
|
||||
bool settings = false;
|
||||
bool start_screen = false;
|
||||
bool trophy_collection = false;
|
||||
bool user_management = false;
|
||||
bool connecting_please_wait = false;
|
||||
bool please_wait = false;
|
||||
};
|
||||
|
||||
struct FileMenuState {
|
||||
@@ -150,6 +153,7 @@ struct DebugMenuState {
|
||||
struct ConfigurationMenuState {
|
||||
bool custom_settings_dialog = false;
|
||||
bool settings_dialog = false;
|
||||
bool v3kn_dialog = false;
|
||||
};
|
||||
|
||||
struct ControlMenuState {
|
||||
@@ -181,6 +185,18 @@ enum DateTime {
|
||||
HOUR,
|
||||
};
|
||||
|
||||
enum class AvatarSize : uint32_t {
|
||||
SMALL = 34,
|
||||
MEDIUM = 130,
|
||||
LARGE = 160,
|
||||
V3KN = 80
|
||||
};
|
||||
|
||||
struct AvatarInfo {
|
||||
ImVec2 pos;
|
||||
ImVec2 size;
|
||||
};
|
||||
|
||||
struct User {
|
||||
std::string id;
|
||||
std::string name = "Vita3K";
|
||||
@@ -287,6 +303,10 @@ struct GuiState {
|
||||
|
||||
std::map<std::string, User> users;
|
||||
std::map<std::string, ImGui_Texture> users_avatar;
|
||||
std::map<std::string, std::map<AvatarSize, AvatarInfo>> users_avatar_infos;
|
||||
|
||||
ImGui_Texture v3kn_avatar;
|
||||
ImGui_Texture v3kn_panel;
|
||||
|
||||
MemoryEditor memory_editor;
|
||||
MemoryEditor gxp_shader_editor;
|
||||
|
||||
@@ -33,6 +33,8 @@
|
||||
#include <util/safe_time.h>
|
||||
#include <util/string_utils.h>
|
||||
|
||||
#include <v3kn/storage.h>
|
||||
|
||||
#include <SDL3/SDL_cpuinfo.h>
|
||||
#include <SDL3/SDL_misc.h>
|
||||
#undef main
|
||||
@@ -879,6 +881,8 @@ void draw_app_context_menu(GuiState &gui, EmuEnvState &emuenv, const std::string
|
||||
open_manual(gui, emuenv, app_path);
|
||||
if (ImGui::MenuItem(gui.lang.home_screen["refresh"].c_str()))
|
||||
refresh_app(gui, emuenv, app_path);
|
||||
if (title_id.starts_with("PCS") && ImGui::MenuItem(gui.lang.online_storage["online_storage"].c_str()))
|
||||
v3kn::open_online_storage(gui, emuenv, title_id);
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
if (ImGui::BeginMenu(common["delete"].c_str())) {
|
||||
|
||||
@@ -463,6 +463,54 @@ void init_app_background(GuiState &gui, EmuEnvState &emuenv, const std::string &
|
||||
stbi_image_free(data);
|
||||
}
|
||||
|
||||
static void draw_connecting_please_wait(GuiState &gui, EmuEnvState &emuenv) {
|
||||
const ImVec2 VIEWPORT_SIZE(emuenv.logical_viewport_size.x, emuenv.logical_viewport_size.y);
|
||||
const ImVec2 VIEWPORT_POS(emuenv.logical_viewport_pos.x, emuenv.logical_viewport_pos.y);
|
||||
const auto RES_SCALE = ImVec2(emuenv.gui_scale.x, emuenv.gui_scale.y);
|
||||
const auto SCALE = ImVec2(RES_SCALE.x * emuenv.manual_dpi_scale, RES_SCALE.y * emuenv.manual_dpi_scale);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.f);
|
||||
ImGui::SetNextWindowPos(VIEWPORT_POS);
|
||||
ImGui::SetNextWindowSize(VIEWPORT_SIZE);
|
||||
ImGui::Begin("please_wait", &gui.vita_area.connecting_please_wait, ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoDecoration);
|
||||
const ImVec2 WINDOW_SIZE(520.f * SCALE.x, 120.f * SCALE.y);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 15.f * SCALE.x);
|
||||
ImGui::SetNextWindowPos(ImVec2(VIEWPORT_POS.x + ((VIEWPORT_SIZE.x - WINDOW_SIZE.x) / 2.f), VIEWPORT_POS.y + ((VIEWPORT_SIZE.y - WINDOW_SIZE.y) / 2.f)));
|
||||
ImGui::BeginChild("please_wait_child", WINDOW_SIZE, ImGuiChildFlags_Borders, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings);
|
||||
const auto text = emuenv.common_dialog.lang.common["connecting_please_wait"];
|
||||
const auto text_size = ImGui::CalcTextSize(text.c_str());
|
||||
ImGui::SetCursorPos(ImVec2((WINDOW_SIZE.x - text_size.x) / 2.f, (WINDOW_SIZE.y - text_size.y) / 2.f));
|
||||
ImGui::Text("%s", text.c_str());
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::End();
|
||||
ImGui::PopStyleVar(2);
|
||||
}
|
||||
|
||||
static void draw_please_wait(GuiState &gui, EmuEnvState &emuenv) {
|
||||
const ImVec2 VIEWPORT_SIZE(emuenv.logical_viewport_size.x, emuenv.logical_viewport_size.y);
|
||||
const ImVec2 VIEWPORT_POS(emuenv.logical_viewport_pos.x, emuenv.logical_viewport_pos.y);
|
||||
const auto RES_SCALE = ImVec2(emuenv.gui_scale.x, emuenv.gui_scale.y);
|
||||
const auto SCALE = ImVec2(RES_SCALE.x * emuenv.manual_dpi_scale, RES_SCALE.y * emuenv.manual_dpi_scale);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.f);
|
||||
ImGui::SetNextWindowPos(VIEWPORT_POS);
|
||||
ImGui::SetNextWindowSize(VIEWPORT_SIZE);
|
||||
ImGui::Begin("please_wait", &gui.vita_area.please_wait, ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoDecoration);
|
||||
const ImVec2 WINDOW_SIZE(520.f * SCALE.x, 120.f * SCALE.y);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 15.f * SCALE.x);
|
||||
ImGui::SetNextWindowPos(ImVec2(VIEWPORT_POS.x + ((VIEWPORT_SIZE.x - WINDOW_SIZE.x) / 2.f), VIEWPORT_POS.y + ((VIEWPORT_SIZE.y - WINDOW_SIZE.y) / 2.f)));
|
||||
ImGui::BeginChild("please_wait_child", WINDOW_SIZE, ImGuiChildFlags_Borders, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings);
|
||||
const auto text = emuenv.common_dialog.lang.common["please_wait"];
|
||||
const auto text_size = ImGui::CalcTextSize(text.c_str());
|
||||
ImGui::SetCursorPos(ImVec2((WINDOW_SIZE.x - text_size.x) / 2.f, (WINDOW_SIZE.y - text_size.y) / 2.f));
|
||||
ImGui::Text("%s", text.c_str());
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::End();
|
||||
ImGui::PopStyleVar(2);
|
||||
}
|
||||
|
||||
std::string get_sys_lang_name(uint32_t lang_id) {
|
||||
const auto current_sys_lang = std::find_if(LIST_SYS_LANG.begin(), LIST_SYS_LANG.end(), [&](const auto &l) {
|
||||
return l.first == lang_id;
|
||||
@@ -927,14 +975,21 @@ void draw_vita_area(GuiState &gui, EmuEnvState &emuenv) {
|
||||
if (gui.vita_area.app_close)
|
||||
draw_app_close(gui, emuenv);
|
||||
|
||||
if (gui.vita_area.connecting_please_wait)
|
||||
draw_connecting_please_wait(gui, emuenv);
|
||||
|
||||
if (gui.vita_area.home_screen)
|
||||
draw_home_screen(gui, emuenv);
|
||||
|
||||
if (gui.vita_area.live_area_screen)
|
||||
draw_live_area_screen(gui, emuenv);
|
||||
|
||||
if (gui.vita_area.manual)
|
||||
draw_manual(gui, emuenv);
|
||||
|
||||
if (gui.vita_area.please_wait)
|
||||
draw_please_wait(gui, emuenv);
|
||||
|
||||
// Draw install dialogs
|
||||
if (gui.file_menu.archive_install_dialog)
|
||||
draw_archive_install_dialog(gui, emuenv);
|
||||
@@ -961,6 +1016,9 @@ void draw_vita_area(GuiState &gui, EmuEnvState &emuenv) {
|
||||
if (gui.vita_area.content_manager)
|
||||
draw_content_manager(gui, emuenv);
|
||||
|
||||
if (gui.vita_area.online_storage)
|
||||
draw_online_storage(gui, emuenv);
|
||||
|
||||
if (gui.vita_area.settings)
|
||||
draw_settings(gui, emuenv);
|
||||
|
||||
@@ -985,6 +1043,9 @@ void draw_ui(GuiState &gui, EmuEnvState &emuenv) {
|
||||
if ((gui.vita_area.home_screen || !emuenv.io.app_path.empty()) && get_sys_apps_state(gui) && !gui.vita_area.live_area_screen && !gui.vita_area.user_management && (!emuenv.cfg.show_info_bar || !gui.vita_area.information_bar))
|
||||
draw_main_menu_bar(gui, emuenv);
|
||||
|
||||
if (gui.configuration_menu.v3kn_dialog)
|
||||
draw_v3kn_dialog(gui, emuenv);
|
||||
|
||||
if (gui.configuration_menu.custom_settings_dialog || gui.configuration_menu.settings_dialog)
|
||||
draw_settings_dialog(gui, emuenv);
|
||||
|
||||
|
||||
@@ -35,9 +35,12 @@
|
||||
|
||||
#include <util/log.h>
|
||||
|
||||
#include <v3kn/storage.h>
|
||||
|
||||
#include <pugixml.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <imgui_internal.h>
|
||||
#include <stb_image.h>
|
||||
|
||||
@@ -66,7 +69,7 @@ void GateAnimation::update() {
|
||||
namespace gui {
|
||||
|
||||
bool get_sys_apps_state(GuiState &gui) {
|
||||
return !gui.vita_area.content_manager && !gui.vita_area.settings && !gui.vita_area.trophy_collection && !gui.vita_area.manual;
|
||||
return !gui.vita_area.content_manager && !gui.vita_area.online_storage && !gui.vita_area.settings && !gui.vita_area.trophy_collection && !gui.vita_area.manual;
|
||||
}
|
||||
|
||||
struct FRAME {
|
||||
@@ -577,7 +580,8 @@ enum LiveAreaType {
|
||||
GATE,
|
||||
SEARCH,
|
||||
MANUAL,
|
||||
REFRESH
|
||||
REFRESH,
|
||||
CLOUD
|
||||
};
|
||||
|
||||
struct ButtonEntry {
|
||||
@@ -912,6 +916,7 @@ void draw_live_area_screen(GuiState &gui, EmuEnvState &emuenv) {
|
||||
const ImVec2 background_pos_max(background_pos_min.x + bg_scal_size.x, background_pos_min.y + bg_scal_size.y);
|
||||
DRAW_LIST->AddImage(gui.live_items[app_path][frame.id]["background"][current_item[app_path][frame.id]], background_pos_min, background_pos_max);
|
||||
}
|
||||
|
||||
if (gui.live_items[app_path][frame.id].contains("image")) {
|
||||
const auto image_pos_min = set_screen_pos(img_pos);
|
||||
img_scal_size = apply_zoom_size(img_scal_size);
|
||||
@@ -1238,6 +1243,155 @@ void draw_live_area_screen(GuiState &gui, EmuEnvState &emuenv) {
|
||||
}
|
||||
|
||||
if (app_device == VitaIoDevice::ux0) {
|
||||
const auto rotate_point = [](const ImVec2 &point, const ImVec2 ¢er, float cos_angle, float sin_angle) -> ImVec2 {
|
||||
const ImVec2 offset(point.x - center.x, point.y - center.y);
|
||||
return ImVec2(
|
||||
center.x + (offset.x * cos_angle) - (offset.y * sin_angle),
|
||||
center.y + (offset.x * sin_angle) + (offset.y * cos_angle));
|
||||
};
|
||||
|
||||
const auto draw_cloud_icon = [&](ImDrawList *draw_list, const ImVec2 &pos_min, const ImVec2 &size) {
|
||||
const float BG_ROUNDING = 12.0f * SCALE.x;
|
||||
const float BORDER_THICKNESS = 1.5f * SCALE.x;
|
||||
const float ANGLE = -0.24f;
|
||||
const float COS_ANGLE = std::cos(ANGLE);
|
||||
const float SIN_ANGLE = std::sin(ANGLE);
|
||||
const float BRANCH_SQUARE = size.x * 0.225f;
|
||||
const float HALF_SQUARE = BRANCH_SQUARE * 0.50f;
|
||||
const float HALF_EXTENT = BRANCH_SQUARE * 1.50f;
|
||||
const float SYMBOL_STROKE = 2.0f * SCALE.x;
|
||||
const float SYMBOL_TRIANGLE_RADIUS = size.x * 0.053f;
|
||||
const float SYMBOL_SQUARE_RADIUS = size.x * 0.051f;
|
||||
const float SYMBOL_CIRCLE_RADIUS = size.x * 0.052f;
|
||||
const float SYMBOL_CROSS_RADIUS = size.x * 0.047f;
|
||||
const ImVec2 POS_MAX(pos_min.x + size.x, pos_min.y + size.y);
|
||||
const ImVec2 CENTER(pos_min.x + size.x * 0.53f, pos_min.y + size.y * 0.44f);
|
||||
const ImVec2 DEPTH(-5.6f * SCALE.x, 6.4f * SCALE.y);
|
||||
const ImU32 BG_COLOR = IM_COL32(221, 221, 221, 255);
|
||||
const ImU32 BORDER_COLOR = IM_COL32(192, 193, 196, 255);
|
||||
const ImU32 FRONT_COLOR = IM_COL32(255, 226, 86, 255);
|
||||
const ImU32 OUTLINE_COLOR = IM_COL32(220, 164, 20, 255);
|
||||
const ImU32 SYMBOL_COLOR = IM_COL32(158, 103, 6, 255);
|
||||
const ImVec2 RAW[12] = {
|
||||
ImVec2(-HALF_SQUARE, -HALF_EXTENT), ImVec2(HALF_SQUARE, -HALF_EXTENT),
|
||||
ImVec2(HALF_SQUARE, -HALF_SQUARE), ImVec2(HALF_EXTENT, -HALF_SQUARE),
|
||||
ImVec2(HALF_EXTENT, HALF_SQUARE), ImVec2(HALF_SQUARE, HALF_SQUARE),
|
||||
ImVec2(HALF_SQUARE, HALF_EXTENT), ImVec2(-HALF_SQUARE, HALF_EXTENT),
|
||||
ImVec2(-HALF_SQUARE, HALF_SQUARE), ImVec2(-HALF_EXTENT, HALF_SQUARE),
|
||||
ImVec2(-HALF_EXTENT, -HALF_SQUARE), ImVec2(-HALF_SQUARE, -HALF_SQUARE)
|
||||
};
|
||||
const ImVec2 FRONT_SQUARES[5] = {
|
||||
ImVec2(0.0f, 0.0f),
|
||||
ImVec2(0.0f, -BRANCH_SQUARE),
|
||||
ImVec2(-BRANCH_SQUARE, 0.0f),
|
||||
ImVec2(BRANCH_SQUARE, 0.0f),
|
||||
ImVec2(0.0f, BRANCH_SQUARE)
|
||||
};
|
||||
struct FaceDef {
|
||||
int first;
|
||||
int second;
|
||||
ImU32 color;
|
||||
ImVec2 offset_scale;
|
||||
};
|
||||
const FaceDef FACES[] = {
|
||||
{ 4, 5, IM_COL32(220, 160, 52, 255), ImVec2(0.22f, 0.88f) },
|
||||
{ 6, 7, IM_COL32(205, 145, 36, 255), ImVec2(1.00f, 1.00f) },
|
||||
{ 7, 8, IM_COL32(212, 151, 42, 255), ImVec2(1.00f, 1.00f) },
|
||||
{ 8, 9, IM_COL32(219, 158, 48, 255), ImVec2(1.00f, 1.00f) },
|
||||
{ 9, 10, IM_COL32(214, 153, 44, 255), ImVec2(1.00f, 1.00f) },
|
||||
{ 10, 11, IM_COL32(208, 148, 40, 255), ImVec2(0.92f, 0.92f) },
|
||||
{ 11, 0, IM_COL32(198, 140, 34, 255), ImVec2(0.70f, 0.78f) }
|
||||
};
|
||||
|
||||
draw_list->AddRectFilled(pos_min, POS_MAX, BG_COLOR, BG_ROUNDING, ImDrawFlags_RoundCornersBottom);
|
||||
draw_list->AddRect(pos_min, POS_MAX, BORDER_COLOR, BG_ROUNDING, ImDrawFlags_RoundCornersBottom, BORDER_THICKNESS);
|
||||
|
||||
const auto to_world = [&](const ImVec2 &point) -> ImVec2 {
|
||||
return ImVec2(CENTER.x + point.x * COS_ANGLE - point.y * SIN_ANGLE, CENTER.y + point.x * SIN_ANGLE + point.y * COS_ANGLE);
|
||||
};
|
||||
const auto rotate_symbol_point = [&](const ImVec2 &point, const ImVec2 &pivot) -> ImVec2 {
|
||||
return rotate_point(point, pivot, COS_ANGLE, SIN_ANGLE);
|
||||
};
|
||||
const auto make_vec2 = [](float x, float y) -> ImVec2 {
|
||||
ImVec2 value;
|
||||
value.x = x;
|
||||
value.y = y;
|
||||
return value;
|
||||
};
|
||||
const auto draw_face = [&](int first, int second, ImU32 color, const ImVec2 &offset_scale) {
|
||||
const ImVec2 offset = make_vec2(DEPTH.x * offset_scale.x, DEPTH.y * offset_scale.y);
|
||||
const ImVec2 first_world = to_world(RAW[first]);
|
||||
const ImVec2 second_world = to_world(RAW[second]);
|
||||
const ImVec2 quad[4] = {
|
||||
make_vec2(first_world.x, first_world.y),
|
||||
make_vec2(second_world.x, second_world.y),
|
||||
make_vec2(second_world.x + offset.x, second_world.y + offset.y),
|
||||
make_vec2(first_world.x + offset.x, first_world.y + offset.y)
|
||||
};
|
||||
draw_list->AddConvexPolyFilled(quad, IM_ARRAYSIZE(quad), color);
|
||||
};
|
||||
const auto draw_square = [&](const ImVec2 &square_center, ImU32 color) {
|
||||
const ImVec2 quad[4] = {
|
||||
to_world(ImVec2(square_center.x - HALF_SQUARE, square_center.y - HALF_SQUARE)),
|
||||
to_world(ImVec2(square_center.x + HALF_SQUARE, square_center.y - HALF_SQUARE)),
|
||||
to_world(ImVec2(square_center.x + HALF_SQUARE, square_center.y + HALF_SQUARE)),
|
||||
to_world(ImVec2(square_center.x - HALF_SQUARE, square_center.y + HALF_SQUARE))
|
||||
};
|
||||
draw_list->AddConvexPolyFilled(quad, IM_ARRAYSIZE(quad), color);
|
||||
};
|
||||
const auto symbol_center = [&](const ImVec2 &offset) -> ImVec2 {
|
||||
return to_world(offset);
|
||||
};
|
||||
|
||||
for (const auto &face : FACES)
|
||||
draw_face(face.first, face.second, face.color, face.offset_scale);
|
||||
|
||||
for (const auto &square_center : FRONT_SQUARES)
|
||||
draw_square(square_center, FRONT_COLOR);
|
||||
|
||||
ImVec2 TOP[12];
|
||||
for (int i = 0; i < IM_ARRAYSIZE(RAW); ++i)
|
||||
TOP[i] = to_world(RAW[i]);
|
||||
draw_list->AddPolyline(TOP, IM_ARRAYSIZE(TOP), OUTLINE_COLOR, ImDrawFlags_Closed, 1.8f * SCALE.x);
|
||||
|
||||
{
|
||||
const ImVec2 TRI_CENTER = symbol_center(ImVec2(0.0f, -BRANCH_SQUARE));
|
||||
ImVec2 tri[3] = {
|
||||
ImVec2(TRI_CENTER.x, TRI_CENTER.y - SYMBOL_TRIANGLE_RADIUS),
|
||||
ImVec2(TRI_CENTER.x - (SYMBOL_TRIANGLE_RADIUS * 0.87f), TRI_CENTER.y + (SYMBOL_TRIANGLE_RADIUS * 0.50f)),
|
||||
ImVec2(TRI_CENTER.x + (SYMBOL_TRIANGLE_RADIUS * 0.87f), TRI_CENTER.y + (SYMBOL_TRIANGLE_RADIUS * 0.50f))
|
||||
};
|
||||
for (auto &point : tri)
|
||||
point = rotate_symbol_point(point, TRI_CENTER);
|
||||
draw_list->AddPolyline(tri, IM_ARRAYSIZE(tri), SYMBOL_COLOR, ImDrawFlags_Closed, SYMBOL_STROKE);
|
||||
}
|
||||
|
||||
{
|
||||
const ImVec2 SQUARE_CENTER = symbol_center(ImVec2(-BRANCH_SQUARE, 0.0f));
|
||||
ImVec2 square[4] = {
|
||||
ImVec2(SQUARE_CENTER.x - SYMBOL_SQUARE_RADIUS, SQUARE_CENTER.y - SYMBOL_SQUARE_RADIUS),
|
||||
ImVec2(SQUARE_CENTER.x + SYMBOL_SQUARE_RADIUS, SQUARE_CENTER.y - SYMBOL_SQUARE_RADIUS),
|
||||
ImVec2(SQUARE_CENTER.x + SYMBOL_SQUARE_RADIUS, SQUARE_CENTER.y + SYMBOL_SQUARE_RADIUS),
|
||||
ImVec2(SQUARE_CENTER.x - SYMBOL_SQUARE_RADIUS, SQUARE_CENTER.y + SYMBOL_SQUARE_RADIUS)
|
||||
};
|
||||
for (auto &point : square)
|
||||
point = rotate_symbol_point(point, SQUARE_CENTER);
|
||||
draw_list->AddPolyline(square, IM_ARRAYSIZE(square), SYMBOL_COLOR, ImDrawFlags_Closed, SYMBOL_STROKE);
|
||||
}
|
||||
|
||||
draw_list->AddCircle(symbol_center(ImVec2(BRANCH_SQUARE, 0.0f)), SYMBOL_CIRCLE_RADIUS, SYMBOL_COLOR, 0, SYMBOL_STROKE);
|
||||
|
||||
{
|
||||
const ImVec2 CROSS_CENTER = symbol_center(ImVec2(0.0f, BRANCH_SQUARE));
|
||||
const ImVec2 LINE_A = rotate_symbol_point(ImVec2(CROSS_CENTER.x - SYMBOL_CROSS_RADIUS, CROSS_CENTER.y - SYMBOL_CROSS_RADIUS), CROSS_CENTER);
|
||||
const ImVec2 LINE_B = rotate_symbol_point(ImVec2(CROSS_CENTER.x + SYMBOL_CROSS_RADIUS, CROSS_CENTER.y + SYMBOL_CROSS_RADIUS), CROSS_CENTER);
|
||||
const ImVec2 LINE_C = rotate_symbol_point(ImVec2(CROSS_CENTER.x - SYMBOL_CROSS_RADIUS, CROSS_CENTER.y + SYMBOL_CROSS_RADIUS), CROSS_CENTER);
|
||||
const ImVec2 LINE_D = rotate_symbol_point(ImVec2(CROSS_CENTER.x + SYMBOL_CROSS_RADIUS, CROSS_CENTER.y - SYMBOL_CROSS_RADIUS), CROSS_CENTER);
|
||||
draw_list->AddLine(LINE_A, LINE_B, SYMBOL_COLOR, SYMBOL_STROKE);
|
||||
draw_list->AddLine(LINE_C, LINE_D, SYMBOL_COLOR, SYMBOL_STROKE);
|
||||
}
|
||||
};
|
||||
|
||||
auto APP_INDEX = get_app_index(gui, app_path);
|
||||
|
||||
const auto widget_scal_size = apply_zoom_size(ImVec2(70.0f * SCALE.x, 70.f * SCALE.y));
|
||||
@@ -1251,7 +1405,9 @@ void draw_live_area_screen(GuiState &gui, EmuEnvState &emuenv) {
|
||||
if (manual_exist)
|
||||
buttons.emplace_back("Manual", MANUAL, [&]() { open_manual(gui, emuenv, app_path); });
|
||||
buttons.emplace_back("Refresh", REFRESH, [&]() { refresh_app(gui, emuenv, app_path); });
|
||||
|
||||
if (app_path.find("PCS") != std::string::npos) {
|
||||
buttons.emplace_back("Cloud", CLOUD, [&]() { v3kn::open_online_storage(gui, emuenv, APP_INDEX->title_id); });
|
||||
}
|
||||
const auto BUTTONS_COUNT = static_cast<int>(buttons.size());
|
||||
const float spacing = 42.0f * SCALE.x;
|
||||
const ImVec2 BUTTONS_POS = ImVec2(
|
||||
@@ -1273,8 +1429,12 @@ void draw_live_area_screen(GuiState &gui, EmuEnvState &emuenv) {
|
||||
if (TYPE == MANUAL)
|
||||
WIDGET_COLOR = IM_COL32(202, 0, 106, 255);
|
||||
|
||||
DRAW_LIST->AddRectFilled(WIDGET_POS_MIN, ImVec2(WIDGET_POS_MIN.x + widget_scal_size.x, WIDGET_POS_MIN.y + widget_scal_size.y), WIDGET_COLOR, 12.0f * SCALE.x, ImDrawFlags_RoundCornersAll);
|
||||
DRAW_LIST->AddText(gui.vita_font[emuenv.current_font_level], widget_font_size, WIDGET_STR_POS, IM_COL32(255, 255, 255, 255), WIDGET_STR);
|
||||
if (TYPE == CLOUD)
|
||||
draw_cloud_icon(DRAW_LIST, WIDGET_POS_MIN, widget_scal_size);
|
||||
else {
|
||||
DRAW_LIST->AddRectFilled(WIDGET_POS_MIN, ImVec2(WIDGET_POS_MIN.x + widget_scal_size.x, WIDGET_POS_MIN.y + widget_scal_size.y), WIDGET_COLOR, 12.0f * SCALE.x, ImDrawFlags_RoundCornersAll);
|
||||
DRAW_LIST->AddText(gui.vita_font[emuenv.current_font_level], widget_font_size, WIDGET_STR_POS, IM_COL32(255, 255, 255, 255), WIDGET_STR);
|
||||
}
|
||||
ImGui::SetCursorPos(button_pos_scal);
|
||||
if (!is_current_animated) {
|
||||
if (ImGui::Selectable(("##" + TITLE).c_str(), gui.is_nav_button && (live_area_type_selected == TYPE), ImGuiSelectableFlags_None, widget_scal_size))
|
||||
|
||||
@@ -79,7 +79,7 @@ static void draw_emulation_menu(GuiState &gui, EmuEnvState &emuenv) {
|
||||
}
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_SelectableTextAlign, ImVec2(0.5f, 0.5f));
|
||||
if (ImGui::Selectable(app.title.c_str(), false, ImGuiSelectableFlags_SpanAllColumns, ImVec2(0, ICON_SIZE.y)))
|
||||
pre_load_app(gui, emuenv, emuenv.cfg.show_live_area_screen, app.title_id);
|
||||
pre_load_app(gui, emuenv, emuenv.cfg.show_live_area_screen, app.path);
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PopID();
|
||||
ImGui::PopStyleColor();
|
||||
@@ -135,6 +135,7 @@ static void draw_config_menu(GuiState &gui, EmuEnvState &emuenv) {
|
||||
init_config(gui, emuenv, emuenv.io.app_path);
|
||||
if (ImGui::MenuItem(lang["user_management"].c_str(), nullptr, &gui.vita_area.user_management, (!gui.vita_area.user_management && emuenv.io.title_id.empty())))
|
||||
init_user_management(gui, emuenv);
|
||||
ImGui::MenuItem("Vita3K Network", nullptr, &gui.configuration_menu.v3kn_dialog);
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,366 @@
|
||||
// Vita3K emulator project
|
||||
// Copyright (C) 2026 Vita3K team
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation; either version 2 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along
|
||||
// with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
#include "private.h"
|
||||
|
||||
#include <v3kn/account.h>
|
||||
#include <v3kn/state.h>
|
||||
#include <v3kn/storage.h>
|
||||
|
||||
#include <config/state.h>
|
||||
#include <dialog/state.h>
|
||||
|
||||
#include <gui/functions.h>
|
||||
#include <util/safe_time.h>
|
||||
|
||||
namespace gui {
|
||||
|
||||
static std::string get_remaining_str(LangState &lang, const uint64_t remaining) {
|
||||
if (remaining > 60)
|
||||
return fmt::format(fmt::runtime(lang.online_storage["minutes_left"]), remaining / 60);
|
||||
else
|
||||
return fmt::format(fmt::runtime(lang.online_storage["seconds_left"]), remaining);
|
||||
}
|
||||
|
||||
void draw_online_storage(GuiState &gui, EmuEnvState &emuenv) {
|
||||
const ImVec2 VIEWPORT_SIZE(emuenv.logical_viewport_size.x, emuenv.logical_viewport_size.y);
|
||||
const ImVec2 VIEWPORT_POS(emuenv.logical_viewport_pos.x, emuenv.logical_viewport_pos.y);
|
||||
const auto RES_SCALE = ImVec2(emuenv.gui_scale.x, emuenv.gui_scale.y);
|
||||
const auto SCALE = ImVec2(RES_SCALE.x * emuenv.manual_dpi_scale, RES_SCALE.y * emuenv.manual_dpi_scale);
|
||||
const auto INDICATOR_SIZE = ImVec2(VIEWPORT_SIZE.x, 32.f * SCALE.y);
|
||||
const ImVec2 WINDOW_SIZE(VIEWPORT_SIZE.x, VIEWPORT_SIZE.y - INDICATOR_SIZE.y);
|
||||
const ImVec2 WINDOW_POS(VIEWPORT_POS.x, VIEWPORT_POS.y + INDICATOR_SIZE.y);
|
||||
|
||||
const auto &app_path = emuenv.cfg.show_live_area_screen && (gui.live_area_app_current_open >= 0) ? gui.live_area_current_open_apps_list[gui.live_area_app_current_open] : emuenv.app_path;
|
||||
const auto APP_INDEX = get_app_index(gui, app_path);
|
||||
const auto &app_titleid = APP_INDEX->title_id;
|
||||
const auto &app_title = APP_INDEX->title;
|
||||
const auto &app_icon = gui.app_selector.user_apps_icon[app_path];
|
||||
|
||||
auto &storage_state = emuenv.v3kn.storage_state;
|
||||
auto &lang = gui.lang.online_storage;
|
||||
auto &common_lang = emuenv.common_dialog.lang.common;
|
||||
const auto is_12_hour_format = emuenv.cfg.sys_time_format == SCE_SYSTEM_PARAM_TIME_FORMAT_12HOUR;
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.f);
|
||||
ImGui::SetNextWindowPos(WINDOW_POS, ImGuiCond_Always);
|
||||
ImGui::SetNextWindowSize(WINDOW_SIZE, ImGuiCond_Always);
|
||||
ImGui::Begin("##cloud_save", &gui.vita_area.online_storage, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoSavedSettings);
|
||||
|
||||
const auto &draw_list = ImGui::GetWindowDrawList();
|
||||
draw_list->AddRectFilled(ImVec2(VIEWPORT_POS.x, VIEWPORT_POS.y + INDICATOR_SIZE.y), ImVec2(VIEWPORT_POS.x + VIEWPORT_SIZE.x, VIEWPORT_POS.y + VIEWPORT_SIZE.y + INDICATOR_SIZE.y), IM_COL32(0, 55, 145, 255));
|
||||
|
||||
const ImVec2 CLOSE_BUTTON_SIZE(46.f * SCALE.x, 46.f * SCALE.y);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 5.f * SCALE.x);
|
||||
|
||||
ImGui::SetCursorPos(ImVec2(10.f * SCALE.x, 10.f * SCALE.y));
|
||||
if (ImGui::Button("X", CLOSE_BUTTON_SIZE)) {
|
||||
gui.vita_area.online_storage = false;
|
||||
if (gui.live_area_app_current_open >= 0)
|
||||
gui.vita_area.live_area_screen = true;
|
||||
else
|
||||
gui.vita_area.home_screen = true;
|
||||
}
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
ImGui::SetCursorPosX((WINDOW_SIZE.x - CLOSE_BUTTON_SIZE.x) / 2.f);
|
||||
ImGui::SetWindowFontScale(1.85f * RES_SCALE.y);
|
||||
const std::string window_title = lang["online_storage"];
|
||||
const auto window_title_width = ImGui::CalcTextSize(window_title.c_str()).x;
|
||||
|
||||
ImGui::SetCursorPos(ImVec2((WINDOW_SIZE.x - window_title_width) / 2.f, ((64.f * SCALE.y) - ImGui::GetFontSize()) / 2.f));
|
||||
ImGui::Text("%s", window_title.c_str());
|
||||
const auto SEPARATOR_CLOUD_POS_Y = 64.f * SCALE.y;
|
||||
const auto LOCATION_SECTION_SIZE_Y = 92.f * SCALE.y;
|
||||
ImGui::SetCursorPos(ImVec2(0.f, SEPARATOR_CLOUD_POS_Y));
|
||||
ImGui::Separator();
|
||||
ImGui::SetCursorPos(ImVec2(54.f * SCALE.x, SEPARATOR_CLOUD_POS_Y + ((LOCATION_SECTION_SIZE_Y - ImGui::GetFontSize()) / 2.f)));
|
||||
|
||||
ImGui::Text("Cloud");
|
||||
|
||||
const auto has_cloud_save = storage_state.savedata_info.contains(SAVE_DATA_TYPE_CLOUD);
|
||||
const auto has_local_save = storage_state.savedata_info.contains(SAVE_DATA_TYPE_LOCAL);
|
||||
|
||||
const auto begin_text_pos_x = 182.f * SCALE.x;
|
||||
|
||||
ImGui::SetWindowFontScale(1.68f * RES_SCALE.y);
|
||||
const auto app_title_height_size = ImGui::CalcTextSize(app_title.c_str()).y;
|
||||
if (has_cloud_save) {
|
||||
ImGui::SetCursorPos(ImVec2(begin_text_pos_x, SEPARATOR_CLOUD_POS_Y + (LOCATION_SECTION_SIZE_Y / 2.f) - app_title_height_size));
|
||||
ImGui::Text("%s", app_title.c_str());
|
||||
const auto &cloud_info = storage_state.savedata_info[SAVE_DATA_TYPE_CLOUD];
|
||||
ImGui::SetCursorPos(ImVec2(begin_text_pos_x, SEPARATOR_CLOUD_POS_Y + (LOCATION_SECTION_SIZE_Y / 2.f) + (10.f * SCALE.y)));
|
||||
tm updated_at_tm = {};
|
||||
SAFE_LOCALTIME(&cloud_info.last_updated, &updated_at_tm);
|
||||
auto UPDATED_AT = get_date_time(gui, emuenv, updated_at_tm);
|
||||
ImGui::SetWindowFontScale(1.12f * RES_SCALE.y);
|
||||
ImGui::TextColored(GUI_COLOR_TEXT, "%s", fmt::format("{} {} {} {}", UPDATED_AT[DateTime::DATE_MINI], UPDATED_AT[DateTime::CLOCK], is_12_hour_format ? UPDATED_AT[DateTime::DAY_MOMENT] : "", get_unit_size(cloud_info.total_size)).c_str());
|
||||
} else {
|
||||
ImGui::SetCursorPos(ImVec2(begin_text_pos_x, SEPARATOR_CLOUD_POS_Y + ((LOCATION_SECTION_SIZE_Y - ImGui::GetFontSize()) / 2.f)));
|
||||
ImGui::Text("-");
|
||||
}
|
||||
|
||||
const auto SEPARATOR_BUTTON_POS_Y = SEPARATOR_CLOUD_POS_Y + LOCATION_SECTION_SIZE_Y;
|
||||
|
||||
ImGui::SetCursorPos(ImVec2(0.f, SEPARATOR_BUTTON_POS_Y));
|
||||
ImGui::Separator();
|
||||
|
||||
const auto BUTTONS_SECTION_SIZE_Y = 184.f * SCALE.y;
|
||||
const auto BUTTONS_SEPARATION = 40.f * SCALE.x;
|
||||
|
||||
ImGui::SetWindowFontScale(1.06f * RES_SCALE.y);
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(1, 1, 1, 0.08f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1, 1, 1, 0.12f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1, 1, 1, 0.18f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 15.f * SCALE.x);
|
||||
const ImVec2 CLOUD_BUTTON_SIZE(284.f * SCALE.x, 116.f * SCALE.y);
|
||||
const auto BUTTONS_POS_Y = SEPARATOR_BUTTON_POS_Y + ((BUTTONS_SECTION_SIZE_Y - CLOUD_BUTTON_SIZE.y) / 2.f);
|
||||
ImGui::SetCursorPos(ImVec2((WINDOW_SIZE.x / 2.f) - CLOUD_BUTTON_SIZE.x - (BUTTONS_SEPARATION / 2.f), BUTTONS_POS_Y));
|
||||
ImGui::BeginDisabled(!has_local_save);
|
||||
if (ImGui::Button(lang["upload"].c_str(), CLOUD_BUTTON_SIZE)) {
|
||||
if (has_cloud_save) {
|
||||
storage_state.online_storage_state = ONLINE_STORAGE_UPLOAD;
|
||||
} else {
|
||||
storage_state.online_storage_state = ONLINE_STORAGE_COPYING;
|
||||
v3kn::upload_savedata(gui, emuenv, app_titleid);
|
||||
}
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SetCursorPos(ImVec2((WINDOW_SIZE.x / 2.f) + (BUTTONS_SEPARATION / 2.f), BUTTONS_POS_Y));
|
||||
ImGui::BeginDisabled(!has_cloud_save);
|
||||
if (ImGui::Button(lang["download"].c_str(), CLOUD_BUTTON_SIZE)) {
|
||||
if (has_local_save) {
|
||||
storage_state.online_storage_state = ONLINE_STORAGE_DOWNLOAD;
|
||||
} else {
|
||||
storage_state.online_storage_state = ONLINE_STORAGE_COPYING;
|
||||
v3kn::download_savedata(gui, emuenv, app_titleid);
|
||||
}
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PopStyleColor(3);
|
||||
|
||||
static const auto get_timestamp_ms = []() {
|
||||
return duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
|
||||
};
|
||||
|
||||
if (storage_state.online_storage_state != ONLINE_STORAGE_SELECT) {
|
||||
const ImVec2 OVERLAY_SIZE(VIEWPORT_SIZE.x, VIEWPORT_SIZE.y - INDICATOR_SIZE.y);
|
||||
const ImVec2 OVERLAY_POS(VIEWPORT_POS.x, VIEWPORT_POS.y + INDICATOR_SIZE.y);
|
||||
ImGui::SetNextWindowPos(OVERLAY_POS, ImGuiCond_Always);
|
||||
ImGui::SetNextWindowSize(OVERLAY_SIZE, ImGuiCond_Always);
|
||||
ImGui::Begin("##cloud_save_overlay", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoSavedSettings);
|
||||
const auto &window_draw_list = ImGui::GetWindowDrawList();
|
||||
|
||||
switch (storage_state.online_storage_state) {
|
||||
case ONLINE_STORAGE_UPLOAD:
|
||||
case ONLINE_STORAGE_DOWNLOAD: {
|
||||
const ImVec2 POPUP_SIZE(868.f * SCALE.x, 478.f * SCALE.y);
|
||||
const ImVec2 POPUP_POS(VIEWPORT_POS.x + ((VIEWPORT_SIZE.x - POPUP_SIZE.x) / 2.f), VIEWPORT_POS.y + ((VIEWPORT_SIZE.y - POPUP_SIZE.y) / 2.f));
|
||||
ImGui::SetNextWindowPos(POPUP_POS, ImGuiCond_Always);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 15.f * SCALE.x);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.f, 0.f));
|
||||
ImGui::BeginChild("##confirm_popup", POPUP_SIZE, ImGuiChildFlags_Borders, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoSavedSettings);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.f, 0.f));
|
||||
|
||||
const ImVec2 ICON_SIZE(76.f * SCALE.x, 76.f * SCALE.y);
|
||||
const ImVec2 ICON_POS(50.f * SCALE.x, 26.f * SCALE.y);
|
||||
if (app_icon) {
|
||||
const ImVec2 ICON_POS_MIN(POPUP_POS.x + ICON_POS.x, POPUP_POS.y + ICON_POS.y);
|
||||
const ImVec2 ICON_POS_MAX(ICON_POS_MIN.x + ICON_SIZE.x, ICON_POS_MIN.y + ICON_SIZE.y);
|
||||
window_draw_list->AddImageRounded(app_icon, ICON_POS_MIN, ICON_POS_MAX, ImVec2(0, 0), ImVec2(1, 1), IM_COL32_WHITE, ICON_SIZE.x);
|
||||
}
|
||||
ImGui::SetWindowFontScale(1.52f * RES_SCALE.y);
|
||||
ImGui::SetCursorPos(ImVec2(ICON_POS.x + ICON_SIZE.x + (22.f * SCALE.x), ICON_POS.y + ((ICON_SIZE.y - app_title_height_size) / 2.f)));
|
||||
ImGui::Text("%s", app_title.c_str());
|
||||
|
||||
const bool is_upload = (storage_state.online_storage_state == ONLINE_STORAGE_UPLOAD);
|
||||
|
||||
const auto &src_save = is_upload ? storage_state.savedata_info[SAVE_DATA_TYPE_LOCAL] : storage_state.savedata_info[SAVE_DATA_TYPE_CLOUD];
|
||||
const auto &dst_save = is_upload ? storage_state.savedata_info[SAVE_DATA_TYPE_CLOUD] : storage_state.savedata_info[SAVE_DATA_TYPE_LOCAL];
|
||||
|
||||
const auto ¤t_ps_system = emuenv.cfg.pstv_mode ? lang["ps_tv_system"] : lang["ps_vita_system"];
|
||||
const auto &src_title = is_upload ? current_ps_system : lang["online_storage"];
|
||||
const auto &dst_title = is_upload ? lang["online_storage"] : current_ps_system;
|
||||
|
||||
const auto draw_save_info = [&](const ImVec2 &begin_pos, const std::string &title, const SaveDataInfo &save_info) {
|
||||
ImGui::SetCursorPos(begin_pos);
|
||||
ImGui::SetWindowFontScale(1.1f * RES_SCALE.y);
|
||||
ImGui::Text("%s", title.c_str());
|
||||
ImGui::SetCursorPos(ImVec2(begin_pos.x, ImGui::GetCursorPosY() + (15.f * SCALE.y)));
|
||||
tm updated_at_tm = {};
|
||||
SAFE_LOCALTIME(&save_info.last_updated, &updated_at_tm);
|
||||
auto UPDATED_AT = get_date_time(gui, emuenv, updated_at_tm);
|
||||
ImGui::TextColored(GUI_COLOR_TEXT, "%s", fmt::format("{} {} {}", UPDATED_AT[DateTime::DATE_MINI], UPDATED_AT[DateTime::CLOCK], is_12_hour_format ? UPDATED_AT[DateTime::DAY_MOMENT] : "").c_str());
|
||||
ImGui::SetCursorPos(ImVec2(begin_pos.x, ImGui::GetCursorPosY() + (5.f * SCALE.y)));
|
||||
ImGui::TextColored(GUI_COLOR_TEXT, "%s", get_unit_size(save_info.total_size).c_str());
|
||||
};
|
||||
|
||||
const float INFO_START_Y = ICON_POS.y + ICON_SIZE.y + (22.f * SCALE.y);
|
||||
|
||||
// Draw source save info
|
||||
const auto SRC_POS_X = 50.f * SCALE.x;
|
||||
draw_save_info(ImVec2(SRC_POS_X, INFO_START_Y), src_title, src_save);
|
||||
|
||||
// Draw arrow
|
||||
const auto ARROW_COLOR = IM_COL32(161, 162, 165, 255);
|
||||
const auto ARROW_BAR_SIZE = ImVec2(42.f * SCALE.x, 8.f * SCALE.y);
|
||||
const auto ARROW_HEAD_SIZE = ImVec2(12.f * SCALE.x, 18.f * SCALE.y);
|
||||
const ImVec2 ARROW_BAR_POS_MIN(POPUP_POS.x + (POPUP_SIZE.x / 2.f) - (5.f * SCALE.x) - ARROW_BAR_SIZE.x - ARROW_HEAD_SIZE.x, POPUP_POS.y + (132.f * SCALE.y));
|
||||
const ImVec2 ARROW_BAR_POS_MAX(ARROW_BAR_POS_MIN.x + ARROW_BAR_SIZE.x, ARROW_BAR_POS_MIN.y + ARROW_BAR_SIZE.y);
|
||||
window_draw_list->AddRectFilled(ARROW_BAR_POS_MIN, ARROW_BAR_POS_MAX, ARROW_COLOR);
|
||||
const ImVec2 ARROW_HEAD_POS1(ARROW_BAR_POS_MAX.x, ARROW_BAR_POS_MIN.y - ((ARROW_HEAD_SIZE.y - ARROW_BAR_SIZE.y) / 2.f));
|
||||
const ImVec2 ARROW_HEAD_POS2(ARROW_BAR_POS_MAX.x + ARROW_HEAD_SIZE.x, ARROW_BAR_POS_MIN.y + (ARROW_BAR_SIZE.y / 2.f));
|
||||
const ImVec2 ARROW_HEAD_POS3(ARROW_BAR_POS_MAX.x, ARROW_BAR_POS_MAX.y + ((ARROW_HEAD_SIZE.y - ARROW_BAR_SIZE.y) / 2.f));
|
||||
window_draw_list->AddTriangleFilled(ARROW_HEAD_POS1, ARROW_HEAD_POS2, ARROW_HEAD_POS3, ARROW_COLOR);
|
||||
|
||||
// Draw destination save info
|
||||
const auto DST_POS_X = (POPUP_SIZE.x / 2.f) + 42.f * SCALE.x;
|
||||
draw_save_info(ImVec2(DST_POS_X, INFO_START_Y), dst_title, dst_save);
|
||||
|
||||
ImGui::SetCursorPosY(215.f * SCALE.y);
|
||||
ImGui::Separator();
|
||||
ImGui::SetWindowFontScale(1.38f * RES_SCALE.y);
|
||||
ImGui::SetCursorPos(ImVec2(60.f * SCALE.x, ImGui::GetCursorPosY() + (14.f * SCALE.y)));
|
||||
ImGui::Text("%s", lang["overwrite_saved_data"].c_str());
|
||||
const auto CONFIRM_BUTTON_SIZE = ImVec2(310.f * SCALE.x, 46.f * SCALE.y);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 15.f * SCALE.x);
|
||||
const auto BUTTON_SPACING = 40.f * SCALE.x;
|
||||
ImGui::SetCursorPos(ImVec2((POPUP_SIZE.x / 2.f) - CONFIRM_BUTTON_SIZE.x - (BUTTON_SPACING / 2.f), POPUP_SIZE.y - CONFIRM_BUTTON_SIZE.y - (24.f * SCALE.y)));
|
||||
if (ImGui::Button(common_lang["no"].c_str(), CONFIRM_BUTTON_SIZE)) {
|
||||
storage_state.online_storage_state = ONLINE_STORAGE_SELECT;
|
||||
}
|
||||
ImGui::SetCursorPos(ImVec2((POPUP_SIZE.x / 2.f) + (BUTTON_SPACING / 2.f), POPUP_SIZE.y - CONFIRM_BUTTON_SIZE.y - (24.f * SCALE.y)));
|
||||
if (ImGui::Button(common_lang["yes"].c_str(), CONFIRM_BUTTON_SIZE)) {
|
||||
if (storage_state.online_storage_state == ONLINE_STORAGE_UPLOAD)
|
||||
v3kn::upload_savedata(gui, emuenv, app_titleid);
|
||||
else
|
||||
v3kn::download_savedata(gui, emuenv, app_titleid);
|
||||
storage_state.online_storage_state = ONLINE_STORAGE_COPYING;
|
||||
}
|
||||
ImGui::PopStyleVar(2);
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleVar(2);
|
||||
break;
|
||||
}
|
||||
case ONLINE_STORAGE_COPYING: {
|
||||
const ImVec2 ICON_SIZE(64.f * SCALE.x, 64.f * SCALE.y);
|
||||
const ImVec2 POPUP_SIZE(760 * SCALE.x, 436.f * SCALE.y);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 15.f * SCALE.x);
|
||||
const auto PUPUP_POS = ImVec2(VIEWPORT_POS.x + ((VIEWPORT_SIZE.x - POPUP_SIZE.x) / 2.f), VIEWPORT_POS.y + ((VIEWPORT_SIZE.y - POPUP_SIZE.y) / 2.f));
|
||||
ImGui::SetNextWindowPos(PUPUP_POS, ImGuiCond_Always);
|
||||
ImGui::BeginChild("##copying_popup", POPUP_SIZE, ImGuiChildFlags_Borders, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoSavedSettings);
|
||||
const auto ©_str = lang["copying"];
|
||||
ImGui::SetCursorPos(ImVec2((POPUP_SIZE.x - ImGui::CalcTextSize(copy_str.c_str()).x) / 2.f, 82.f * SCALE.y));
|
||||
ImGui::Text("%s", copy_str.c_str());
|
||||
const auto ICON_POS = ImVec2(100.f * SCALE.x, 132.f * SCALE.y);
|
||||
if (app_icon) {
|
||||
const ImVec2 ICON_POS_MIN(PUPUP_POS.x + ICON_POS.x, PUPUP_POS.y + ICON_POS.y);
|
||||
const ImVec2 ICON_POS_MAX(ICON_POS_MIN.x + ICON_SIZE.x, ICON_POS_MIN.y + ICON_SIZE.y);
|
||||
window_draw_list->AddImageRounded(app_icon, ICON_POS_MIN, ICON_POS_MAX, ImVec2(0, 0), ImVec2(1, 1), IM_COL32_WHITE, ICON_SIZE.x);
|
||||
}
|
||||
ImGui::SetCursorPos(ImVec2(ICON_POS.x + ICON_SIZE.x + (12.f * SCALE.x), ICON_POS.y + ((ICON_SIZE.y - ImGui::GetFontSize()) / 2.f)));
|
||||
ImGui::Text("%s", app_title.c_str());
|
||||
const auto remaining_str = get_remaining_str(gui.lang, storage_state.remaining.load());
|
||||
ImGui::SetCursorPos(ImVec2(POPUP_SIZE.x - (100 * SCALE.x) - (ImGui::CalcTextSize(remaining_str.c_str()).x), 214.f * SCALE.y));
|
||||
ImGui::Text("%s", remaining_str.c_str());
|
||||
const float PROGRESS_BAR_WIDTH = 560.f * SCALE.x;
|
||||
ImGui::SetCursorPos(ImVec2((ImGui::GetWindowWidth() / 2) - (PROGRESS_BAR_WIDTH / 2.f), 236.f * SCALE.y));
|
||||
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, GUI_PROGRESS_BAR);
|
||||
ImGui::ProgressBar(storage_state.progress.load(), ImVec2(PROGRESS_BAR_WIDTH, 14.f * SCALE.y), "");
|
||||
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + (16.f * SCALE.y));
|
||||
TextColoredCentered(GUI_COLOR_TEXT, std::to_string(static_cast<uint32_t>(storage_state.progress.load() * 100.f)).append("%").c_str());
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 15.f * SCALE.x);
|
||||
const ImVec2 CANCEL_BUTTON_SIZE(320.f * SCALE.x, 46.f * SCALE.y);
|
||||
ImGui::SetCursorPos(ImVec2((POPUP_SIZE.x - CANCEL_BUTTON_SIZE.x) / 2.f, POPUP_SIZE.y - CANCEL_BUTTON_SIZE.y - (24.f * SCALE.y)));
|
||||
ImGui::BeginDisabled(storage_state.progress.load() >= 1.f);
|
||||
if (ImGui::Button(emuenv.common_dialog.lang.common["cancel"].c_str(), CANCEL_BUTTON_SIZE)) {
|
||||
std::lock_guard<std::mutex> lock(storage_state.progress_state.mutex);
|
||||
storage_state.progress_state.canceled = true;
|
||||
storage_state.online_storage_state = ONLINE_STORAGE_SELECT;
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
|
||||
// Hold the dialog at 100% for a short moment to ensure the user perceives the completion state
|
||||
if (storage_state.progress.load() >= 1.f) {
|
||||
const auto now = get_timestamp_ms();
|
||||
|
||||
if (storage_state.progress_done_timestamp == 0)
|
||||
storage_state.progress_done_timestamp = now;
|
||||
|
||||
if (now - storage_state.progress_done_timestamp >= 900) {
|
||||
gui.vita_area.please_wait = true;
|
||||
storage_state.please_wait_timestamp = now;
|
||||
storage_state.online_storage_state = ONLINE_STORAGE_SELECT;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleVar();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
// Hide please wait after a short delay to improve UX
|
||||
if (gui.vita_area.please_wait && !storage_state.please_wait_done.load()) {
|
||||
const auto now = get_timestamp_ms();
|
||||
|
||||
if (now - storage_state.please_wait_timestamp >= 900) {
|
||||
storage_state.please_wait_done.store(true);
|
||||
{
|
||||
std::lock_guard<std::mutex> lck(storage_state.mutex_progress);
|
||||
storage_state.cv_progress.notify_all();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const auto END_SEPARATOR_POS_Y = SEPARATOR_BUTTON_POS_Y + BUTTONS_SECTION_SIZE_Y;
|
||||
ImGui::SetCursorPos(ImVec2(0.f, END_SEPARATOR_POS_Y));
|
||||
ImGui::Separator();
|
||||
|
||||
ImGui::SetWindowFontScale(1.85f * RES_SCALE.y);
|
||||
ImGui::SetCursorPos(ImVec2(54.f * SCALE.x, END_SEPARATOR_POS_Y + ((LOCATION_SECTION_SIZE_Y - ImGui::GetFontSize()) / 2.f)));
|
||||
ImGui::Text("Local");
|
||||
|
||||
ImGui::SetWindowFontScale(1.68f * RES_SCALE.y);
|
||||
if (has_local_save) {
|
||||
const auto &local_info = storage_state.savedata_info[SAVE_DATA_TYPE_LOCAL];
|
||||
ImGui::SetCursorPos(ImVec2(begin_text_pos_x, END_SEPARATOR_POS_Y + (LOCATION_SECTION_SIZE_Y / 2.f) - app_title_height_size));
|
||||
ImGui::Text("%s", app_title.c_str());
|
||||
ImGui::SetCursorPos(ImVec2(begin_text_pos_x, END_SEPARATOR_POS_Y + (LOCATION_SECTION_SIZE_Y / 2.f) + (10.f * SCALE.y)));
|
||||
tm updated_at_tm = {};
|
||||
SAFE_LOCALTIME(&local_info.last_updated, &updated_at_tm);
|
||||
auto UPDATED_AT = get_date_time(gui, emuenv, updated_at_tm);
|
||||
ImGui::SetWindowFontScale(1.12f * RES_SCALE.y);
|
||||
ImGui::TextColored(GUI_COLOR_TEXT, "%s", fmt::format("{} {} {} {}", UPDATED_AT[DateTime::DATE_MINI], UPDATED_AT[DateTime::CLOCK], is_12_hour_format ? UPDATED_AT[DateTime::DAY_MOMENT] : "", get_unit_size(local_info.total_size)).c_str());
|
||||
} else {
|
||||
ImGui::SetCursorPos(ImVec2(begin_text_pos_x, END_SEPARATOR_POS_Y + (LOCATION_SECTION_SIZE_Y - ImGui::GetFontSize()) / 2.f));
|
||||
ImGui::Text("-");
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
ImGui::PopStyleVar(2);
|
||||
}
|
||||
|
||||
} // namespace gui
|
||||
@@ -58,6 +58,7 @@ void draw_event_flags_dialog(GuiState &gui, EmuEnvState &emuenv);
|
||||
void draw_allocations_dialog(GuiState &gui, EmuEnvState &emuenv);
|
||||
void draw_disassembly_dialog(GuiState &gui, EmuEnvState &emuenv);
|
||||
void draw_settings_dialog(GuiState &gui, EmuEnvState &emuenv);
|
||||
void draw_v3kn_dialog(GuiState &gui, EmuEnvState &emuenv);
|
||||
void draw_overlay_dialog(GuiState &gui, EmuEnvState &emuenv);
|
||||
void draw_controls_dialog(GuiState &gui, EmuEnvState &emuenv);
|
||||
void draw_controllers_dialog(GuiState &gui, EmuEnvState &emuenv);
|
||||
@@ -71,6 +72,7 @@ void draw_home_screen(GuiState &gui, EmuEnvState &emuenv);
|
||||
void draw_information_bar(GuiState &gui, EmuEnvState &emuenv);
|
||||
void draw_live_area_screen(GuiState &gui, EmuEnvState &emuenv);
|
||||
void draw_manual(GuiState &gui, EmuEnvState &emuenv);
|
||||
void draw_online_storage(GuiState &gui, EmuEnvState &emuenv);
|
||||
void draw_settings(GuiState &gui, EmuEnvState &emuenv);
|
||||
void draw_start_screen(GuiState &gui, EmuEnvState &emuenv);
|
||||
void draw_trophy_collection(GuiState &gui, EmuEnvState &emuenv);
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
#include <util/string_utils.h>
|
||||
#include <util/vector_utils.h>
|
||||
|
||||
#include <v3kn/account.h>
|
||||
|
||||
#include <pugixml.hpp>
|
||||
#include <stb_image.h>
|
||||
|
||||
@@ -38,22 +40,10 @@
|
||||
|
||||
namespace gui {
|
||||
|
||||
enum AvatarSize {
|
||||
SMALL = 34,
|
||||
MEDIUM = 130,
|
||||
LARGE = 160
|
||||
};
|
||||
|
||||
static ImVec2 get_avatar_size(AvatarSize size, const ImVec2 scale = { 1, 1 }) {
|
||||
return ImVec2(static_cast<float>(size) * scale.x, static_cast<float>(size) * scale.y);
|
||||
}
|
||||
|
||||
struct AvatarInfo {
|
||||
ImVec2 pos;
|
||||
ImVec2 size;
|
||||
};
|
||||
|
||||
static std::map<std::string, std::map<AvatarSize, AvatarInfo>> users_avatar_infos;
|
||||
static bool init_avatar(GuiState &gui, EmuEnvState &emuenv, const std::string &user_id, const std::string &avatar_path) {
|
||||
const auto avatar_path_path = avatar_path == "default" ? emuenv.static_assets_path / "data/image/icon.png" : fs_utils::utf8_to_path(avatar_path);
|
||||
|
||||
@@ -78,11 +68,11 @@ static bool init_avatar(GuiState &gui, EmuEnvState &emuenv, const std::string &u
|
||||
|
||||
// Calculate avatar size and position based of aspect ratio
|
||||
// Resize for all size of avatar
|
||||
constexpr std::array<AvatarSize, 3> sizes = { SMALL, MEDIUM, LARGE };
|
||||
constexpr std::array<AvatarSize, 4> sizes = { AvatarSize::SMALL, AvatarSize::MEDIUM, AvatarSize::LARGE, AvatarSize::V3KN };
|
||||
for (const auto size : sizes) {
|
||||
const auto avatar_size = get_avatar_size(size);
|
||||
const auto ratio = std::min(avatar_size.x / static_cast<float>(width), avatar_size.y / static_cast<float>(height));
|
||||
auto &avatar = users_avatar_infos[user_id][size];
|
||||
auto &avatar = gui.users_avatar_infos[user_id][size];
|
||||
avatar.size = ImVec2(width * ratio, height * ratio);
|
||||
avatar.pos = ImVec2((avatar_size.x / 2.f) - (avatar.size.x / 2.f), (avatar_size.y / 2.f) - (avatar.size.y / 2.f));
|
||||
}
|
||||
@@ -240,6 +230,11 @@ void init_user(GuiState &gui, EmuEnvState &emuenv, const std::string &user_id) {
|
||||
init_theme(gui, emuenv, gui.users[user_id].theme_id);
|
||||
init_notice_info(gui, emuenv);
|
||||
init_last_time_apps(gui, emuenv);
|
||||
|
||||
// Init V3KN user info asynchronously to avoid blocking the GUI
|
||||
std::thread([&gui, &emuenv]() {
|
||||
v3kn::init_v3kn_user_info(gui, emuenv);
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void open_user(GuiState &gui, EmuEnvState &emuenv) {
|
||||
@@ -316,7 +311,7 @@ static void create_and_save_user(GuiState &gui, EmuEnvState &emuenv) {
|
||||
|
||||
// Move avatar texture + infos
|
||||
gui.users_avatar[user_id_selected] = std::move(gui.users_avatar["temp"]);
|
||||
users_avatar_infos[user_id_selected] = users_avatar_infos["temp"];
|
||||
gui.users_avatar_infos[user_id_selected] = gui.users_avatar_infos["temp"];
|
||||
current_user_id_selected = string_utils::stoi_def(user_id_selected, 0, "selected user id");
|
||||
save_user(gui, emuenv, user_id_selected);
|
||||
|
||||
@@ -517,9 +512,9 @@ void draw_user_management(GuiState &gui, EmuEnvState &emuenv) {
|
||||
|
||||
const auto user_path{ emuenv.pref_path / "ux0/user" };
|
||||
|
||||
const auto SMALL_AVATAR_SIZE = get_avatar_size(SMALL, SCALE);
|
||||
const auto MED_AVATAR_SIZE = get_avatar_size(MEDIUM, SCALE);
|
||||
const auto LARGE_AVATAR_SIZE = get_avatar_size(LARGE, SCALE);
|
||||
const auto SMALL_AVATAR_SIZE = get_avatar_size(AvatarSize::SMALL, SCALE);
|
||||
const auto MED_AVATAR_SIZE = get_avatar_size(AvatarSize::MEDIUM, SCALE);
|
||||
const auto LARGE_AVATAR_SIZE = get_avatar_size(AvatarSize::LARGE, SCALE);
|
||||
const auto USER_NAME_BG_SIZE = ImVec2(MED_AVATAR_SIZE.x, 60.f * SCALE.y);
|
||||
|
||||
ImGui::SetWindowFontScale(1.4f * RES_SCALE.x);
|
||||
@@ -549,7 +544,7 @@ void draw_user_management(GuiState &gui, EmuEnvState &emuenv) {
|
||||
const auto BG_AVATAR_SIZE = get_avatar_size(size, SCALE);
|
||||
const auto &DRAW_LIST = ImGui::GetWindowDrawList();
|
||||
DRAW_LIST->AddRectFilled(SCREEN_POS, ImVec2(SCREEN_POS.x + BG_AVATAR_SIZE.x, SCREEN_POS.y + BG_AVATAR_SIZE.y), IM_COL32(19.f, 69.f, 167.f, 150.f));
|
||||
if (size == MEDIUM) {
|
||||
if (size == AvatarSize::MEDIUM) {
|
||||
ImGui::SetCursorPos(ImVec2(origin_pos.x, origin_pos.y + MED_AVATAR_SIZE.y));
|
||||
DRAW_LIST->AddRectFilled(SCREEN_POS, ImVec2(SCREEN_POS.x + BG_AVATAR_SIZE.x, SCREEN_POS.y + USER_NAME_BG_SIZE.y), IM_COL32(19.f, 69.f, 167.f, 255.f));
|
||||
}
|
||||
@@ -559,7 +554,7 @@ void draw_user_management(GuiState &gui, EmuEnvState &emuenv) {
|
||||
const auto draw_avatar = [&](const std::string &user_id, const AvatarSize size, const ImVec2 origin_pos) {
|
||||
draw_user_bg(size, origin_pos);
|
||||
if (gui.users_avatar.contains(user_id)) {
|
||||
const auto &user_avatar_infos = users_avatar_infos[user_id][size];
|
||||
const auto &user_avatar_infos = gui.users_avatar_infos[user_id][size];
|
||||
ImVec2 AVATAR_POS = ImVec2(origin_pos.x + (user_avatar_infos.pos.x * SCALE.x), origin_pos.y + (user_avatar_infos.pos.y * SCALE.y));
|
||||
ImVec2 AVATAR_SIZE = ImVec2(user_avatar_infos.size.x * SCALE.x, user_avatar_infos.size.y * SCALE.y);
|
||||
ImGui::SetCursorPos(AVATAR_POS);
|
||||
@@ -637,7 +632,7 @@ void draw_user_management(GuiState &gui, EmuEnvState &emuenv) {
|
||||
// Users list
|
||||
title = lang["select_user"];
|
||||
ImGui::SetWindowFontScale(1.6f);
|
||||
draw_user_bg(MEDIUM, CREATE_USER_POS);
|
||||
draw_user_bg(AvatarSize::MEDIUM, CREATE_USER_POS);
|
||||
ImGui::SetCursorPos(CREATE_USER_POS);
|
||||
if (ImGui::InvisibleButton("+", SELECTABLE_USER_SIZE)) {
|
||||
if (menu_selected == CREATE)
|
||||
@@ -685,7 +680,7 @@ void draw_user_management(GuiState &gui, EmuEnvState &emuenv) {
|
||||
if (is_edit_user_selected)
|
||||
draw_frame(EDIT_USER_POS_SEL, EDIT_USER_SIZE);
|
||||
trigger_scroll_to_item(is_current_user_id_selected);
|
||||
draw_avatar(user.first, MEDIUM, USER_POS);
|
||||
draw_avatar(user.first, AvatarSize::MEDIUM, USER_POS);
|
||||
ImGui::SetCursorPos(USER_POS);
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, GUI_COLOR_TEXT_TITLE);
|
||||
if (ImGui::InvisibleButton("##avatar", SELECTABLE_USER_SIZE)) {
|
||||
@@ -712,7 +707,7 @@ void draw_user_management(GuiState &gui, EmuEnvState &emuenv) {
|
||||
ImGui::SetCursorPos(ImVec2(USER_POS.x + SPACE_AVATAR, USER_POS.y));
|
||||
}
|
||||
if (!gui.users.empty()) {
|
||||
draw_user_bg(MEDIUM, DELETE_USER_POS);
|
||||
draw_user_bg(AvatarSize::MEDIUM, DELETE_USER_POS);
|
||||
ImGui::SetCursorPos(DELETE_USER_POS);
|
||||
ImGui::SetWindowFontScale(1.6f);
|
||||
if (ImGui::InvisibleButton("-", SELECTABLE_USER_SIZE)) {
|
||||
@@ -745,7 +740,7 @@ void draw_user_management(GuiState &gui, EmuEnvState &emuenv) {
|
||||
temp.avatar = "default";
|
||||
init_avatar(gui, emuenv, "temp", "default");
|
||||
}
|
||||
draw_avatar("temp", LARGE, AVATAR_POS);
|
||||
draw_avatar("temp", AvatarSize::LARGE, AVATAR_POS);
|
||||
const ImVec2 CHANGE_AVATAR_BTN_SIZE(LARGE_AVATAR_SIZE.x, 36.f * SCALE.y);
|
||||
ImGui::SetCursorPos(ImVec2(AVATAR_POS.x + (LARGE_AVATAR_SIZE.x / 2.f) - (CHANGE_AVATAR_BTN_SIZE.x / 2.f), AVATAR_POS.y + LARGE_AVATAR_SIZE.y + (5.f * SCALE.y)));
|
||||
if (ImGui::Button(lang["choose_avatar"].c_str(), CHANGE_AVATAR_BTN_SIZE)) {
|
||||
@@ -788,7 +783,7 @@ void draw_user_management(GuiState &gui, EmuEnvState &emuenv) {
|
||||
ImGui::SetCursorPosY(44.f * SCALE.y);
|
||||
TextColoredCentered(GUI_COLOR_TEXT, lang["user_created"].c_str());
|
||||
const auto AVATAR_CONFIRM_POS = ImVec2((SIZE_USER.x / 2) - (MED_AVATAR_SIZE.x / 2.f), 96.f * SCALE.y);
|
||||
draw_avatar(user_id_selected, MEDIUM, AVATAR_CONFIRM_POS);
|
||||
draw_avatar(user_id_selected, AvatarSize::MEDIUM, AVATAR_CONFIRM_POS);
|
||||
ImGui::SetWindowFontScale(0.7f);
|
||||
ImGui::SetCursorPos(ImVec2(AVATAR_CONFIRM_POS.x + USER_NAME_PADDING, AVATAR_CONFIRM_POS.y + MED_AVATAR_SIZE.y + (5.f * SCALE.y)));
|
||||
ImGui::PushTextWrapPos(AVATAR_CONFIRM_POS.x + MED_AVATAR_SIZE.x - USER_NAME_PADDING);
|
||||
@@ -822,7 +817,7 @@ void draw_user_management(GuiState &gui, EmuEnvState &emuenv) {
|
||||
ImGui::Separator();
|
||||
for (const auto &user : gui.users) {
|
||||
users_list_available.push_back(string_utils::stoi_def(user.first, 0, "gui user id"));
|
||||
draw_avatar(user.first, SMALL, ImVec2(ImGui::GetCursorPosX(), ImGui::GetCursorPosY() + (SELECT_SIZE.y / 2.f) - (SMALL_AVATAR_SIZE.y / 2.f)));
|
||||
draw_avatar(user.first, AvatarSize::SMALL, ImVec2(ImGui::GetCursorPosX(), ImGui::GetCursorPosY() + (SELECT_SIZE.y / 2.f) - (SMALL_AVATAR_SIZE.y / 2.f)));
|
||||
ImGui::NextColumn();
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_SelectableTextAlign, ImVec2(0.0f, 0.5f));
|
||||
if (ImGui::Selectable(user.second.name.c_str(), gui.is_nav_button && (current_user_id_selected == string_utils::stoi_def(user.first, 0, "gui user id")), ImGuiSelectableFlags_SpanAllColumns, SELECT_SIZE))
|
||||
@@ -913,7 +908,7 @@ void draw_user_management(GuiState &gui, EmuEnvState &emuenv) {
|
||||
config::serialize_config(emuenv.cfg, emuenv.cfg.config_path);
|
||||
}
|
||||
if (!gui.users.empty() && gui.users.contains(emuenv.cfg.user_id)) {
|
||||
draw_avatar(emuenv.cfg.user_id, SMALL, ImVec2(WINDOW_SIZE.x - (220.f * SCALE.x), WINDOW_SIZE.y - (POS_SEPARATOR / 2.f) - (SMALL_AVATAR_SIZE.y / 2.f)));
|
||||
draw_avatar(emuenv.cfg.user_id, AvatarSize::SMALL, ImVec2(WINDOW_SIZE.x - (220.f * SCALE.x), WINDOW_SIZE.y - (POS_SEPARATOR / 2.f) - (SMALL_AVATAR_SIZE.y / 2.f)));
|
||||
ImGui::SetCursorPos(ImVec2(WINDOW_SIZE.x - 180.f * SCALE.x, WINDOW_SIZE.y - (POS_SEPARATOR / 2.f) - (ImGui::CalcTextSize(gui.users[emuenv.cfg.user_id].name.c_str()).y / 2.f)));
|
||||
ImGui::TextColored(GUI_COLOR_TEXT, "%s", gui.users[emuenv.cfg.user_id].name.c_str());
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -194,10 +194,11 @@ bool init_vita3k_update(GuiState &gui) {
|
||||
|
||||
static std::atomic<float> progress(0);
|
||||
static std::atomic<uint64_t> remaining(0);
|
||||
static std::atomic<uint64_t> downloaded(0);
|
||||
static net_utils::ProgressState progress_state{};
|
||||
|
||||
static void download_update(const fs::path &base_path) {
|
||||
progress_state.download = true;
|
||||
progress_state.canceled = false;
|
||||
progress_state.pause = false;
|
||||
std::thread download([base_path]() {
|
||||
std::string download_continuous_link = "https://github.com/Vita3K/Vita3K/releases/download/continuous/";
|
||||
@@ -272,9 +273,10 @@ static void download_update(const fs::path &base_path) {
|
||||
// Download latest Vita3K version
|
||||
LOG_INFO("Attempting to download and extract the latest Vita3K version {} in progress...", git_version);
|
||||
|
||||
static const auto progress_callback = [](float updated_progress, uint64_t updated_remaining) {
|
||||
static const auto progress_callback = [](float updated_progress, uint64_t updated_remaining, uint64_t updated_downloaded) -> net_utils::ProgressState * {
|
||||
progress = updated_progress;
|
||||
remaining = updated_remaining;
|
||||
downloaded = updated_downloaded;
|
||||
return &progress_state;
|
||||
};
|
||||
|
||||
@@ -322,7 +324,7 @@ static void download_update(const fs::path &base_path) {
|
||||
std::system(vita3K_batch.c_str());
|
||||
#endif
|
||||
} else {
|
||||
if (progress_state.download) {
|
||||
if (!progress_state.canceled) {
|
||||
LOG_WARN("Download failed, please try again later.");
|
||||
state = NOT_COMPLETE_UPDATE;
|
||||
} else
|
||||
@@ -513,7 +515,7 @@ void draw_vita3k_update(GuiState &gui, EmuEnvState &emuenv) {
|
||||
ImGui::SameLine(0, 40.f * SCALE.x);
|
||||
if (ImGui::Button(common["yes"].c_str(), LARGE_BUTTON_SIZE)) {
|
||||
std::unique_lock<std::mutex> lock(progress_state.mutex);
|
||||
progress_state.download = false;
|
||||
progress_state.canceled = true;
|
||||
progress_state.pause = false; // Unpause so it gets handled by the callback
|
||||
progress_state.cv.notify_one();
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ struct DialogLangState {
|
||||
std::map<std::string, std::string> common = {
|
||||
{ "an_error_occurred", "An error occurred.\nError code: {}" },
|
||||
{ "cancel", "Cancel" },
|
||||
{ "connecting_please_wait", "Connecting...\nPlease wait." },
|
||||
{ "close", "Close" },
|
||||
{ "could_not_load", "Could not load the file." },
|
||||
{ "could_not_save", "Could not save the file." },
|
||||
@@ -47,7 +48,11 @@ struct DialogLangState {
|
||||
{ "select_all", "Select All" },
|
||||
{ "select", "Select" },
|
||||
{ "submit", "Submit" },
|
||||
{ "yes", "Yes" }
|
||||
{ "yes", "Yes" },
|
||||
{ "could_not_connect_server", "Could not connect to the server." },
|
||||
{ "must_sign_in_vita3k_network", "You must sign in to Vita3K Network to use this feature." },
|
||||
{ "could_not_connect_internet", "Could not connect to the Internet." },
|
||||
{ "vita3k_network_connection_lost", "The connection to Vita3K Network has been lost." }
|
||||
};
|
||||
std::map<std::string, std::string> message = { { "load_app_failed", "Failed to load \"{}\".\nCheck vita3k.log to see console output for details.\n1. Have you installed the firmware?\n2. Re-dump your own PS Vita app/game and install it on Vita3K.\n3. If you want to install or boot Vitamin, it is not supported." } };
|
||||
std::map<std::string, std::string> trophy = { { "preparing_start_app", "Preparing to start the application..." } };
|
||||
@@ -469,6 +474,26 @@ struct LangState {
|
||||
};
|
||||
};
|
||||
LiveArea live_area;
|
||||
std::map<std::string, std::string> online_storage = {
|
||||
{ "save_data_auto_upload", "Saved Data Auto - Upload" },
|
||||
{ "copying", "Copying..." },
|
||||
{ "download", "Download" },
|
||||
{ "eligible", "Eligible" },
|
||||
{ "ineligible", "Ineligible" },
|
||||
{ "location", "Location" },
|
||||
{ "memory_card", "Memory Card" },
|
||||
{ "minutes_left", "{} Minutes Left" },
|
||||
{ "online_storage", "Online Storage" },
|
||||
{ "overwrite_saved_data", "Do you want to overwrite the saved data ?" },
|
||||
{ "ps_vita_system", "PS Vita System" },
|
||||
{ "ps_tv_system", "PS TV System" },
|
||||
{ "seconds_left", "{} Seconds Left" },
|
||||
{ "size", "Size" },
|
||||
{ "trophy_earning", "Trophy Earning" },
|
||||
{ "updated", "Updated" },
|
||||
{ "upload", "Upload" },
|
||||
{ "uploaded", "Uploaded" }
|
||||
};
|
||||
std::map<std::string, std::string> overlay = {
|
||||
{ "title", "Overlay" },
|
||||
{ "gamepad_overlay", "Gamepad Overlay" },
|
||||
|
||||
@@ -313,6 +313,9 @@ void init_lang(LangState &lang, EmuEnvState &emuenv) {
|
||||
// Message
|
||||
set_lang_string(emuenv.common_dialog.lang.message, lang_child.child("message"));
|
||||
|
||||
// Online Storage
|
||||
set_lang_string(lang.online_storage, lang_child.child("online_storage"));
|
||||
|
||||
// Overlay
|
||||
set_lang_string(lang.overlay, lang_child.child("overlay"));
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
|
||||
using Sha256Hash = std::array<uint8_t, 32>;
|
||||
|
||||
std::string base64_encode(const std::string &input);
|
||||
std::string derive_password(const std::string_view &password);
|
||||
Sha256Hash sha256(const void *data, size_t size);
|
||||
typedef std::array<char, 65> Sha256HashText;
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <http/state.h>
|
||||
#include <util/log.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <regex>
|
||||
@@ -36,7 +37,7 @@ struct parsedUrl {
|
||||
struct ProgressState {
|
||||
std::condition_variable cv;
|
||||
std::mutex mutex;
|
||||
bool download = true;
|
||||
bool canceled = false;
|
||||
bool pause = false;
|
||||
};
|
||||
|
||||
@@ -51,12 +52,31 @@ struct AssignedAddr {
|
||||
std::string netMask; // Network mask
|
||||
};
|
||||
|
||||
typedef const std::function<ProgressState *(float, uint64_t)> &ProgressCallback;
|
||||
typedef const std::function<ProgressState *(float, uint64_t, uint64_t)> &ProgressCallback;
|
||||
typedef std::pair<ProgressData, ProgressCallback> CallbackData;
|
||||
|
||||
struct WebResponse {
|
||||
int curl_res = 0;
|
||||
std::string body;
|
||||
};
|
||||
|
||||
struct CurlSession {
|
||||
void *handle = nullptr;
|
||||
void *headers = nullptr;
|
||||
};
|
||||
|
||||
bool download_file(const std::string &url, const std::string &output_file_path, ProgressCallback progress_callback = nullptr);
|
||||
WebResponse download_file_ex(const std::string &url, const std::string &output_file_path, ProgressCallback progress_callback = nullptr, const std::string &token = "", CurlSession *session = nullptr);
|
||||
std::string get_web_response(const std::string &url);
|
||||
WebResponse get_web_response_ex(const std::string &url, const std::string &token = "", const std::string &post_data = "", CurlSession *session = nullptr, const std::atomic<bool> *cancel_flag = nullptr);
|
||||
std::string get_web_regex_result(const std::string &url, const std::regex ®ex);
|
||||
std::vector<std::string> get_web_regex_results(const std::string &url, const std::regex ®ex);
|
||||
WebResponse upload_file(const std::string &url, const std::string &input_file_path, const std::string &token, const std::string &metadata = "", ProgressCallback progress_callback = nullptr, CurlSession *session = nullptr);
|
||||
WebResponse upload_data(const std::string &url, const std::vector<unsigned char> &data, const std::string &filename, const std::string &token, CurlSession *session = nullptr);
|
||||
|
||||
CurlSession init_curl_download_session(const std::string &token, bool is_keep_alive = true);
|
||||
CurlSession init_curl_upload_session(const std::string &token, bool is_keep_alive = true);
|
||||
void cleanup_curl_session(CurlSession &session);
|
||||
|
||||
SceHttpErrorCode parse_url(const std::string &url, parsedUrl &out);
|
||||
const char *int_method_to_char(const int n);
|
||||
|
||||
@@ -15,10 +15,67 @@
|
||||
// with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include <util/hash.h>
|
||||
|
||||
#include <openssl/evp.h>
|
||||
|
||||
static const char b64_table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
|
||||
std::string base64_encode(const std::string &input) {
|
||||
const unsigned char *data = reinterpret_cast<const unsigned char *>(input.data());
|
||||
size_t len = input.size();
|
||||
std::string out;
|
||||
out.reserve(((len + 2) / 3) * 4);
|
||||
|
||||
for (size_t i = 0; i < len; i += 3) {
|
||||
unsigned int val = data[i] << 16;
|
||||
if (i + 1 < len)
|
||||
val |= data[i + 1] << 8;
|
||||
if (i + 2 < len)
|
||||
val |= data[i + 2];
|
||||
|
||||
out.push_back(b64_table[(val >> 18) & 0x3F]);
|
||||
out.push_back(b64_table[(val >> 12) & 0x3F]);
|
||||
out.push_back((i + 1 < len) ? b64_table[(val >> 6) & 0x3F] : '=');
|
||||
out.push_back((i + 2 < len) ? b64_table[val & 0x3F] : '=');
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string derive_password(const std::string_view &password) {
|
||||
const std::string_view salt_str = "No matter where you store, everyone has access somewhere.";
|
||||
|
||||
// Size of SHA3-256 digest
|
||||
constexpr int digest_len = 32; // 256 bits
|
||||
|
||||
unsigned char derived_password_digest[digest_len];
|
||||
|
||||
// PBKDF2-HMAC-SHA3-256
|
||||
if (!PKCS5_PBKDF2_HMAC(
|
||||
password.data(),
|
||||
static_cast<int>(password.size()),
|
||||
reinterpret_cast<const unsigned char *>(salt_str.data()),
|
||||
static_cast<int>(salt_str.size()),
|
||||
200000, // number of iterations
|
||||
EVP_sha3_256(), // SHA3-256 algorithm
|
||||
digest_len,
|
||||
derived_password_digest)) {
|
||||
throw std::runtime_error("PBKDF2 failed");
|
||||
}
|
||||
|
||||
// Conversion to hex
|
||||
std::string derived_password;
|
||||
derived_password.reserve(digest_len * 2);
|
||||
for (int i = 0; i < digest_len; i++) {
|
||||
derived_password += fmt::format("{:02X}", derived_password_digest[i]);
|
||||
}
|
||||
|
||||
return derived_password;
|
||||
}
|
||||
|
||||
Sha256Hash sha256(const void *data, size_t size) {
|
||||
Sha256Hash hash;
|
||||
|
||||
|
||||
+353
-76
@@ -334,41 +334,89 @@ bool socketSetBlocking(int sockfd, bool blocking) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string get_web_response(const std::string &url) {
|
||||
auto curl = curl_easy_init();
|
||||
if (!curl)
|
||||
return {};
|
||||
static int curl_cancel_callback(void *user_data, curl_off_t, curl_off_t, curl_off_t, curl_off_t) {
|
||||
const auto *cancel_flag = static_cast<const std::atomic<bool> *>(user_data);
|
||||
return (cancel_flag && cancel_flag->load()) ? 1 : 0;
|
||||
}
|
||||
|
||||
WebResponse get_web_response_ex(const std::string &url, const std::string &token, const std::string &post_data, CurlSession *session, const std::atomic<bool> *cancel_flag) {
|
||||
WebResponse web_response{};
|
||||
|
||||
const bool has_session_given = session;
|
||||
|
||||
CurlSession local_session;
|
||||
|
||||
if (!has_session_given) {
|
||||
local_session = init_curl_download_session(token, false);
|
||||
if (!local_session.handle) {
|
||||
web_response.curl_res = CURLE_FAILED_INIT;
|
||||
return web_response;
|
||||
}
|
||||
session = &local_session;
|
||||
}
|
||||
|
||||
CURL *curl = static_cast<CURL *>(session->handle);
|
||||
curl_slist *headers = static_cast<curl_slist *>(session->headers);
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, "Vita3K Emulator");
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, true); // Follow redirects
|
||||
curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1L);
|
||||
if (cancel_flag) {
|
||||
curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L);
|
||||
curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, curl_cancel_callback);
|
||||
curl_easy_setopt(curl, CURLOPT_XFERINFODATA, cancel_flag);
|
||||
} else {
|
||||
curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1L);
|
||||
}
|
||||
|
||||
#ifdef __ANDROID__
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
|
||||
#endif
|
||||
|
||||
std::string response_string;
|
||||
if (!post_data.empty()) {
|
||||
curl_easy_setopt(curl, CURLOPT_POST, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post_data.c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, post_data.size());
|
||||
}
|
||||
|
||||
const auto writeFunc = +[](void *ptr, size_t size, size_t nmemb, std::string *data) {
|
||||
data->append((char *)ptr, size * nmemb);
|
||||
return size * nmemb;
|
||||
};
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeFunc);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_string);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &web_response.body);
|
||||
|
||||
if (headers)
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
|
||||
web_response.curl_res = curl_easy_perform(curl);
|
||||
|
||||
long response_code;
|
||||
curl_easy_perform(curl);
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);
|
||||
|
||||
curl_easy_cleanup(curl);
|
||||
if (!has_session_given)
|
||||
cleanup_curl_session(local_session);
|
||||
|
||||
if (response_code / 100 == 2)
|
||||
return response_string;
|
||||
return web_response;
|
||||
|
||||
return {};
|
||||
web_response.body.clear();
|
||||
return web_response;
|
||||
}
|
||||
|
||||
std::string get_web_response(const std::string &url) {
|
||||
const auto response = get_web_response_ex(url, "");
|
||||
return response.body;
|
||||
}
|
||||
|
||||
std::string get_web_regex_result(const std::string &url, const std::regex ®ex) {
|
||||
std::string result;
|
||||
const auto result = get_web_regex_results(url, regex);
|
||||
if (result.empty())
|
||||
return {};
|
||||
|
||||
return result.back();
|
||||
}
|
||||
|
||||
std::vector<std::string> get_web_regex_results(const std::string &url, const std::regex ®ex) {
|
||||
std::vector<std::string> results;
|
||||
|
||||
// Get the response of the web
|
||||
const auto response = get_web_response(url);
|
||||
@@ -378,12 +426,13 @@ std::string get_web_regex_result(const std::string &url, const std::regex ®ex
|
||||
std::smatch match;
|
||||
// Check if the response matches the regex
|
||||
if (std::regex_search(response, match, regex)) {
|
||||
result = match[1];
|
||||
for (size_t i = 1; i < match.size(); ++i)
|
||||
results.push_back(match[i].str());
|
||||
} else
|
||||
LOG_ERROR("No success found regex: {}", response);
|
||||
}
|
||||
|
||||
return result;
|
||||
return results;
|
||||
}
|
||||
|
||||
static uint64_t get_current_time_ms() {
|
||||
@@ -395,56 +444,85 @@ static size_t write_data(void *ptr, size_t size, size_t nmemb, void *stream) {
|
||||
return written;
|
||||
}
|
||||
|
||||
typedef int (*CurlDownloadCallback)(void *c, long t, long d);
|
||||
static int curl_callback(void *user_data, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) {
|
||||
auto *data = (CallbackData *)user_data;
|
||||
|
||||
bool download_file(const std::string &url, const std::string &output_file_path, ProgressCallback progress_callback) {
|
||||
CURL *curl_download = curl_easy_init();
|
||||
if (!curl_download)
|
||||
return false;
|
||||
// Detect mode (download/upload)
|
||||
const bool is_download = (dltotal > 0);
|
||||
const bool is_upload = (ultotal > 0);
|
||||
|
||||
CurlDownloadCallback curl_callback = +[](void *user_data, long total_bytes, long downloaded_bytes) {
|
||||
const auto data = (CallbackData *)user_data;
|
||||
|
||||
if ((total_bytes == 0) || (downloaded_bytes == 0))
|
||||
return 0; // Ignore if we dont have the total yet or if we haven't downloaded anything
|
||||
|
||||
if (!data->second)
|
||||
return 0;
|
||||
|
||||
// Calculate progress percentage
|
||||
const auto total_bytes_downloaded = static_cast<double>(downloaded_bytes + data->first.bytes_already_downloaded);
|
||||
const auto file_size = total_bytes + data->first.bytes_already_downloaded;
|
||||
const auto progress_percent = static_cast<float>(total_bytes_downloaded) / static_cast<float>(file_size) * 100.0f;
|
||||
|
||||
// Calculate elapsed time
|
||||
const auto current_time = get_current_time_ms();
|
||||
const auto elapsed_time_ms = std::difftime(current_time, data->first.time);
|
||||
|
||||
// Calculate remaining time in seconds
|
||||
const auto remaining_bytes = static_cast<double>(total_bytes - downloaded_bytes);
|
||||
const auto remaining_time = static_cast<uint64_t>((remaining_bytes / downloaded_bytes) * elapsed_time_ms) / 1000;
|
||||
|
||||
ProgressState *callback_result = data->second(progress_percent, remaining_time);
|
||||
|
||||
std::unique_lock<std::mutex> lock(callback_result->mutex);
|
||||
|
||||
// Store the current pause state
|
||||
const auto pause = callback_result->pause;
|
||||
|
||||
// Wait until the pause state becomes false (unpaused)
|
||||
callback_result->cv.wait(lock, [&]() {
|
||||
return !callback_result->pause;
|
||||
});
|
||||
|
||||
// When coming out of pause, add the time spent to the start time to keep the time consistent
|
||||
if (pause)
|
||||
data->first.time += std::difftime(get_current_time_ms(), current_time);
|
||||
|
||||
if (!callback_result->download) {
|
||||
return 1; // Returning anything that's not 0 aborts the request
|
||||
}
|
||||
// Skip if curl doesn't have info yet
|
||||
if (!is_download && !is_upload)
|
||||
return 0;
|
||||
};
|
||||
|
||||
if (!data->second)
|
||||
return 0;
|
||||
|
||||
float progress = 0.f;
|
||||
double bytes_done = 0;
|
||||
double bytes_total = 0;
|
||||
|
||||
if (is_download) {
|
||||
bytes_done = static_cast<double>(dlnow);
|
||||
bytes_total = static_cast<double>(dltotal);
|
||||
const auto total_bytes_done = static_cast<double>(dlnow + data->first.bytes_already_downloaded);
|
||||
const auto file_size = static_cast<double>(dltotal + data->first.bytes_already_downloaded);
|
||||
progress = static_cast<float>(total_bytes_done) / (static_cast<float>(file_size));
|
||||
} else if (is_upload) {
|
||||
bytes_done = static_cast<double>(ulnow);
|
||||
bytes_total = static_cast<double>(ultotal);
|
||||
progress = static_cast<float>(bytes_done) / static_cast<float>(bytes_total);
|
||||
}
|
||||
|
||||
if (bytes_done == 0)
|
||||
return 0;
|
||||
|
||||
// Calculate elapsed time
|
||||
const auto current_time = get_current_time_ms();
|
||||
const auto elapsed_time_ms = std::difftime(current_time, data->first.time);
|
||||
|
||||
// Calculate remaining time in seconds
|
||||
const auto remaining_bytes = static_cast<double>(bytes_total - bytes_done);
|
||||
const uint64_t remaining_time = static_cast<uint64_t>((remaining_bytes / (double)bytes_done) * elapsed_time_ms) / 1000;
|
||||
|
||||
// Callback UI
|
||||
ProgressState *callback_result = data->second(progress, remaining_time, bytes_done);
|
||||
|
||||
std::unique_lock<std::mutex> lock(callback_result->mutex);
|
||||
|
||||
const bool was_paused = callback_result->pause;
|
||||
|
||||
callback_result->cv.wait(lock, [&]() {
|
||||
return !callback_result->pause;
|
||||
});
|
||||
|
||||
if (was_paused)
|
||||
data->first.time += std::difftime(get_current_time_ms(), current_time);
|
||||
|
||||
if (callback_result->canceled)
|
||||
return 1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
WebResponse download_file_ex(const std::string &url, const std::string &output_file_path, ProgressCallback progress_callback, const std::string &token, CurlSession *session) {
|
||||
WebResponse web_response{};
|
||||
|
||||
CurlSession local_session;
|
||||
|
||||
const bool has_session_given = session;
|
||||
|
||||
if (!has_session_given) {
|
||||
local_session = init_curl_download_session(token);
|
||||
if (!local_session.handle) {
|
||||
web_response.curl_res = CURLE_FAILED_INIT;
|
||||
return web_response;
|
||||
}
|
||||
session = &local_session;
|
||||
}
|
||||
|
||||
CURL *curl_download = static_cast<CURL *>(session->handle);
|
||||
curl_slist *headers = static_cast<curl_slist *>(session->headers);
|
||||
|
||||
const auto start_time = get_current_time_ms();
|
||||
const uint64_t bytes_already_downloaded = fs::exists(output_file_path) ? fs::file_size(output_file_path) : 0;
|
||||
@@ -456,33 +534,232 @@ bool download_file(const std::string &url, const std::string &output_file_path,
|
||||
curl_easy_setopt(curl_download, CURLOPT_RESUME_FROM_LARGE, bytes_already_downloaded);
|
||||
curl_easy_setopt(curl_download, CURLOPT_XFERINFODATA, &callbackData);
|
||||
curl_easy_setopt(curl_download, CURLOPT_XFERINFOFUNCTION, curl_callback);
|
||||
curl_easy_setopt(curl_download, CURLOPT_FOLLOWLOCATION, true); // Follow redirects
|
||||
|
||||
#ifdef __ANDROID__
|
||||
curl_easy_setopt(curl_download, CURLOPT_SSL_VERIFYPEER, 0L);
|
||||
#endif
|
||||
if (headers)
|
||||
curl_easy_setopt(curl_download, CURLOPT_HTTPHEADER, headers);
|
||||
|
||||
auto fp = fopen(output_file_path.c_str(), "ab");
|
||||
if (!fp) {
|
||||
LOG_CRITICAL("Could not fopen file {}", output_file_path);
|
||||
curl_easy_cleanup(curl_download);
|
||||
if (!has_session_given)
|
||||
cleanup_curl_session(local_session);
|
||||
if (fs::exists(output_file_path))
|
||||
fs::remove(output_file_path);
|
||||
return false;
|
||||
web_response.curl_res = CURLE_FAILED_INIT;
|
||||
return web_response;
|
||||
}
|
||||
|
||||
curl_easy_setopt(curl_download, CURLOPT_WRITEDATA, fp);
|
||||
int res = curl_easy_perform(curl_download);
|
||||
web_response.curl_res = curl_easy_perform(curl_download);
|
||||
|
||||
long http_code = 0;
|
||||
curl_easy_getinfo(curl_download, CURLINFO_RESPONSE_CODE, &http_code);
|
||||
if ((http_code != 200) && (http_code != 206)) {
|
||||
LOG_WARN("Download failed with HTTP code: {}", http_code);
|
||||
fclose(fp);
|
||||
if (!has_session_given)
|
||||
cleanup_curl_session(local_session);
|
||||
if (fs::exists(output_file_path))
|
||||
fs::remove(output_file_path);
|
||||
web_response.curl_res = CURLE_HTTP_RETURNED_ERROR;
|
||||
return web_response;
|
||||
}
|
||||
|
||||
fclose(fp);
|
||||
curl_easy_cleanup(curl_download);
|
||||
if (progress_callback)
|
||||
progress_callback(0, 0);
|
||||
|
||||
if (res == CURLE_ABORTED_BY_CALLBACK)
|
||||
LOG_CRITICAL("Aborted update by user");
|
||||
if (!has_session_given)
|
||||
cleanup_curl_session(local_session);
|
||||
|
||||
return res == CURLE_OK;
|
||||
if (web_response.curl_res == CURLE_ABORTED_BY_CALLBACK)
|
||||
LOG_WARN("Aborted by user");
|
||||
|
||||
return web_response;
|
||||
}
|
||||
|
||||
bool download_file(const std::string &url, const std::string &output_file_path, ProgressCallback progress_callback) {
|
||||
return download_file_ex(url, output_file_path, progress_callback).curl_res == CURLE_OK;
|
||||
}
|
||||
|
||||
static size_t write_string(void *ptr, size_t size, size_t nmemb, void *userdata) {
|
||||
std::stringstream *ss = static_cast<std::stringstream *>(userdata);
|
||||
ss->write(static_cast<char *>(ptr), size * nmemb);
|
||||
return size * nmemb;
|
||||
}
|
||||
|
||||
static WebResponse upload_impl(const std::string &url, const char *data, size_t data_size, const std::string &filename, const std::string &token, const std::string &metadata, ProgressCallback progress_callback, CurlSession *session) {
|
||||
WebResponse response{};
|
||||
|
||||
const bool has_session_given = session;
|
||||
|
||||
CurlSession local_session;
|
||||
|
||||
if (!has_session_given) {
|
||||
local_session = init_curl_upload_session(token, false);
|
||||
if (!local_session.handle) {
|
||||
response.curl_res = CURLE_FAILED_INIT;
|
||||
return response;
|
||||
}
|
||||
session = &local_session;
|
||||
}
|
||||
|
||||
CURL *curl_upload = static_cast<CURL *>(session->handle);
|
||||
curl_slist *headers = static_cast<curl_slist *>(session->headers);
|
||||
|
||||
curl_easy_setopt(curl_upload, CURLOPT_URL, url.c_str());
|
||||
|
||||
const auto start_time = get_current_time_ms();
|
||||
const auto callbackData = CallbackData({ start_time }, progress_callback);
|
||||
curl_easy_setopt(curl_upload, CURLOPT_NOPROGRESS, progress_callback ? false : true);
|
||||
if (progress_callback) {
|
||||
curl_easy_setopt(curl_upload, CURLOPT_XFERINFODATA, &callbackData);
|
||||
curl_easy_setopt(curl_upload, CURLOPT_XFERINFOFUNCTION, curl_callback);
|
||||
}
|
||||
|
||||
std::stringstream response_stream;
|
||||
curl_easy_setopt(curl_upload, CURLOPT_WRITEFUNCTION, write_string);
|
||||
curl_easy_setopt(curl_upload, CURLOPT_WRITEDATA, &response_stream);
|
||||
|
||||
if (headers)
|
||||
curl_easy_setopt(curl_upload, CURLOPT_HTTPHEADER, headers);
|
||||
|
||||
curl_mime *mime = curl_mime_init(curl_upload);
|
||||
curl_mimepart *part = nullptr;
|
||||
|
||||
part = curl_mime_addpart(mime);
|
||||
curl_mime_name(part, "file");
|
||||
curl_mime_filename(part, filename.c_str());
|
||||
curl_mime_data(part, data, data_size);
|
||||
|
||||
if (!metadata.empty()) {
|
||||
part = curl_mime_addpart(mime);
|
||||
curl_mime_name(part, "xml");
|
||||
curl_mime_data(part, metadata.c_str(), CURL_ZERO_TERMINATED);
|
||||
}
|
||||
|
||||
curl_easy_setopt(curl_upload, CURLOPT_MIMEPOST, mime);
|
||||
|
||||
response.curl_res = curl_easy_perform(curl_upload);
|
||||
response.body = response_stream.str();
|
||||
|
||||
curl_mime_free(mime);
|
||||
|
||||
if (!has_session_given)
|
||||
cleanup_curl_session(local_session);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
WebResponse upload_file(const std::string &url, const std::string &input_file_path, const std::string &token, const std::string &metadata, ProgressCallback progress_callback, CurlSession *session) {
|
||||
WebResponse response{};
|
||||
|
||||
std::ifstream file(input_file_path, std::ios::binary);
|
||||
if (!file) {
|
||||
LOG_CRITICAL("Could not open file {}", input_file_path);
|
||||
response.curl_res = CURLE_READ_ERROR;
|
||||
return response;
|
||||
}
|
||||
|
||||
std::vector<char> file_buffer((std::istreambuf_iterator<char>(file)),
|
||||
std::istreambuf_iterator<char>());
|
||||
|
||||
return upload_impl(url, file_buffer.data(), file_buffer.size(), fs::path(input_file_path).filename().string(), token, metadata, progress_callback, session);
|
||||
}
|
||||
|
||||
WebResponse upload_data(const std::string &url, const std::vector<unsigned char> &data, const std::string &filename, const std::string &token, CurlSession *session) {
|
||||
WebResponse response{};
|
||||
|
||||
if (data.empty()) {
|
||||
response.curl_res = CURLE_READ_ERROR;
|
||||
return response;
|
||||
}
|
||||
|
||||
return upload_impl(url, reinterpret_cast<const char *>(data.data()), data.size(), filename, token, "", nullptr, session);
|
||||
}
|
||||
|
||||
CurlSession init_curl_download_session(const std::string &token, bool is_keep_alive) {
|
||||
CurlSession session;
|
||||
session.handle = curl_easy_init();
|
||||
|
||||
if (!session.handle)
|
||||
return session;
|
||||
|
||||
CURL *curl = static_cast<CURL *>(session.handle);
|
||||
|
||||
std::string os;
|
||||
#if defined(_WIN32)
|
||||
os = "Windows";
|
||||
#elif defined(__ANDROID__)
|
||||
os = "Android";
|
||||
#elif defined(__linux__)
|
||||
os = "Linux";
|
||||
#elif defined(__APPLE__)
|
||||
os = "macOS";
|
||||
#else
|
||||
os = "Unknown OS";
|
||||
#endif
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, ("Vita3K Emulator on (" + os + ")").c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, true);
|
||||
if (is_keep_alive) {
|
||||
curl_easy_setopt(curl, CURLOPT_TCP_KEEPALIVE, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_TCP_KEEPIDLE, 120L);
|
||||
curl_easy_setopt(curl, CURLOPT_TCP_KEEPINTVL, 60L);
|
||||
}
|
||||
|
||||
#ifdef __ANDROID__
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
|
||||
#endif
|
||||
|
||||
if (!token.empty()) {
|
||||
curl_slist *headers = nullptr;
|
||||
std::string auth_header = "Authorization: Bearer " + token;
|
||||
headers = curl_slist_append(headers, auth_header.c_str());
|
||||
session.headers = headers;
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
CurlSession init_curl_upload_session(const std::string &token, bool is_keep_alive) {
|
||||
CurlSession session;
|
||||
session.handle = curl_easy_init();
|
||||
|
||||
if (!session.handle)
|
||||
return session;
|
||||
|
||||
CURL *curl = static_cast<CURL *>(session.handle);
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, "Vita3K Emulator");
|
||||
|
||||
if (is_keep_alive) {
|
||||
curl_easy_setopt(curl, CURLOPT_TCP_KEEPALIVE, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_TCP_KEEPIDLE, 120L);
|
||||
curl_easy_setopt(curl, CURLOPT_TCP_KEEPINTVL, 60L);
|
||||
}
|
||||
|
||||
#ifdef __ANDROID__
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
|
||||
#endif
|
||||
|
||||
if (!token.empty()) {
|
||||
curl_slist *headers = nullptr;
|
||||
std::string auth_header = "Authorization: Bearer " + token;
|
||||
headers = curl_slist_append(headers, auth_header.c_str());
|
||||
session.headers = headers;
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
void cleanup_curl_session(CurlSession &session) {
|
||||
if (session.headers) {
|
||||
curl_slist_free_all(static_cast<curl_slist *>(session.headers));
|
||||
session.headers = nullptr;
|
||||
}
|
||||
if (session.handle) {
|
||||
curl_easy_cleanup(static_cast<CURL *>(session.handle));
|
||||
session.handle = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<AssignedAddr> get_all_assigned_addrs() {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
add_library(
|
||||
v3kn
|
||||
STATIC
|
||||
include/v3kn/account.h
|
||||
include/v3kn/state.h
|
||||
include/v3kn/storage.h
|
||||
src/account.cpp
|
||||
src/storage.cpp
|
||||
)
|
||||
|
||||
target_include_directories(v3kn PUBLIC include)
|
||||
target_link_libraries(v3kn PUBLIC gui emuenv util packages)
|
||||
target_link_libraries(v3kn PRIVATE pugixml::pugixml miniz stb)
|
||||
@@ -0,0 +1,71 @@
|
||||
// Vita3K emulator project
|
||||
// Copyright (C) 2026 Vita3K team
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation; either version 2 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along
|
||||
// with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <util/net_utils.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct EmuEnvState;
|
||||
struct GuiState;
|
||||
|
||||
inline const std::vector<const char *> V3KN_SERVER_LIST_NAME = { "Official V3KN Server", "Local V3KN Test Server" };
|
||||
inline const std::vector<const char *> V3KN_SERVER_LIST_URL = { "np.vita3k.org", "localhost:3000" };
|
||||
inline const std::map<std::string, std::string> V3KN_SERVER_PROTOCOL = {
|
||||
{ "np.vita3k.org", "https://" },
|
||||
{ "localhost:3000", "http://" },
|
||||
};
|
||||
|
||||
struct UserInfo {
|
||||
std::string host = V3KN_SERVER_LIST_URL[0];
|
||||
std::string online_id;
|
||||
std::string password;
|
||||
std::string token;
|
||||
time_t created_at = 0;
|
||||
uint64_t quota_used = 0;
|
||||
uint64_t quota_total = 0;
|
||||
};
|
||||
|
||||
namespace v3kn {
|
||||
|
||||
void set_v3kn_logged_in(bool logged_in);
|
||||
bool is_v3kn_logged_in();
|
||||
bool wait_for_v3kn_login(uint32_t timeout_ms);
|
||||
void handle_v3kn_status(EmuEnvState &emuenv, const net_utils::WebResponse &response);
|
||||
std::string get_v3kn_error_message(EmuEnvState &emuenv, const net_utils::WebResponse &response);
|
||||
std::string get_v3kn_server_url(const std::string &host, const std::string &path);
|
||||
|
||||
net_utils::WebResponse v3kn_create(const UserInfo &user_info, const std::string &online_id, const std::string &password);
|
||||
net_utils::WebResponse v3kn_login(const UserInfo &user_info, const std::string &online_id, const std::string &password);
|
||||
net_utils::WebResponse v3kn_delete(const UserInfo &user_info, const std::string &password);
|
||||
net_utils::WebResponse v3kn_change_online_id(const UserInfo &user_info, const std::string &new_online_id);
|
||||
net_utils::WebResponse v3kn_change_password(const UserInfo &user_info, const std::string &old_password, const std::string &new_password);
|
||||
net_utils::WebResponse v3kn_change_about_me(const UserInfo &user_info, const std::string &about_me);
|
||||
net_utils::WebResponse v3kn_avatar_upload(const UserInfo &user_info, const std::string &file_path);
|
||||
net_utils::WebResponse v3kn_avatar_upload(const UserInfo &user_info, const std::vector<unsigned char> &png_data);
|
||||
net_utils::WebResponse v3kn_avatar_download(const UserInfo &user_info, const std::string &target_online_id, net_utils::CurlSession *session = nullptr);
|
||||
net_utils::WebResponse v3kn_panel_upload(const UserInfo &user_info, const std::vector<unsigned char> &png_data);
|
||||
net_utils::WebResponse v3kn_panel_download(const UserInfo &user_info, const std::string &target_online_id, net_utils::CurlSession *session = nullptr);
|
||||
void save_v3kn_user_info(EmuEnvState &emuenv);
|
||||
void init_v3kn_user_info(GuiState &gui, EmuEnvState &emuenv);
|
||||
|
||||
} // namespace v3kn
|
||||
@@ -0,0 +1,59 @@
|
||||
// Vita3K emulator project
|
||||
// Copyright (C) 2026 Vita3K team
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation; either version 2 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along
|
||||
// with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <v3kn/account.h>
|
||||
#include <v3kn/storage.h>
|
||||
|
||||
#include <util/net_utils.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <ctime>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
struct AccountState {
|
||||
UserInfo user_info;
|
||||
std::atomic<bool> is_logged_in{ false };
|
||||
uint32_t selected_server_index = 0;
|
||||
};
|
||||
|
||||
struct StorageState {
|
||||
OnlineStorageState online_storage_state = ONLINE_STORAGE_SELECT;
|
||||
std::map<SaveDataType, SaveDataInfo> savedata_info;
|
||||
std::atomic<float> progress{ 0 };
|
||||
std::atomic<uint64_t> remaining{ 0 };
|
||||
std::atomic<uint64_t> bytes_done{ 0 };
|
||||
net_utils::ProgressState progress_state{};
|
||||
|
||||
uint64_t progress_done_timestamp = 0;
|
||||
uint64_t please_wait_timestamp = 0;
|
||||
std::atomic<bool> please_wait_done{ false };
|
||||
std::mutex mutex_progress;
|
||||
std::condition_variable cv_progress;
|
||||
};
|
||||
|
||||
struct V3KNState {
|
||||
AccountState account_state;
|
||||
StorageState storage_state;
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
// Vita3K emulator project
|
||||
// Copyright (C) 2026 Vita3K team
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation; either version 2 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along
|
||||
// with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <np/trophy/context.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <ctime>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct EmuEnvState;
|
||||
struct GuiState;
|
||||
|
||||
struct SaveEntry {
|
||||
std::string rel_path;
|
||||
time_t last_updated;
|
||||
};
|
||||
|
||||
struct SaveDataInfo {
|
||||
time_t last_updated;
|
||||
time_t last_uploaded;
|
||||
uint64_t total_size;
|
||||
std::vector<SaveEntry> files;
|
||||
};
|
||||
|
||||
struct TrophySync {
|
||||
time_t last_updated;
|
||||
std::vector<std::string> unlocked_ids;
|
||||
uint32_t platinum = 0;
|
||||
uint32_t gold = 0;
|
||||
uint32_t silver = 0;
|
||||
uint32_t bronze = 0;
|
||||
uint32_t progress = 0;
|
||||
};
|
||||
|
||||
struct TrophyProgressInfo {
|
||||
std::map<std::string, uint32_t> progress = {};
|
||||
std::map<std::string, std::vector<std::string>> unlocked_ids = {};
|
||||
std::map<std::string, std::map<np::trophy::SceNpTrophyGrade, uint32_t>> unlocked_type_count = {};
|
||||
time_t last_updated;
|
||||
};
|
||||
|
||||
enum SaveDataType {
|
||||
SAVE_DATA_TYPE_CLOUD = 0,
|
||||
SAVE_DATA_TYPE_LOCAL = 1,
|
||||
};
|
||||
|
||||
enum OnlineStorageState {
|
||||
ONLINE_STORAGE_SELECT,
|
||||
ONLINE_STORAGE_UPLOAD,
|
||||
ONLINE_STORAGE_DOWNLOAD,
|
||||
ONLINE_STORAGE_COPYING,
|
||||
};
|
||||
|
||||
bool load_and_compute_trophy_stats(EmuEnvState &emuenv, np::trophy::Context &context, const std::string &np_com_id, TrophyProgressInfo &trophy_progress_info);
|
||||
|
||||
namespace v3kn {
|
||||
void start_trophy_sync(GuiState &gui, EmuEnvState &emuenv, std::map<std::string, TrophySync> &trophy_progress, std::atomic<float> &sync_progress, std::atomic<bool> &sync_cancel, std::vector<std::string> &sync_downloaded);
|
||||
void start_trophy_auto_sync(GuiState &gui, EmuEnvState &emuenv);
|
||||
void add_pending_trophy_sync(EmuEnvState &emuenv, const std::string &np_com_id);
|
||||
void download_savedata(GuiState &gui, EmuEnvState &emuenv, const std::string &titleid);
|
||||
void upload_savedata(GuiState &gui, EmuEnvState &emuenv, const std::string &titleid);
|
||||
void open_online_storage(GuiState &gui, EmuEnvState &emuenv, const std::string &titleid);
|
||||
void sync_game_stitle(EmuEnvState &emuenv);
|
||||
|
||||
} // namespace v3kn
|
||||
@@ -0,0 +1,386 @@
|
||||
// Vita3K emulator project
|
||||
// Copyright (C) 2026 Vita3K team
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation; either version 2 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along
|
||||
// with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
#include <v3kn/account.h>
|
||||
#include <v3kn/state.h>
|
||||
|
||||
#include <config/state.h>
|
||||
#include <dialog/state.h>
|
||||
#include <emuenv/state.h>
|
||||
#include <gui/state.h>
|
||||
#include <io/state.h>
|
||||
|
||||
#include <util/fs.h>
|
||||
#include <util/log.h>
|
||||
#include <util/net_utils.h>
|
||||
#include <util/safe_time.h>
|
||||
#include <util/vector_utils.h>
|
||||
|
||||
#include <pugixml.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstdlib>
|
||||
#include <mutex>
|
||||
#include <stb_image.h>
|
||||
#include <thread>
|
||||
|
||||
namespace v3kn {
|
||||
|
||||
static std::atomic<bool> is_v3kn_user_logged_in(false);
|
||||
static std::condition_variable v3kn_login_cv;
|
||||
static std::mutex v3kn_login_mutex;
|
||||
static bool v3kn_login_init_completed = false;
|
||||
constexpr char V3KN_VERSION[] = "0.01";
|
||||
|
||||
static fs::path get_v3kn_avatar_cache_path(const EmuEnvState &emuenv, const UserInfo &user_info, const std::string &online_id) {
|
||||
return emuenv.cache_path / "v3kn" / user_info.host / online_id / "avatar.png";
|
||||
}
|
||||
|
||||
static bool load_v3kn_avatar_texture(GuiState &gui, const fs::path &avatar_path) {
|
||||
if (!fs::exists(avatar_path))
|
||||
return false;
|
||||
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
stbi_uc *data = stbi_load(avatar_path.string().c_str(), &width, &height, nullptr, STBI_rgb_alpha);
|
||||
if (!data)
|
||||
return false;
|
||||
|
||||
gui.v3kn_avatar = ImGui_Texture(gui.imgui_state.get(), data, width, height);
|
||||
stbi_image_free(data);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void init_v3kn_avatar(GuiState &gui, EmuEnvState &emuenv, const UserInfo &user_info) {
|
||||
gui.v3kn_avatar = {};
|
||||
|
||||
if (user_info.online_id.empty() || user_info.token.empty())
|
||||
return;
|
||||
|
||||
const fs::path avatar_cache_path = get_v3kn_avatar_cache_path(emuenv, user_info, user_info.online_id);
|
||||
if (load_v3kn_avatar_texture(gui, avatar_cache_path))
|
||||
return;
|
||||
|
||||
fs::create_directories(avatar_cache_path.parent_path());
|
||||
|
||||
const auto res = v3kn_avatar_download(user_info, user_info.online_id);
|
||||
handle_v3kn_status(emuenv, res);
|
||||
if (!res.body.starts_with("OK") || res.body.empty())
|
||||
return;
|
||||
|
||||
fs_utils::dump_data(avatar_cache_path, res.body.data(), static_cast<std::streamsize>(res.body.size()));
|
||||
load_v3kn_avatar_texture(gui, avatar_cache_path);
|
||||
}
|
||||
|
||||
static void set_v3kn_login_init_completed(bool completed) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(v3kn_login_mutex);
|
||||
v3kn_login_init_completed = completed;
|
||||
}
|
||||
|
||||
v3kn_login_cv.notify_all();
|
||||
}
|
||||
|
||||
void set_v3kn_logged_in(bool logged_in) {
|
||||
is_v3kn_user_logged_in.store(logged_in);
|
||||
v3kn_login_cv.notify_all();
|
||||
}
|
||||
|
||||
bool is_v3kn_logged_in() {
|
||||
return is_v3kn_user_logged_in.load();
|
||||
}
|
||||
|
||||
bool wait_for_v3kn_login(uint32_t timeout_ms) {
|
||||
std::unique_lock<std::mutex> lock(v3kn_login_mutex);
|
||||
v3kn_login_cv.wait_for(lock, std::chrono::milliseconds(timeout_ms), [] {
|
||||
return is_v3kn_user_logged_in.load() || v3kn_login_init_completed;
|
||||
});
|
||||
|
||||
return is_v3kn_user_logged_in.load();
|
||||
}
|
||||
|
||||
std::string get_v3kn_error_message(EmuEnvState &emuenv, const net_utils::WebResponse &response) {
|
||||
std::string error_msg;
|
||||
if (response.body.starts_with("ERR:") || !response.body.starts_with("OK:")) {
|
||||
if (response.body == "ERR:MissingToken")
|
||||
error_msg = emuenv.common_dialog.lang.common["must_sign_in_vita3k_network"];
|
||||
else if (response.body == "ERR:InvalidToken")
|
||||
error_msg = "Your session on Vita3K Network has expired. Please sign in again.";
|
||||
else if (response.body == "ERR:MissingTitleID")
|
||||
error_msg = "V3KN get error Missing title ID.";
|
||||
else if (response.body == "ERR:UserExists")
|
||||
error_msg = "V3KN create account error User already exists.";
|
||||
else if (response.body == "ERR:MissingPassword")
|
||||
error_msg = "V3KN get error Missing password.";
|
||||
else if (response.body == "ERR:InvalidOnlineID")
|
||||
error_msg = "Invalid Online ID for V3KN.\nMake sure it follows the required format.";
|
||||
else if (response.body == "ERR:InvalidPassword")
|
||||
error_msg = "V3KN login/change password/delete error Invalid password.";
|
||||
else if (response.body == "ERR:ServerError")
|
||||
error_msg = "V3KN server error.";
|
||||
else if (response.body == "ERR:QuotaExceeded")
|
||||
error_msg = "V3KN error Quota exceeded.";
|
||||
else if (response.body == "ERR:UserNotFound")
|
||||
error_msg = "V3KN login error User not found.";
|
||||
else if (response.body == "ERR:InvalidType")
|
||||
error_msg = "V3KN error Invalid type of download file.";
|
||||
else if (response.body == "ERR:InvalidID")
|
||||
error_msg = "V3KN error Invalid ID of App.";
|
||||
else if (response.body == "ERR:FileNotFound")
|
||||
error_msg = "V3KN error File not found.";
|
||||
else if (response.body == "ERR:MissingNewPassword")
|
||||
error_msg = "V3KN change password error Missing new password.";
|
||||
else if (response.body == "ERR:MissingOldPassword")
|
||||
error_msg = "V3KN change password error Missing old password.";
|
||||
else if (response.body == "ERR:SamePassword")
|
||||
error_msg = "V3KN change password error New password cannot be same as the old password.";
|
||||
else if (response.body == "ERR:MissingTargetOnlineId")
|
||||
error_msg = "V3KN error Missing target online ID.";
|
||||
else if (response.body == "ERR:AlreadyFriends")
|
||||
error_msg = "V3KN error Already friends with this user.";
|
||||
else if (response.body == "ERR:RequestAlreadySent")
|
||||
error_msg = "V3KN error Friend request already sent to this user.";
|
||||
else if (response.body == "ERR:NoRequestFound")
|
||||
error_msg = "V3KN error No friend request found from this user.";
|
||||
else if (response.body == "ERR:CannotAddYourself")
|
||||
error_msg = "V3KN error Cannot add yourself as a friend.";
|
||||
else if (response.body == "ERR:AboutMeTooLong")
|
||||
error_msg = "V3KN error 'About Me' text is too long.";
|
||||
else if (response.body == "ERR:OutdatedClient")
|
||||
error_msg = "Outdated client version. Please update Vita3K to the latest version.";
|
||||
else if (response.body.empty()) {
|
||||
if (response.curl_res == 3)
|
||||
error_msg = "V3KN get error Malformed URL, check server URL.";
|
||||
else if (response.curl_res == 6)
|
||||
error_msg = emuenv.common_dialog.lang.common["could_not_connect_internet"];
|
||||
else if (response.curl_res == 7)
|
||||
error_msg = emuenv.common_dialog.lang.common["could_not_connect_server"];
|
||||
else if ((response.curl_res == 55) || (response.curl_res == 56))
|
||||
error_msg = emuenv.common_dialog.lang.common["vita3k_network_connection_lost"];
|
||||
else
|
||||
error_msg = fmt::format("V3KN get Unexpected server error: {}", response.curl_res);
|
||||
} else {
|
||||
error_msg = fmt::format("V3KN: Unexpected server response: {}", response.body);
|
||||
}
|
||||
}
|
||||
|
||||
return error_msg;
|
||||
}
|
||||
|
||||
std::string get_v3kn_server_url(const std::string &host, const std::string &path) {
|
||||
if (V3KN_SERVER_PROTOCOL.find(host) == V3KN_SERVER_PROTOCOL.end()) {
|
||||
LOG_WARN("V3KN: Unknown server host {}, defaulting to https", host);
|
||||
return fmt::format("https://{}/{}", host, path);
|
||||
}
|
||||
|
||||
return V3KN_SERVER_PROTOCOL.at(host) + host + "/" + path;
|
||||
}
|
||||
|
||||
void handle_v3kn_status(EmuEnvState &emuenv, const net_utils::WebResponse &response) {
|
||||
if (response.body.starts_with("OK:")) {
|
||||
set_v3kn_logged_in(true);
|
||||
} else if (response.body.starts_with("ERR:")) {
|
||||
if (response.body == "ERR:InvalidToken") {
|
||||
set_v3kn_logged_in(false);
|
||||
emuenv.v3kn.account_state.user_info.token.clear();
|
||||
save_v3kn_user_info(emuenv);
|
||||
}
|
||||
} else if (!response.body.empty() && response.curl_res == 0) {
|
||||
set_v3kn_logged_in(true);
|
||||
}
|
||||
}
|
||||
|
||||
net_utils::WebResponse v3kn_create(const UserInfo &user_info, const std::string &online_id, const std::string &password) {
|
||||
const std::string url = get_v3kn_server_url(user_info.host, "v3kn/create");
|
||||
const std::string post_data = fmt::format("version={}&online_id={}&password={}", V3KN_VERSION, online_id, password);
|
||||
|
||||
const auto res = net_utils::get_web_response_ex(url, "", post_data);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
net_utils::WebResponse v3kn_delete(const UserInfo &user_info, const std::string &password) {
|
||||
const std::string url = get_v3kn_server_url(user_info.host, "v3kn/delete");
|
||||
const std::string post_data = fmt::format("password={}", password);
|
||||
const auto res = net_utils::get_web_response_ex(url, user_info.token, post_data);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
net_utils::WebResponse v3kn_login(const UserInfo &user_info, const std::string &online_id, const std::string &password) {
|
||||
const std::string url = get_v3kn_server_url(user_info.host, "v3kn/login");
|
||||
const std::string post_data = fmt::format("version={}&online_id={}&password={}", V3KN_VERSION, online_id, password);
|
||||
|
||||
const auto res = net_utils::get_web_response_ex(url, "", post_data);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
net_utils::WebResponse v3kn_change_online_id(const UserInfo &user_info, const std::string &new_online_id) {
|
||||
const std::string url = get_v3kn_server_url(user_info.host, "v3kn/change_online_id");
|
||||
const std::string post_data = fmt::format("new_online_id={}", new_online_id);
|
||||
const auto res = net_utils::get_web_response_ex(url, user_info.token, post_data);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
net_utils::WebResponse v3kn_change_password(const UserInfo &user_info, const std::string &old_password, const std::string &new_password) {
|
||||
const std::string url = get_v3kn_server_url(user_info.host, "v3kn/change_password");
|
||||
const std::string post_data = fmt::format("old_password={}&new_password={}", old_password, new_password);
|
||||
const auto res = net_utils::get_web_response_ex(url, user_info.token, post_data);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
net_utils::WebResponse v3kn_change_about_me(const UserInfo &user_info, const std::string &about_me) {
|
||||
const std::string url = get_v3kn_server_url(user_info.host, "v3kn/change_about_me");
|
||||
const std::string post_data = fmt::format("about_me={}", about_me);
|
||||
const auto res = net_utils::get_web_response_ex(url, user_info.token, post_data);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
net_utils::WebResponse v3kn_avatar_upload(const UserInfo &user_info, const std::string &file_path) {
|
||||
const std::string url = get_v3kn_server_url(user_info.host, "v3kn/avatar");
|
||||
const auto res = net_utils::upload_file(url, file_path, user_info.token, "");
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
net_utils::WebResponse v3kn_avatar_upload(const UserInfo &user_info, const std::vector<unsigned char> &png_data) {
|
||||
const std::string url = get_v3kn_server_url(user_info.host, "v3kn/avatar");
|
||||
const auto res = net_utils::upload_data(url, png_data, "Avatar.png", user_info.token);
|
||||
return res;
|
||||
}
|
||||
|
||||
net_utils::WebResponse v3kn_avatar_download(const UserInfo &user_info, const std::string &target_online_id, net_utils::CurlSession *session) {
|
||||
const std::string path = fmt::format("v3kn/avatar?target_online_id={}", target_online_id);
|
||||
const std::string url = get_v3kn_server_url(user_info.host, path);
|
||||
const auto res = net_utils::get_web_response_ex(url, user_info.token, "", session);
|
||||
return res;
|
||||
}
|
||||
|
||||
net_utils::WebResponse v3kn_panel_upload(const UserInfo &user_info, const std::vector<unsigned char> &png_data) {
|
||||
const std::string url = get_v3kn_server_url(user_info.host, "v3kn/panel");
|
||||
const auto res = net_utils::upload_data(url, png_data, "Panel.png", user_info.token);
|
||||
return res;
|
||||
}
|
||||
|
||||
net_utils::WebResponse v3kn_panel_download(const UserInfo &user_info, const std::string &target_online_id, net_utils::CurlSession *session) {
|
||||
const std::string path = fmt::format("v3kn/panel?target_online_id={}", target_online_id);
|
||||
const std::string url = get_v3kn_server_url(user_info.host, path);
|
||||
const auto res = net_utils::get_web_response_ex(url, user_info.token, "", session);
|
||||
return res;
|
||||
}
|
||||
|
||||
void save_v3kn_user_info(EmuEnvState &emuenv) {
|
||||
const fs::path user_info_path{ emuenv.pref_path / "ux0/user" / emuenv.io.user_id / "v3kn.xml" };
|
||||
auto &user_info = emuenv.v3kn.account_state.user_info;
|
||||
pugi::xml_document doc;
|
||||
pugi::xml_node user_node = doc.append_child("user");
|
||||
user_node.append_attribute("version").set_value(1);
|
||||
user_node.append_child("host").text().set(user_info.host.c_str());
|
||||
user_node.append_child("online_id").text().set(user_info.online_id.c_str());
|
||||
user_node.append_child("password").text().set(user_info.password.c_str());
|
||||
user_node.append_child("token").text().set(user_info.token.c_str());
|
||||
doc.save_file(user_info_path.string().c_str());
|
||||
}
|
||||
|
||||
void init_v3kn_user_info(GuiState &gui, EmuEnvState &emuenv) {
|
||||
auto &account_state = emuenv.v3kn.account_state;
|
||||
auto &user_info = account_state.user_info;
|
||||
user_info = {};
|
||||
account_state.selected_server_index = 0;
|
||||
set_v3kn_logged_in(false);
|
||||
set_v3kn_login_init_completed(false);
|
||||
|
||||
const fs::path user_info_path{ emuenv.pref_path / "ux0/user" / emuenv.io.user_id / "v3kn.xml" };
|
||||
pugi::xml_document doc;
|
||||
pugi::xml_parse_result result = doc.load_file(user_info_path.string().c_str());
|
||||
if (!result) {
|
||||
set_v3kn_login_init_completed(true);
|
||||
return;
|
||||
}
|
||||
pugi::xml_node user_node = doc.child("user");
|
||||
if (!user_node) {
|
||||
LOG_WARN("V3KN user info: missing <user> node");
|
||||
set_v3kn_login_init_completed(true);
|
||||
return;
|
||||
}
|
||||
const auto version = user_node.attribute("version").as_uint();
|
||||
if (version != 1) {
|
||||
LOG_WARN("V3KN user info: unsupported version {}", version);
|
||||
set_v3kn_login_init_completed(true);
|
||||
return;
|
||||
}
|
||||
|
||||
user_info.host = user_node.child("host").text().as_string(V3KN_SERVER_LIST_URL[0]);
|
||||
user_info.online_id = user_node.child("online_id").text().as_string();
|
||||
user_info.password = user_node.child("password").text().as_string();
|
||||
user_info.token = user_node.child("token").text().as_string();
|
||||
|
||||
account_state.selected_server_index = static_cast<uint32_t>(vector_utils::find_index(V3KN_SERVER_LIST_URL, user_info.host));
|
||||
if (account_state.selected_server_index == static_cast<uint32_t>(-1))
|
||||
account_state.selected_server_index = 0;
|
||||
|
||||
if (user_info.token.empty()) {
|
||||
set_v3kn_login_init_completed(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string url = get_v3kn_server_url(user_info.host, "v3kn/check?version=" + std::string(V3KN_VERSION));
|
||||
const auto res = net_utils::get_web_response_ex(url, user_info.token, "");
|
||||
|
||||
handle_v3kn_status(emuenv, res);
|
||||
|
||||
if (!res.body.starts_with("OK:")) {
|
||||
gui.info_message.function = "v3kn_check";
|
||||
gui.info_message.title = emuenv.common_dialog.lang.common["error"];
|
||||
gui.info_message.level = spdlog::level::err;
|
||||
gui.info_message.msg = get_v3kn_error_message(emuenv, res);
|
||||
|
||||
set_v3kn_login_init_completed(true);
|
||||
return;
|
||||
}
|
||||
|
||||
std::smatch match;
|
||||
std::regex regex_pattern(R"(OK:Connected:([^:]+):([^:]+):([^:]+):([^]+))");
|
||||
if (std::regex_search(res.body, match, regex_pattern) && (match.size() == 5)) {
|
||||
const std::string current_online_id = match[1].str();
|
||||
user_info.created_at = std::strtoull(match[2].str().c_str(), nullptr, 10);
|
||||
user_info.quota_used = std::strtoull(match[3].str().c_str(), nullptr, 10);
|
||||
user_info.quota_total = std::strtoull(match[4].str().c_str(), nullptr, 10);
|
||||
if (current_online_id != user_info.online_id) {
|
||||
LOG_INFO("V3KN user info: online ID on server ({}) does not match local info ({}), updating local info", current_online_id, user_info.online_id);
|
||||
user_info.online_id = current_online_id;
|
||||
save_v3kn_user_info(emuenv);
|
||||
}
|
||||
} else {
|
||||
LOG_WARN("V3KN user info: unexpected server response, connecting again will be required");
|
||||
set_v3kn_login_init_completed(true);
|
||||
return;
|
||||
}
|
||||
|
||||
set_v3kn_logged_in(true);
|
||||
init_v3kn_avatar(gui, emuenv, user_info);
|
||||
|
||||
set_v3kn_login_init_completed(true);
|
||||
LOG_INFO("V3KN user info: successfully logged in as {}", user_info.online_id);
|
||||
}
|
||||
|
||||
} // namespace v3kn
|
||||
@@ -0,0 +1,408 @@
|
||||
// Vita3K emulator project
|
||||
// Copyright (C) 2026 Vita3K team
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation; either version 2 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along
|
||||
// with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
#include <v3kn/account.h>
|
||||
#include <v3kn/state.h>
|
||||
#include <v3kn/storage.h>
|
||||
|
||||
#include <config/state.h>
|
||||
#include <dialog/state.h>
|
||||
#include <emuenv/state.h>
|
||||
#include <gui/state.h>
|
||||
#include <io/device.h>
|
||||
#include <io/functions.h>
|
||||
#include <io/state.h>
|
||||
|
||||
#include <packages/sfo.h>
|
||||
|
||||
#include <boost/algorithm/string/trim.hpp>
|
||||
|
||||
#include <util/fs.h>
|
||||
#include <util/log.h>
|
||||
#include <util/net_utils.h>
|
||||
|
||||
#include <miniz.h>
|
||||
#include <pugixml.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <ctime>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
#include <thread>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace v3kn {
|
||||
|
||||
// Online Storage
|
||||
static bool get_savedata_info(GuiState &gui, EmuEnvState &emuenv, const std::string &titleid) {
|
||||
emuenv.v3kn.storage_state.savedata_info.clear();
|
||||
|
||||
// Get local savedata info
|
||||
const fs::path save_data_path = { emuenv.pref_path / "ux0/user" / emuenv.io.user_id / "savedata" / titleid };
|
||||
if (fs::exists(save_data_path) && !fs::is_empty(save_data_path)) {
|
||||
auto &save_data_info_local = emuenv.v3kn.storage_state.savedata_info[SAVE_DATA_TYPE_LOCAL];
|
||||
uint64_t total_size_local = 0;
|
||||
time_t global_last_updated = 0;
|
||||
for (auto &file : fs::recursive_directory_iterator(save_data_path)) {
|
||||
if (!file.is_regular_file())
|
||||
continue;
|
||||
|
||||
const auto file_size = fs::file_size(file.path());
|
||||
const auto last_updated = fs::last_write_time(file.path());
|
||||
SaveEntry entry{
|
||||
.rel_path = fs::relative(file.path(), save_data_path).generic_string(),
|
||||
.last_updated = last_updated
|
||||
};
|
||||
save_data_info_local.files.push_back(entry);
|
||||
global_last_updated = std::max(global_last_updated, last_updated);
|
||||
total_size_local += file_size;
|
||||
}
|
||||
save_data_info_local.last_updated = global_last_updated;
|
||||
save_data_info_local.total_size = total_size_local;
|
||||
}
|
||||
|
||||
const std::string url = get_v3kn_server_url(emuenv.v3kn.account_state.user_info.host, fmt::format("v3kn/save_info?titleid={}", titleid));
|
||||
const auto response = net_utils::get_web_response_ex(url, emuenv.v3kn.account_state.user_info.token);
|
||||
|
||||
// Handle response and update connection status
|
||||
handle_v3kn_status(emuenv, response);
|
||||
|
||||
if (response.body.starts_with("WARN:")) {
|
||||
LOG_WARN("V3KN get savedata info: server warning: {}", response.body);
|
||||
return true;
|
||||
} else if (response.body.starts_with("ERR:")) {
|
||||
gui.info_message.title = emuenv.common_dialog.lang.common["error"];
|
||||
gui.info_message.level = spdlog::level::err;
|
||||
gui.info_message.msg = get_v3kn_error_message(emuenv, response);
|
||||
return false;
|
||||
}
|
||||
|
||||
pugi::xml_document doc;
|
||||
pugi::xml_parse_result result = doc.load_string(response.body.c_str());
|
||||
if (!result) {
|
||||
LOG_WARN("V3KN get savedata info: failed to parse XML: {}", result.description());
|
||||
return false;
|
||||
}
|
||||
|
||||
pugi::xml_node savedata_node = doc.child("savedata");
|
||||
if (!savedata_node) {
|
||||
LOG_WARN("V3KN get savedata info: missing <savedata> node");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto &save_data_info_cloud = emuenv.v3kn.storage_state.savedata_info[SAVE_DATA_TYPE_CLOUD];
|
||||
save_data_info_cloud.last_updated = static_cast<time_t>(savedata_node.attribute("last_updated").as_ullong());
|
||||
save_data_info_cloud.last_uploaded = static_cast<time_t>(savedata_node.attribute("last_uploaded").as_ullong());
|
||||
save_data_info_cloud.total_size = savedata_node.attribute("total_size").as_ullong();
|
||||
for (pugi::xml_node file_node : savedata_node.children("file")) {
|
||||
SaveEntry entry{
|
||||
.rel_path = file_node.attribute("path").as_string(),
|
||||
.last_updated = static_cast<time_t>(file_node.attribute("last_updated").as_ullong())
|
||||
};
|
||||
save_data_info_cloud.files.push_back(entry);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool create_zip_from_directory(const fs::path &directory, const fs::path &zip_path) {
|
||||
mz_zip_archive zip;
|
||||
memset(&zip, 0, sizeof(zip));
|
||||
|
||||
if (!mz_zip_writer_init_file(&zip, zip_path.string().c_str(), 0)) {
|
||||
LOG_ERROR("Failed to init zip writer");
|
||||
return false;
|
||||
}
|
||||
|
||||
for (auto &entry : fs::recursive_directory_iterator(directory)) {
|
||||
if (!entry.is_regular_file())
|
||||
continue;
|
||||
|
||||
fs::path file_path = entry.path();
|
||||
fs::path rel_path = fs::relative(file_path, directory);
|
||||
|
||||
if (!mz_zip_writer_add_file(
|
||||
&zip,
|
||||
rel_path.generic_string().c_str(), // path in the zip
|
||||
file_path.string().c_str(), // source file
|
||||
nullptr,
|
||||
0,
|
||||
MZ_BEST_SPEED)) { // fast compression
|
||||
LOG_ERROR("Failed to add file {}", file_path.string());
|
||||
mz_zip_writer_end(&zip);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!mz_zip_writer_finalize_archive(&zip)) {
|
||||
LOG_ERROR("Failed to finalize zip");
|
||||
mz_zip_writer_end(&zip);
|
||||
return false;
|
||||
}
|
||||
|
||||
mz_zip_writer_end(&zip);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool extract_zip_to_directory(const fs::path &zip_path, const fs::path &output_dir) {
|
||||
mz_zip_archive zip;
|
||||
memset(&zip, 0, sizeof(zip));
|
||||
|
||||
if (!mz_zip_reader_init_file(&zip, zip_path.string().c_str(), 0)) {
|
||||
LOG_ERROR("Failed to init zip reader");
|
||||
return false;
|
||||
}
|
||||
|
||||
int file_count = (int)mz_zip_reader_get_num_files(&zip);
|
||||
|
||||
for (int i = 0; i < file_count; i++) {
|
||||
mz_zip_archive_file_stat stat;
|
||||
if (!mz_zip_reader_file_stat(&zip, i, &stat)) {
|
||||
LOG_ERROR("Failed to stat file {}", i);
|
||||
continue;
|
||||
}
|
||||
|
||||
fs::path out_path = output_dir / stat.m_filename;
|
||||
|
||||
if (stat.m_is_directory) {
|
||||
fs::create_directories(out_path);
|
||||
continue;
|
||||
}
|
||||
|
||||
fs::create_directories(out_path.parent_path());
|
||||
|
||||
if (!mz_zip_reader_extract_to_file(&zip, i, out_path.string().c_str(), 0)) {
|
||||
LOG_ERROR("Failed to extract {}", stat.m_filename);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
mz_zip_reader_end(&zip);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void reset_progress(StorageState &storage_state) {
|
||||
storage_state.progress = 0.f;
|
||||
storage_state.bytes_done = 0;
|
||||
storage_state.remaining = 0;
|
||||
storage_state.progress_done_timestamp = 0;
|
||||
storage_state.please_wait_timestamp = 0;
|
||||
storage_state.please_wait_done.store(false);
|
||||
storage_state.progress_state.canceled = false;
|
||||
}
|
||||
|
||||
void download_savedata(GuiState &gui, EmuEnvState &emuenv, const std::string &titleid) {
|
||||
const fs::path savedata_path = emuenv.pref_path / "ux0/user" / emuenv.io.user_id / "savedata" / titleid;
|
||||
const fs::path savedata_cache_path = emuenv.cache_path / fmt::format("{}.psvimg", titleid);
|
||||
|
||||
const auto &savedata_info_cloud = emuenv.v3kn.storage_state.savedata_info[SAVE_DATA_TYPE_CLOUD];
|
||||
|
||||
reset_progress(emuenv.v3kn.storage_state);
|
||||
|
||||
std::thread([&gui, &emuenv, titleid, savedata_path, savedata_cache_path, savedata_info_cloud]() mutable {
|
||||
auto &storage_state = emuenv.v3kn.storage_state;
|
||||
|
||||
auto progress_callback = [&storage_state](float updated_progress, uint64_t updated_remaining, uint64_t updated_bytes_done) {
|
||||
storage_state.progress = updated_progress;
|
||||
storage_state.bytes_done = updated_bytes_done;
|
||||
storage_state.remaining = updated_remaining;
|
||||
return &storage_state.progress_state;
|
||||
};
|
||||
|
||||
const std::string url = get_v3kn_server_url(emuenv.v3kn.account_state.user_info.host, fmt::format("v3kn/download_file?type=savedata&id={}", titleid));
|
||||
|
||||
if (fs::exists(savedata_cache_path))
|
||||
fs::remove(savedata_cache_path);
|
||||
|
||||
const auto savedata_cache_str = fs_utils::path_to_utf8(savedata_cache_path);
|
||||
const auto res = net_utils::download_file_ex(url, savedata_cache_str, progress_callback, emuenv.v3kn.account_state.user_info.token);
|
||||
|
||||
// Handle response and update connection status
|
||||
handle_v3kn_status(emuenv, res);
|
||||
|
||||
// Wait for progress to reach 100% and display please wait dialog a minimum time before proceeding for a better UX
|
||||
{
|
||||
std::unique_lock<std::mutex> lck(storage_state.mutex_progress);
|
||||
storage_state.cv_progress.wait(lck, [&] {
|
||||
return storage_state.please_wait_done.load();
|
||||
});
|
||||
}
|
||||
|
||||
if (res.body.starts_with("ERR:") || (res.curl_res != 0)) {
|
||||
gui.info_message.function = spdlog::level::err;
|
||||
gui.info_message.title = emuenv.common_dialog.lang.common["error"];
|
||||
gui.info_message.msg = get_v3kn_error_message(emuenv, res);
|
||||
gui.vita_area.please_wait = false;
|
||||
LOG_ERROR("Failed to download {}", savedata_cache_str);
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify downloaded file size
|
||||
const auto file_size = fs::file_size(savedata_cache_path);
|
||||
if ((file_size == 0) || (file_size != savedata_info_cloud.total_size)) {
|
||||
fs::remove(savedata_cache_path);
|
||||
gui.info_message.function = spdlog::level::err;
|
||||
gui.info_message.title = emuenv.common_dialog.lang.common["error"];
|
||||
gui.info_message.msg = "Downloaded savedata size mismatch, please try again.";
|
||||
gui.vita_area.please_wait = false;
|
||||
LOG_ERROR("Downloaded savedata size mismatch: expected {}, got {}", savedata_info_cloud.total_size, file_size);
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract savedata zip in a temp folder first to avoid partial overwrite if extraction fails
|
||||
const auto save_data_temp_path = fs::path{ savedata_path.parent_path() / (titleid + "_v3kn") };
|
||||
if (fs::exists(save_data_temp_path))
|
||||
fs::remove_all(save_data_temp_path);
|
||||
const auto ret = extract_zip_to_directory(savedata_cache_path, save_data_temp_path);
|
||||
fs::remove(savedata_cache_path);
|
||||
if (!ret) {
|
||||
fs::remove_all(save_data_temp_path);
|
||||
gui.info_message.function = spdlog::level::err;
|
||||
gui.info_message.title = emuenv.common_dialog.lang.common["error"];
|
||||
gui.info_message.msg = "Failed to extract savedata zip, please try again.";
|
||||
gui.vita_area.please_wait = false;
|
||||
LOG_ERROR("Failed to extract savedata zip for titleid {}", titleid);
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove existing savedata if extracted successfully
|
||||
if (fs::exists(savedata_path))
|
||||
fs::remove_all(savedata_path);
|
||||
|
||||
// Rename temp folder to actual savedata folder
|
||||
fs::rename(save_data_temp_path, savedata_path);
|
||||
|
||||
auto &savedata_info_local = emuenv.v3kn.storage_state.savedata_info[SAVE_DATA_TYPE_LOCAL];
|
||||
savedata_info_local = savedata_info_cloud;
|
||||
uint64_t total_size_local = 0;
|
||||
for (auto &entry : savedata_info_cloud.files) {
|
||||
const fs::path local_file_path = savedata_path / entry.rel_path;
|
||||
total_size_local += fs::file_size(local_file_path);
|
||||
fs::last_write_time(local_file_path, entry.last_updated);
|
||||
}
|
||||
|
||||
savedata_info_local.total_size = total_size_local;
|
||||
fs::last_write_time(savedata_path, savedata_info_cloud.last_updated);
|
||||
gui.vita_area.please_wait = false;
|
||||
LOG_INFO("Download successful for titleid {}", titleid);
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void upload_savedata(GuiState &gui, EmuEnvState &emuenv, const std::string &titleid) {
|
||||
const fs::path savedata_path = { emuenv.pref_path / "ux0/user" / emuenv.io.user_id / "savedata" / titleid };
|
||||
if (!fs::exists(savedata_path) || fs::is_empty(savedata_path)) {
|
||||
LOG_WARN("No savedata to upload for titleid {}", titleid);
|
||||
return;
|
||||
}
|
||||
|
||||
const fs::path savedata_cache_path{ emuenv.cache_path / fmt::format("{}.psvimg", titleid) };
|
||||
|
||||
reset_progress(emuenv.v3kn.storage_state);
|
||||
|
||||
std::thread([&gui, &emuenv, titleid, savedata_path, savedata_cache_path]() mutable {
|
||||
auto &storage_state = emuenv.v3kn.storage_state;
|
||||
|
||||
auto progress_callback = [&storage_state](float updated_progress, uint64_t updated_remaining, uint64_t updated_bytes_done) {
|
||||
storage_state.progress = updated_progress;
|
||||
storage_state.bytes_done = updated_bytes_done;
|
||||
storage_state.remaining = updated_remaining;
|
||||
return &storage_state.progress_state;
|
||||
};
|
||||
|
||||
auto &savedata_info_local = emuenv.v3kn.storage_state.savedata_info[SAVE_DATA_TYPE_LOCAL];
|
||||
|
||||
if (!create_zip_from_directory(savedata_path, savedata_cache_path)) {
|
||||
LOG_ERROR("Failed to create savedata zip");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto total_size = fs::file_size(savedata_cache_path);
|
||||
const auto last_uploaded = std::time(0);
|
||||
|
||||
std::stringstream savedata_info_xml;
|
||||
pugi::xml_document doc;
|
||||
pugi::xml_node savedata_node = doc.append_child("savedata");
|
||||
for (const auto &entry : savedata_info_local.files) {
|
||||
auto file_node = savedata_node.append_child("file");
|
||||
file_node.append_attribute("path").set_value(entry.rel_path.c_str());
|
||||
file_node.append_attribute("last_updated").set_value(static_cast<unsigned long long>(entry.last_updated));
|
||||
}
|
||||
savedata_node.append_attribute("last_uploaded").set_value(static_cast<unsigned long long>(last_uploaded));
|
||||
savedata_node.append_attribute("last_updated").set_value(static_cast<unsigned long long>(savedata_info_local.last_updated));
|
||||
savedata_node.append_attribute("total_size").set_value(total_size);
|
||||
doc.save(savedata_info_xml);
|
||||
|
||||
const std::string url = get_v3kn_server_url(emuenv.v3kn.account_state.user_info.host, fmt::format("v3kn/upload_file?type=savedata&id={}", titleid));
|
||||
|
||||
const std::string filepath_str = fs_utils::path_to_utf8(savedata_cache_path);
|
||||
const auto res = net_utils::upload_file(url, filepath_str, emuenv.v3kn.account_state.user_info.token, savedata_info_xml.str(), progress_callback);
|
||||
// Handle response and update connection status
|
||||
handle_v3kn_status(emuenv, res);
|
||||
|
||||
// Wait for progress to reach 100% and display please wait dialog a minimum time before proceeding for a better UX
|
||||
{
|
||||
std::unique_lock<std::mutex> lck(storage_state.mutex_progress);
|
||||
storage_state.cv_progress.wait(lck, [&] {
|
||||
return storage_state.please_wait_done.load();
|
||||
});
|
||||
}
|
||||
|
||||
fs::remove(savedata_cache_path);
|
||||
|
||||
if (!res.body.starts_with("OK:")) {
|
||||
gui.info_message.function = spdlog::level::err;
|
||||
gui.info_message.title = emuenv.common_dialog.lang.common["error"];
|
||||
gui.info_message.msg = get_v3kn_error_message(emuenv, res);
|
||||
gui.vita_area.please_wait = false;
|
||||
LOG_ERROR("Upload failed for {}", filepath_str);
|
||||
return;
|
||||
}
|
||||
|
||||
std::smatch match;
|
||||
// Expected format: "OK:quota_used:quota_total"
|
||||
std::regex regex_pattern(R"(OK:(\d+):(\d+))");
|
||||
if (std::regex_search(res.body, match, regex_pattern) && match.size() == 3) {
|
||||
// Update user info of quota
|
||||
emuenv.v3kn.account_state.user_info.quota_used = std::strtoull(match[1].str().c_str(), nullptr, 10);
|
||||
emuenv.v3kn.account_state.user_info.quota_total = std::strtoull(match[2].str().c_str(), nullptr, 10);
|
||||
|
||||
// Update cloud savedata info
|
||||
savedata_info_local.last_uploaded = last_uploaded;
|
||||
auto &savedata_info_cloud = emuenv.v3kn.storage_state.savedata_info[SAVE_DATA_TYPE_CLOUD];
|
||||
savedata_info_cloud = savedata_info_local;
|
||||
savedata_info_cloud.total_size = total_size;
|
||||
LOG_INFO("Upload successful for titleid {}", titleid);
|
||||
} else
|
||||
LOG_WARN("Unexpected upload response format: {}", res.body);
|
||||
|
||||
gui.vita_area.please_wait = false;
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void open_online_storage(GuiState &gui, EmuEnvState &emuenv, const std::string &titleid) {
|
||||
gui.vita_area.connecting_please_wait = true;
|
||||
std::thread([&gui, &emuenv, titleid]() mutable {
|
||||
if (get_savedata_info(gui, emuenv, titleid)) {
|
||||
gui.vita_area.online_storage = true;
|
||||
gui.vita_area.home_screen = false;
|
||||
gui.vita_area.live_area_screen = false;
|
||||
}
|
||||
gui.vita_area.connecting_please_wait = false;
|
||||
}).detach();
|
||||
}
|
||||
|
||||
} // namespace v3kn
|
||||
Reference in New Issue
Block a user