diff --git a/src/common/config.cpp b/src/common/config.cpp index 0b720c5b4..36566a14c 100644 --- a/src/common/config.cpp +++ b/src/common/config.cpp @@ -53,6 +53,7 @@ static bool isShaderDebug = false; static bool isShowSplash = false; static bool isAutoUpdate = false; static bool isAlwaysShowChangelog = false; +static bool isLeftSideTrophy = false; static bool isNullGpu = false; static bool shouldCopyGPUBuffers = false; static bool shouldDumpShaders = false; @@ -69,6 +70,7 @@ static bool isFpsColor = true; static bool isSeparateLogFilesEnabled = false; static s16 cursorState = HideCursorState::Idle; static int cursorHideTimeout = 5; // 5 seconds (default) +static double trophyNotificationDuration = 6.0; static bool useUnifiedInputConfig = true; static bool overrideControllerColor = false; static int controllerCustomColorRGB[3] = {0, 0, 255}; @@ -196,6 +198,10 @@ int getCursorHideTimeout() { return cursorHideTimeout; } +double getTrophyNotificationDuration() { + return trophyNotificationDuration; +} + u32 getScreenWidth() { return screenWidth; } @@ -264,6 +270,10 @@ bool alwaysShowChangelog() { return isAlwaysShowChangelog; } +bool leftSideTrophy() { + return isLeftSideTrophy; +} + bool nullGpu() { return isNullGpu; } @@ -371,6 +381,9 @@ void setAutoUpdate(bool enable) { void setAlwaysShowChangelog(bool enable) { isAlwaysShowChangelog = enable; } +void setLeftSideTrophy(bool enable) { + isLeftSideTrophy = enable; +} void setNullGpu(bool enable) { isNullGpu = enable; @@ -435,6 +448,9 @@ void setCursorState(s16 newCursorState) { void setCursorHideTimeout(int newcursorHideTimeout) { cursorHideTimeout = newcursorHideTimeout; } +void setTrophyNotificationDuration(double newTrophyNotificationDuration) { + trophyNotificationDuration = newTrophyNotificationDuration; +} void setLanguage(u32 language) { m_language = language; @@ -706,6 +722,8 @@ void load(const std::filesystem::path& path) { isNeo = toml::find_or(general, "isPS4Pro", false); playBGM = toml::find_or(general, "playBGM", false); isTrophyPopupDisabled = toml::find_or(general, "isTrophyPopupDisabled", false); + trophyNotificationDuration = + toml::find_or(general, "trophyNotificationDuration", 5.0); BGMvolume = toml::find_or(general, "BGMvolume", 50); enableDiscordRPC = toml::find_or(general, "enableDiscordRPC", true); logFilter = toml::find_or(general, "logFilter", ""); @@ -719,6 +737,7 @@ void load(const std::filesystem::path& path) { isShowSplash = toml::find_or(general, "showSplash", true); isAutoUpdate = toml::find_or(general, "autoUpdate", false); isAlwaysShowChangelog = toml::find_or(general, "alwaysShowChangelog", false); + isLeftSideTrophy = toml::find_or(general, "leftSideTrophy", false); separateupdatefolder = toml::find_or(general, "separateUpdateEnabled", false); compatibilityData = toml::find_or(general, "compatibilityEnabled", false); checkCompatibilityOnStartup = @@ -857,6 +876,7 @@ void save(const std::filesystem::path& path) { data["General"]["isPS4Pro"] = isNeo; data["General"]["isTrophyPopupDisabled"] = isTrophyPopupDisabled; + data["General"]["trophyNotificationDuration"] = trophyNotificationDuration; data["General"]["playBGM"] = playBGM; data["General"]["BGMvolume"] = BGMvolume; data["General"]["enableDiscordRPC"] = enableDiscordRPC; @@ -868,6 +888,7 @@ void save(const std::filesystem::path& path) { data["General"]["showSplash"] = isShowSplash; data["General"]["autoUpdate"] = isAutoUpdate; data["General"]["alwaysShowChangelog"] = isAlwaysShowChangelog; + data["General"]["leftSideTrophy"] = isLeftSideTrophy; data["General"]["separateUpdateEnabled"] = separateupdatefolder; data["General"]["compatibilityEnabled"] = compatibilityData; data["General"]["checkCompatibilityOnStartup"] = checkCompatibilityOnStartup; @@ -988,6 +1009,7 @@ void setDefaultValues() { chooseHomeTab = "General"; cursorState = HideCursorState::Idle; cursorHideTimeout = 5; + trophyNotificationDuration = 6.0; backButtonBehavior = "left"; useSpecialPad = false; specialPadClass = 1; @@ -996,6 +1018,7 @@ void setDefaultValues() { isShowSplash = false; isAutoUpdate = false; isAlwaysShowChangelog = false; + isLeftSideTrophy = false; isNullGpu = false; shouldDumpShaders = false; vblankDivider = 1; diff --git a/src/common/config.h b/src/common/config.h index abf8da8aa..988734b93 100644 --- a/src/common/config.h +++ b/src/common/config.h @@ -41,6 +41,7 @@ std::string getChooseHomeTab(); s16 getCursorState(); int getCursorHideTimeout(); +double getTrophyNotificationDuration(); std::string getBackButtonBehavior(); bool getUseSpecialPad(); int getSpecialPadClass(); @@ -62,6 +63,7 @@ bool collectShadersForDebug(); bool showSplash(); bool autoUpdate(); bool alwaysShowChangelog(); +bool leftSideTrophy(); bool nullGpu(); bool copyGPUCmdBuffers(); bool dumpShaders(); @@ -75,6 +77,7 @@ void setCollectShaderForDebug(bool enable); void setShowSplash(bool enable); void setAutoUpdate(bool enable); void setAlwaysShowChangelog(bool enable); +void setLeftSideTrophy(bool enable); void setNullGpu(bool enable); void setAllowHDR(bool enable); void setCopyGPUCmdBuffers(bool enable); @@ -104,6 +107,7 @@ void setShowBackgroundImage(bool show); void setCursorState(s16 cursorState); void setCursorHideTimeout(int newcursorHideTimeout); +void setTrophyNotificationDuration(double newTrophyNotificationDuration); void setBackButtonBehavior(const std::string& type); void setUseSpecialPad(bool use); void setSpecialPadClass(int type); diff --git a/src/common/path_util.cpp b/src/common/path_util.cpp index a4312fada..d48e8c3fe 100644 --- a/src/common/path_util.cpp +++ b/src/common/path_util.cpp @@ -128,6 +128,7 @@ static auto UserPaths = [] { create_path(PathType::CheatsDir, user_dir / CHEATS_DIR); create_path(PathType::PatchesDir, user_dir / PATCHES_DIR); create_path(PathType::MetaDataDir, user_dir / METADATA_DIR); + create_path(PathType::CustomTrophy, user_dir / CUSTOM_TROPHY); return paths; }(); diff --git a/src/common/path_util.h b/src/common/path_util.h index 7190378d6..2fd9b1588 100644 --- a/src/common/path_util.h +++ b/src/common/path_util.h @@ -27,6 +27,7 @@ enum class PathType { CheatsDir, // Where cheats are stored. PatchesDir, // Where patches are stored. MetaDataDir, // Where game metadata (e.g. trophies and menu backgrounds) is stored. + CustomTrophy, // Where custom files for trophies are stored. }; constexpr auto PORTABLE_DIR = "user"; @@ -44,6 +45,7 @@ constexpr auto CAPTURES_DIR = "captures"; constexpr auto CHEATS_DIR = "cheats"; constexpr auto PATCHES_DIR = "patches"; constexpr auto METADATA_DIR = "game_data"; +constexpr auto CUSTOM_TROPHY = "custom_trophy"; // Filenames constexpr auto LOG_FILE = "shad_log.txt"; diff --git a/src/core/libraries/np_trophy/np_trophy.cpp b/src/core/libraries/np_trophy/np_trophy.cpp index 91dd5b4b4..a951d5655 100644 --- a/src/core/libraries/np_trophy/np_trophy.cpp +++ b/src/core/libraries/np_trophy/np_trophy.cpp @@ -923,15 +923,16 @@ int PS4_SYSV_ABI sceNpTrophyUnlockTrophy(OrbisNpTrophyContext context, OrbisNpTr node.attribute("unlockstate").set_value("true"); } - Rtc::OrbisRtcTick trophyTimestamp; - Rtc::sceRtcGetCurrentTick(&trophyTimestamp); + auto trophyTimestamp = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); if (node.attribute("timestamp").empty()) { node.append_attribute("timestamp") = - std::to_string(trophyTimestamp.tick).c_str(); + std::to_string(trophyTimestamp).c_str(); } else { node.attribute("timestamp") - .set_value(std::to_string(trophyTimestamp.tick).c_str()); + .set_value(std::to_string(trophyTimestamp).c_str()); } std::string trophy_icon_file = "TROP"; @@ -955,15 +956,16 @@ int PS4_SYSV_ABI sceNpTrophyUnlockTrophy(OrbisNpTrophyContext context, OrbisNpTr platinum_node.attribute("unlockstate").set_value("true"); } - Rtc::OrbisRtcTick trophyTimestamp; - Rtc::sceRtcGetCurrentTick(&trophyTimestamp); + auto trophyTimestamp = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); if (platinum_node.attribute("timestamp").empty()) { platinum_node.append_attribute("timestamp") = - std::to_string(trophyTimestamp.tick).c_str(); + std::to_string(trophyTimestamp).c_str(); } else { platinum_node.attribute("timestamp") - .set_value(std::to_string(trophyTimestamp.tick).c_str()); + .set_value(std::to_string(trophyTimestamp).c_str()); } int platinum_trophy_id = diff --git a/src/core/libraries/np_trophy/trophy_ui.cpp b/src/core/libraries/np_trophy/trophy_ui.cpp index efa02e9c4..2564cbf5d 100644 --- a/src/core/libraries/np_trophy/trophy_ui.cpp +++ b/src/core/libraries/np_trophy/trophy_ui.cpp @@ -2,9 +2,17 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include +#include +#include #include #include +#include #include + +#ifdef ENABLE_QT_GUI +#include +#endif + #include "common/assert.h" #include "common/config.h" #include "common/singleton.h" @@ -12,18 +20,23 @@ #include "trophy_ui.h" CMRC_DECLARE(res); - +namespace fs = std::filesystem; using namespace ImGui; namespace Libraries::NpTrophy { std::optional current_trophy_ui; std::queue trophy_queue; std::mutex queueMtx; +bool isLeftSide; +double trophy_timer; TrophyUI::TrophyUI(const std::filesystem::path& trophyIconPath, const std::string& trophyName, const std::string_view& rarity) : trophy_name(trophyName), trophy_type(rarity) { + isLeftSide = Config::leftSideTrophy(); + trophy_timer = Config::getTrophyNotificationDuration(); + if (std::filesystem::exists(trophyIconPath)) { trophy_icon = RefCountedTexture::DecodePngFile(trophyIconPath); } else { @@ -31,23 +44,57 @@ TrophyUI::TrophyUI(const std::filesystem::path& trophyIconPath, const std::strin fmt::UTF(trophyIconPath.u8string())); } - std::string pathString; + std::string pathString = "src/images/"; + if (trophy_type == "P") { - pathString = "src/images/platinum.png"; + pathString += "platinum.png"; } else if (trophy_type == "G") { - pathString = "src/images/gold.png"; + pathString += "gold.png"; } else if (trophy_type == "S") { - pathString = "src/images/silver.png"; + pathString += "silver.png"; } else if (trophy_type == "B") { - pathString = "src/images/bronze.png"; + pathString += "bronze.png"; + } + + const auto CustomTrophy_Dir = Common::FS::GetUserPath(Common::FS::PathType::CustomTrophy); + std::string customPath; + + if (trophy_type == "P" && fs::exists(CustomTrophy_Dir / "platinum.png")) { + customPath = (CustomTrophy_Dir / "platinum.png").string(); + } else if (trophy_type == "G" && fs::exists(CustomTrophy_Dir / "gold.png")) { + customPath = (CustomTrophy_Dir / "gold.png").string(); + } else if (trophy_type == "S" && fs::exists(CustomTrophy_Dir / "silver.png")) { + customPath = (CustomTrophy_Dir / "silver.png").string(); + } else if (trophy_type == "B" && fs::exists(CustomTrophy_Dir / "bronze.png")) { + customPath = (CustomTrophy_Dir / "bronze.png").string(); + } + + std::vector imgdata; + if (!customPath.empty()) { + std::ifstream file(customPath, std::ios::binary); + if (file) { + imgdata = std::vector(std::istreambuf_iterator(file), + std::istreambuf_iterator()); + } else { + LOG_ERROR(Lib_NpTrophy, "Could not open custom file for trophy in {}", customPath); + } + } else { + auto resource = cmrc::res::get_filesystem(); + auto file = resource.open(pathString); + imgdata = std::vector(file.begin(), file.end()); } - auto resource = cmrc::res::get_filesystem(); - auto file = resource.open(pathString); - std::vector imgdata(file.begin(), file.end()); trophy_type_icon = RefCountedTexture::DecodePngTexture(imgdata); AddLayer(this); + +#ifdef ENABLE_QT_GUI + QString musicPath = QString::fromStdString(CustomTrophy_Dir.string() + "/trophy.mp3"); + if (fs::exists(musicPath.toStdString())) { + BackgroundMusicPlayer::getInstance().setVolume(100); + BackgroundMusicPlayer::getInstance().playMusic(musicPath, false); + } +#endif } TrophyUI::~TrophyUI() { @@ -58,6 +105,13 @@ void TrophyUI::Finish() { RemoveLayer(this); } +float fade_opacity = 0.0f; // Initial opacity (invisible) +ImVec2 start_pos = ImVec2(1280.0f, 50.0f); // Starts off screen, right +ImVec2 target_pos = ImVec2(0.0f, 50.0f); // Final position +float animation_duration = 0.5f; // Animation duration +float elapsed_time = 0.0f; // Animation time +float fade_out_duration = 0.5f; // Final fade duration + void TrophyUI::Draw() { const auto& io = GetIO(); @@ -68,26 +122,60 @@ void TrophyUI::Draw() { std::min(io.DisplaySize.y, (70 * AdjustHeight)), }; + elapsed_time += io.DeltaTime; + float progress = std::min(elapsed_time / animation_duration, 1.0f); + + // left or right position + float final_pos_x; + if (isLeftSide) { + start_pos.x = -window_size.x; + final_pos_x = 20 * AdjustWidth; + } else { + start_pos.x = io.DisplaySize.x; + final_pos_x = io.DisplaySize.x - window_size.x - 20 * AdjustWidth; + } + + ImVec2 current_pos = ImVec2(start_pos.x + (final_pos_x - start_pos.x) * progress, + start_pos.y + (target_pos.y - start_pos.y) * progress); + + trophy_timer -= io.DeltaTime; + + // If the remaining time of the trophy is less than or equal to 1 second, the fade-out begins. + if (trophy_timer <= 1.0f) { + float fade_out_time = 1.0f - (trophy_timer / 1.0f); + fade_opacity = 1.0f - fade_out_time; + } else { + // Fade in , 0 to 1 + fade_opacity = progress; + } + + fade_opacity = std::max(0.0f, std::min(fade_opacity, 1.0f)); + SetNextWindowSize(window_size); + SetNextWindowPos(current_pos); SetNextWindowCollapsed(false); - SetNextWindowPos(ImVec2(io.DisplaySize.x - (370 * AdjustWidth), (50 * AdjustHeight))); KeepNavHighlight(); + PushStyleVar(ImGuiStyleVar_Alpha, fade_opacity); + if (Begin("Trophy Window", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoInputs)) { + + // Displays the trophy icon if (trophy_type_icon) { SetCursorPosY((window_size.y * 0.5f) - (25 * AdjustHeight)); Image(trophy_type_icon.GetTexture().im_id, ImVec2((50 * AdjustWidth), (50 * AdjustHeight))); ImGui::SameLine(); } else { - // placeholder + // Placeholder const auto pos = GetCursorScreenPos(); ImGui::GetWindowDrawList()->AddRectFilled(pos, pos + ImVec2{50.0f * AdjustHeight}, GetColorU32(ImVec4{0.7f})); ImGui::Indent(60); } + // Displays the name of the trophy const std::string combinedString = "Trophy earned!\n%s" + trophy_name; const float wrap_width = CalcWrapWidthForPos(GetCursorScreenPos(), (window_size.x - (60 * AdjustWidth))); @@ -108,11 +196,12 @@ void TrophyUI::Draw() { TextWrapped("Trophy earned!\n%s", trophy_name.c_str()); ImGui::SameLine(window_size.x - (60 * AdjustWidth)); + // Displays the trophy icon if (trophy_icon) { SetCursorPosY((window_size.y * 0.5f) - (25 * AdjustHeight)); Image(trophy_icon.GetTexture().im_id, ImVec2((50 * AdjustWidth), (50 * AdjustHeight))); } else { - // placeholder + // Placeholder const auto pos = GetCursorScreenPos(); ImGui::GetWindowDrawList()->AddRectFilled(pos, pos + ImVec2{50.0f * AdjustHeight}, GetColorU32(ImVec4{0.7f})); @@ -120,7 +209,8 @@ void TrophyUI::Draw() { } End(); - trophy_timer -= io.DeltaTime; + PopStyleVar(); + if (trophy_timer <= 0) { std::lock_guard lock(queueMtx); if (!trophy_queue.empty()) { @@ -141,13 +231,27 @@ void AddTrophyToQueue(const std::filesystem::path& trophyIconPath, const std::st if (Config::getisTrophyPopupDisabled()) { return; } else if (current_trophy_ui.has_value()) { - TrophyInfo new_trophy; - new_trophy.trophy_icon_path = trophyIconPath; - new_trophy.trophy_name = trophyName; - new_trophy.trophy_type = rarity; - trophy_queue.push(new_trophy); - } else { - current_trophy_ui.emplace(trophyIconPath, trophyName, rarity); + current_trophy_ui.reset(); + } + + TrophyInfo new_trophy; + new_trophy.trophy_icon_path = trophyIconPath; + new_trophy.trophy_name = trophyName; + new_trophy.trophy_type = rarity; + trophy_queue.push(new_trophy); + + if (!current_trophy_ui.has_value()) { +#ifdef ENABLE_QT_GUI + BackgroundMusicPlayer::getInstance().stopMusic(); +#endif + // Resetting the animation for the next trophy + elapsed_time = 0.0f; // Resetting animation time + fade_opacity = 0.0f; // Starts invisible + start_pos = ImVec2(1280.0f, 50.0f); // Starts off screen, right + TrophyInfo next_trophy = trophy_queue.front(); + trophy_queue.pop(); + current_trophy_ui.emplace(next_trophy.trophy_icon_path, next_trophy.trophy_name, + next_trophy.trophy_type); } } diff --git a/src/core/libraries/np_trophy/trophy_ui.h b/src/core/libraries/np_trophy/trophy_ui.h index 16e707059..553c99f6f 100644 --- a/src/core/libraries/np_trophy/trophy_ui.h +++ b/src/core/libraries/np_trophy/trophy_ui.h @@ -28,7 +28,6 @@ public: private: std::string trophy_name; std::string_view trophy_type; - float trophy_timer = 5.0f; ImGui::RefCountedTexture trophy_icon; ImGui::RefCountedTexture trophy_type_icon; }; diff --git a/src/qt_gui/background_music_player.cpp b/src/qt_gui/background_music_player.cpp index a40c5bfae..a63f1d1be 100644 --- a/src/qt_gui/background_music_player.cpp +++ b/src/qt_gui/background_music_player.cpp @@ -7,7 +7,6 @@ BackgroundMusicPlayer::BackgroundMusicPlayer(QObject* parent) : QObject(parent) m_mediaPlayer = new QMediaPlayer(this); m_audioOutput = new QAudioOutput(this); m_mediaPlayer->setAudioOutput(m_audioOutput); - m_mediaPlayer->setLoops(QMediaPlayer::Infinite); } void BackgroundMusicPlayer::setVolume(int volume) { @@ -16,7 +15,7 @@ void BackgroundMusicPlayer::setVolume(int volume) { m_audioOutput->setVolume(linearVolume); } -void BackgroundMusicPlayer::playMusic(const QString& snd0path) { +void BackgroundMusicPlayer::playMusic(const QString& snd0path, bool loops) { if (snd0path.isEmpty()) { stopMusic(); return; @@ -28,6 +27,12 @@ void BackgroundMusicPlayer::playMusic(const QString& snd0path) { return; } + if (loops) { + m_mediaPlayer->setLoops(QMediaPlayer::Infinite); + } else { + m_mediaPlayer->setLoops(1); + } + m_currentMusic = newMusic; m_mediaPlayer->setSource(newMusic); m_mediaPlayer->play(); diff --git a/src/qt_gui/background_music_player.h b/src/qt_gui/background_music_player.h index 6d70fe68c..078710a01 100644 --- a/src/qt_gui/background_music_player.h +++ b/src/qt_gui/background_music_player.h @@ -17,7 +17,7 @@ public: } void setVolume(int volume); - void playMusic(const QString& snd0path); + void playMusic(const QString& snd0path, bool loops = true); void stopMusic(); private: diff --git a/src/qt_gui/gui_context_menus.h b/src/qt_gui/gui_context_menus.h index 1a059a850..385b5fda9 100644 --- a/src/qt_gui/gui_context_menus.h +++ b/src/qt_gui/gui_context_menus.h @@ -97,11 +97,13 @@ public: QAction* deleteUpdate = new QAction(tr("Delete Update"), widget); QAction* deleteSaveData = new QAction(tr("Delete Save Data"), widget); QAction* deleteDLC = new QAction(tr("Delete DLC"), widget); + QAction* deleteTrophy = new QAction(tr("Delete Trophy"), widget); deleteMenu->addAction(deleteGame); deleteMenu->addAction(deleteUpdate); deleteMenu->addAction(deleteSaveData); deleteMenu->addAction(deleteDLC); + deleteMenu->addAction(deleteTrophy); menu.addMenu(deleteMenu); @@ -380,9 +382,9 @@ public: } if (selected == deleteGame || selected == deleteUpdate || selected == deleteDLC || - selected == deleteSaveData) { + selected == deleteSaveData || selected == deleteTrophy) { bool error = false; - QString folder_path, game_update_path, dlc_path, save_data_path; + QString folder_path, game_update_path, dlc_path, save_data_path, trophy_data_path; Common::FS::PathToQString(folder_path, m_games[itemID].path); game_update_path = folder_path + "-UPDATE"; Common::FS::PathToQString( @@ -391,6 +393,11 @@ public: Common::FS::PathToQString(save_data_path, Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "savedata/1" / m_games[itemID].serial); + + Common::FS::PathToQString(trophy_data_path, + Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / + m_games[itemID].serial / "TrophyFiles"); + QString message_type = tr("Game"); if (selected == deleteUpdate) { @@ -420,6 +427,16 @@ public: folder_path = save_data_path; message_type = tr("Save Data"); } + } else if (selected == deleteTrophy) { + if (!std::filesystem::exists(Common::FS::PathFromQString(trophy_data_path))) { + QMessageBox::critical( + nullptr, tr("Error"), + QString(tr("This game has no saved trophies to delete!"))); + error = true; + } else { + folder_path = trophy_data_path; + message_type = tr("Trophy"); + } } if (!error) { QString gameName = QString::fromStdString(m_games[itemID].name); diff --git a/src/qt_gui/settings_dialog.cpp b/src/qt_gui/settings_dialog.cpp index 9a946658f..bde104828 100644 --- a/src/qt_gui/settings_dialog.cpp +++ b/src/qt_gui/settings_dialog.cpp @@ -225,6 +225,17 @@ SettingsDialog::SettingsDialog(std::span physical_devices, Config::setShowBackgroundImage(state == Qt::Checked); }); } + + // User TAB + { + connect(ui->OpenCustomTrophyLocationButton, &QPushButton::clicked, this, []() { + QString userPath; + Common::FS::PathToQString(userPath, + Common::FS::GetUserPath(Common::FS::PathType::CustomTrophy)); + QDesktopServices::openUrl(QUrl::fromLocalFile(userPath)); + }); + } + // Input TAB { connect(ui->hideCursorComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, @@ -280,8 +291,8 @@ SettingsDialog::SettingsDialog(std::span physical_devices, connect(ui->OpenLogLocationButton, &QPushButton::clicked, this, []() { QString userPath; Common::FS::PathToQString(userPath, - Common::FS::GetUserPath(Common::FS::PathType::UserDir)); - QDesktopServices::openUrl(QUrl::fromLocalFile(userPath + "/log")); + Common::FS::GetUserPath(Common::FS::PathType::LogDir)); + QDesktopServices::openUrl(QUrl::fromLocalFile(userPath)); }); } @@ -308,6 +319,9 @@ SettingsDialog::SettingsDialog(std::span physical_devices, ui->checkCompatibilityOnStartupCheckBox->installEventFilter(this); ui->updateCompatibilityButton->installEventFilter(this); + // User + ui->OpenCustomTrophyLocationButton->installEventFilter(this); + // Input ui->hideCursorGroupBox->installEventFilter(this); ui->idleTimeoutGroupBox->installEventFilter(this); @@ -403,6 +417,9 @@ void SettingsDialog::LoadValuesFromConfig() { ui->playBGMCheckBox->setChecked(toml::find_or(data, "General", "playBGM", false)); ui->disableTrophycheckBox->setChecked( toml::find_or(data, "General", "isTrophyPopupDisabled", false)); + ui->popUpDurationSpinBox->setValue(Config::getTrophyNotificationDuration()); + ui->radioButton_Left->setChecked(Config::leftSideTrophy()); + ui->radioButton_Right->setChecked(!ui->radioButton_Left->isChecked()); ui->BGMVolumeSlider->setValue(toml::find_or(data, "General", "BGMvolume", 50)); ui->discordRPCCheckbox->setChecked( toml::find_or(data, "General", "enableDiscordRPC", true)); @@ -593,6 +610,11 @@ void SettingsDialog::updateNoteTextEdit(const QString& elementName) { text = tr("Update Compatibility Database:\\nImmediately update the compatibility database."); } + //User + if (elementName == "OpenCustomTrophyLocationButton") { + text = tr("Open the custom trophy images/sounds folder:\\nYou can add custom images to the trophies and an audio.\\nAdd the files to custom_trophy with the following names:\\nthophy.mp3, bronze.png, gold.png, platinum.png, silver.png"); + } + // Input if (elementName == "hideCursorGroupBox") { text = tr("Hide Cursor:\\nChoose when the cursor will disappear:\\nNever: You will always see the mouse.\\nidle: Set a time for it to disappear after being idle.\\nAlways: you will never see the mouse."); @@ -683,6 +705,8 @@ void SettingsDialog::UpdateSettings() { screenModeMap.value(ui->displayModeComboBox->currentText()).toStdString()); Config::setIsMotionControlsEnabled(ui->motionControlsCheckBox->isChecked()); Config::setisTrophyPopupDisabled(ui->disableTrophycheckBox->isChecked()); + Config::setTrophyNotificationDuration(ui->popUpDurationSpinBox->value()); + Config::setLeftSideTrophy(ui->radioButton_Left->isChecked()); Config::setPlayBGM(ui->playBGMCheckBox->isChecked()); Config::setAllowHDR(ui->enableHDRCheckBox->isChecked()); Config::setLogType(logTypeMap.value(ui->logTypeComboBox->currentText()).toStdString()); diff --git a/src/qt_gui/settings_dialog.ui b/src/qt_gui/settings_dialog.ui index 2df328fbe..c793aced5 100644 --- a/src/qt_gui/settings_dialog.ui +++ b/src/qt_gui/settings_dialog.ui @@ -59,7 +59,7 @@ - 6 + 0 @@ -73,8 +73,8 @@ 0 0 - 718 - 332 + 946 + 545 @@ -454,8 +454,8 @@ 0 0 - 646 - 395 + 946 + 545 @@ -903,8 +903,8 @@ 0 0 - 545 - 141 + 946 + 545 @@ -1198,8 +1198,8 @@ 0 0 - 234 - 292 + 946 + 545 @@ -1264,30 +1264,121 @@ - Disable Trophy Pop-ups + Disable Trophy Notification - + + + 0 + + + + + + 0 + 0 + + + + Trophy Notification Position + + + + + + + Left + + + + + + + + 0 + 0 + + + + Right + + + + + + + + + 0 + + + + + + 0 + 0 + + + + Notification Duration + + + + + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + + 0 + + + + + Trophy Key + + + + + + + + 0 + 0 + + + + + 10 + false + + + + + + + + - Trophy Key - - - - - - - - 0 - 0 - - - - - 10 - false - + Open the custom trophy images/sounds folder @@ -1342,8 +1433,8 @@ 0 0 - 455 - 252 + 946 + 545 @@ -1626,8 +1717,8 @@ 0 0 - 216 - 254 + 946 + 545 @@ -1717,7 +1808,7 @@ 0 0 946 - 536 + 545 diff --git a/src/qt_gui/translations/en_US.ts b/src/qt_gui/translations/en_US.ts index 263267aba..24ad63ca3 100644 --- a/src/qt_gui/translations/en_US.ts +++ b/src/qt_gui/translations/en_US.ts @@ -775,6 +775,10 @@ Delete DLC Delete DLC + + Delete Trophy + Delete Trophy + Compatibility... Compatibility... @@ -859,10 +863,18 @@ This game has no save data to delete! + + This game has no saved trophies to delete! + + Save Data + + Trophy + + SFO Viewer for @@ -1311,6 +1323,10 @@ Trophy Trophy + + Open the custom trophy images/sounds folder + Open the custom trophy images/sounds folder + Logger Logger @@ -1476,8 +1492,8 @@ Title Music - Disable Trophy Pop-ups - Disable Trophy Pop-ups + Disable Trophy Notification + Disable Trophy Notification Background Image @@ -1611,6 +1627,10 @@ Update Compatibility Database:\nImmediately update the compatibility database. Update Compatibility Database:\nImmediately update the compatibility database. + + Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\nthophy.mp3, bronze.png, gold.png, platinum.png, silver.png + Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\nthophy.mp3, bronze.png, gold.png, platinum.png, silver.png + Never Never @@ -1803,6 +1823,22 @@ Separate Log Files:\nWrites a separate logfile for each game. + + Trophy Notification Position + Trophy Notification Position + + + Left + Left + + + Right + Right + + + Notification Duration + Notification Duration + TrophyViewer @@ -1810,5 +1846,21 @@ Trophy Viewer Trophy Viewer + + Progress + + + + Show Earned Trophies + + + + Show Not Earned Trophies + + + + Show Hidden Trophies + + - + \ No newline at end of file diff --git a/src/qt_gui/trophy_viewer.cpp b/src/qt_gui/trophy_viewer.cpp index 63e9f04dd..ace475a7f 100644 --- a/src/qt_gui/trophy_viewer.cpp +++ b/src/qt_gui/trophy_viewer.cpp @@ -1,27 +1,169 @@ // SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include +#include +#include #include #include +#include #include "common/path_util.h" +#include "main_window_themes.h" #include "trophy_viewer.h" +namespace fs = std::filesystem; + CMRC_DECLARE(res); +// true: European format; false: American format +bool useEuropeanDateFormat = true; + +void TrophyViewer::updateTrophyInfo() { + int total = 0; + int unlocked = 0; + + // Cycles through each tab (table) of the QTabWidget + for (int i = 0; i < tabWidget->count(); i++) { + QTableWidget* table = qobject_cast(tabWidget->widget(i)); + if (table) { + total += table->rowCount(); + for (int row = 0; row < table->rowCount(); ++row) { + QString cellText; + // The "Unlocked" column can be a widget or a simple item + QWidget* widget = table->cellWidget(row, 0); + if (widget) { + // Looks for the QLabel inside the widget (as defined in SetTableItem) + QLabel* label = widget->findChild(); + if (label) { + cellText = label->text(); + } + } else { + QTableWidgetItem* item = table->item(row, 0); + if (item) { + cellText = item->text(); + } + } + if (cellText == "unlocked") + unlocked++; + } + } + } + int progress = (total > 0) ? (unlocked * 100 / total) : 0; + trophyInfoLabel->setText( + QString(tr("Progress") + ": %1% (%2/%3)").arg(progress).arg(unlocked).arg(total)); +} + +void TrophyViewer::updateTableFilters() { + bool showEarned = showEarnedCheck->isChecked(); + bool showNotEarned = showNotEarnedCheck->isChecked(); + bool showHidden = showHiddenCheck->isChecked(); + + // Cycles through each tab of the QTabWidget + for (int i = 0; i < tabWidget->count(); ++i) { + QTableWidget* table = qobject_cast(tabWidget->widget(i)); + if (!table) + continue; + for (int row = 0; row < table->rowCount(); ++row) { + QString unlockedText; + // Gets the text of the "Unlocked" column (index 0) + QWidget* widget = table->cellWidget(row, 0); + if (widget) { + QLabel* label = widget->findChild(); + if (label) + unlockedText = label->text(); + } else { + QTableWidgetItem* item = table->item(row, 0); + if (item) + unlockedText = item->text(); + } + + QString hiddenText; + // Gets the text of the "Hidden" column (index 7) + QWidget* hiddenWidget = table->cellWidget(row, 7); + if (hiddenWidget) { + QLabel* label = hiddenWidget->findChild(); + if (label) + hiddenText = label->text(); + } else { + QTableWidgetItem* item = table->item(row, 7); + if (item) + hiddenText = item->text(); + } + + bool visible = true; + if (unlockedText == "unlocked" && !showEarned) + visible = false; + if (unlockedText == "locked" && !showNotEarned) + visible = false; + if (hiddenText.toLower() == "yes" && !showHidden) + visible = false; + + table->setRowHidden(row, !visible); + } + } +} + TrophyViewer::TrophyViewer(QString trophyPath, QString gameTrpPath) : QMainWindow() { this->setWindowTitle(tr("Trophy Viewer")); this->setAttribute(Qt::WA_DeleteOnClose); tabWidget = new QTabWidget(this); + + auto lan = Config::getEmulatorLanguage(); + if (lan == "en_US" || lan == "zh_CN" || lan == "zh_TW" || lan == "ja_JP" || lan == "ko_KR" || + lan == "lt_LT" || lan == "nb_NO" || lan == "nl_NL") { + useEuropeanDateFormat = false; + } + gameTrpPath_ = gameTrpPath; headers << "Unlocked" << "Trophy" << "Name" << "Description" + << "Time Unlocked" + << "Type" << "ID" << "Hidden" - << "Type" << "PID"; PopulateTrophyWidget(trophyPath); + + QDockWidget* trophyInfoDock = new QDockWidget("", this); + QWidget* dockWidget = new QWidget(trophyInfoDock); + QVBoxLayout* dockLayout = new QVBoxLayout(dockWidget); + dockLayout->setAlignment(Qt::AlignTop); + + trophyInfoLabel = new QLabel(tr("Progress") + ": 0% (0/0)", dockWidget); + trophyInfoLabel->setStyleSheet( + "font-weight: bold; font-size: 16px; color: white; background: #333; padding: 5px;"); + dockLayout->addWidget(trophyInfoLabel); + + // Creates QCheckBox to filter trophies + showEarnedCheck = new QCheckBox(tr("Show Earned Trophies"), dockWidget); + showNotEarnedCheck = new QCheckBox(tr("Show Not Earned Trophies"), dockWidget); + showHiddenCheck = new QCheckBox(tr("Show Hidden Trophies"), dockWidget); + + // Defines the initial states (all checked) + showEarnedCheck->setChecked(true); + showNotEarnedCheck->setChecked(true); + showHiddenCheck->setChecked(false); + + // Adds checkboxes to the layout + dockLayout->addWidget(showEarnedCheck); + dockLayout->addWidget(showNotEarnedCheck); + dockLayout->addWidget(showHiddenCheck); + + dockWidget->setLayout(dockLayout); + trophyInfoDock->setWidget(dockWidget); + + // Adds the dock to the left area + this->addDockWidget(Qt::LeftDockWidgetArea, trophyInfoDock); + + // Connects checkbox signals to update trophy display + connect(showEarnedCheck, &QCheckBox::stateChanged, this, &TrophyViewer::updateTableFilters); + connect(showNotEarnedCheck, &QCheckBox::stateChanged, this, &TrophyViewer::updateTableFilters); + connect(showHiddenCheck, &QCheckBox::stateChanged, this, &TrophyViewer::updateTableFilters); + + updateTrophyInfo(); + updateTableFilters(); } void TrophyViewer::PopulateTrophyWidget(QString title) { @@ -68,6 +210,7 @@ void TrophyViewer::PopulateTrophyWidget(QString title) { QStringList trpPid; QStringList trophyNames; QStringList trophyDetails; + QStringList trpTimeUnlocked; QString xmlPath = trpDir + "/Xml/TROP.XML"; QFile file(xmlPath); @@ -84,14 +227,35 @@ void TrophyViewer::PopulateTrophyWidget(QString title) { trpHidden.append(reader.attributes().value("hidden").toString()); trpType.append(reader.attributes().value("ttype").toString()); trpPid.append(reader.attributes().value("pid").toString()); + if (reader.attributes().hasAttribute("unlockstate")) { if (reader.attributes().value("unlockstate").toString() == "true") { trpUnlocked.append("unlocked"); } else { trpUnlocked.append("locked"); } + if (reader.attributes().hasAttribute("timestamp")) { + QString ts = reader.attributes().value("timestamp").toString(); + if (ts.length() > 10) + trpTimeUnlocked.append("unknown"); + else { + bool ok; + qint64 timestampInt = ts.toLongLong(&ok); + if (ok) { + QDateTime dt = QDateTime::fromSecsSinceEpoch(timestampInt); + QString format = useEuropeanDateFormat ? "dd/MM/yyyy HH:mm:ss" + : "MM/dd/yyyy HH:mm:ss"; + trpTimeUnlocked.append(dt.toString(format)); + } else { + trpTimeUnlocked.append("unknown"); + } + } + } else { + trpTimeUnlocked.append(""); + } } else { trpUnlocked.append("locked"); + trpTimeUnlocked.append(""); } } @@ -105,7 +269,7 @@ void TrophyViewer::PopulateTrophyWidget(QString title) { } QTableWidget* tableWidget = new QTableWidget(this); tableWidget->setShowGrid(false); - tableWidget->setColumnCount(8); + tableWidget->setColumnCount(9); tableWidget->setHorizontalHeaderLabels(headers); tableWidget->setSelectionBehavior(QAbstractItemView::SelectRows); tableWidget->setSelectionMode(QAbstractItemView::SingleSelection); @@ -113,6 +277,8 @@ void TrophyViewer::PopulateTrophyWidget(QString title) { tableWidget->horizontalHeader()->setStretchLastSection(true); tableWidget->verticalHeader()->setVisible(false); tableWidget->setRowCount(icons.size()); + tableWidget->setSortingEnabled(true); + for (int row = 0; auto& icon : icons) { QTableWidgetItem* item = new QTableWidgetItem(); item->setData(Qt::DecorationRole, icon); @@ -122,15 +288,34 @@ void TrophyViewer::PopulateTrophyWidget(QString title) { const std::string filename = GetTrpType(trpType[row].at(0)); QTableWidgetItem* typeitem = new QTableWidgetItem(); - auto resource = cmrc::res::get_filesystem(); - std::string resourceString = "src/images/" + filename; - auto file = resource.open(resourceString); - std::vector imgdata(file.begin(), file.end()); - QImage type_icon = QImage::fromData(imgdata).scaled(QSize(64, 64), Qt::KeepAspectRatio, - Qt::SmoothTransformation); + const auto CustomTrophy_Dir = + Common::FS::GetUserPath(Common::FS::PathType::CustomTrophy); + std::string customPath; + + if (fs::exists(CustomTrophy_Dir / filename)) { + customPath = (CustomTrophy_Dir / filename).string(); + } + + std::vector imgdata; + + if (!customPath.empty()) { + std::ifstream file(customPath, std::ios::binary); + if (file) { + imgdata = std::vector(std::istreambuf_iterator(file), + std::istreambuf_iterator()); + } + } else { + auto resource = cmrc::res::get_filesystem(); + std::string resourceString = "src/images/" + filename; + auto file = resource.open(resourceString); + imgdata = std::vector(file.begin(), file.end()); + } + + QImage type_icon = QImage::fromData(imgdata).scaled( + QSize(100, 100), Qt::KeepAspectRatio, Qt::SmoothTransformation); typeitem->setData(Qt::DecorationRole, type_icon); typeitem->setFlags(typeitem->flags() & ~Qt::ItemIsEditable); - tableWidget->setItem(row, 6, typeitem); + tableWidget->setItem(row, 5, typeitem); std::string detailString = trophyDetails[row].toStdString(); std::size_t newline_pos = 0; @@ -143,46 +328,45 @@ void TrophyViewer::PopulateTrophyWidget(QString title) { SetTableItem(tableWidget, row, 0, trpUnlocked[row]); SetTableItem(tableWidget, row, 2, trophyNames[row]); SetTableItem(tableWidget, row, 3, QString::fromStdString(detailString)); - SetTableItem(tableWidget, row, 4, trpId[row]); - SetTableItem(tableWidget, row, 5, trpHidden[row]); - SetTableItem(tableWidget, row, 7, trpPid[row]); + SetTableItem(tableWidget, row, 4, trpTimeUnlocked[row]); + SetTableItem(tableWidget, row, 6, trpId[row]); + SetTableItem(tableWidget, row, 7, trpHidden[row]); + SetTableItem(tableWidget, row, 8, trpPid[row]); } tableWidget->verticalHeader()->resizeSection(row, icon.height()); row++; } tableWidget->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); int width = 16; - for (int i = 0; i < 8; i++) { + for (int i = 0; i < 9; i++) { width += tableWidget->horizontalHeader()->sectionSize(i); } tableWidget->resize(width, 720); tabWidget->addTab(tableWidget, tabName.insert(6, " ").replace(0, 1, tabName.at(0).toUpper())); - this->resize(width + 20, 720); + + this->showMaximized(); + + tableWidget->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Fixed); + tableWidget->setColumnWidth(3, 650); } this->setCentralWidget(tabWidget); } void TrophyViewer::SetTableItem(QTableWidget* parent, int row, int column, QString str) { - QWidget* widget = new QWidget(); - QVBoxLayout* layout = new QVBoxLayout(); - QLabel* label = new QLabel(str); - QTableWidgetItem* item = new QTableWidgetItem(); - label->setWordWrap(true); - label->setStyleSheet("color: white; font-size: 15px; font-weight: bold;"); + QTableWidgetItem* item = new QTableWidgetItem(str); - // Create shadow effect - QGraphicsDropShadowEffect* shadowEffect = new QGraphicsDropShadowEffect(); - shadowEffect->setBlurRadius(5); // Set the blur radius of the shadow - shadowEffect->setColor(QColor(0, 0, 0, 160)); // Set the color and opacity of the shadow - shadowEffect->setOffset(2, 2); // Set the offset of the shadow - - label->setGraphicsEffect(shadowEffect); // Apply shadow effect to the QLabel - - layout->addWidget(label); if (column != 1 && column != 2 && column != 3) - layout->setAlignment(Qt::AlignCenter); - widget->setLayout(layout); + item->setTextAlignment(Qt::AlignCenter); + item->setFont(QFont("Arial", 12, QFont::Bold)); + + Theme theme = static_cast(Config::getMainWindowTheme()); + + if (theme == Theme::Light) { + item->setForeground(QBrush(Qt::black)); + } else { + item->setForeground(QBrush(Qt::white)); + } + parent->setItem(row, column, item); - parent->setCellWidget(row, column, widget); } diff --git a/src/qt_gui/trophy_viewer.h b/src/qt_gui/trophy_viewer.h index 089de433e..bd99e1a8c 100644 --- a/src/qt_gui/trophy_viewer.h +++ b/src/qt_gui/trophy_viewer.h @@ -23,6 +23,10 @@ class TrophyViewer : public QMainWindow { public: explicit TrophyViewer(QString trophyPath, QString gameTrpPath); + void updateTrophyInfo(); + + void updateTableFilters(); + private: void PopulateTrophyWidget(QString title); void SetTableItem(QTableWidget* parent, int row, int column, QString str); @@ -31,6 +35,10 @@ private: QStringList headers; QString gameTrpPath_; TRP trp; + QLabel* trophyInfoLabel; + QCheckBox* showEarnedCheck; + QCheckBox* showNotEarnedCheck; + QCheckBox* showHiddenCheck; std::string GetTrpType(const QChar trp_) { switch (trp_.toLatin1()) {