From 8186f14a5764111716ca2ee7e38966b5f7a50beb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sat, 7 Sep 2024 15:01:13 +0200 Subject: [PATCH] Much more UI work on savedata import fix --- Core/Util/GameManager.cpp | 18 ++++- Core/Util/GameManager.h | 8 +- UI/CwCheatScreen.cpp | 2 +- UI/GameInfoCache.cpp | 66 ++++++++++++++-- UI/GameInfoCache.h | 18 ++++- UI/GameScreen.cpp | 2 +- UI/InstallZipScreen.cpp | 48 ++++++++++-- UI/InstallZipScreen.h | 4 + UI/MainScreen.cpp | 1 + UI/SavedataScreen.cpp | 154 +++++++++++++++++++++++--------------- UI/SavedataScreen.h | 14 ++++ 11 files changed, 247 insertions(+), 88 deletions(-) diff --git a/Core/Util/GameManager.cpp b/Core/Util/GameManager.cpp index 1e00d7cf12..732829ca4e 100644 --- a/Core/Util/GameManager.cpp +++ b/Core/Util/GameManager.cpp @@ -266,11 +266,17 @@ void DetectZipFileContents(struct zip *z, ZipFileInfo *info) { int directoriesInRoot = 0; bool hasParamSFO = false; bool hasIcon0PNG = false; + s64 totalFileSize = 0; // TODO: It might be cleaner to write separate detection functions, but this big loop doing it all at once // is quite convenient and makes it easy to add shared heuristics. for (int i = 0; i < numFiles; i++) { const char *fn = zip_get_name(z, i, 0); + + zip_stat_t stat{}; + zip_stat_index(z, i, 0, &stat); + totalFileSize += stat.size; + std::string zippedName = fn; std::transform(zippedName.begin(), zippedName.end(), zippedName.begin(), [](unsigned char c) { return asciitolower(c); }); // Not using std::tolower to avoid Turkish I->ı conversion. @@ -282,6 +288,7 @@ void DetectZipFileContents(struct zip *z, ZipFileInfo *info) { // A directory. Not all zips bother including these. continue; } + int prevSlashLocation = -1; int slashCount = countSlashes(zippedName, &prevSlashLocation); if (zippedName.find("eboot.pbp") != std::string::npos) { @@ -318,9 +325,12 @@ void DetectZipFileContents(struct zip *z, ZipFileInfo *info) { ParamSFOData sfo; if (sfo.ReadSFO((const u8 *)paramSFOContents.data(), paramSFOContents.size())) { if (sfo.HasKey("TITLE")) { - std::string title = sfo.GetValueString("TITLE") + ": " + sfo.GetValueString("SAVEDATA_TITLE"); - std::string details = sfo.GetValueString("SAVEDATA_DETAIL"); - info->contentName = title + "\n\n" + details; + info->gameTitle = sfo.GetValueString("TITLE"); + info->savedataTitle = sfo.GetValueString("SAVEDATA_TITLE"); + char buff[20]; + strftime(buff, 20, "%Y-%m-%d %H:%M:%S", localtime(&stat.mtime)); + info->mTime = buff; + info->savedataDetails = sfo.GetValueString("SAVEDATA_DETAIL"); info->savedataDir = sfo.GetValueString("SAVEDATA_DIRECTORY"); // should also be parsable from the path. hasParamSFO = true; } @@ -339,6 +349,7 @@ void DetectZipFileContents(struct zip *z, ZipFileInfo *info) { info->isoFileIndex = isoFileIndex; info->textureIniIndex = textureIniIndex; info->ignoreMetaFiles = false; + info->totalFileSize = totalFileSize; // Priority ordering for detecting the various kinds of zip file content.s if (isPSPMemstickGame) { @@ -613,7 +624,6 @@ bool GameManager::ExtractFile(struct zip *z, int file_index, const Path &outFile struct zip_stat zstat; zip_stat_index(z, file_index, 0, &zstat); size_t size = zstat.size; - zip_file *zf = zip_fopen_index(z, file_index, 0); if (!zf) { ERROR_LOG(Log::HLE, "Failed to open file by index (%d) (%s)", file_index, outFilename.c_str()); diff --git a/Core/Util/GameManager.h b/Core/Util/GameManager.h index fccca0af13..dffddfd4fd 100644 --- a/Core/Util/GameManager.h +++ b/Core/Util/GameManager.h @@ -50,8 +50,14 @@ struct ZipFileInfo { int isoFileIndex; // for ISO int textureIniIndex; // for textures bool ignoreMetaFiles; - std::string contentName; + std::string gameTitle; // from PARAM.SFO if available + std::string savedataTitle; + std::string savedataDetails; std::string savedataDir; + std::string mTime; + s64 totalFileSize; + + std::string contentName; }; struct ZipFileTask { diff --git a/UI/CwCheatScreen.cpp b/UI/CwCheatScreen.cpp index a97fbf9ff0..2cf3225600 100644 --- a/UI/CwCheatScreen.cpp +++ b/UI/CwCheatScreen.cpp @@ -53,7 +53,7 @@ bool CwCheatScreen::TryLoadCheatInfo() { if (!info->Ready(GameInfoFlags::PARAM_SFO)) { return false; } - gameID = info->paramSFO.GetValueString("DISC_ID"); + gameID = info->GetParamSFO().GetValueString("DISC_ID"); if ((info->id.empty() || !info->disc_total) && gamePath_.FilePathContainsNoCase("PSP/GAME/")) { gameID = g_paramSFO.GenerateFakeID(gamePath_); diff --git a/UI/GameInfoCache.cpp b/UI/GameInfoCache.cpp index 6139ee3446..00b4e13cdd 100644 --- a/UI/GameInfoCache.cpp +++ b/UI/GameInfoCache.cpp @@ -33,6 +33,7 @@ #include "Core/FileSystems/ISOFileSystem.h" #include "Core/FileSystems/DirectoryFileSystem.h" #include "Core/FileSystems/VirtualDiscFileSystem.h" +#include "Core/HLE/sceUtility.h" #include "Core/ELF/PBPReader.h" #include "Core/SaveState.h" #include "Core/System.h" @@ -128,7 +129,7 @@ bool GameInfo::Delete() { } } -u64 GameInfo::GetGameSizeOnDiskInBytes() { +u64 GameInfo::GetSizeOnDiskInBytes() { switch (fileType) { case IdentifiedFileType::PSP_PBP_DIRECTORY: case IdentifiedFileType::PSP_SAVEDATA_DIRECTORY: @@ -140,7 +141,7 @@ u64 GameInfo::GetGameSizeOnDiskInBytes() { } } -u64 GameInfo::GetGameSizeUncompressedInBytes() { +u64 GameInfo::GetSizeUncompressedInBytes() { switch (fileType) { case IdentifiedFileType::PSP_PBP_DIRECTORY: case IdentifiedFileType::PSP_SAVEDATA_DIRECTORY: @@ -161,6 +162,39 @@ u64 GameInfo::GetGameSizeUncompressedInBytes() { } } +std::string GetFileDateAsString(const Path &filename) { + tm time; + if (File::GetModifTime(filename, time)) { + char buf[256]; + switch (g_Config.iDateFormat) { + case PSP_SYSTEMPARAM_DATE_FORMAT_YYYYMMDD: + strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &time); + break; + case PSP_SYSTEMPARAM_DATE_FORMAT_MMDDYYYY: + strftime(buf, sizeof(buf), "%m-%d-%Y %H:%M:%S", &time); + break; + case PSP_SYSTEMPARAM_DATE_FORMAT_DDMMYYYY: + strftime(buf, sizeof(buf), "%d-%m-%Y %H:%M:%S", &time); + break; + default: // Should never happen + return ""; + } + return std::string(buf); + } + return ""; +} + +std::string GameInfo::GetMTime() const { + switch (fileType) { + case IdentifiedFileType::PSP_SAVEDATA_DIRECTORY: + return GetFileDateAsString(GetFilePath() / "PARAM.SFO"); + case IdentifiedFileType::PSP_PBP_DIRECTORY: + return GetFileDateAsString(GetFilePath() / "EBOOT.PBP"); + default: + return GetFileDateAsString(GetFilePath()); + } +} + // Not too meaningful if the object itself is a savedata directory... // Call this under lock. std::vector GameInfo::GetSaveDataDirectories() { @@ -183,7 +217,7 @@ std::vector GameInfo::GetSaveDataDirectories() { return directories; } -u64 GameInfo::GetSaveDataSizeInBytes() { +u64 GameInfo::GetGameSavedataSizeInBytes() { if (fileType == IdentifiedFileType::PSP_SAVEDATA_DIRECTORY || fileType == IdentifiedFileType::PPSSPP_SAVESTATE) { return 0; } @@ -466,6 +500,10 @@ public: info_->fileType = Identify_File(info_->GetFileLoader().get(), &errorString); } + if (!info_->Ready(GameInfoFlags::FILE_TYPE) && !(flags_ & GameInfoFlags::FILE_TYPE)) { + _dbg_assert_(false); + } + switch (info_->fileType) { case IdentifiedFileType::PSP_PBP: case IdentifiedFileType::PSP_PBP_DIRECTORY: @@ -768,12 +806,24 @@ handleELF: if (flags_ & GameInfoFlags::SIZE) { std::lock_guard lock(info_->lock); - info_->gameSizeOnDisk = info_->GetGameSizeOnDiskInBytes(); - info_->saveDataSize = info_->GetSaveDataSizeInBytes(); - info_->installDataSize = info_->GetInstallDataSizeInBytes(); + info_->gameSizeOnDisk = info_->GetSizeOnDiskInBytes(); + switch (info_->fileType) { + case IdentifiedFileType::PSP_ISO: + case IdentifiedFileType::PSP_ISO_NP: + case IdentifiedFileType::PSP_DISC_DIRECTORY: + case IdentifiedFileType::PSP_PBP: + case IdentifiedFileType::PSP_PBP_DIRECTORY: + info_->saveDataSize = info_->GetGameSavedataSizeInBytes(); + info_->installDataSize = info_->GetInstallDataSizeInBytes(); + break; + default: + info_->saveDataSize = 0; + info_->installDataSize = 0; + break; + } } if (flags_ & GameInfoFlags::UNCOMPRESSED_SIZE) { - info_->gameSizeUncompressed = info_->GetGameSizeUncompressedInBytes(); + info_->gameSizeUncompressed = info_->GetSizeUncompressedInBytes(); } // Time to update the flags. @@ -883,6 +933,8 @@ void GameInfoCache::PurgeType(IdentifiedFileType fileType) { std::shared_ptr GameInfoCache::GetInfo(Draw::DrawContext *draw, const Path &gamePath, GameInfoFlags wantFlags) { const std::string &pathStr = gamePath.ToString(); + // _dbg_assert_(gamePath != GetSysDirectory(DIRECTORY_SAVEDATA)); + // This is always needed to determine the method to get the other info, so make sure it's computed first. wantFlags |= GameInfoFlags::FILE_TYPE; diff --git a/UI/GameInfoCache.h b/UI/GameInfoCache.h index b7f0ed5510..ea50e3fd68 100644 --- a/UI/GameInfoCache.h +++ b/UI/GameInfoCache.h @@ -96,12 +96,20 @@ public: std::shared_ptr GetFileLoader(); void DisposeFileLoader(); - u64 GetGameSizeUncompressedInBytes(); // NOTE: More expensive than GetGameSizeOnDiskInBytes(). - u64 GetGameSizeOnDiskInBytes(); - u64 GetSaveDataSizeInBytes(); + u64 GetSizeUncompressedInBytes(); // NOTE: More expensive than GetGameSizeOnDiskInBytes(). + u64 GetSizeOnDiskInBytes(); + u64 GetGameSavedataSizeInBytes(); // For games u64 GetInstallDataSizeInBytes(); + // For various kinds of savedata, mainly. + // NOTE: This one actually performs I/O directly, not cached. + std::string GetMTime() const; + void ParseParamSFO(); + const ParamSFOData &GetParamSFO() const { + _dbg_assert_(hasFlags & GameInfoFlags::PARAM_SFO); + return paramSFO; + } void FinishPendingTextureLoads(Draw::DrawContext *draw); std::vector GetSaveDataDirectories(); @@ -152,7 +160,6 @@ public: int disc_number = 0; int region = -1; IdentifiedFileType fileType; - ParamSFOData paramSFO; bool hasConfig = false; // Pre read the data, create a texture the next time (GL thread..) @@ -171,6 +178,7 @@ public: u64 installDataSize = 0; protected: + ParamSFOData paramSFO; // Note: this can change while loading, use GetTitle(). std::string title; @@ -182,6 +190,7 @@ protected: private: DISALLOW_COPY_AND_ASSIGN(GameInfo); + friend class GameInfoWorkItem; }; class GameInfoCache { @@ -197,6 +206,7 @@ public: // but filled in later asynchronously in the background. So keep calling this, // redrawing the UI often. Only set flags to GAMEINFO_WANTBG or WANTSND if you really want them // because they're big. bgTextures and sound may be discarded over time as well. + // NOTE: This never returns null, so you don't need to check for that. Do check Ready() flags though. std::shared_ptr GetInfo(Draw::DrawContext *draw, const Path &gamePath, GameInfoFlags wantFlags); void FlushBGs(); // Gets rid of all BG textures. Also gets rid of bg sounds. diff --git a/UI/GameScreen.cpp b/UI/GameScreen.cpp index 7050eaafc5..6aa5d10f82 100644 --- a/UI/GameScreen.cpp +++ b/UI/GameScreen.cpp @@ -503,7 +503,7 @@ UI::EventReturn GameScreen::OnPlay(UI::EventParams &e) { UI::EventReturn GameScreen::OnGameSettings(UI::EventParams &e) { std::shared_ptr info = g_gameInfoCache->GetInfo(NULL, gamePath_, GameInfoFlags::PARAM_SFO); if (info && info->Ready(GameInfoFlags::PARAM_SFO)) { - std::string discID = info->paramSFO.GetValueString("DISC_ID"); + std::string discID = info->GetParamSFO().GetValueString("DISC_ID"); if ((discID.empty() || !info->disc_total) && gamePath_.FilePathContainsNoCase("PSP/GAME/")) discID = g_paramSFO.GenerateFakeID(gamePath_); screenManager()->push(new GameSettingsScreen(gamePath_, discID, true)); diff --git a/UI/InstallZipScreen.cpp b/UI/InstallZipScreen.cpp index 8ac9db4526..c7316a2b1f 100644 --- a/UI/InstallZipScreen.cpp +++ b/UI/InstallZipScreen.cpp @@ -21,8 +21,10 @@ #include "Common/StringUtils.h" #include "Common/Data/Text/I18n.h" +#include "Common/Data/Text/Parsers.h" #include "Core/System.h" #include "Core/Util/GameManager.h" +#include "Core/Loaders.h" #include "UI/InstallZipScreen.h" #include "UI/MainScreen.h" #include "UI/OnScreenDisplay.h" @@ -41,6 +43,7 @@ void InstallZipScreen::CreateViews() { auto di = GetI18NCategory(I18NCat::DIALOG); auto iz = GetI18NCategory(I18NCat::INSTALLZIP); auto er = GetI18NCategory(I18NCat::ERRORS); + auto ga = GetI18NCategory(I18NCat::GAME); Margins actionMenuMargins(0, 100, 15, 0); @@ -61,7 +64,7 @@ void InstallZipScreen::CreateViews() { installChoice_ = nullptr; doneView_ = nullptr; installChoice_ = nullptr; - + existingSaveView_ = nullptr; if (z) { DetectZipFileContents(z, &zipFileInfo_); // Even if this fails, it sets zipInfo->contents. if (zipFileInfo_.contents == ZipFileContents::ISO_FILE || zipFileInfo_.contents == ZipFileContents::PSP_GAME_DIR) { @@ -69,6 +72,9 @@ void InstallZipScreen::CreateViews() { leftColumn->Add(new TextView(question)); leftColumn->Add(new TextView(shortFilename)); + if (!zipFileInfo_.contentName.empty()) { + leftColumn->Add(new TextView(zipFileInfo_.contentName)); + } doneView_ = leftColumn->Add(new TextView("")); @@ -89,15 +95,36 @@ void InstallZipScreen::CreateViews() { showDeleteCheckbox = true; } else if (zipFileInfo_.contents == ZipFileContents::SAVE_DATA) { - std::string_view question = iz->T("Install savedata?"); - leftColumn->Add(new TextView(question, ALIGN_LEFT, false, new AnchorLayoutParams(10, 10, NONE, NONE))); - leftColumn->Add(new TextView(zipFileInfo_.contentName)); + std::string_view question = iz->T("Import savedata from ZIP file"); + leftColumn->Add(new TextView(question))->SetBig(true); + leftColumn->Add(new TextView(zipFileInfo_.gameTitle + ": " + zipFileInfo_.savedataDir)); + + Path savedataDir = GetSysDirectory(DIRECTORY_SAVEDATA); + bool overwrite = !CanExtractWithoutOverwrite(z, savedataDir, 50); + + leftColumn->Add(new NoticeView(NoticeLevel::WARN, di->T("Confirm Overwrite"), "")); + + int columnWidth = 300; + + LinearLayout *compareColumns = leftColumn->Add(new LinearLayout(UI::ORIENT_HORIZONTAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT))); + compareColumns->Add(new SavedataView(*screenManager()->getUIContext(), Path(), IdentifiedFileType::PSP_SAVEDATA_DIRECTORY, + zipFileInfo_.gameTitle, zipFileInfo_.savedataTitle, zipFileInfo_.savedataDetails, NiceSizeFormat(zipFileInfo_.totalFileSize), zipFileInfo_.mTime, false, new LinearLayoutParams(columnWidth, WRAP_CONTENT))); // Check for potential overwrite at destination, and ask the user if it's OK to overwrite. - Path saveDir = GetSysDirectory(DIRECTORY_SAVEDATA); - if (!CanExtractWithoutOverwrite(z, saveDir, 50)) { - leftColumn->Add(new NoticeView(NoticeLevel::WARN, di->T("Confirm Overwrite"), "", new AnchorLayoutParams(10, 60, NONE, NONE))); - leftColumn->Add(new SavedataButton(GetSysDirectory(DIRECTORY_SAVEDATA) / zipFileInfo_.savedataDir)); + if (overwrite) { + savedataToOverwrite_ = savedataDir / zipFileInfo_.savedataDir; + std::shared_ptr ginfo = g_gameInfoCache->GetInfo(screenManager()->getDrawContext(), savedataToOverwrite_, GameInfoFlags::FILE_TYPE | GameInfoFlags::PARAM_SFO | GameInfoFlags::ICON | GameInfoFlags::SIZE); + + LinearLayout *rightCompare = new LinearLayout(UI::ORIENT_VERTICAL); + rightCompare->Add(new TextView(iz->T("Existing data"))); + compareColumns->Add(rightCompare); + existingSaveView_ = rightCompare->Add(new SavedataView(*screenManager()->getUIContext(), ginfo.get(), IdentifiedFileType::PSP_SAVEDATA_DIRECTORY, false, new LinearLayoutParams(columnWidth, WRAP_CONTENT))); + if (System_GetPropertyBool(SYSPROP_CAN_SHOW_FILE)) { + rightCompare->Add(new Button(ga->T("Show In Folder")))->OnClick.Add([=](UI::EventParams &) { + System_ShowFileInFolder(savedataToOverwrite_); + return UI::EVENT_DONE; + }); + } } installChoice_ = rightColumnItems->Add(new Choice(iz->T("Install"))); @@ -166,5 +193,10 @@ void InstallZipScreen::update() { MainScreen::showHomebrewTab = returnToHomebrew_; } } + + if (existingSaveView_) { + std::shared_ptr ginfo = g_gameInfoCache->GetInfo(screenManager()->getDrawContext(), savedataToOverwrite_, GameInfoFlags::FILE_TYPE | GameInfoFlags::PARAM_SFO | GameInfoFlags::ICON | GameInfoFlags::SIZE); + existingSaveView_->Update(ginfo.get()); + } UIScreen::update(); } diff --git a/UI/InstallZipScreen.h b/UI/InstallZipScreen.h index 17bea53dc9..68ad95c427 100644 --- a/UI/InstallZipScreen.h +++ b/UI/InstallZipScreen.h @@ -24,6 +24,8 @@ #include "UI/MiscScreens.h" +class SavedataView; + class InstallZipScreen : public UIDialogScreenWithBackground { public: InstallZipScreen(const Path &zipPath); @@ -41,6 +43,8 @@ private: UI::Choice *installChoice_ = nullptr; UI::Choice *backChoice_ = nullptr; UI::TextView *doneView_ = nullptr; + SavedataView *existingSaveView_ = nullptr; + Path savedataToOverwrite_; Path zipPath_; ZipFileInfo zipFileInfo_{}; bool returnToHomebrew_ = true; diff --git a/UI/MainScreen.cpp b/UI/MainScreen.cpp index d9967550e0..2730803e47 100644 --- a/UI/MainScreen.cpp +++ b/UI/MainScreen.cpp @@ -1544,6 +1544,7 @@ UI::EventReturn MainScreen::OnGameHighlight(UI::EventParams &e) { } UI::EventReturn MainScreen::OnGameSelectedInstant(UI::EventParams &e) { + // TODO: This is really not necessary here in all cases. g_Config.Save("MainScreen::OnGameSelectedInstant"); ScreenManager *screen = screenManager(); LaunchFile(screen, Path(e.s)); diff --git a/UI/SavedataScreen.cpp b/UI/SavedataScreen.cpp index 79b034110b..11a140e219 100644 --- a/UI/SavedataScreen.cpp +++ b/UI/SavedataScreen.cpp @@ -23,6 +23,7 @@ #include "Common/Data/Encoding/Utf8.h" #include "Common/Data/Text/I18n.h" #include "Common/Math/curves.h" +#include "Common/Data/Text/Parsers.h" #include "Common/System/NativeApp.h" #include "Common/System/Request.h" #include "Common/Data/Encoding/Utf8.h" @@ -46,85 +47,112 @@ class SavedataButton; -std::string GetFileDateAsString(const Path &filename) { - tm time; - if (File::GetModifTime(filename, time)) { - char buf[256]; - switch (g_Config.iDateFormat) { - case PSP_SYSTEMPARAM_DATE_FORMAT_YYYYMMDD: - strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &time); - break; - case PSP_SYSTEMPARAM_DATE_FORMAT_MMDDYYYY: - strftime(buf, sizeof(buf), "%m-%d-%Y %H:%M:%S", &time); - break; - case PSP_SYSTEMPARAM_DATE_FORMAT_DDMMYYYY: - strftime(buf, sizeof(buf), "%d-%m-%Y %H:%M:%S", &time); - break; - default: // Should never happen - return ""; +SavedataView::SavedataView(UIContext &dc, const Path &savePath, IdentifiedFileType type, std::string_view title, std::string_view savedataTitle, std::string_view savedataDetail, std::string_view fileSize, std::string_view mtime, bool showIcon, UI::LayoutParams *layoutParams) + : LinearLayout(UI::ORIENT_VERTICAL, layoutParams) +{ + using namespace UI; + + const Style &textStyle = dc.theme->popupStyle; + LinearLayout *toprow = new LinearLayout(ORIENT_HORIZONTAL, new LayoutParams(FILL_PARENT, WRAP_CONTENT)); + Add(toprow); + toprow->SetSpacing(0.0); + + savedataTitle_ = nullptr; + fileSize_ = nullptr; + mTime_ = nullptr; + detail_ = nullptr; + if (type == IdentifiedFileType::PSP_SAVEDATA_DIRECTORY) { + if (showIcon) { + toprow->Add(new GameIconView(savePath, 2.0f, new LinearLayoutParams(Margins(5, 5)))); } - return std::string(buf); + LinearLayout *topright = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT, 1.0f)); + topright->SetSpacing(1.0f); + savedataTitle_ = topright->Add(new TextView(savedataTitle, ALIGN_LEFT | FLAG_WRAP_TEXT, false)); + savedataTitle_->SetTextColor(textStyle.fgColor); + fileSize_ = topright->Add(new TextView(fileSize, 0, true)); + fileSize_->SetTextColor(textStyle.fgColor); + mTime_ = topright->Add(new TextView(mtime, 0, true)); + mTime_->SetTextColor(textStyle.fgColor); + toprow->Add(topright); + Add(new Spacer(3.0)); + detail_ = Add(new TextView(ReplaceAll(savedataDetail, "\r", ""), ALIGN_LEFT | FLAG_WRAP_TEXT, true, new LinearLayoutParams(Margins(10, 0)))); + detail_->SetTextColor(textStyle.fgColor); + Add(new Spacer(3.0)); + } else { + _dbg_assert_(type == IdentifiedFileType::PPSSPP_SAVESTATE); + Path image_path = savePath.WithReplacedExtension(".ppst", ".jpg"); + if (File::Exists(image_path)) { + toprow->Add(new AsyncImageFileView(image_path, IS_KEEP_ASPECT, new LinearLayoutParams(480, 272, Margins(10, 0)))); + } else { + auto sa = GetI18NCategory(I18NCat::SAVEDATA); + toprow->Add(new TextView(sa->T("No screenshot"), new LinearLayoutParams(Margins(10, 5))))->SetTextColor(textStyle.fgColor); + } + mTime_ = Add(new TextView(mtime, 0, true, new LinearLayoutParams(Margins(10, 5)))); + mTime_->SetTextColor(textStyle.fgColor); } - return ""; } -static std::string TrimString(const std::string &str) { - size_t pos = str.find_last_not_of(" \r\n\t"); - if (pos != str.npos) { - return str.substr(0, pos + 1); +void SavedataView::Update(GameInfo *ginfo) { + if (!ginfo->Ready(GameInfoFlags::PARAM_SFO | GameInfoFlags::SIZE)) { + return; + } + _dbg_assert_(savedataTitle_); + if (savedataTitle_) { + savedataTitle_->SetText(ginfo->GetParamSFO().GetValueString("SAVEDATA_TITLE")); + } + if (detail_) { + detail_->SetText(ginfo->GetParamSFO().GetValueString("SAVEDATA_DETAIL")); + } + if (fileSize_) { + fileSize_->SetText(NiceSizeFormat(ginfo->gameSizeOnDisk)); + } + if (mTime_) { + mTime_->SetText(ginfo->GetMTime()); } - return str; } +SavedataView::SavedataView(UIContext &dc, GameInfo *ginfo, IdentifiedFileType type, bool showIcon, UI::LayoutParams *layoutParams) + : SavedataView(dc, + ginfo->GetFilePath(), + type, + "", + "", + "", + "", + "", + showIcon, + layoutParams) {} + class SavedataPopupScreen : public PopupScreen { public: - SavedataPopupScreen(std::string savePath, std::string title) : PopupScreen(TrimString(title)), savePath_(savePath) { } + SavedataPopupScreen(Path savePath, std::string_view title) : PopupScreen(StripSpaces(title)), savePath_(savePath) { } const char *tag() const override { return "SavedataPopup"; } + void update() override { + std::shared_ptr ginfo = g_gameInfoCache->GetInfo(screenManager()->getDrawContext(), savePath_, GameInfoFlags::PARAM_SFO | GameInfoFlags::ICON | GameInfoFlags::SIZE); + if (!ginfo->Ready(GameInfoFlags::PARAM_SFO)) { + // Hm, this is no good. But hopefully the previous screen loaded it. + return; + } + if (savedataView_) { + savedataView_->Update(ginfo.get()); + } + } void CreatePopupContents(UI::ViewGroup *parent) override { using namespace UI; UIContext &dc = *screenManager()->getUIContext(); - const Style &textStyle = dc.theme->popupStyle; std::shared_ptr ginfo = g_gameInfoCache->GetInfo(screenManager()->getDrawContext(), savePath_, GameInfoFlags::PARAM_SFO | GameInfoFlags::ICON | GameInfoFlags::SIZE); - if (!ginfo->Ready(GameInfoFlags::PARAM_SFO)) - return; + if (!ginfo->Ready(GameInfoFlags::PARAM_SFO)) { + // This is OK, handled in Update. Though most likely, the previous screen loaded it. + } ScrollView *contentScroll = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 1.0f, UI::Margins(0, 3))); - LinearLayout *content = new LinearLayout(ORIENT_VERTICAL); parent->Add(contentScroll); - contentScroll->Add(content); - LinearLayout *toprow = new LinearLayout(ORIENT_HORIZONTAL, new LayoutParams(FILL_PARENT, WRAP_CONTENT)); - content->Add(toprow); - toprow->SetSpacing(0.0); - if (ginfo->fileType == IdentifiedFileType::PSP_SAVEDATA_DIRECTORY) { - std::string savedata_detail = ginfo->paramSFO.GetValueString("SAVEDATA_DETAIL"); - std::string savedata_title = ginfo->paramSFO.GetValueString("SAVEDATA_TITLE"); - - if (ginfo->icon.texture) { - toprow->Add(new GameIconView(savePath_, 2.0f, new LinearLayoutParams(Margins(5, 5)))); - } - LinearLayout *topright = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT, 1.0f)); - topright->SetSpacing(1.0f); - topright->Add(new TextView(savedata_title, ALIGN_LEFT | FLAG_WRAP_TEXT, false))->SetTextColor(textStyle.fgColor); - topright->Add(new TextView(StringFromFormat("%lld kB", ginfo->gameSizeOnDisk / 1024), 0, true))->SetTextColor(textStyle.fgColor); - topright->Add(new TextView(GetFileDateAsString(savePath_ / "PARAM.SFO"), 0, true))->SetTextColor(textStyle.fgColor); - toprow->Add(topright); - content->Add(new Spacer(3.0)); - content->Add(new TextView(ReplaceAll(savedata_detail, "\r", ""), ALIGN_LEFT | FLAG_WRAP_TEXT, true, new LinearLayoutParams(Margins(10, 0))))->SetTextColor(textStyle.fgColor); - content->Add(new Spacer(3.0)); - } else { - Path image_path = savePath_.WithReplacedExtension(".ppst", ".jpg"); - if (File::Exists(image_path)) { - toprow->Add(new AsyncImageFileView(image_path, IS_KEEP_ASPECT, new LinearLayoutParams(480, 272, Margins(10, 0)))); - } else { - auto sa = GetI18NCategory(I18NCat::SAVEDATA); - toprow->Add(new TextView(sa->T("No screenshot"), new LinearLayoutParams(Margins(10, 5))))->SetTextColor(textStyle.fgColor); - } - content->Add(new TextView(GetFileDateAsString(savePath_), 0, true, new LinearLayoutParams(Margins(10, 5))))->SetTextColor(textStyle.fgColor); - } + // TODO: If the game info wasn't already loaded, we'll get a bogus fileType here. + savedataView_ = contentScroll->Add(new SavedataView(dc, ginfo.get(), ginfo->fileType, true)); auto di = GetI18NCategory(I18NCat::DIALOG); LinearLayout *buttons = new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)); @@ -141,6 +169,7 @@ protected: private: UI::EventReturn OnDeleteButtonClick(UI::EventParams &e); + SavedataView *savedataView_ = nullptr; Path savePath_; }; @@ -244,14 +273,15 @@ bool SavedataButton::UpdateText() { } void SavedataButton::UpdateText(const std::shared_ptr &ginfo) { + _dbg_assert_(ginfo->Ready(GameInfoFlags::PARAM_SFO)); const std::string currentTitle = ginfo->GetTitle(); if (!currentTitle.empty()) { title_ = CleanSaveString(currentTitle); } if (subtitle_.empty() && ginfo->gameSizeOnDisk > 0) { - std::string date = GetFileDateAsString(ginfo->GetFilePath() / "PARAM.SFO"); - std::string savedata_title = ginfo->paramSFO.GetValueString("SAVEDATA_TITLE"); - subtitle_ = CleanSaveString(savedata_title) + StringFromFormat(" (%lld kB, %s)", ginfo->gameSizeOnDisk / 1024, date.c_str()); + std::string date = ginfo->GetMTime(); + std::string savedata_title = ginfo->GetParamSFO().GetValueString("SAVEDATA_TITLE"); + subtitle_ = CleanSaveString(savedata_title) + " (" + NiceSizeFormat(ginfo->gameSizeOnDisk) + ", " + date.c_str() + ")"; } } @@ -648,7 +678,7 @@ UI::EventReturn SavedataScreen::OnSavedataButtonClick(UI::EventParams &e) { if (!ginfo->Ready(GameInfoFlags::PARAM_SFO)) { return UI::EVENT_DONE; } - SavedataPopupScreen *popupScreen = new SavedataPopupScreen(e.s, ginfo->GetTitle()); + SavedataPopupScreen *popupScreen = new SavedataPopupScreen(Path(e.s), ginfo->GetTitle()); if (e.v) { popupScreen->SetPopupOrigin(e.v); } diff --git a/UI/SavedataScreen.h b/UI/SavedataScreen.h index 69544d9bf4..5980f370c8 100644 --- a/UI/SavedataScreen.h +++ b/UI/SavedataScreen.h @@ -145,3 +145,17 @@ private: bool hasDateSeconds_ = false; }; +// View used for the detailed popup, and also in the import savedata comparison. +// It doesn't do its own data loading for that reason. +class SavedataView : public UI::LinearLayout { +public: + SavedataView(UIContext &dc, GameInfo *ginfo, IdentifiedFileType type, bool showIcon, UI::LayoutParams *layoutParams = nullptr); + SavedataView(UIContext &dc, const Path &savePath, IdentifiedFileType type, std::string_view title, std::string_view savedataTitle, std::string_view savedataDetail, std::string_view fileSize, std::string_view mtime, bool showIcon, UI::LayoutParams *layoutParams = nullptr); + + void Update(GameInfo *ginfo); +private: + UI::TextView *savedataTitle_ = nullptr; + UI::TextView *detail_ = nullptr; + UI::TextView *mTime_ = nullptr; + UI::TextView *fileSize_ = nullptr; +};