From 0afdb11a428432afd595781d18bf6282763d1424 Mon Sep 17 00:00:00 2001 From: Christian Heimlich Date: Tue, 26 Nov 2024 01:47:36 -0500 Subject: [PATCH 01/20] Using explicit object parameters for CRTP --- CMakeLists.txt | 2 +- app/src/frontend/attractmode/am-items.h | 4 +- .../frontend/attractmode/am-settings-items.h | 8 +-- app/src/frontend/fe-items.h | 69 +++++++++++-------- app/src/frontend/launchbox/lb-items.h | 18 ++--- 5 files changed, 57 insertions(+), 44 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 43dfe32..a8a85fa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,7 +27,7 @@ set(TARGET_FP_VERSION_PREFIX 13.0) option(BUILD_SHARED_LIBS "Build FIL with shared libraries" OFF) # C++ -set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) # Build augmentation diff --git a/app/src/frontend/attractmode/am-items.h b/app/src/frontend/attractmode/am-items.h index b2accaf..8fb4029 100644 --- a/app/src/frontend/attractmode/am-items.h +++ b/app/src/frontend/attractmode/am-items.h @@ -80,7 +80,7 @@ class RomEntry : public Fe::Game QString rating() const; }; -class RomEntry::Builder : public Fe::Game::Builder +class RomEntry::Builder : public Fe::Game::Builder { //-Constructor------------------------------------------------------------------------------------------------- public: @@ -132,7 +132,7 @@ class EmulatorArtworkEntry : public Fe::Item QStringList paths() const; }; -class EmulatorArtworkEntry::Builder : public Fe::Item::Builder +class EmulatorArtworkEntry::Builder : public Fe::Item::Builder { //-Constructor------------------------------------------------------------------------------------------------- public: diff --git a/app/src/frontend/attractmode/am-settings-items.h b/app/src/frontend/attractmode/am-settings-items.h index ea1029e..106d046 100644 --- a/app/src/frontend/attractmode/am-settings-items.h +++ b/app/src/frontend/attractmode/am-settings-items.h @@ -50,7 +50,7 @@ class DisplayGlobalFilter : public SettingsItem QStringList exceptions() const; }; -class DisplayGlobalFilter::Builder : public Fe::Item::Builder +class DisplayGlobalFilter::Builder : public Fe::Item::Builder { //-Constructor------------------------------------------------------------------------------------------------- public: @@ -129,7 +129,7 @@ class DisplayFilter : public SettingsItem int listLimit() const; }; -class DisplayFilter::Builder : public Fe::Item::Builder +class DisplayFilter::Builder : public Fe::Item::Builder { //-Constructor------------------------------------------------------------------------------------------------- public: @@ -183,7 +183,7 @@ class Display : public SettingsItem const QList& filters() const; }; -class Display::Builder : public Fe::Item::Builder +class Display::Builder : public Fe::Item::Builder { //-Constructor------------------------------------------------------------------------------------------------- public: @@ -243,7 +243,7 @@ class OtherSetting : public SettingsItem QList contents() const; }; -class OtherSetting::Builder : public Fe::Item::Builder +class OtherSetting::Builder : public Fe::Item::Builder { //-Constructor------------------------------------------------------------------------------------------------- public: diff --git a/app/src/frontend/fe-items.h b/app/src/frontend/fe-items.h index 1c2cf1b..8122e83 100644 --- a/app/src/frontend/fe-items.h +++ b/app/src/frontend/fe-items.h @@ -31,7 +31,7 @@ class Item { //-Inner Classes--------------------------------------------------------------------------------------------------- public: - template + template requires std::derived_from class Builder; @@ -54,7 +54,7 @@ class Item void transferOtherFields(QHash& otherFields); }; -template +template requires std::derived_from class Item::Builder { @@ -72,10 +72,11 @@ class Item::Builder //-Instance Functions------------------------------------------------------------------------------------------ public: - B& wOtherField(QPair otherField) + template + auto wOtherField(this Self&& self, QPair otherField) { - mItemBlueprint.mOtherFields[otherField.first] = otherField.second; - return static_cast(*this); + self.mItemBlueprint.mOtherFields[otherField.first] = otherField.second; + return self; } T build() { return mItemBlueprint; } std::shared_ptr buildShared() { return std::make_shared(mItemBlueprint); } @@ -85,7 +86,7 @@ class BasicItem : public Item { //-Inner Classes--------------------------------------------------------------------------------------------------- public: - template + template requires std::derived_from class Builder; @@ -105,22 +106,27 @@ class BasicItem : public Item QString name() const; }; -template +template requires std::derived_from -class BasicItem::Builder : public Item::Builder +class BasicItem::Builder : public Item::Builder { //-Instance Functions------------------------------------------------------------------------------------------ public: - B& wId(const QString& rawId) { Item::Builder::mItemBlueprint.mId = QUuid(rawId); return static_cast(*this); } - B& wId(const QUuid& id) { Item::Builder::mItemBlueprint.mId = id; return static_cast(*this); } - B& wName(const QString& name) { Item::Builder::mItemBlueprint.mName = name; return static_cast(*this);} + template + auto wId(this Self&& self, const QString& rawId) { self.mItemBlueprint.mId = QUuid(rawId); return self; } + + template + auto wId(this Self&& self, const QUuid& id) { self.mItemBlueprint.mId = id; return self; } + + template + auto wName(this Self&& self, const QString& name) { self.mItemBlueprint.mName = name; return self;} }; class Game : public BasicItem { //-Inner Classes--------------------------------------------------------------------------------------------------- public: - template + template requires std::derived_from class Builder; @@ -143,20 +149,21 @@ class Game : public BasicItem QString platform() const; }; -template +template requires std::derived_from -class Game::Builder : public BasicItem::Builder +class Game::Builder : public BasicItem::Builder { //-Instance Functions------------------------------------------------------------------------------------------ public: - B& wPlatform(const QString& platform) { Item::Builder::mItemBlueprint.mPlatform = platform; return static_cast(*this); } + template + auto wPlatform(this Self&& self, const QString& platform) { self.mItemBlueprint.mPlatform = platform; return self; } }; class AddApp : public BasicItem { //-Inner Classes--------------------------------------------------------------------------------------------------- public: - template + template requires std::derived_from class Builder; @@ -174,21 +181,24 @@ class AddApp : public BasicItem QUuid gameId() const; }; -template +template requires std::derived_from -class AddApp::Builder : public BasicItem::Builder +class AddApp::Builder : public BasicItem::Builder { //-Instance Functions------------------------------------------------------------------------------------------ public: - B& wGameId(const QString& rawGameId) { Item::Builder::mItemBlueprint.mGameId = QUuid(rawGameId); return static_cast(*this); } - B& wGameId(const QUuid& gameId) { Item::Builder::mItemBlueprint.mGameId = gameId; return *this; } + template + auto wGameId(this Self&& self, const QString& rawGameId) { self.mItemBlueprint.mGameId = QUuid(rawGameId); return self; } + + template + auto wGameId(this Self&& self, const QUuid& gameId) { self.mItemBlueprint.mGameId = gameId; return self; } }; class PlaylistHeader : public BasicItem { //-Inner Classes--------------------------------------------------------------------------------------------------- public: - template + template requires std::derived_from class Builder; @@ -198,16 +208,16 @@ class PlaylistHeader : public BasicItem PlaylistHeader(QUuid id, QString name); }; -template +template requires std::derived_from -class PlaylistHeader::Builder : public BasicItem::Builder +class PlaylistHeader::Builder : public BasicItem::Builder {}; class PlaylistGame : public BasicItem { //-Inner Classes--------------------------------------------------------------------------------------------------- public: - template + template requires std::derived_from class Builder; @@ -224,15 +234,18 @@ class PlaylistGame : public BasicItem QUuid gameId() const; }; -template +template requires std::derived_from -class PlaylistGame::Builder : public BasicItem::Builder +class PlaylistGame::Builder : public BasicItem::Builder { //-Instance Functions------------------------------------------------------------------------------------------ public: // These reuse the main ID on purpose, in this case gameId is a proxy for Id - B& wGameId(QString rawGameId) { Item::Builder::mItemBlueprint.mId = QUuid(rawGameId); return static_cast(*this); } - B& wGameId(QUuid gameId) { Item::Builder::mItemBlueprint.mId = gameId; return static_cast(*this); } + template + auto wGameId(this Self&& self, QString rawGameId) { self.mItemBlueprint.mId = QUuid(rawGameId); return self; } + + template + auto wGameId(this Self&& self, QUuid gameId) { self.mItemBlueprint.mId = gameId; return self; } }; } diff --git a/app/src/frontend/launchbox/lb-items.h b/app/src/frontend/launchbox/lb-items.h index 9c7ad1d..685dd3e 100644 --- a/app/src/frontend/launchbox/lb-items.h +++ b/app/src/frontend/launchbox/lb-items.h @@ -68,7 +68,7 @@ class Game : public Fe::Game QString releaseType() const; }; -class Game::Builder : public Fe::Game::Builder +class Game::Builder : public Fe::Game::Builder { //-Constructor------------------------------------------------------------------------------------------------- public: @@ -122,7 +122,7 @@ class AddApp : public Fe::AddApp bool isWaitForExit() const; }; -class AddApp::Builder : public Fe::AddApp::Builder +class AddApp::Builder : public Fe::AddApp::Builder { //-Constructor------------------------------------------------------------------------------------------------- public: @@ -163,7 +163,7 @@ class CustomField : public Fe::Item QString value() const; }; -class CustomField::Builder : public Fe::Item::Builder +class CustomField::Builder : public Fe::Item::Builder { //-Constructor------------------------------------------------------------------------------------------------- public: @@ -200,7 +200,7 @@ class PlaylistHeader : public Fe::PlaylistHeader QString notes() const; }; -class PlaylistHeader::Builder : public Fe::PlaylistHeader::Builder +class PlaylistHeader::Builder : public Fe::PlaylistHeader::Builder { //-Constructor------------------------------------------------------------------------------------------------- public: @@ -260,7 +260,7 @@ class PlaylistGame : public Fe::PlaylistGame void setLBDatabaseId(int lbDbId); }; -class PlaylistGame::Builder : public Fe::PlaylistGame::Builder +class PlaylistGame::Builder : public Fe::PlaylistGame::Builder { //-Constructor------------------------------------------------------------------------------------------------- public: @@ -296,7 +296,7 @@ class Platform : public Fe::Item // QString category() const; }; -class Platform::Builder : public Fe::Item::Builder +class Platform::Builder : public Fe::Item::Builder { //-Constructor------------------------------------------------------------------------------------------------- public: @@ -332,7 +332,7 @@ class PlatformFolder : public Fe::Item QString identifier() const; }; -class PlatformFolder::Builder : public Fe::Item::Builder +class PlatformFolder::Builder : public Fe::Item::Builder { //-Constructor------------------------------------------------------------------------------------------------- public: @@ -366,7 +366,7 @@ class PlatformCategory : public Fe::Item QString nestedName() const; }; -class PlatformCategory::Builder : public Fe::Item::Builder +class PlatformCategory::Builder : public Fe::Item::Builder { //-Constructor------------------------------------------------------------------------------------------------- public: @@ -403,7 +403,7 @@ class Parent : public Fe::Item QUuid playlistId() const; }; -class Parent::Builder : public Fe::Item::Builder +class Parent::Builder : public Fe::Item::Builder { //-Constructor------------------------------------------------------------------------------------------------- public: From 7ac9c05eb30c065cb1e25d27599448b8c0cdcc86 Mon Sep 17 00:00:00 2001 From: Christian Heimlich Date: Sat, 30 Nov 2024 19:05:32 -0500 Subject: [PATCH 02/20] Restructure project, use bindable properties, use "Launcher" Multi-component overhaul in pursuit of overall streamlining. - Restructure the project for overall better organization - Use "Launcher" nomenclature over "Frontend" to better match with the name of the app and to avoid ambiguity with the UI itself - Use Qx Bindable Properties (originally was to be Qt Bindable Properties, but oh boy that was a rabbit whole that lead to the creation of the former) for the UI - Minor bug fixes --- .github/workflows/build-project.yml | 22 +- CMakeLists.txt | 6 +- app/CMakeLists.txt | 74 +- .../AttractMode/icon.png | Bin .../AttractMode/marquee.png | Bin .../{frontend => launcher}/LaunchBox/icon.svg | 0 app/res/resources.qrc | 6 +- app/src/import/properties.cpp | 151 +++ app/src/import/properties.h | 98 ++ app/src/import/settings.h | 50 + .../{import-worker.cpp => import/worker.cpp} | 174 +-- app/src/{import-worker.h => import/worker.h} | 66 +- app/src/{ => kernel}/clifp.cpp | 0 app/src/{ => kernel}/clifp.h | 0 app/src/kernel/controller.cpp | 341 ++++++ app/src/kernel/controller.h | 105 ++ .../attractmode/am-data.cpp | 80 +- .../attractmode/am-data.h | 46 +- .../attractmode/am-install.cpp | 97 +- .../attractmode/am-install.h | 52 +- .../attractmode/am-install_linux.cpp | 0 .../attractmode/am-install_win.cpp | 0 .../attractmode/am-items.cpp | 10 +- .../attractmode/am-items.h | 22 +- .../attractmode/am-settings-data.cpp | 8 +- .../attractmode/am-settings-data.h | 10 +- .../attractmode/am-settings-items.cpp | 0 .../attractmode/am-settings-items.h | 12 +- .../launchbox/lb-data.cpp | 100 +- .../launchbox/lb-data.h | 58 +- .../launchbox/lb-install.cpp | 103 +- .../launchbox/lb-install.h | 50 +- .../launchbox/lb-items.cpp | 10 +- .../launchbox/lb-items.h | 38 +- .../fe-data.cpp => launcher/lr-data.cpp} | 69 +- .../fe-data.h => launcher/lr-data.h} | 61 +- .../lr-install.cpp} | 30 +- .../fe-install.h => launcher/lr-install.h} | 46 +- .../lr-installfoundation.cpp} | 24 +- .../lr-installfoundation.h} | 26 +- .../lr-installfoundation_linux.cpp} | 4 +- .../lr-installfoundation_win.cpp} | 5 +- .../fe-items.cpp => launcher/lr-items.cpp} | 4 +- .../fe-items.h => launcher/lr-items.h} | 14 +- app/src/main.cpp | 12 +- app/src/ui/mainwindow.cpp | 1044 +++++------------ app/src/ui/mainwindow.h | 247 ++-- app/src/ui/mainwindow.ui | 191 ++- app/src/ui/progresspresenter.cpp | 5 +- app/src/ui/progresspresenter.h | 6 +- 50 files changed, 1888 insertions(+), 1689 deletions(-) rename app/res/{frontend => launcher}/AttractMode/icon.png (100%) rename app/res/{frontend => launcher}/AttractMode/marquee.png (100%) rename app/res/{frontend => launcher}/LaunchBox/icon.svg (100%) create mode 100644 app/src/import/properties.cpp create mode 100644 app/src/import/properties.h create mode 100644 app/src/import/settings.h rename app/src/{import-worker.cpp => import/worker.cpp} (85%) rename app/src/{import-worker.h => import/worker.h} (75%) rename app/src/{ => kernel}/clifp.cpp (100%) rename app/src/{ => kernel}/clifp.h (100%) create mode 100644 app/src/kernel/controller.cpp create mode 100644 app/src/kernel/controller.h rename app/src/{frontend => launcher}/attractmode/am-data.cpp (93%) rename app/src/{frontend => launcher}/attractmode/am-data.h (93%) rename app/src/{frontend => launcher}/attractmode/am-install.cpp (84%) rename app/src/{frontend => launcher}/attractmode/am-install.h (70%) rename app/src/{frontend => launcher}/attractmode/am-install_linux.cpp (100%) rename app/src/{frontend => launcher}/attractmode/am-install_win.cpp (100%) rename app/src/{frontend => launcher}/attractmode/am-items.cpp (97%) rename app/src/{frontend => launcher}/attractmode/am-items.h (88%) rename app/src/{frontend => launcher}/attractmode/am-settings-data.cpp (98%) rename app/src/{frontend => launcher}/attractmode/am-settings-data.h (96%) rename app/src/{frontend => launcher}/attractmode/am-settings-items.cpp (100%) rename app/src/{frontend => launcher}/attractmode/am-settings-items.h (96%) rename app/src/{frontend => launcher}/launchbox/lb-data.cpp (94%) rename app/src/{frontend => launcher}/launchbox/lb-data.h (85%) rename app/src/{frontend => launcher}/launchbox/lb-install.cpp (81%) rename app/src/{frontend => launcher}/launchbox/lb-install.h (74%) rename app/src/{frontend => launcher}/launchbox/lb-items.cpp (98%) rename app/src/{frontend => launcher}/launchbox/lb-items.h (93%) rename app/src/{frontend/fe-data.cpp => launcher/lr-data.cpp} (91%) rename app/src/{frontend/fe-data.h => launcher/lr-data.h} (93%) rename app/src/{frontend/fe-install.cpp => launcher/lr-install.cpp} (80%) rename app/src/{frontend/fe-install.h => launcher/lr-install.h} (73%) rename app/src/{frontend/fe-installfoundation.cpp => launcher/lr-installfoundation.cpp} (90%) rename app/src/{frontend/fe-installfoundation.h => launcher/lr-installfoundation.h} (87%) rename app/src/{frontend/fe-installfoundation_linux.cpp => launcher/lr-installfoundation_linux.cpp} (92%) rename app/src/{frontend/fe-installfoundation_win.cpp => launcher/lr-installfoundation_win.cpp} (97%) rename app/src/{frontend/fe-items.cpp => launcher/lr-items.cpp} (99%) rename app/src/{frontend/fe-items.h => launcher/lr-items.h} (96%) diff --git a/.github/workflows/build-project.yml b/.github/workflows/build-project.yml index dd88b75..6e02549 100644 --- a/.github/workflows/build-project.yml +++ b/.github/workflows/build-project.yml @@ -12,6 +12,7 @@ on: # the "pull_request" trigger works (it works off master correctly for a merged PR but is triggered # by dev +# No GCC because it's not packaged for 20.04 or 22.04 yet jobs: trigger-build: name: Build Project @@ -23,5 +24,24 @@ jobs: runs_exclude: > [ { "linkage": "shared" }, - { "compiler": "g++-10" } + { "compiler": "g++-10" }, + { "compiler": "g++-12" }, + { "compiler": "clang++-12" }, + { "compiler": "clang++-14" } ] + runs_include: > + [ + { "os": "ubuntu-20.04", "compiler": "clang++-18", "linkage": "static" }, + { "os": "ubuntu-22.04", "compiler": "clang++-18", "linkage": "static" } + ] + pre_build_steps: | + - name: Install Clang 18 [Linux] + if: env.run_is_linux == 'true' && env.run_compiler == 'clang++-18' + shell: bash + run: | + wget https://apt.llvm.org/llvm.sh + chmod u+x llvm.sh + sudo ./llvm.sh 18 + + + \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index a8a85fa..8c59f0a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,7 +13,7 @@ project(FIL # Get helper scripts include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/FetchOBCMake.cmake) -fetch_ob_cmake("9f45dec8dc1cd09d99862b7b7ea335b361cf7ff7") +fetch_ob_cmake("e88d54a044319c0cd12a9e68c5538bf1194e6285") # Initialize project according to standard rules include(OB/Project) @@ -74,7 +74,7 @@ endif() include(OB/FetchQx) ob_fetch_qx( - REF "0572d288936afd63ff6f5b6ce4b1fbfc0f03b0eb" + REF "6db968d73e681eac7b687b0f9105cde934e4a6b2" COMPONENTS ${FIL_QX_COMPONENTS} ) @@ -104,7 +104,7 @@ ob_fetch_clifp("7139ae998b292eb595e751ba4cb8599230435358") # Fetch Neargye's Magic Enum include(OB/FetchMagicEnum) -ob_fetch_magicenum("v0.9.3") +ob_fetch_magicenum("v0.9.7") # Process Targets set(APP_TARGET_NAME ${PROJECT_NAMESPACE_LC}_${PROJECT_NAMESPACE_LC}) diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index d5e7e9b..5dc495c 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -23,48 +23,53 @@ add_custom_target(fil_copy_clifp # ------------------ Setup FIL -------------------------- set(FIL_SOURCE - frontend/fe-data.h - frontend/fe-data.cpp - frontend/fe-installfoundation.h - frontend/fe-installfoundation.cpp - frontend/fe-installfoundation_win.cpp - frontend/fe-installfoundation_linux.cpp - frontend/fe-install.h - frontend/fe-install.cpp - frontend/fe-items.h - frontend/fe-items.cpp - frontend/attractmode/am-data.h - frontend/attractmode/am-data.cpp - frontend/attractmode/am-install.h - frontend/attractmode/am-install.cpp - frontend/attractmode/am-install_win.cpp - frontend/attractmode/am-install_linux.cpp - frontend/attractmode/am-items.h - frontend/attractmode/am-items.cpp - frontend/attractmode/am-settings-data.h - frontend/attractmode/am-settings-data.cpp - frontend/attractmode/am-settings-items.h - frontend/attractmode/am-settings-items.cpp + import/properties.h + import/properties.cpp + import/settings.h + import/worker.h + import/worker.cpp + launcher/lr-data.h + launcher/lr-data.cpp + launcher/lr-installfoundation.h + launcher/lr-installfoundation.cpp + launcher/lr-installfoundation_win.cpp + launcher/lr-installfoundation_linux.cpp + launcher/lr-install.h + launcher/lr-install.cpp + launcher/lr-items.h + launcher/lr-items.cpp + launcher/attractmode/am-data.h + launcher/attractmode/am-data.cpp + launcher/attractmode/am-install.h + launcher/attractmode/am-install.cpp + launcher/attractmode/am-install_win.cpp + launcher/attractmode/am-install_linux.cpp + launcher/attractmode/am-items.h + launcher/attractmode/am-items.cpp + launcher/attractmode/am-settings-data.h + launcher/attractmode/am-settings-data.cpp + launcher/attractmode/am-settings-items.h + launcher/attractmode/am-settings-items.cpp ui/mainwindow.h ui/mainwindow.cpp ui/mainwindow.ui ui/progresspresenter.h ui/progresspresenter.cpp - clifp.h - clifp.cpp - import-worker.h - import-worker.cpp + kernel/controller.h + kernel/controller.cpp + kernel/clifp.h + kernel/clifp.cpp main.cpp ) if(CMAKE_SYSTEM_NAME STREQUAL Windows) list(APPEND FIL_SOURCE - frontend/launchbox/lb-data.h - frontend/launchbox/lb-data.cpp - frontend/launchbox/lb-install.h - frontend/launchbox/lb-install.cpp - frontend/launchbox/lb-items.h - frontend/launchbox/lb-items.cpp + launcher/launchbox/lb-data.h + launcher/launchbox/lb-data.cpp + launcher/launchbox/lb-install.h + launcher/launchbox/lb-install.cpp + launcher/launchbox/lb-items.h + launcher/launchbox/lb-items.cpp ) endif() @@ -104,6 +109,11 @@ ob_add_standard_executable(${APP_TARGET_NAME} WIN32 ) +target_sources(fil_fil + PRIVATE + +) + # Note that the executable depends on CLIFp being copied into its build dir add_dependencies(${APP_TARGET_NAME} fil_copy_clifp) diff --git a/app/res/frontend/AttractMode/icon.png b/app/res/launcher/AttractMode/icon.png similarity index 100% rename from app/res/frontend/AttractMode/icon.png rename to app/res/launcher/AttractMode/icon.png diff --git a/app/res/frontend/AttractMode/marquee.png b/app/res/launcher/AttractMode/marquee.png similarity index 100% rename from app/res/frontend/AttractMode/marquee.png rename to app/res/launcher/AttractMode/marquee.png diff --git a/app/res/frontend/LaunchBox/icon.svg b/app/res/launcher/LaunchBox/icon.svg similarity index 100% rename from app/res/frontend/LaunchBox/icon.svg rename to app/res/launcher/LaunchBox/icon.svg diff --git a/app/res/resources.qrc b/app/res/resources.qrc index badfcbb..0ba26f3 100644 --- a/app/res/resources.qrc +++ b/app/res/resources.qrc @@ -8,9 +8,9 @@ ui/CLIFp.png ui/Exit.png ui/GitHub.png - frontend/LaunchBox/icon.svg - frontend/AttractMode/icon.png - frontend/AttractMode/marquee.png + launcher/LaunchBox/icon.svg + launcher/AttractMode/icon.png + launcher/AttractMode/marquee.png flashpoint/icon.png ui/About.png diff --git a/app/src/import/properties.cpp b/app/src/import/properties.cpp new file mode 100644 index 0000000..91dae9c --- /dev/null +++ b/app/src/import/properties.cpp @@ -0,0 +1,151 @@ +// Unit Include +#include "properties.h" + +// Qx Includes +#include + +// libfp Includes +#include + +// Project Includes +#include "launcher/lr-install.h" + +namespace Import +{ + +//=============================================================================================================== +// Properties +//=============================================================================================================== + +//-Constructor------------------------------------------------------------- +//Public: +Properties::Properties() : + mHasLinkPerms(testForLinkPermissions()), + mLauncher(nullptr), + mFlashpoint(nullptr) +{ + mLauncherReady.setBinding([this]{ return mLauncher.value() && mLauncher->isValid(); }); + mFlashpointReady.setBinding([this]{ return mFlashpoint.value() && mFlashpoint->isValid(); }); + mBothTargetsReady.setBinding([this]{ return mLauncherReady && mFlashpointReady; }); + mBothTargetsReady.addLifetimeNotifier([this]{ + if(mBothTargetsReady) + gatherTargetData(); + else + { + // Clear out selection lists + mPlatforms = QList(); + mPlaylists = QList(); + } + }); + mFlashpointTargetSeries.setBinding([this]{ return mFlashpointReady && installMatchesTargetSeries(*mFlashpoint.value()); }); + mImageModeOrder.setBinding([this]{ + /* Even though technically we only need the launcher, check for both installs to prevent the selection + * from moving until its section is available + */ + static QList defOrder{Import::ImageMode::Link, Import::ImageMode::Reference, Import::ImageMode::Copy}; + bool def = !mBothTargetsReady; + auto order = def ? defOrder : mLauncher->preferredImageModeOrder(); + if(!mHasLinkPerms) + order.removeAll(Import::ImageMode::Link); + + return order; + }); + mImageDownloadable.setBinding([this]{ + return mFlashpointReady && mFlashpoint->preferences().onDemandImages; + } ); + mLauncherInfo.setBinding([this]{ return mLauncherReady ? mLauncher->name() + ' ' + mLauncher->versionString() : QString(); }); + mFlashpointInfo.setBinding([this]{ return mFlashpointReady ? mFlashpoint->versionInfo()->fullString() : QString(); }); + mTagMap.setBinding([this]{ return mFlashpointReady ? mFlashpoint->database()->tags() : QMap(); }); +} + +//-Class Functions------------------------------------------------------------- +//Private: +bool Properties::testForLinkPermissions() +{ + QTemporaryDir testLinkDir; + if(testLinkDir.isValid()) + { + QFile testLinkTarget(testLinkDir.filePath(u"linktarget.tmp"_s)); + + if(testLinkTarget.open(QIODevice::WriteOnly)) + { + testLinkTarget.close(); + std::error_code symlinkError; + std::filesystem::create_symlink(testLinkTarget.fileName().toStdString(), testLinkDir.filePath(u"testlink.tmp"_s).toStdString(), symlinkError); + + if(!symlinkError) + return true; + } + } + + // Default + return false; +} + +bool Properties::installMatchesTargetSeries(const Fp::Install& fpInstall) +{ + Qx::VersionNumber fpVersion = fpInstall.versionInfo()->version(); + return TARGET_FP_VERSION_PREFIX.isPrefixOf(fpVersion) || + TARGET_FP_VERSION_PREFIX.normalized() == fpVersion; // Accounts for if FP doesn't use a trailing zero for major releases +} + +//-Instance Functions------------------------------------------------------------- +//Private: +void Properties::gatherTargetData() +{ + // IO Error check instance + Qx::Error existingCheck; + + // Get list of existing platforms and playlists + existingCheck = mLauncher->refreshExistingDocs(); + + // IO Error Check + if(existingCheck.isValid()) + { + Qx::postBlockingError(existingCheck); + mLauncher = nullptr; + return; + } + + /* We set the platform/playlist properties here instead of using a binding because gatherLauncherData() + * might need to be called in contexts where there is no trivial way to cause the binding to re-evaluate + * without adding a hacky bool property specifically for that purpose. + */ + QList plats; + for(const QString& p : mFlashpoint->database()->platformNames()) + plats.append({.name = p, .existing = mLauncher->containsPlatform(p)}); + + QList plays; + for(const QString& p : mFlashpoint->playlistManager()->playlistTitles()) + plays.append({.name = p, .existing = mLauncher->containsPlaylist(p)}); + + mPlatforms.setValue(std::move(plats)); + mPlaylists.setValue(std::move(plays)); +} + +//Public: +bool Properties::hasLinkPermissions() const { return mHasLinkPerms; } +bool Properties::isLauncherReady() const { return mLauncherReady; } +bool Properties::isFlashpointReady() const { return mFlashpointReady; } +bool Properties::isBothTargetsReady() const { return mBothTargetsReady; } +bool Properties::isFlashpointTargetSeries() const { return mFlashpointTargetSeries; } +const Qx::Bindable> Properties::bindableImageModeOrder() const { return mImageModeOrder; } +QList Properties::imageModeOrder() const { return mImageModeOrder; } +bool Properties::isImageDownloadable() const { return mImageDownloadable; } +QString Properties::launcherInfo() const { return mLauncherInfo; } +QString Properties::flashpointInfo() const { return mFlashpointInfo; } +const Qx::Bindable> Properties::bindableTagMap() const { return mTagMap; } +QMap Properties::tagMap() const { return mTagMap; } +const Qx::Bindable> Properties::bindablePlatforms() const { return mPlatforms; } +QList Properties::platforms() const { return mPlatforms; } +const Qx::Bindable> Properties::bindablePlaylists() const { return mPlaylists; } +QList Properties::playlists() const { return mPlaylists; } + +void Properties::setLauncher(std::unique_ptr&& launcher) { mLauncher = std::move(launcher); } +void Properties::setFlashpoint(std::unique_ptr&& flashpoint) { mFlashpoint = std::move(flashpoint); } +void Properties::refreshInstallData() { gatherTargetData(); } + +Lr::Install* Properties::launcher() { Q_ASSERT(*mLauncher); return (*mLauncher).get(); } +Fp::Install* Properties::flashpoint() { Q_ASSERT(*mFlashpoint); return (*mFlashpoint).get(); }; + +} diff --git a/app/src/import/properties.h b/app/src/import/properties.h new file mode 100644 index 0000000..6a4d830 --- /dev/null +++ b/app/src/import/properties.h @@ -0,0 +1,98 @@ +#ifndef IMPORT_PROPERTIES_H +#define IMPORT_PROPERTIES_H + +// Qx Includes +#include + +// Qx Includes +#include + +// libfp Includes +#include + +// Project Includes +#include "import/settings.h" +#include "project_vars.h" + +/* TODO: PROBABLY OK NOW The number of properties here has gotten somewhat out of hand. + * Mainwindow should probably just be given access to something like + * const QBindable> (and same for + * launcher) so that it can setup its own bindings/properties directly + * off of that. Since it would be read only, that still lets Controller + * have control of the instances. + */ + +namespace Lr { class Install; } +namespace Fp { class Install; } + +namespace Import +{ + +class Properties +{ +//-Class Variables--------------------------------------------------------------- +private: + // Flashpoint version check + static inline const Qx::VersionNumber TARGET_FP_VERSION_PREFIX = Qx::VersionNumber::fromString(PROJECT_TARGET_FP_VER_PFX_STR); + +//-Instance Variables------------------------------------------------------------- +private: + bool mHasLinkPerms; + Qx::Property> mLauncher; + Qx::Property> mFlashpoint; + Qx::Property mLauncherReady; + Qx::Property mFlashpointReady; + Qx::Property mBothTargetsReady; + Qx::Property mFlashpointTargetSeries; + Qx::Property> mImageModeOrder; + Qx::Property mImageDownloadable; + Qx::Property mLauncherInfo; + Qx::Property mFlashpointInfo; + Qx::Property> mTagMap; + Qx::Property> mPlatforms; + Qx::Property> mPlaylists; + +//-Constructor------------------------------------------------------------- +public: + Properties(); + +//-Class Functions------------------------------------------------------------- +private: + static bool testForLinkPermissions(); + +public: + static bool installMatchesTargetSeries(const Fp::Install& fpInstall); + +//-Instance Functions------------------------------------------------------------- +private: + void gatherTargetData(); + +public: + bool hasLinkPermissions() const; + bool isLauncherReady() const; + bool isFlashpointReady() const; + bool isBothTargetsReady() const; + bool isFlashpointTargetSeries() const; + const Qx::Bindable> bindableImageModeOrder() const; + QList imageModeOrder() const; + bool isImageDownloadable() const; + QString launcherInfo() const; + QString flashpointInfo() const; + const Qx::Bindable> bindableTagMap() const; + QMap tagMap() const; + const Qx::Bindable> bindablePlatforms() const; + QList platforms() const; + const Qx::Bindable> bindablePlaylists() const; + QList playlists() const; + + void setLauncher(std::unique_ptr&& launcher); + void setFlashpoint(std::unique_ptr&& flashpoint); + void refreshInstallData(); + + Lr::Install* launcher(); + Fp::Install* flashpoint(); +}; + +} + +#endif // IMPORT_PROPERTIES_H diff --git a/app/src/import/settings.h b/app/src/import/settings.h new file mode 100644 index 0000000..40c990c --- /dev/null +++ b/app/src/import/settings.h @@ -0,0 +1,50 @@ +#ifndef IMPORT_SETTINGS_H +#define IMPORT_SETTINGS_H + +// Qt Includes +#include +#include + +// libfp Includes +#include + +namespace Import +{ + +// Enums +enum class Install{ Launcher, Flashpoint }; +enum class UpdateMode {OnlyNew, NewAndExisting}; +enum class ImageMode {Copy, Reference, Link}; +enum class PlaylistGameMode {SelectedPlatform, ForceAll}; + +// Structs +struct Importee +{ + QString name; + bool existing = false; +}; + +struct Selections +{ + QStringList platforms; + QStringList playlists; +}; + +struct UpdateOptions +{ + UpdateMode importMode; + bool removeObsolete; +}; + +struct OptionSet +{ + UpdateOptions updateOptions; + ImageMode imageMode; + bool downloadImages; + PlaylistGameMode playlistMode; + Fp::Db::InclusionOptions inclusionOptions; +}; + +} + +#endif // IMPORT_SETTINGS_H diff --git a/app/src/import-worker.cpp b/app/src/import/worker.cpp similarity index 85% rename from app/src/import-worker.cpp rename to app/src/import/worker.cpp index 8207e55..0eb26a9 100644 --- a/app/src/import-worker.cpp +++ b/app/src/import/worker.cpp @@ -1,5 +1,5 @@ // Unit Include -#include "import-worker.h" +#include "worker.h" // Standard Library Includes #include @@ -12,7 +12,10 @@ #include // Project Includes -#include "clifp.h" +#include "kernel/clifp.h" + +namespace Import +{ //=============================================================================================================== // ImageTransferError @@ -55,16 +58,13 @@ QString ImageTransferError::deriveDetails() const QString ImageTransferError::deriveCaption() const { return CAPTION_IMAGE_ERR; } //=============================================================================================================== -// IMPORT WORKER +// Worker //=============================================================================================================== //-Constructor--------------------------------------------------------------------------------------------------- -ImportWorker::ImportWorker(std::shared_ptr fpInstallForWork, - std::shared_ptr feInstallForWork, - ImportSelections importSelections, - OptionSet optionSet) : - mFlashpointInstall(fpInstallForWork), - mFrontendInstall(feInstallForWork), +Worker::Worker(Fp::Install* flashpoint, Lr::Install* launcher, Selections importSelections, OptionSet optionSet) : + mFlashpointInstall(flashpoint), + mLauncherInstall(launcher), mImportSelections(importSelections), mOptionSet(optionSet), mCurrentProgress(0), @@ -73,7 +73,7 @@ ImportWorker::ImportWorker(std::shared_ptr fpInstallForWork, //-Instance Functions-------------------------------------------------------------------------------------------- //Private: -Qx::ProgressGroup* ImportWorker::initializeProgressGroup(const QString& groupName, quint64 weight) +Qx::ProgressGroup* Worker::initializeProgressGroup(const QString& groupName, quint64 weight) { Qx::ProgressGroup* pg = mProgressManager.addGroup(groupName); pg->setWeight(weight); @@ -82,7 +82,7 @@ Qx::ProgressGroup* ImportWorker::initializeProgressGroup(const QString& groupNam return pg; } -Qx::Error ImportWorker::preloadPlaylists(QList& targetPlaylists) +Qx::Error Worker::preloadPlaylists(QList& targetPlaylists) { // Reset playlists targetPlaylists.clear(); @@ -103,7 +103,7 @@ Qx::Error ImportWorker::preloadPlaylists(QList& targetPlaylists) return Qx::Error(); } -QList ImportWorker::getPlaylistSpecificGameIds(const QList& playlists) +QList Worker::getPlaylistSpecificGameIds(const QList& playlists) { QList playlistSpecGameIds; @@ -114,7 +114,7 @@ QList ImportWorker::getPlaylistSpecificGameIds(const QList& return playlistSpecGameIds; } -ImageTransferError ImportWorker::transferImage(bool symlink, QString sourcePath, QString destinationPath) +ImageTransferError Worker::transferImage(bool symlink, QString sourcePath, QString destinationPath) { /* TODO: Ideally the error handlers here don't need to include "Retry?" text and therefore need less use of QString::arg(); however, this largely * would require use of a button labeled "Ignore All" so that the errors could presented as is without a prompt, with the prompt being inferred @@ -156,7 +156,7 @@ ImageTransferError ImportWorker::transferImage(bool symlink, QString sourcePath, return ImageTransferError(ImageTransferError::CantCreateDirectory, QString(), destinationDir.absolutePath()); // Determine backup path - QString backupPath = Fe::Install::filePathToBackupPath(destinationInfo.absoluteFilePath()); + QString backupPath = Lr::Install::filePathToBackupPath(destinationInfo.absoluteFilePath()); // Temporarily backup image if it already exists (also acts as deletion marking in case images for the title were removed in an update) if(destinationOccupied) @@ -178,7 +178,7 @@ ImageTransferError ImportWorker::transferImage(bool symlink, QString sourcePath, else if(QFile::exists(backupPath)) QFile::remove(backupPath); else - mFrontendInstall->addRevertableFile(destinationPath); // Only queue image to be removed on failure if its new, so existing images aren't deleted on revert + mLauncherInstall->addRevertableFile(destinationPath); // Only queue image to be removed on failure if its new, so existing images aren't deleted on revert } else { @@ -190,21 +190,21 @@ ImageTransferError ImportWorker::transferImage(bool symlink, QString sourcePath, else if(QFile::exists(backupPath)) QFile::remove(backupPath); else - mFrontendInstall->addRevertableFile(destinationPath); // Only queue image to be removed on failure if its new, so existing images aren't deleted on revert + mLauncherInstall->addRevertableFile(destinationPath); // Only queue image to be removed on failure if its new, so existing images aren't deleted on revert } // Return null error on success return ImageTransferError(); } -bool ImportWorker::performImageJobs(const QList& jobs, bool symlink, Qx::ProgressGroup* pg) +bool Worker::performImageJobs(const QList& jobs, bool symlink, Qx::ProgressGroup* pg) { // Setup for image transfers ImageTransferError imageTransferError; // Error return reference *mBlockingErrorResponse = QMessageBox::NoToAll; // Default to choice "NoToAll" in case the signal is not correctly connected using Qt::BlockingQueuedConnection bool ignoreAllTransferErrors = false; // NoToAll response tracker - for(const Fe::Install::ImageMap& imageJob : jobs) + for(const Lr::Install::ImageMap& imageJob : jobs) { while((imageTransferError = transferImage(symlink, imageJob.sourcePath, imageJob.destPath)).isValid() && !ignoreAllTransferErrors) { @@ -229,7 +229,7 @@ bool ImportWorker::performImageJobs(const QList& jobs, bo return true; } -ImportWorker::ImportResult ImportWorker::processPlatformGames(Qx::Error& errorReport, std::unique_ptr& platformDoc, Fp::Db::QueryBuffer& gameQueryResult) +Worker::Result Worker::processPlatformGames(Qx::Error& errorReport, std::unique_ptr& platformDoc, Fp::Db::QueryBuffer& gameQueryResult) { const Fp::Toolkit* tk = mFlashpointInstall->toolkit(); @@ -280,7 +280,7 @@ ImportWorker::ImportResult ImportWorker::processPlatformGames(Qx::Error& errorRe // Add set to doc QString checkedLogoPath = (logoLocalInfo.exists() || mOptionSet.downloadImages) ? logoLocalInfo.absoluteFilePath() : QString(); QString checkedScreenshotPath = (ssLocalInfo.exists() || mOptionSet.downloadImages) ? ssLocalInfo.absoluteFilePath() : QString(); - platformDoc->addSet(builtSet, Fe::ImageSources(checkedLogoPath, checkedScreenshotPath)); + platformDoc->addSet(builtSet, Lr::ImageSources(checkedLogoPath, checkedScreenshotPath)); // Add ID to imported game cache mImportedGameIdsCache.insert(builtGame.id()); @@ -306,7 +306,7 @@ ImportWorker::ImportResult ImportWorker::processPlatformGames(Qx::Error& errorRe } // Handle image transfer progress - if(mOptionSet.imageMode == Fe::ImageMode::Copy || mOptionSet.imageMode == Fe::ImageMode::Link) + if(mOptionSet.imageMode == ImageMode::Copy || mOptionSet.imageMode == ImageMode::Link) { // Adjust progress if images aren't available if(checkedLogoPath.isEmpty()) @@ -330,7 +330,7 @@ ImportWorker::ImportResult ImportWorker::processPlatformGames(Qx::Error& errorRe return Successful; } -void ImportWorker::cullUnimportedPlaylistGames(QList& playlists) +void Worker::cullUnimportedPlaylistGames(QList& playlists) { const auto& idCache = mImportedGameIdsCache; for(auto& pl : playlists) @@ -341,7 +341,7 @@ void ImportWorker::cullUnimportedPlaylistGames(QList& playlists) } } -ImportWorker::ImportResult ImportWorker::preloadAddApps(Qx::Error& errorReport, Fp::Db::QueryBuffer& addAppQuery) +Worker::Result Worker::preloadAddApps(Qx::Error& errorReport, Fp::Db::QueryBuffer& addAppQuery) { mAddAppsCache.reserve(addAppQuery.size); for(int i = 0; i < addAppQuery.size; i++) @@ -380,25 +380,25 @@ ImportWorker::ImportResult ImportWorker::preloadAddApps(Qx::Error& errorReport, return Successful; } -ImportWorker::ImportResult ImportWorker::processGames(Qx::Error& errorReport, QList& primary, QList& playlistSpecific) +Worker::Result Worker::processGames(Qx::Error& errorReport, QList& primary, QList& playlistSpecific) { // Status tracking - ImportResult platformImportStatus; + Result platformImportStatus; // Track total platforms that have been handled qsizetype remainingPlatforms = primary.size() + playlistSpecific.size(); // Use lambda to handle both lists due to major overlap - auto platformsHandler = [&remainingPlatforms, &errorReport, this](QList& platformQueryResults, QString label) -> ImportResult { - ImportResult result; + auto platformsHandler = [&remainingPlatforms, &errorReport, this](QList& platformQueryResults, QString label) -> Result { + Result result; for(int i = 0; i < platformQueryResults.size(); i++) { Fp::Db::QueryBuffer& currentQueryResult = platformQueryResults[i]; - // Open frontend platform doc - std::unique_ptr currentPlatformDoc; - Fe::DocHandlingError platformReadError = mFrontendInstall->checkoutPlatformDoc(currentPlatformDoc, currentQueryResult.source); + // Open launcher platform doc + std::unique_ptr currentPlatformDoc; + Lr::DocHandlingError platformReadError = mLauncherInstall->checkoutPlatformDoc(currentPlatformDoc, currentQueryResult.source); // Stop import if error occurred if(platformReadError.isValid()) @@ -424,8 +424,8 @@ ImportWorker::ImportResult ImportWorker::processGames(Qx::Error& errorReport, QL } // Forfeit document lease and save it - Fe::DocHandlingError saveError; - if((saveError = mFrontendInstall->commitPlatformDoc(std::move(currentPlatformDoc))).isValid()) + Lr::DocHandlingError saveError; + if((saveError = mLauncherInstall->commitPlatformDoc(std::move(currentPlatformDoc))).isValid()) { errorReport = saveError; return Failed; @@ -453,16 +453,16 @@ ImportWorker::ImportResult ImportWorker::processGames(Qx::Error& errorReport, QL return Successful; } -ImportWorker::ImportResult ImportWorker::processPlaylists(Qx::Error& errorReport, const QList& playlists) +Worker::Result Worker::processPlaylists(Qx::Error& errorReport, const QList& playlists) { for(const auto& currentPlaylist : playlists) { // Update progress dialog label emit progressStepChanged(STEP_IMPORTING_PLAYLISTS.arg(currentPlaylist.title())); - // Open frontend playlist doc - std::unique_ptr currentPlaylistDoc; - Fe::DocHandlingError playlistReadError = mFrontendInstall->checkoutPlaylistDoc(currentPlaylistDoc, currentPlaylist.title()); + // Open launcher playlist doc + std::unique_ptr currentPlaylistDoc; + Lr::DocHandlingError playlistReadError = mLauncherInstall->checkoutPlaylistDoc(currentPlaylistDoc, currentPlaylist.title()); // Stop import if error occurred if(playlistReadError.isValid()) @@ -485,8 +485,8 @@ ImportWorker::ImportResult ImportWorker::processPlaylists(Qx::Error& errorReport } // Forfeit document lease and save it - Fe::DocHandlingError saveError; - if((saveError = mFrontendInstall->commitPlaylistDoc(std::move(currentPlaylistDoc))).isValid()) + Lr::DocHandlingError saveError; + if((saveError = mLauncherInstall->commitPlaylistDoc(std::move(currentPlaylistDoc))).isValid()) { errorReport = saveError; return Failed; @@ -507,7 +507,7 @@ ImportWorker::ImportResult ImportWorker::processPlaylists(Qx::Error& errorReport return Successful; } -ImportWorker::ImportResult ImportWorker::processImages(Qx::Error& errorReport) +Worker::Result Worker::processImages(Qx::Error& errorReport) { //-Image Download--------------------------------------------------------------------------------- if(mOptionSet.downloadImages && mImageDownloadManager.hasTasks()) @@ -528,8 +528,8 @@ ImportWorker::ImportResult ImportWorker::processImages(Qx::Error& errorReport) *ignore = *mBlockingErrorResponse == QMessageBox::Yes; }); - connect(&mImageDownloadManager, &Qx::SyncDownloadManager::authenticationRequired, this, &ImportWorker::authenticationRequired); - connect(&mImageDownloadManager, &Qx::SyncDownloadManager::proxyAuthenticationRequired, this, &ImportWorker::authenticationRequired); + connect(&mImageDownloadManager, &Qx::SyncDownloadManager::authenticationRequired, this, &Worker::authenticationRequired); + connect(&mImageDownloadManager, &Qx::SyncDownloadManager::proxyAuthenticationRequired, this, &Worker::authenticationRequired); connect(&mImageDownloadManager, &Qx::SyncDownloadManager::downloadFinished, this, [this]() { // clazy:exclude=lambda-in-connect mProgressManager.group(Pg::ImageDownload)->incrementValue(); @@ -558,16 +558,16 @@ ImportWorker::ImportResult ImportWorker::processImages(Qx::Error& errorReport) // Update progress dialog label emit progressStepChanged(STEP_IMPORTING_IMAGES); - // Provide frontend with bulk reference locations and acquire any transfer tasks - QList imageTransferJobs; - Fe::ImageSources bulkSources; - if(mOptionSet.imageMode == Fe::ImageMode::Reference) + // Provide launcher with bulk reference locations and acquire any transfer tasks + QList imageTransferJobs; + Lr::ImageSources bulkSources; + if(mOptionSet.imageMode == ImageMode::Reference) { bulkSources.setLogoPath(QDir::toNativeSeparators(mFlashpointInstall->entryLogosDirectory().absolutePath())); bulkSources.setScreenshotPath(QDir::toNativeSeparators(mFlashpointInstall->entryScreenshotsDirectory().absolutePath())); } - Qx::Error imageExchangeError = mFrontendInstall->preImageProcessing(imageTransferJobs, bulkSources); + Qx::Error imageExchangeError = mLauncherInstall->preImageProcessing(imageTransferJobs, bulkSources); if(imageExchangeError.isValid()) { @@ -577,7 +577,7 @@ ImportWorker::ImportResult ImportWorker::processImages(Qx::Error& errorReport) } // Perform transfers if required - if(mOptionSet.imageMode == Fe::ImageMode::Copy || mOptionSet.imageMode == Fe::ImageMode::Link) + if(mOptionSet.imageMode == ImageMode::Copy || mOptionSet.imageMode == ImageMode::Link) { /* * Account for potential mismatch between assumed and actual job count. @@ -587,32 +587,32 @@ ImportWorker::ImportResult ImportWorker::processImages(Qx::Error& errorReport) if(static_cast(imageTransferJobs.size()) != mProgressManager.group(Pg::ImageTransfer)->maximum()) mProgressManager.group(Pg::ImageTransfer)->setMaximum(imageTransferJobs.size()); - if(!performImageJobs(imageTransferJobs, mOptionSet.imageMode == Fe::ImageMode::Link, mProgressManager.group(Pg::ImageTransfer))) + if(!performImageJobs(imageTransferJobs, mOptionSet.imageMode == ImageMode::Link, mProgressManager.group(Pg::ImageTransfer))) return Canceled; } else if(!imageTransferJobs.isEmpty()) - qWarning("the frontend provided image transfers when the mode wasn't link/copy"); + qWarning("the launcher provided image transfers when the mode wasn't link/copy"); - // Handle frontend specific actions - mFrontendInstall->postImageProcessing(); + // Handle launcher specific actions + mLauncherInstall->postImageProcessing(); // Report successful step completion errorReport = Qx::Error(); return Successful; } -ImportWorker::ImportResult ImportWorker::processIcons(Qx::Error& errorReport, const QStringList& platforms, const QList& playlists) +Worker::Result Worker::processIcons(Qx::Error& errorReport, const QStringList& platforms, const QList& playlists) { - QList jobs; - QString mainDest = mFrontendInstall->platformCategoryIconPath(); - std::optional platformDestDir = mFrontendInstall->platformIconsDirectory(); - std::optional playlistDestDir = mFrontendInstall->playlistIconsDirectory(); + QList jobs; + QString mainDest = mLauncherInstall->platformCategoryIconPath(); + std::optional platformDestDir = mLauncherInstall->platformIconsDirectory(); + std::optional playlistDestDir = mLauncherInstall->playlistIconsDirectory(); const Fp::Toolkit* tk = mFlashpointInstall->toolkit(); // Main Job if(!mainDest.isEmpty()) - jobs.emplace_back(Fe::Install::ImageMap{.sourcePath = u":/flashpoint/icon.png"_s, .destPath = mainDest}); + jobs.emplace_back(Lr::Install::ImageMap{.sourcePath = u":/flashpoint/icon.png"_s, .destPath = mainDest}); // Platform jobs if(platformDestDir) @@ -622,7 +622,7 @@ ImportWorker::ImportResult ImportWorker::processIcons(Qx::Error& errorReport, co { QString src = tk->platformLogoPath(p); if(QFile::exists(src)) - jobs.emplace_back(Fe::Install::ImageMap{.sourcePath = src, + jobs.emplace_back(Lr::Install::ImageMap{.sourcePath = src, .destPath = pdd.absoluteFilePath(p + ".png")}); } } @@ -650,19 +650,19 @@ ImportWorker::ImportResult ImportWorker::processIcons(Qx::Error& errorReport, co continue; /* NOTE: This is LaunchBox specific since it's currently the only FE to support icons. If this changes a general solution is needed - * Like allowing the frontend to filter out specific icons + * Like allowing the launcher to filter out specific icons * * Don't copy the favorites icon as LB already has its own. */ if(p.title().trimmed() == u"Favorites"_s) continue; - /* NOTE: This may not work for all frontends + /* NOTE: This may not work for all launchers * - * Use translated name for destination since that's what the frontend is expecting + * Use translated name for destination since that's what the launcher is expecting */ QString sFilename = p.title() + ".png"; - QString dFilename = mFrontendInstall->translateDocName(p.title(), Fe::DataDoc::Type::Playlist) + ".png";; + QString dFilename = mLauncherInstall->translateDocName(p.title(), Lr::DataDoc::Type::Playlist) + ".png";; QString source = iconInflateDir.filePath(sFilename); QString dest = pdd.absoluteFilePath(dFilename); @@ -673,7 +673,7 @@ ImportWorker::ImportResult ImportWorker::processIcons(Qx::Error& errorReport, co return Failed; } - jobs.emplace_back(Fe::Install::ImageMap{.sourcePath = source, .destPath = dest}); + jobs.emplace_back(Lr::Install::ImageMap{.sourcePath = source, .destPath = dest}); } } @@ -691,12 +691,12 @@ ImportWorker::ImportResult ImportWorker::processIcons(Qx::Error& errorReport, co } //Public -ImportWorker::ImportResult ImportWorker::doImport(Qx::Error& errorReport) +Worker::Result Worker::doImport(Qx::Error& errorReport) { //-Setup---------------------------------------------------------------- // Import step status - ImportResult importStepStatus; + Result importStepStatus; // Process query status Fp::DbError queryError; @@ -798,7 +798,7 @@ ImportWorker::ImportResult ImportWorker::doImport(Qx::Error& errorReport) } // Screenshot and Logo transfer - if(mOptionSet.imageMode != Fe::ImageMode::Reference) + if(mOptionSet.imageMode != ImageMode::Reference) { Qx::ProgressGroup* pgImageTransfer = initializeProgressGroup(Pg::ImageTransfer, 3); pgImageTransfer->setMaximum(totalGameCount * 2); @@ -807,11 +807,11 @@ ImportWorker::ImportResult ImportWorker::doImport(Qx::Error& errorReport) // Icon transfers // TOD: Somewhat wasteful because these create a temporary copy, but not a big deal for now quint64 iconCount = 0; - if(!mFrontendInstall->platformCategoryIconPath().isEmpty()) + if(!mLauncherInstall->platformCategoryIconPath().isEmpty()) iconCount++; - if(mFrontendInstall->platformIconsDirectory()) + if(mLauncherInstall->platformIconsDirectory()) iconCount += involvedPlatforms.size(); - if(mFrontendInstall->playlistIconsDirectory()) + if(mLauncherInstall->playlistIconsDirectory()) iconCount += targetPlaylists.size(); if(iconCount > 0) @@ -828,10 +828,10 @@ ImportWorker::ImportResult ImportWorker::doImport(Qx::Error& errorReport) } // Connect progress manager signal - connect(&mProgressManager, &Qx::GroupedProgressManager::progressUpdated, this, &ImportWorker::pmProgressUpdated); + connect(&mProgressManager, &Qx::GroupedProgressManager::progressUpdated, this, &Worker::pmProgressUpdated); - //-Handle Frontend Specific Import Setup------------------------------ - Fe::Install::ImportDetails details{ + //-Handle Launcher Specific Import Setup------------------------------ + Lr::Install::ImportDetails details{ .updateOptions = mOptionSet.updateOptions, .imageMode = mOptionSet.imageMode, .clifpPath = CLIFp::standardCLIFpPath(*mFlashpointInstall), @@ -839,7 +839,7 @@ ImportWorker::ImportResult ImportWorker::doImport(Qx::Error& errorReport) .involvedPlaylists = mImportSelections.playlists }; - errorReport = mFrontendInstall->preImport(details); + errorReport = mLauncherInstall->preImport(details); if(errorReport.isValid()) return Failed; @@ -853,8 +853,8 @@ ImportWorker::ImportResult ImportWorker::doImport(Qx::Error& errorReport) if((importStepStatus = preloadAddApps(errorReport, addAppQuery)) != Successful) return importStepStatus; - // Handle Frontend specific pre-platform tasks - errorReport = mFrontendInstall->prePlatformsImport(); + // Handle Launcher specific pre-platform tasks + errorReport = mLauncherInstall->prePlatformsImport(); if(errorReport.isValid()) return Failed; @@ -862,8 +862,8 @@ ImportWorker::ImportResult ImportWorker::doImport(Qx::Error& errorReport) if((importStepStatus = processGames(errorReport, gameQueries, playlistSpecGameQueries)) != Successful) return importStepStatus; - // Handle Frontend specific post-platform tasks - errorReport = mFrontendInstall->postPlatformsImport(); + // Handle Launcher specific post-platform tasks + errorReport = mLauncherInstall->postPlatformsImport(); if(errorReport.isValid()) return Failed; @@ -881,23 +881,23 @@ ImportWorker::ImportResult ImportWorker::doImport(Qx::Error& errorReport) // Remove un-imported games from playlists cullUnimportedPlaylistGames(targetPlaylists); - // Handle Frontend specific pre-playlist tasks - errorReport = mFrontendInstall->prePlaylistsImport(); + // Handle Launcher specific pre-playlist tasks + errorReport = mLauncherInstall->prePlaylistsImport(); if(errorReport.isValid()) return Failed; if((importStepStatus = processPlaylists(errorReport, targetPlaylists)) != Successful) return importStepStatus; - // Handle Frontend specific pre-playlist tasks - errorReport = mFrontendInstall->postPlaylistsImport(); + // Handle Launcher specific pre-playlist tasks + errorReport = mLauncherInstall->postPlaylistsImport(); if(errorReport.isValid()) return Failed; } - // Handle Frontend specific cleanup + // Handle Launcher specific cleanup emit progressStepChanged(STEP_FINALIZING); - errorReport = mFrontendInstall->postImport(); + errorReport = mLauncherInstall->postImport(); if(errorReport.isValid()) return Failed; @@ -909,7 +909,7 @@ ImportWorker::ImportResult ImportWorker::doImport(Qx::Error& errorReport) } // Reset install - mFrontendInstall->softReset(); + mLauncherInstall->softReset(); // Report successful import completion errorReport = Qx::Error(); @@ -918,7 +918,7 @@ ImportWorker::ImportResult ImportWorker::doImport(Qx::Error& errorReport) //-Slots--------------------------------------------------------------------------------------------------------- //Private Slots: -void ImportWorker::pmProgressUpdated(quint64 currentProgress) +void Worker::pmProgressUpdated(quint64 currentProgress) { /* NOTE: This is required because if the value isn't actually different than the current when * the connected QProgressDialog::setValue() is triggered then processEvents() won't be called. @@ -940,4 +940,6 @@ void ImportWorker::pmProgressUpdated(quint64 currentProgress) } //Public Slots: -void ImportWorker::notifyCanceled() { mCanceled = true; } +void Worker::notifyCanceled() { mCanceled = true; } + +} diff --git a/app/src/import-worker.h b/app/src/import/worker.h similarity index 75% rename from app/src/import-worker.h rename to app/src/import/worker.h index 7f0a9a9..8d0444e 100644 --- a/app/src/import-worker.h +++ b/app/src/import/worker.h @@ -1,5 +1,5 @@ -#ifndef COREIMPORTWORKER_H -#define COREIMPORTWORKER_H +#ifndef IMPORT_WORKER_H +#define IMPORT_WORKER_H // Qt Includes #include @@ -13,7 +13,10 @@ #include // Project Includes -#include "frontend/fe-install.h" +#include "launcher/lr-install.h" + +namespace Import +{ class QX_ERROR_TYPE(ImageTransferError, "ImageTransferError", 1351) { @@ -69,14 +72,13 @@ class QX_ERROR_TYPE(ImageTransferError, "ImageTransferError", 1351) QString deriveCaption() const override; }; -class ImportWorker : public QObject +class Worker : public QObject { Q_OBJECT // Required for classes that use Qt elements //-Class Enums--------------------------------------------------------------------------------------------------- public: - enum ImportResult {Failed, Canceled, Taskless, Successful}; - enum PlaylistGameMode {SelectedPlatform, ForceAll}; + enum Result {Failed, Canceled, Taskless, Successful}; //-Inner Classes------------------------------------------------------------------------------------------------ private: @@ -91,23 +93,6 @@ class ImportWorker : public QObject static inline const QString PlaylistImport = u"PlaylistImport"_s; }; -//-Class Structs------------------------------------------------------------------------------------------------- -public: - struct ImportSelections - { - QStringList platforms; - QStringList playlists; - }; - - struct OptionSet - { - Fe::UpdateOptions updateOptions; - Fe::ImageMode imageMode; - bool downloadImages; - PlaylistGameMode playlistMode; - Fp::Db::InclusionOptions inclusionOptions; - }; - //-Class Variables----------------------------------------------------------------------------------------------- public: // Import Steps @@ -122,14 +107,14 @@ class ImportWorker : public QObject //-Instance Variables-------------------------------------------------------------------------------------------- private: // Install links - std::shared_ptr mFlashpointInstall; - std::shared_ptr mFrontendInstall; + Fp::Install* mFlashpointInstall; + Lr::Install* mLauncherInstall; // Image processing Qx::SyncDownloadManager mImageDownloadManager; // Job details - ImportSelections mImportSelections; + Selections mImportSelections; OptionSet mOptionSet; // Job Caches @@ -148,10 +133,7 @@ class ImportWorker : public QObject //-Constructor--------------------------------------------------------------------------------------------------- public: - ImportWorker(std::shared_ptr fpInstallForWork, - std::shared_ptr feInstallForWork, - ImportSelections importSelections, - OptionSet optionSet); + Worker(Fp::Install* flashpoint, Lr::Install* launcher, Selections importSelections, OptionSet optionSet); //-Instance Functions--------------------------------------------------------------------------------------------------------- private: @@ -159,18 +141,18 @@ class ImportWorker : public QObject Qx::Error preloadPlaylists(QList& targetPlaylists); QList getPlaylistSpecificGameIds(const QList& playlists); ImageTransferError transferImage(bool symlink, QString sourcePath, QString destPath); - bool performImageJobs(const QList& jobs, bool symlink, Qx::ProgressGroup* pg = nullptr); - ImportResult processPlatformGames(Qx::Error& errorReport, std::unique_ptr& platformDoc, Fp::Db::QueryBuffer& gameQueryResult); + bool performImageJobs(const QList& jobs, bool symlink, Qx::ProgressGroup* pg = nullptr); + Result processPlatformGames(Qx::Error& errorReport, std::unique_ptr& platformDoc, Fp::Db::QueryBuffer& gameQueryResult); void cullUnimportedPlaylistGames(QList& playlists); - ImportResult preloadAddApps(Qx::Error& errorReport, Fp::Db::QueryBuffer& addAppQuery); - ImportResult processGames(Qx::Error& errorReport, QList& primary, QList& playlistSpecific); - ImportResult processPlaylists(Qx::Error& errorReport, const QList& playlists); - ImportResult processImages(Qx::Error& errorReport); - ImportResult processIcons(Qx::Error& errorReport, const QStringList& platforms, const QList& playlists); + Result preloadAddApps(Qx::Error& errorReport, Fp::Db::QueryBuffer& addAppQuery); + Result processGames(Qx::Error& errorReport, QList& primary, QList& playlistSpecific); + Result processPlaylists(Qx::Error& errorReport, const QList& playlists); + Result processImages(Qx::Error& errorReport); + Result processIcons(Qx::Error& errorReport, const QStringList& platforms, const QList& playlists); public: - ImportResult doImport(Qx::Error& errorReport); + Result doImport(Qx::Error& errorReport); //-Slots---------------------------------------------------------------------------------------------------------- private slots: @@ -191,10 +173,12 @@ public slots: void authenticationRequired(const QString& prompt, QAuthenticator* authenticator); // Finished - void importCompleted(ImportWorker::ImportResult importResult, const Qx::Error& errorReport); + void importCompleted(Worker::Result importResult, const Qx::Error& errorReport); }; +} + //-Metatype declarations------------------------------------------------------------------------------------------- -Q_DECLARE_METATYPE(ImportWorker::ImportResult); +Q_DECLARE_METATYPE(Import::Worker::Result); -#endif // COREIMPORTWORKER_H +#endif // IMPORT_WORKER_H diff --git a/app/src/clifp.cpp b/app/src/kernel/clifp.cpp similarity index 100% rename from app/src/clifp.cpp rename to app/src/kernel/clifp.cpp diff --git a/app/src/clifp.h b/app/src/kernel/clifp.h similarity index 100% rename from app/src/clifp.h rename to app/src/kernel/clifp.h diff --git a/app/src/kernel/controller.cpp b/app/src/kernel/controller.cpp new file mode 100644 index 0000000..cbdee59 --- /dev/null +++ b/app/src/kernel/controller.cpp @@ -0,0 +1,341 @@ +// Unit Include +#include "controller.h" + +// Qt Includes +#include +#include + +// Qx Includes +#include +#include +#include + +/* TODO: Consider having this tool deploy a .ini file (or the like) into the target launcher install + * (with the exact location probably being guided by the specific Install child) that saves the settings + * used for the import, so that they can be loaded again when that install is targeted by future versions + * of the tool. Would have to account for an initial import vs update (likely just leaving the update settings + * blank). Wouldn't be a huge difference but could be a nice little time saver. + */ + +//=============================================================================================================== +// Controller +//=============================================================================================================== + +//-Constructor------------------------------------------------------------- +//Public: +Controller::Controller() : + mImportProperties(), + mMainWindow(mImportProperties), + mProgressPresenter(&mMainWindow) +{ + QApplication::setApplicationName(PROJECT_FULL_NAME); + + /*Register metatypes + * NOTE: Qt docs note these should be needed, as always, but since Qt6 signals/slots with these types seem to + * work fine without the following calls. + * See https://forum.qt.io/topic/136627/undocumented-automatic-metatype-registration-in-qt6 + */ + //qRegisterMetaType(); + //qRegisterMetaType(); + //qRegisterMetaType>(); + + // Ensure built-in CLIFp version is valid + if(CLIFp::internalVersion().isNull()) + { + QMessageBox::critical(&mMainWindow, CAPTION_GENERAL_FATAL_ERROR, MSG_FATAL_NO_INTERNAL_CLIFP_VER); + QApplication::exit(1); + return; + } + + // Check if Flashpoint is running + if(Qx::processIsRunning(Fp::Install::LAUNCHER_NAME)) + QMessageBox::warning(&mMainWindow, QApplication::applicationName(), MSG_FP_CLOSE_PROMPT); + + // Connect main window + connect(&mMainWindow, &MainWindow::installPathChanged, this, &Controller::updateInstallPath); + connect(&mMainWindow, &MainWindow::importTriggered, this, &Controller::startImport); + connect(&mMainWindow, &MainWindow::standaloneDeployTriggered, this, &Controller::standaloneCLIFpDeploy); + + // Spawn main window + mMainWindow.show(); + mProgressPresenter.attachWindow(mMainWindow.windowHandle()); // Must be after show() for handle to be valid +} + +//-Instance Functions------------------------------------------------------------- +//Private: +void Controller::processImportResult(Import::Worker::Result importResult, const Qx::Error& errorReport) +{ + // Reset progress presenter + mProgressPresenter.reset(); + + // Post error report if present + if(errorReport.isValid()) + Qx::postBlockingError(errorReport, QMessageBox::Ok); + + if(importResult == Import::Worker::Successful) + { + deployCLIFp(*mImportProperties.flashpoint(), QMessageBox::Ignore); + + // Post-import message + QMessageBox::information(&mMainWindow, QApplication::applicationName(), MSG_POST_IMPORT); + + // Update selection lists to reflect newly existing platforms + mImportProperties.refreshInstallData(); + } + else if(importResult == Import::Worker::Taskless) + { + QMessageBox::warning(&mMainWindow, CAPTION_TASKLESS_IMPORT, MSG_NO_WORK); + } + else if(importResult == Import::Worker::Canceled) + { + QMessageBox::critical(&mMainWindow, CAPTION_REVERT, MSG_USER_CANCELED); + revertAllLauncherChanges(); + } + else if(importResult == Import::Worker::Failed) + { + // Show general next steps message + QMessageBox::warning(&mMainWindow, CAPTION_REVERT, MSG_HAVE_TO_REVERT); + revertAllLauncherChanges(); + } + else + qCritical("unhandled import worker result type."); +} + +void Controller::revertAllLauncherChanges() +{ + auto launcher = mImportProperties.launcher(); + + // Trackers + bool tempSkip = false; + bool alwaysSkip = false; + Lr::RevertError currentError; + int retryChoice; + + // Progress + mProgressPresenter.setMinimum(0); + mProgressPresenter.setMaximum(launcher->revertQueueCount()); + mProgressPresenter.setCaption(CAPTION_REVERT); + while(launcher->revertNextChange(currentError, alwaysSkip || tempSkip) != 0) + { + // Check for error + if(!currentError.isValid()) + { + tempSkip = false; + mProgressPresenter.setValue(mProgressPresenter.value() + 1); + } + else + { + retryChoice = Qx::postBlockingError(currentError, QMessageBox::Retry | QMessageBox::Ignore | QMessageBox::Abort, QMessageBox::Retry); + + if(retryChoice == QMessageBox::Ignore) + tempSkip = true; + else if(retryChoice == QMessageBox::Abort) + alwaysSkip = true; + } + } + + // Ensure progress dialog is closed + mProgressPresenter.reset(); + + // Reset instance + launcher->softReset(); +} + +void Controller::deployCLIFp(const Fp::Install& fp, QMessageBox::Button abandonButton) +{ + bool willDeploy = true; + + // Check for existing CLIFp + if(CLIFp::hasCLIFp(fp)) + { + // Notify user if this will be a downgrade + if(CLIFp::internalVersion() < CLIFp::installedVersion(fp)) + willDeploy = (QMessageBox::warning(&mMainWindow, CAPTION_CLIFP_DOWNGRADE, MSG_FP_CLFIP_WILL_DOWNGRADE, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes); + } + + // Deploy CLIFp if applicable + if(willDeploy) + { + // Deploy exe + QString deployError; + while(!CLIFp::deployCLIFp(deployError, fp)) + if(QMessageBox::critical(&mMainWindow, CAPTION_CLIFP_ERR, MSG_FP_CANT_DEPLOY_CLIFP.arg(deployError), QMessageBox::Retry | abandonButton, QMessageBox::Retry) == abandonButton) + break; + } +} + +//-Signals & Slots------------------------------------------------------------- +//Private Slots: +void Controller::handleBlockingError(std::shared_ptr response, const Qx::Error& blockingError, QMessageBox::StandardButtons choices) +{ + mProgressPresenter.setErrorState(); + + // Post error and get response + int userChoice = Qx::postBlockingError(blockingError, choices); + + // If applicable return selection + if(response) + *response = userChoice; + + mProgressPresenter.resetState(); +} + +void Controller::handleAuthRequest(const QString& prompt, QAuthenticator* authenticator) +{ + Qx::LoginDialog ld; + ld.setPrompt(prompt); + + int choice = ld.exec(); + + if(choice == QDialog::Accepted) + { + authenticator->setUser(ld.username()); + authenticator->setPassword(ld.password()); + } +} + +//Public Slots: +void Controller::updateInstallPath(const QString& installPath, Import::Install type) +{ + /* NOTE: The launcher and flashpoint properties here are sometimes updated with a + * finer granularity instead of all at once at the end in a common spot in order to + * control the exact moment that dependent properties, like status icons, update. + */ + QDir installDir(installPath); + QString checkedPath = installDir.isAbsolute() && installDir.exists() ? QDir::cleanPath(installPath) : QString(); + + using enum Import::Install; + switch(type) + { + case Launcher: + { + if(checkedPath.isEmpty()) + mImportProperties.setLauncher(nullptr); + else + { + std::unique_ptr launcher; + launcher = Lr::Install::acquireMatch(checkedPath); + if(!launcher->isValid()) + { + QMessageBox::critical(&mMainWindow, QApplication::applicationName(), MSG_LR_INSTALL_INVALID); + launcher.reset(); + } + + mImportProperties.setLauncher(std::move(launcher)); + } + + break; + } + case Flashpoint: + { + + if(checkedPath.isEmpty()) + mImportProperties.setFlashpoint(nullptr); + else + { + std::unique_ptr flashpoint; + flashpoint = std::make_unique(checkedPath, true); + if(!flashpoint->isValid()) + { + Qx::postBlockingError(flashpoint->error(), QMessageBox::Ok); + flashpoint.reset(); + mImportProperties.setFlashpoint(std::move(flashpoint)); + } + else + { + mImportProperties.setFlashpoint(std::move(flashpoint)); // Updates target series property (important for status icon) + if(!mImportProperties.isFlashpointTargetSeries()) + QMessageBox::warning(&mMainWindow, QApplication::applicationName(), MSG_FP_VER_NOT_TARGET); + } + } + + break; + } + } +} + +void Controller::startImport(Import::Selections sel, Import::OptionSet opt, bool mayModify) +{ + auto launcher = mImportProperties.launcher(); + auto flashpoint = mImportProperties.flashpoint(); + + // Ensure launcher hasn't changed + bool changed = true; // Assume true for if error occurs + launcher->refreshExistingDocs(&changed); + if(changed) + { + QMessageBox::warning(&mMainWindow, QApplication::applicationName(), MSG_INSTALL_CONTENTS_CHANGED); + updateInstallPath(launcher->path(), Import::Install::Launcher); // Reprocess launcher to make sure it's the same install + return; + } + + // Warn user if they are changing existing files + if(mayModify) + if(QMessageBox::warning(&mMainWindow, QApplication::applicationName(), MSG_PRE_EXISTING_IMPORT, QMessageBox::Yes | QMessageBox::Cancel, QMessageBox::Cancel) == QMessageBox::Cancel) + return; + + // Warn user if Flashpoint is running + // Check if Flashpoint is running + if(Qx::processIsRunning(Fp::Install::LAUNCHER_NAME)) + QMessageBox::warning(&mMainWindow, QApplication::applicationName(), MSG_FP_CLOSE_PROMPT); + + // Only allow proceeding if launcher isn't running + bool lrRunning; + while((lrRunning = launcher->isRunning())) + if(QMessageBox::critical(&mMainWindow, QApplication::applicationName(), MSG_LAUNCHER_CLOSE_PROMPT, QMessageBox::Retry | QMessageBox::Cancel, QMessageBox::Retry) == QMessageBox::Cancel) + break; + + if(lrRunning) + return; + + // Start progress presentation + mProgressPresenter.setCaption(CAPTION_IMPORTING); + mProgressPresenter.setMinimum(0); + mProgressPresenter.setMaximum(0); + mProgressPresenter.setValue(0); + mProgressPresenter.setBusyState(); + mProgressPresenter.setLabelText(STEP_FP_DB_INITIAL_QUERY); + QApplication::processEvents(); // Force show progress immediately + + // Setup import worker + Import::Worker importWorker(flashpoint, launcher, sel, opt); + + // Setup blocking error connection + connect(&importWorker, &Import::Worker::blockingErrorOccured, this, &Controller::handleBlockingError); + + // Setup auth handler + connect(&importWorker, &Import::Worker::authenticationRequired, this, &Controller::handleAuthRequest); + + // Create process update connections + connect(&importWorker, &Import::Worker::progressStepChanged, &mProgressPresenter, &ProgressPresenter::setLabelText); + connect(&importWorker, &Import::Worker::progressValueChanged, &mProgressPresenter, &ProgressPresenter::setValue); + connect(&importWorker, &Import::Worker::progressMaximumChanged, &mProgressPresenter, &ProgressPresenter::setMaximum); + connect(&mProgressPresenter, &ProgressPresenter::canceled, &importWorker, &Import::Worker::notifyCanceled); + + // Import error tracker + Qx::Error importError; + + // Start import and forward result to handler + Import::Worker::Result importResult = importWorker.doImport(importError); + processImportResult(importResult, importError); +} + +void Controller::standaloneCLIFpDeploy() +{ + // Browse for install + QString selectedDir = QFileDialog::getExistingDirectory(&mMainWindow, CAPTION_FLASHPOINT_BROWSE, QDir::currentPath()); + + if(!selectedDir.isEmpty()) + { + Fp::Install tempFlashpointInstall(selectedDir); + if(tempFlashpointInstall.isValid()) + { + if(!Import::Properties::installMatchesTargetSeries(tempFlashpointInstall)) + QMessageBox::warning(&mMainWindow, QApplication::applicationName(), MSG_FP_VER_NOT_TARGET); + + deployCLIFp(tempFlashpointInstall, QMessageBox::Cancel); + } + else + Qx::postBlockingError(tempFlashpointInstall.error(), QMessageBox::Ok); + } +} diff --git a/app/src/kernel/controller.h b/app/src/kernel/controller.h new file mode 100644 index 0000000..79acf5c --- /dev/null +++ b/app/src/kernel/controller.h @@ -0,0 +1,105 @@ +#ifndef CONTROLLER_H +#define CONTROLLER_H + +// Qt Includes +#include + +// Project Includes +#include "import/properties.h" +#include "import/worker.h" +#include "kernel/clifp.h" +#include "ui/mainwindow.h" +#include "ui/progresspresenter.h" + +class Controller : public QObject +{ +//-Class Variables--------------------------------------------------------------- +private: + // Messages - General + static inline const QString MSG_FATAL_NO_INTERNAL_CLIFP_VER = u"Failed to get version information from the internal copy of CLIFp.exe!\n" + "\n" + "Execution cannot continue."_s; + + // Messages - FP General + static inline const QString MSG_FP_CLOSE_PROMPT = u"It is strongly recommended to close Flashpoint before proceeding as it can severely slow or interfere with the import process"_s; + + // Messages - Input + static inline const QString MSG_LR_INSTALL_INVALID = u"The specified directory either doesn't contain a valid launcher install, or it contains a version that is incompatible with this tool."_s; + static inline const QString MSG_FP_INSTALL_INVALID = u"The specified directory either doesn't contain a valid Flashpoint install, or it contains a version that is incompatible with this tool."_s; + static inline const QString MSG_FP_VER_NOT_TARGET = u"The selected Flashpoint install contains a version of Flashpoint that is different from the target version series (" PROJECT_TARGET_FP_VER_PFX_STR "), but appears to have a compatible structure. " + "You may proceed at your own risk as the tool is not guaranteed to work correctly in this circumstance. Please use a newer version of " PROJECT_SHORT_NAME " if available."_s; + + static inline const QString MSG_INSTALL_CONTENTS_CHANGED = u"The contents of your installs have been changed since the initial scan and therefore must be re-evaluated. You will need to make your selections again."_s; + + // Messages - General import procedure + static inline const QString MSG_PRE_EXISTING_IMPORT = u"One or more existing Platforms/Playlists may be affected by this import. These will be altered even if they did not originate from this program (i.e. if you " + "already happened to have a Platform/Playlist with the same name as one present in Flashpoint).\n" + "\n" + "Are you sure you want to proceed?"_s; + static inline const QString MSG_LAUNCHER_CLOSE_PROMPT = u"The importer has detected that the selected launcher is running. It must be closed in order to continue. If recently closed, wait a few moments before trying to proceed again as it performs significant cleanup in the background."_s; + + // Initial import status + static inline const QString STEP_FP_DB_INITIAL_QUERY = u"Making initial Flashpoint database queries..."_s; + + // Messages - Import Result + static inline const QString MSG_POST_IMPORT = u"The Flashpoint import has completed successfully. Next time you start the launcher it may take longer than usual as it may have to fill in some default fields for the imported Platforms/Playlists.\n" + "\n" + "If you wish to import further selections or update to a newer version of Flashpoint, simply re-run this procedure after pointing it to the desired Flashpoint installation."_s; + static inline const QString MSG_NO_WORK = u"The provided import selections/options resulted in no tasks to perform. Double-check your settings."_s; + static inline const QString MSG_USER_CANCELED = u"Import canceled by user, all changes that occurred during import will now be reverted (other than existing images that were replaced with newer versions)."_s; + static inline const QString MSG_HAVE_TO_REVERT = u"Due to previous unrecoverable errors, all changes that occurred during import will now be reverted (other than existing images that were replaced with newer versions).\n" + "\n" + "Afterwards, check to see if there is a newer version of " PROJECT_SHORT_NAME " and try again using that version. If not ask for help on the relevant forums where this tool was released (see Help).\n" + "\n" + "If you believe this to be due to a bug with this software, please submit an issue to its GitHub page (listed under help)"_s; + + // Messages - FP CLIFp + static inline const QString MSG_FP_CLFIP_WILL_DOWNGRADE = u"The existing version of "_s + CLIFp::EXE_NAME + u" in your Flashpoint install is newer than the version package with this tool.\n" + "\n" + "Replacing it with the packaged Version (downgrade) will likely cause compatibility issues unless you are specifically re-importing after downgrading your Flashpoint install to a previous version.\n" + "\n" + "Do you wish to downgrade "_s + CLIFp::EXE_NAME + u"?"_s; + + static inline const QString MSG_FP_CANT_DEPLOY_CLIFP = u"Failed to deploy "_s + CLIFp::EXE_NAME + u" to the selected Flashpoint install.\n" + "\n" + "%1\n" + "\n" + "If you choose to ignore this you will have to place CLIFp in your Flashpoint install directory manually."_s; + // Dialog captions + static inline const QString CAPTION_GENERAL_FATAL_ERROR = u"Fatal Error!"_s; + static inline const QString CAPTION_TASKLESS_IMPORT = u"Nothing to do"_s; + static inline const QString CAPTION_IMPORTING = u"FP Import"_s; + static inline const QString CAPTION_REVERT = u"Reverting changes..."_s; + static inline const QString CAPTION_FLASHPOINT_BROWSE = u"Select the root directory of your Flashpoint install..."_s; + static inline const QString CAPTION_CLIFP_DOWNGRADE = u"Downgrade CLIFp?"_s; + static inline const QString CAPTION_CLIFP_ERR = u"Error deploying CLIFp"_s; + +//-Instance Variables------------------------------------------------------------- +private: + Import::Properties mImportProperties; + MainWindow mMainWindow; + ProgressPresenter mProgressPresenter; + +//-Constructor------------------------------------------------------------- +public: + Controller(); + +//-Instance Functions------------------------------------------------------------- +private: + void processImportResult(Import::Worker::Result importResult, const Qx::Error& errorReport); + void revertAllLauncherChanges(); + void deployCLIFp(const Fp::Install& fp, QMessageBox::Button abandonButton); + +//-Signals & Slots------------------------------------------------------------- +private slots: + // Import Handlers + void handleBlockingError(std::shared_ptr response, const Qx::Error& blockingError, QMessageBox::StandardButtons choices); + void handleAuthRequest(const QString& prompt, QAuthenticator* authenticator); + +public slots: + void updateInstallPath(const QString& installPath, Import::Install type); + void startImport(Import::Selections sel, Import::OptionSet opt, bool mayModify); + void standaloneCLIFpDeploy(); +}; + +#endif // CONTROLLER_H diff --git a/app/src/frontend/attractmode/am-data.cpp b/app/src/launcher/attractmode/am-data.cpp similarity index 93% rename from app/src/frontend/attractmode/am-data.cpp rename to app/src/launcher/attractmode/am-data.cpp index f0f4e68..794d843 100644 --- a/app/src/frontend/attractmode/am-data.cpp +++ b/app/src/launcher/attractmode/am-data.cpp @@ -8,7 +8,7 @@ #include // Project Includes -#include "am-install.h" +#include "launcher/attractmode/am-install.h" namespace Am { @@ -18,8 +18,8 @@ namespace Am //-Constructor-------------------------------------------------------------------------------------------------------- //Protected: -CommonDocReader::CommonDocReader(Fe::DataDoc* targetDoc) : - Fe::DataDoc::Reader(targetDoc), +CommonDocReader::CommonDocReader(Lr::DataDoc* targetDoc) : + Lr::DataDoc::Reader(targetDoc), mStreamReader(targetDoc->path()) {} @@ -39,22 +39,22 @@ QString CommonDocReader::readLineIgnoringComments(qint64 maxlen) } //Public: -Fe::DocHandlingError CommonDocReader::readInto() +Lr::DocHandlingError CommonDocReader::readInto() { // Open file Qx::IoOpReport openError = mStreamReader.openFile(); if(openError.isFailure()) - return Fe::DocHandlingError(*mTargetDocument, Fe::DocHandlingError::DocCantOpen, openError.outcomeInfo()); + return Lr::DocHandlingError(*mTargetDocument, Lr::DocHandlingError::DocCantOpen, openError.outcomeInfo()); // Check that doc is valid bool isValid = false; if(!checkDocValidity(isValid)) - return Fe::DocHandlingError(*mTargetDocument, Fe::DocHandlingError::DocWriteFailed, mStreamReader.status().outcomeInfo()); + return Lr::DocHandlingError(*mTargetDocument, Lr::DocHandlingError::DocWriteFailed, mStreamReader.status().outcomeInfo()); else if(!isValid) - return Fe::DocHandlingError(*mTargetDocument, Fe::DocHandlingError::DocInvalidType); + return Lr::DocHandlingError(*mTargetDocument, Lr::DocHandlingError::DocInvalidType); // Read doc - Fe::DocHandlingError parseError = readTargetDoc(); + Lr::DocHandlingError parseError = readTargetDoc(); // Close file mStreamReader.closeFile(); @@ -63,9 +63,9 @@ Fe::DocHandlingError CommonDocReader::readInto() if(parseError.isValid()) return parseError; else if(mStreamReader.hasError()) - return Fe::DocHandlingError(*mTargetDocument, Fe::DocHandlingError::DocWriteFailed, mStreamReader.status().outcomeInfo()); + return Lr::DocHandlingError(*mTargetDocument, Lr::DocHandlingError::DocWriteFailed, mStreamReader.status().outcomeInfo()); else - return Fe::DocHandlingError(); + return Lr::DocHandlingError(); } //=============================================================================================================== @@ -74,19 +74,19 @@ Fe::DocHandlingError CommonDocReader::readInto() //-Constructor-------------------------------------------------------------------------------------------------------- //Protected: -CommonDocWriter::CommonDocWriter(Fe::DataDoc* sourceDoc) : - Fe::DataDoc::Writer(sourceDoc), +CommonDocWriter::CommonDocWriter(Lr::DataDoc* sourceDoc) : + Lr::DataDoc::Writer(sourceDoc), mStreamWriter(sourceDoc->path(), Qx::WriteMode::Truncate) {} //-Instance Functions------------------------------------------------------------------------------------------------- //Public: -Fe::DocHandlingError CommonDocWriter::writeOutOf() +Lr::DocHandlingError CommonDocWriter::writeOutOf() { // Open file Qx::IoOpReport openError = mStreamWriter.openFile(); if(openError.isFailure()) - return Fe::DocHandlingError(*mSourceDocument, Fe::DocHandlingError::DocCantOpen, openError.outcomeInfo()); + return Lr::DocHandlingError(*mSourceDocument, Lr::DocHandlingError::DocCantOpen, openError.outcomeInfo()); // Write doc bool writeSuccess = writeSourceDoc(); @@ -95,8 +95,8 @@ Fe::DocHandlingError CommonDocWriter::writeOutOf() mStreamWriter.closeFile(); // Return outcome - return writeSuccess ? Fe::DocHandlingError() : - Fe::DocHandlingError(*mSourceDocument, Fe::DocHandlingError::DocWriteFailed, mStreamWriter.status().outcomeInfo()); + return writeSuccess ? Lr::DocHandlingError() : + Lr::DocHandlingError(*mSourceDocument, Lr::DocHandlingError::DocWriteFailed, mStreamWriter.status().outcomeInfo()); } //=============================================================================================================== @@ -106,7 +106,7 @@ Fe::DocHandlingError CommonDocWriter::writeOutOf() //-Constructor-------------------------------------------------------------------------------------------------------- //Public: ConfigDoc::ConfigDoc(Install* const parent, const QString& filePath, QString docName) : - Fe::DataDoc(parent, filePath, docName) + Lr::DataDoc(parent, filePath, docName) {} //-Instance Functions-------------------------------------------------------------------------------------------------- @@ -204,7 +204,7 @@ bool ConfigDoc::Writer::writeSourceDoc() //-Constructor-------------------------------------------------------------------------------------------------------- //Public: Taglist::Taglist(Install* const parent, const QString& listPath, QString docName) : - Fe::DataDoc(parent, listPath, docName) + Lr::DataDoc(parent, listPath, docName) {} //-Instance Functions-------------------------------------------------------------------------------------------------- @@ -254,7 +254,7 @@ PlatformTaglist::PlatformTaglist(Install* const parent, const QString& listPath, //-Instance Functions-------------------------------------------------------------------------------------------------- //Public: -Fe::DataDoc::Type PlatformTaglist::type() const { return Fe::DataDoc::Type::Platform; } +Lr::DataDoc::Type PlatformTaglist::type() const { return Lr::DataDoc::Type::Platform; } //=============================================================================================================== // PlaylistTaglist @@ -268,7 +268,7 @@ PlaylistTaglist::PlaylistTaglist(Install* const parent, const QString& listPath, //-Instance Functions-------------------------------------------------------------------------------------------------- //Public: -Fe::DataDoc::Type PlaylistTaglist::type() const { return Fe::DataDoc::Type::Playlist; } +Lr::DataDoc::Type PlaylistTaglist::type() const { return Lr::DataDoc::Type::Playlist; } //=============================================================================================================== // Romlist @@ -276,14 +276,14 @@ Fe::DataDoc::Type PlaylistTaglist::type() const { return Fe::DataDoc::Type::Play //-Constructor-------------------------------------------------------------------------------------------------------- //Public: -Romlist::Romlist(Install* const parent, const QString& listPath, QString docName, const Fe::UpdateOptions& updateOptions, +Romlist::Romlist(Install* const parent, const QString& listPath, QString docName, const Import::UpdateOptions& updateOptions, const DocKey&) : - Fe::UpdateableDoc(parent, listPath, docName, updateOptions) + Lr::UpdateableDoc(parent, listPath, docName, updateOptions) {} //-Instance Functions-------------------------------------------------------------------------------------------------- //Public: -Fe::DataDoc::Type Romlist::type() const { return Fe::DataDoc::Type::Config; } +Lr::DataDoc::Type Romlist::type() const { return Lr::DataDoc::Type::Config; } bool Romlist::isEmpty() const { @@ -295,7 +295,7 @@ const QHash>& Romlist::finalEntries() const { r bool Romlist::containsGame(QUuid gameId) const { return mEntriesExisting.contains(gameId) || mEntriesFinal.contains(gameId); } bool Romlist::containsAddApp(QUuid addAppId) const { return mEntriesExisting.contains(addAppId) || mEntriesFinal.contains(addAppId); } -void Romlist::addSet(const Fp::Set& set, const Fe::ImageSources& images) +void Romlist::addSet(const Fp::Set& set, const Lr::ImageSources& images) { // Convert to romlist entry std::shared_ptr mainRomEntry = std::make_shared(set.game()); @@ -325,7 +325,7 @@ void Romlist::addSet(const Fp::Set& set, const Fe::ImageSources& images) void Romlist::finalize() { finalizeUpdateableItems(mEntriesExisting, mEntriesFinal); - Fe::UpdateableDoc::finalize(); + Lr::UpdateableDoc::finalize(); } //=============================================================================================================== @@ -355,7 +355,7 @@ bool Romlist::Reader::checkDocValidity(bool& isValid) return !mStreamReader.hasError(); } -Fe::DocHandlingError Romlist::Reader::readTargetDoc() +Lr::DocHandlingError Romlist::Reader::readTargetDoc() { // Read all romlist entries while(!mStreamReader.atEnd()) @@ -365,7 +365,7 @@ Fe::DocHandlingError Romlist::Reader::readTargetDoc() } // Only can have stream errors - return Fe::DocHandlingError(); + return Lr::DocHandlingError(); } void Romlist::Reader::parseRomEntry(const QString& rawEntry) @@ -586,7 +586,7 @@ bool BulkOverviewWriter::writeOverview(const QUuid& gameId, const QString& overv //Public: PlatformInterface::PlatformInterface(Install* const parent, const QString& platformTaglistPath, QString platformName, const QDir& overviewDir,const DocKey&) : - Fe::PlatformDoc(parent, {}, platformName, {}), + Lr::PlatformDoc(parent, {}, platformName, {}), mPlatformTaglist(parent, platformTaglistPath, platformName), mOverviewWriter(overviewDir) {} @@ -613,7 +613,7 @@ bool PlatformInterface::containsAddApp(QUuid addAppId) const return static_cast(parent())->mRomlist->containsAddApp(addAppId); }; -void PlatformInterface::addSet(const Fp::Set& set, const Fe::ImageSources& images) +void PlatformInterface::addSet(const Fp::Set& set, const Lr::ImageSources& images) { if(!hasError()) { @@ -633,7 +633,7 @@ void PlatformInterface::addSet(const Fp::Set& set, const Fe::ImageSources& image if(written) parent()->addRevertableFile(mOverviewWriter.currentFilePath()); else - mError = Fe::DocHandlingError(*this, Fe::DocHandlingError::DocWriteFailed, mOverviewWriter.fileErrorString()); + mError = Lr::DocHandlingError(*this, Lr::DocHandlingError::DocWriteFailed, mOverviewWriter.fileErrorString()); } //-Handle add apps------------------------------------------------------- @@ -662,14 +662,14 @@ void PlatformInterface::addSet(const Fp::Set& set, const Fe::ImageSources& image //-Constructor-------------------------------------------------------------------------------------------------------- //Public: PlatformInterface::Writer::Writer(PlatformInterface* sourceDoc) : - Fe::DataDoc::Writer(sourceDoc), - Fe::PlatformDoc::Writer(sourceDoc), + Lr::DataDoc::Writer(sourceDoc), + Lr::PlatformDoc::Writer(sourceDoc), mTaglistWriter(&sourceDoc->mPlatformTaglist) {} //-Instance Functions-------------------------------------------------------------------------------------------------- //Public: -Fe::DocHandlingError PlatformInterface::Writer::writeOutOf() { return mTaglistWriter.writeOutOf(); } +Lr::DocHandlingError PlatformInterface::Writer::writeOutOf() { return mTaglistWriter.writeOutOf(); } //=============================================================================================================== // PlaylistInterface @@ -679,7 +679,7 @@ Fe::DocHandlingError PlatformInterface::Writer::writeOutOf() { return mTaglistWr //Public: PlaylistInterface::PlaylistInterface(Install* const parent, const QString& playlistTaglistPath, QString playlistName, const DocKey&) : - Fe::PlaylistDoc(parent, {}, playlistName, {}), + Lr::PlaylistDoc(parent, {}, playlistName, {}), mPlaylistTaglist(parent, playlistTaglistPath, playlistName) {} @@ -705,14 +705,14 @@ void PlaylistInterface::setPlaylistData(const Fp::Playlist& playlist) //-Constructor-------------------------------------------------------------------------------------------------------- //Public: PlaylistInterface::Writer::Writer(PlaylistInterface* sourceDoc) : - Fe::DataDoc::Writer(sourceDoc), - Fe::PlaylistDoc::Writer(sourceDoc), + Lr::DataDoc::Writer(sourceDoc), + Lr::PlaylistDoc::Writer(sourceDoc), mTaglistWriter(&sourceDoc->mPlaylistTaglist) {} //-Instance Functions-------------------------------------------------------------------------------------------------- //Public: -Fe::DocHandlingError PlaylistInterface::Writer::writeOutOf() { return mTaglistWriter.writeOutOf(); } +Lr::DocHandlingError PlaylistInterface::Writer::writeOutOf() { return mTaglistWriter.writeOutOf(); } //=============================================================================================================== // Emulator @@ -727,7 +727,7 @@ Emulator::Emulator(Install* const parent, const QString& filePath, const DocKey& //-Instance Functions-------------------------------------------------------------------------------------------------- //Public: bool Emulator::isEmpty() const { return false; } // Can have blank fields, but always has field keys -Fe::DataDoc::Type Emulator::type() const { return Fe::DataDoc::Type::Config; } +Lr::DataDoc::Type Emulator::type() const { return Lr::DataDoc::Type::Config; } QString Emulator::executable() const { return mExecutable; } QString Emulator::args() const { return mArgs; } @@ -762,7 +762,7 @@ EmulatorReader::EmulatorReader(Emulator* targetDoc) : //-Instance Functions-------------------------------------------------------------------------------------------------- //Private: -Fe::DocHandlingError EmulatorReader::readTargetDoc() +Lr::DocHandlingError EmulatorReader::readTargetDoc() { while(!mStreamReader.atEnd()) { @@ -774,7 +774,7 @@ Fe::DocHandlingError EmulatorReader::readTargetDoc() } // Only can have stream related errors - return Fe::DocHandlingError(); + return Lr::DocHandlingError(); } void EmulatorReader::parseKeyValue(const QString& key, const QString& value) diff --git a/app/src/frontend/attractmode/am-data.h b/app/src/launcher/attractmode/am-data.h similarity index 93% rename from app/src/frontend/attractmode/am-data.h rename to app/src/launcher/attractmode/am-data.h index be477a9..c891c28 100644 --- a/app/src/frontend/attractmode/am-data.h +++ b/app/src/launcher/attractmode/am-data.h @@ -9,8 +9,8 @@ #include // Project Includes -#include "am-items.h" -#include "frontend/fe-data.h" +#include "launcher/lr-data.h" +#include "launcher/attractmode/am-items.h" namespace Am { @@ -25,7 +25,7 @@ class DocKey DocKey(const DocKey&) = default; }; -class CommonDocReader : public Fe::DataDoc::Reader +class CommonDocReader : public Lr::DataDoc::Reader { //-Instance Variables-------------------------------------------------------------------------------------------------- protected: @@ -33,20 +33,20 @@ class CommonDocReader : public Fe::DataDoc::Reader //-Constructor-------------------------------------------------------------------------------------------------------- protected: - CommonDocReader(Fe::DataDoc* targetDoc); + CommonDocReader(Lr::DataDoc* targetDoc); //-Instance Functions------------------------------------------------------------------------------------------------- protected: bool lineIsComment(const QString& line); QString readLineIgnoringComments(qint64 maxlen = 0); virtual bool checkDocValidity(bool& isValid) = 0; - virtual Fe::DocHandlingError readTargetDoc() = 0; + virtual Lr::DocHandlingError readTargetDoc() = 0; public: - Fe::DocHandlingError readInto() override; + Lr::DocHandlingError readInto() override; }; -class CommonDocWriter : public Fe::DataDoc::Writer +class CommonDocWriter : public Lr::DataDoc::Writer { //-Instance Variables-------------------------------------------------------------------------------------------------- protected: @@ -54,17 +54,17 @@ class CommonDocWriter : public Fe::DataDoc::Writer //-Constructor-------------------------------------------------------------------------------------------------------- protected: - CommonDocWriter(Fe::DataDoc* sourceDoc); + CommonDocWriter(Lr::DataDoc* sourceDoc); //-Instance Functions------------------------------------------------------------------------------------------------- protected: virtual bool writeSourceDoc() = 0; public: - Fe::DocHandlingError writeOutOf() override; + Lr::DocHandlingError writeOutOf() override; }; -class ConfigDoc : public Fe::DataDoc +class ConfigDoc : public Lr::DataDoc { //-Inner Classes---------------------------------------------------------------------------------------------------- public: @@ -117,7 +117,7 @@ class ConfigDoc::Writer : public CommonDocWriter bool writeSourceDoc() override; }; -class Taglist : public Fe::DataDoc +class Taglist : public Lr::DataDoc { //-Inner Classes---------------------------------------------------------------------------------------------------- public: @@ -174,7 +174,7 @@ class PlaylistTaglist : public Taglist Type type() const override; }; -class Romlist : public Fe::UpdateableDoc +class Romlist : public Lr::UpdateableDoc { /* This class looks like it should inherit PlatformDoc, but it isn't truly one in the context of an Am install * since those are represented by tag lists, and if it did there would be the issue that once modified it would @@ -199,7 +199,7 @@ class Romlist : public Fe::UpdateableDoc //-Constructor-------------------------------------------------------------------------------------------------------- public: - explicit Romlist(Install* const parent, const QString& listPath, QString docName, const Fe::UpdateOptions& updateOptions, + explicit Romlist(Install* const parent, const QString& listPath, QString docName, const Import::UpdateOptions& updateOptions, const DocKey&); //-Instance Functions-------------------------------------------------------------------------------------------------- @@ -212,7 +212,7 @@ class Romlist : public Fe::UpdateableDoc bool containsGame(QUuid gameId) const; bool containsAddApp(QUuid addAppId) const; - void addSet(const Fp::Set& set, const Fe::ImageSources& images); + void addSet(const Fp::Set& set, const Lr::ImageSources& images); void finalize() override; }; @@ -227,7 +227,7 @@ class Romlist::Reader : public CommonDocReader private: QHash>& targetDocExistingRomEntries(); bool checkDocValidity(bool& isValid) override; - Fe::DocHandlingError readTargetDoc() override; + Lr::DocHandlingError readTargetDoc() override; void parseRomEntry(const QString& rawEntry); void addFieldToBuilder(RomEntry::Builder& builder, QString field, quint8 index); }; @@ -263,7 +263,7 @@ class BulkOverviewWriter bool writeOverview(const QUuid& gameId, const QString& overview); }; -class PlatformInterface : public Fe::PlatformDoc +class PlatformInterface : public Lr::PlatformDoc { //-Inner Classes---------------------------------------------------------------------------------------------------- public: @@ -289,10 +289,10 @@ class PlatformInterface : public Fe::PlatformDoc bool containsGame(QUuid gameId) const override; bool containsAddApp(QUuid addAppId) const override; - void addSet(const Fp::Set& set, const Fe::ImageSources& images) override; + void addSet(const Fp::Set& set, const Lr::ImageSources& images) override; }; -class PlatformInterface::Writer : public Fe::PlatformDoc::Writer +class PlatformInterface::Writer : public Lr::PlatformDoc::Writer { // Shell for writing the taglist of the interface @@ -306,10 +306,10 @@ class PlatformInterface::Writer : public Fe::PlatformDoc::Writer //-Instance Functions------------------------------------------------------------------------------------------------- public: - Fe::DocHandlingError writeOutOf() override; + Lr::DocHandlingError writeOutOf() override; }; -class PlaylistInterface : public Fe::PlaylistDoc +class PlaylistInterface : public Lr::PlaylistDoc { //-Inner Classes---------------------------------------------------------------------------------------------------- public: @@ -333,7 +333,7 @@ class PlaylistInterface : public Fe::PlaylistDoc void setPlaylistData(const Fp::Playlist& playlist) override; }; -class PlaylistInterface::Writer : public Fe::PlaylistDoc::Writer +class PlaylistInterface::Writer : public Lr::PlaylistDoc::Writer { // Shell for writing the taglist of the interface @@ -347,7 +347,7 @@ class PlaylistInterface::Writer : public Fe::PlaylistDoc::Writer //-Instance Functions------------------------------------------------------------------------------------------------- private: - Fe::DocHandlingError writeOutOf() override; + Lr::DocHandlingError writeOutOf() override; }; class Emulator : public ConfigDoc @@ -437,7 +437,7 @@ class EmulatorReader : public ConfigDoc::Reader //-Instance Functions------------------------------------------------------------------------------------------------- private: - Fe::DocHandlingError readTargetDoc() override; + Lr::DocHandlingError readTargetDoc() override; void parseKeyValue(const QString& key, const QString& value); void parseExecutable(const QString& value); void parseArgs(const QString& value); diff --git a/app/src/frontend/attractmode/am-install.cpp b/app/src/launcher/attractmode/am-install.cpp similarity index 84% rename from app/src/frontend/attractmode/am-install.cpp rename to app/src/launcher/attractmode/am-install.cpp index adc9a82..636175d 100644 --- a/app/src/frontend/attractmode/am-install.cpp +++ b/app/src/launcher/attractmode/am-install.cpp @@ -9,7 +9,7 @@ #include // Project Includes -#include "clifp.h" +#include "kernel/clifp.h" namespace Am { @@ -20,7 +20,7 @@ namespace Am //-Constructor------------------------------------------------------------------------------------------------ //Public: Install::Install(const QString& installPath) : - Fe::Install(installPath), + Lr::Install(installPath), mEmulatorsDirectory(installPath + '/' + EMULATORS_PATH), mRomlistsDirectory(installPath + '/' + ROMLISTS_PATH), mMainConfigFile(installPath + '/' + MAIN_CFG_PATH), @@ -55,7 +55,7 @@ Install::Install(const QString& installPath) : //Private: void Install::nullify() { - Fe::Install::nullify(); + Lr::Install::nullify(); mEmulatorsDirectory = QDir(); mRomlistsDirectory = QDir(); @@ -89,7 +89,7 @@ Qx::Error Install::populateExistingDocs() return existingCheck; for(const QFileInfo& platformFile : qAsConst(existingList)) - catalogueExistingDoc(Fe::DataDoc::Identifier(Fe::DataDoc::Type::Platform, platformFile.baseName())); + catalogueExistingDoc(Lr::DataDoc::Identifier(Lr::DataDoc::Type::Platform, platformFile.baseName())); // Check for playlists existingCheck = Qx::dirContentInfoList(existingList, mFpTagDirectory, {u"[[]Playlist[]] *."_s + TAG_EXT}, @@ -98,27 +98,27 @@ Qx::Error Install::populateExistingDocs() return existingCheck; for(const QFileInfo& playlistFile : qAsConst(existingList)) - catalogueExistingDoc(Fe::DataDoc::Identifier(Fe::DataDoc::Type::Playlist, playlistFile.baseName())); + catalogueExistingDoc(Lr::DataDoc::Identifier(Lr::DataDoc::Type::Playlist, playlistFile.baseName())); // Check for special "Flashpoint" platform (more like a config doc but OK for now) QFileInfo mainRomlistInfo(mFpRomlist); if(mainRomlistInfo.exists()) - catalogueExistingDoc(Fe::DataDoc::Identifier(Fe::DataDoc::Type::Platform, mainRomlistInfo.baseName())); + catalogueExistingDoc(Lr::DataDoc::Identifier(Lr::DataDoc::Type::Platform, mainRomlistInfo.baseName())); } // Check for config docs QFileInfo mainCfgInfo(mMainConfigFile); - catalogueExistingDoc(Fe::DataDoc::Identifier(Fe::DataDoc::Type::Config, mainCfgInfo.baseName())); // Must exist + catalogueExistingDoc(Lr::DataDoc::Identifier(Lr::DataDoc::Type::Config, mainCfgInfo.baseName())); // Must exist QFileInfo emulatorCfgInfo(mEmulatorConfigFile); if(emulatorCfgInfo.exists()) - catalogueExistingDoc(Fe::DataDoc::Identifier(Fe::DataDoc::Type::Config, emulatorCfgInfo.baseName())); + catalogueExistingDoc(Lr::DataDoc::Identifier(Lr::DataDoc::Type::Config, emulatorCfgInfo.baseName())); // Return success return Qx::Error(); } -QString Install::imageDestinationPath(Fp::ImageType imageType, const Fe::Game* game) const +QString Install::imageDestinationPath(Fp::ImageType imageType, const Lr::Game* game) const { return mFpScraperDirectory.absolutePath() + '/' + (imageType == Fp::ImageType::Logo ? LOGO_FOLDER_NAME : SCREENSHOT_FOLDER_NAME) + '/' + @@ -126,7 +126,7 @@ QString Install::imageDestinationPath(Fp::ImageType imageType, const Fe::Game* g '.' + IMAGE_EXT; } -std::shared_ptr Install::preparePlatformDocCheckout(std::unique_ptr& platformDoc, const QString& translatedName) +std::shared_ptr Install::preparePlatformDocCheckout(std::unique_ptr& platformDoc, const QString& translatedName) { // Determine path to the taglist that corresponds with the interface QString taglistPath = mFpTagDirectory.absoluteFilePath(translatedName + u"."_s + TAG_EXT) ; @@ -138,10 +138,10 @@ std::shared_ptr Install::preparePlatformDocCheckout(std platformDoc = std::make_unique(this, taglistPath, translatedName, overviewDir, DocKey{}); // No reading to be done for this interface (tag lists are always overwritten) - return std::shared_ptr(); + return std::shared_ptr(); } -std::shared_ptr Install::preparePlaylistDocCheckout(std::unique_ptr& playlistDoc, const QString& translatedName) +std::shared_ptr Install::preparePlaylistDocCheckout(std::unique_ptr& playlistDoc, const QString& translatedName) { // Determine path to the taglist that corresponds with the interface QString taglistPath = mFpTagDirectory.absoluteFilePath(translatedName + u"."_s + TAG_EXT) ; @@ -150,28 +150,28 @@ std::shared_ptr Install::preparePlaylistDocCheckout(std playlistDoc = std::make_unique(this, taglistPath, translatedName, DocKey{}); // No reading to be done for this interface (tag lists are always overwritten) - return std::shared_ptr(); + return std::shared_ptr(); } -std::shared_ptr Install::preparePlatformDocCommit(const std::unique_ptr& platformDoc) +std::shared_ptr Install::preparePlatformDocCommit(const std::unique_ptr& platformDoc) { // Construct doc writer - std::shared_ptr docWriter = std::make_shared(static_cast(platformDoc.get())); + std::shared_ptr docWriter = std::make_shared(static_cast(platformDoc.get())); // Return writer return docWriter; } -std::shared_ptr Install::preparePlaylistDocCommit(const std::unique_ptr& playlistDoc) +std::shared_ptr Install::preparePlaylistDocCommit(const std::unique_ptr& playlistDoc) { // Construct doc writer - std::shared_ptr docWriter = std::make_shared(static_cast(playlistDoc.get())); + std::shared_ptr docWriter = std::make_shared(static_cast(playlistDoc.get())); // Return writer return docWriter; } -Fe::DocHandlingError Install::checkoutMainConfig(std::unique_ptr& returnBuffer) +Lr::DocHandlingError Install::checkoutMainConfig(std::unique_ptr& returnBuffer) { // Construct unopened document returnBuffer = std::make_unique(this, mMainConfigFile.fileName(), DocKey{}); @@ -180,7 +180,7 @@ Fe::DocHandlingError Install::checkoutMainConfig(std::unique_ptr& std::shared_ptr docReader = std::make_shared(returnBuffer.get()); // Open document - Fe::DocHandlingError readErrorStatus = checkoutDataDocument(returnBuffer.get(), docReader); + Lr::DocHandlingError readErrorStatus = checkoutDataDocument(returnBuffer.get(), docReader); // Set return null on failure if(readErrorStatus.isValid()) @@ -190,7 +190,7 @@ Fe::DocHandlingError Install::checkoutMainConfig(std::unique_ptr& return readErrorStatus; } -Fe::DocHandlingError Install::checkoutFlashpointRomlist(std::unique_ptr& returnBuffer) +Lr::DocHandlingError Install::checkoutFlashpointRomlist(std::unique_ptr& returnBuffer) { // Construct unopened document returnBuffer = std::make_unique(this, mFpRomlist.fileName(), Fp::NAME, mImportDetails->updateOptions, DocKey{}); @@ -199,7 +199,7 @@ Fe::DocHandlingError Install::checkoutFlashpointRomlist(std::unique_ptr std::shared_ptr docReader = std::make_shared(returnBuffer.get()); // Open document - Fe::DocHandlingError readErrorStatus = checkoutDataDocument(returnBuffer.get(), docReader); + Lr::DocHandlingError readErrorStatus = checkoutDataDocument(returnBuffer.get(), docReader); // Set return null on failure if(readErrorStatus.isValid()) @@ -209,7 +209,7 @@ Fe::DocHandlingError Install::checkoutFlashpointRomlist(std::unique_ptr return readErrorStatus; } -Fe::DocHandlingError Install::checkoutClifpEmulatorConfig(std::unique_ptr& returnBuffer) +Lr::DocHandlingError Install::checkoutClifpEmulatorConfig(std::unique_ptr& returnBuffer) { // Construct unopened document returnBuffer = std::make_unique(this, mEmulatorConfigFile.fileName(), DocKey{}); @@ -218,7 +218,7 @@ Fe::DocHandlingError Install::checkoutClifpEmulatorConfig(std::unique_ptr docReader = std::make_shared(returnBuffer.get()); // Open document - Fe::DocHandlingError readErrorStatus = checkoutDataDocument(returnBuffer.get(), docReader); + Lr::DocHandlingError readErrorStatus = checkoutDataDocument(returnBuffer.get(), docReader); // Set return null on failure if(readErrorStatus.isValid()) @@ -228,7 +228,7 @@ Fe::DocHandlingError Install::checkoutClifpEmulatorConfig(std::unique_ptr document) +Lr::DocHandlingError Install::commitMainConfig(std::unique_ptr document) { assert(document->parent() == this); @@ -236,7 +236,7 @@ Fe::DocHandlingError Install::commitMainConfig(std::unique_ptr do std::shared_ptr docWriter = std::make_shared(document.get()); // Write - Fe::DocHandlingError writeErrorStatus = commitDataDocument(document.get(), docWriter); + Lr::DocHandlingError writeErrorStatus = commitDataDocument(document.get(), docWriter); // Ensure document is cleared document.reset(); @@ -245,7 +245,7 @@ Fe::DocHandlingError Install::commitMainConfig(std::unique_ptr do return writeErrorStatus; } -Fe::DocHandlingError Install::commitFlashpointRomlist(std::unique_ptr document) +Lr::DocHandlingError Install::commitFlashpointRomlist(std::unique_ptr document) { assert(document->parent() == this); @@ -253,7 +253,7 @@ Fe::DocHandlingError Install::commitFlashpointRomlist(std::unique_ptr d std::shared_ptr docWriter = std::make_shared(document.get()); // Write - Fe::DocHandlingError writeErrorStatus = commitDataDocument(document.get(), docWriter); + Lr::DocHandlingError writeErrorStatus = commitDataDocument(document.get(), docWriter); // Ensure document is cleared document.reset(); @@ -263,7 +263,7 @@ Fe::DocHandlingError Install::commitFlashpointRomlist(std::unique_ptr d } -Fe::DocHandlingError Install::commitClifpEmulatorConfig(std::unique_ptr document) +Lr::DocHandlingError Install::commitClifpEmulatorConfig(std::unique_ptr document) { assert(document->parent() == this); @@ -271,7 +271,7 @@ Fe::DocHandlingError Install::commitClifpEmulatorConfig(std::unique_ptr docWriter = std::make_shared(document.get()); // Write - Fe::DocHandlingError writeErrorStatus = commitDataDocument(document.get(), docWriter); + Lr::DocHandlingError writeErrorStatus = commitDataDocument(document.get(), docWriter); // Ensure document is cleared document.reset(); @@ -283,7 +283,7 @@ Fe::DocHandlingError Install::commitClifpEmulatorConfig(std::unique_ptr Install::preferredImageModeOrder() const { return IMAGE_MODE_ORDER; } +QList Install::preferredImageModeOrder() const { return IMAGE_MODE_ORDER; } bool Install::isRunning() const { @@ -324,18 +324,18 @@ QString Install::versionString() const } // Can't determine version - return Fe::Install::versionString(); + return Lr::Install::versionString(); } -QString Install::translateDocName(const QString& originalName, Fe::DataDoc::Type type) const +QString Install::translateDocName(const QString& originalName, Lr::DataDoc::Type type) const { // Perform general kosherization QString translatedName = Qx::kosherizeFileName(originalName); // Prefix platforms/playlists - if(type == Fe::DataDoc::Type::Platform) + if(type == Lr::DataDoc::Type::Platform) translatedName.prepend(PLATFORM_TAG_PREFIX); - else if(type == Fe::DataDoc::Type::Playlist) + else if(type == Lr::DataDoc::Type::Playlist) translatedName.prepend(PLAYLIST_TAG_PREFIX); return translatedName; @@ -357,7 +357,7 @@ Qx::Error Install::preImport(const ImportDetails& details) return Qx::IoOpReport(Qx::IO_OP_WRITE, Qx::IO_ERR_CANT_CREATE, overviewDir); // Logo and screenshot dir - if(details.imageMode == Fe::ImageMode::Copy || details.imageMode == Fe::ImageMode::Link) + if(details.imageMode == Import::ImageMode::Copy || details.imageMode == Import::ImageMode::Link) { QDir logoDir(mFpScraperDirectory.absoluteFilePath(LOGO_FOLDER_NAME)); if(!logoDir.exists()) @@ -371,7 +371,7 @@ Qx::Error Install::preImport(const ImportDetails& details) } // Perform base tasks - return Fe::Install::preImport(details); + return Lr::Install::preImport(details); } Qx::Error Install::prePlatformsImport() @@ -389,17 +389,17 @@ Qx::Error Install::postPlatformsImport() return commitFlashpointRomlist(std::move(mRomlist)); } -Qx::Error Install::preImageProcessing(QList& workerTransfers, const Fe::ImageSources& bulkSources) +Qx::Error Install::preImageProcessing(QList& workerTransfers, const Lr::ImageSources& bulkSources) { Q_UNUSED(bulkSources); switch(mImportDetails->imageMode) { - case Fe::ImageMode::Link: - case Fe::ImageMode::Copy: + case Import::ImageMode::Link: + case Import::ImageMode::Copy: workerTransfers.swap(mWorkerImageJobs); return Qx::Error(); - case Fe::ImageMode::Reference: + case Import::ImageMode::Reference: qWarning("unsupported image mode"); return Qx::Error(); default: @@ -414,7 +414,7 @@ Qx::Error Install::postImport() // Checkout emulator config std::unique_ptr emulatorConfig; - Fe::DocHandlingError emulatorConfigReadError = checkoutClifpEmulatorConfig(emulatorConfig); + Lr::DocHandlingError emulatorConfigReadError = checkoutClifpEmulatorConfig(emulatorConfig); // Stop import if error occurred if(emulatorConfigReadError.isValid()) @@ -445,7 +445,7 @@ Qx::Error Install::postImport() emulatorConfig->setArtworkEntry(aeb.build()); // Commit emulator config - Fe::DocHandlingError emulatorConfigWriteError = commitClifpEmulatorConfig(std::move(emulatorConfig)); + Lr::DocHandlingError emulatorConfigWriteError = commitClifpEmulatorConfig(std::move(emulatorConfig)); // Stop import if error occurred if(emulatorConfigWriteError.isValid()) @@ -455,7 +455,7 @@ Qx::Error Install::postImport() // Checkout main config std::unique_ptr mainConfig; - Fe::DocHandlingError mainConfigReadError = checkoutMainConfig(mainConfig); + Lr::DocHandlingError mainConfigReadError = checkoutMainConfig(mainConfig); // Stop import if error occurred if(mainConfigReadError.isValid()) @@ -511,6 +511,7 @@ Qx::Error Install::postImport() for(const QString& tagFile : tagFiles) { // Escape brackets in name since AM uses regex for value + // TODO: Use Qx for this QString escaped = tagFile; escaped.replace(u"["_s, u"\\["_s).replace(u"]"_s, u"\\]"_s); @@ -524,7 +525,7 @@ Qx::Error Install::postImport() } // Commit main config - Fe::DocHandlingError configCommitError = commitMainConfig(std::move(mainConfig)); + Lr::DocHandlingError configCommitError = commitMainConfig(std::move(mainConfig)); // Stop import if error occurred if(configCommitError.isValid()) @@ -540,10 +541,10 @@ Qx::Error Install::postImport() return Qx::Error(); } -void Install::processDirectGameImages(const Fe::Game* game, const Fe::ImageSources& imageSources) +void Install::processDirectGameImages(const Lr::Game* game, const Lr::ImageSources& imageSources) { - Fe::ImageMode mode = mImportDetails->imageMode; - if(mode == Fe::ImageMode::Link || mode == Fe::ImageMode::Copy) + Import::ImageMode mode = mImportDetails->imageMode; + if(mode == Import::ImageMode::Link || mode == Import::ImageMode::Copy) { if(!imageSources.logoPath().isEmpty()) { diff --git a/app/src/frontend/attractmode/am-install.h b/app/src/launcher/attractmode/am-install.h similarity index 70% rename from app/src/frontend/attractmode/am-install.h rename to app/src/launcher/attractmode/am-install.h index 81b7540..a9c4058 100644 --- a/app/src/frontend/attractmode/am-install.h +++ b/app/src/launcher/attractmode/am-install.h @@ -5,14 +5,14 @@ #include // Project Includes -#include "frontend/fe-install.h" -#include "am-data.h" -#include "am-settings-data.h" +#include "launcher/lr-install.h" +#include "launcher/attractmode/am-data.h" +#include "launcher/attractmode/am-settings-data.h" namespace Am { -class Install : public Fe::Install +class Install : public Lr::Install { friend class PlatformInterface; friend class PlaylistInterface; @@ -21,7 +21,7 @@ class Install : public Fe::Install public: // Identity static inline const QString NAME = u"AttractMode"_s; - static inline const QString ICON_PATH = u":/frontend/AttractMode/icon.png"_s; + static inline const QString ICON_PATH = u":/launcher/AttractMode/icon.png"_s; static inline const QUrl HELP_URL = QUrl(u""_s); // Naming @@ -52,9 +52,9 @@ class Install : public Fe::Install static inline const QString CFG_EXT = u"cfg"_s; // Support - static inline const QList IMAGE_MODE_ORDER { - Fe::ImageMode::Link, - Fe::ImageMode::Copy + static inline const QList IMAGE_MODE_ORDER { + Import::ImageMode::Link, + Import::ImageMode::Copy }; /* * NOTE: In order to support reference, thousands of folders would have to be added to the image search list which is likely impractical. @@ -62,7 +62,7 @@ class Install : public Fe::Install */ // Extra - static inline const QString MARQUEE_PATH = u":/frontend/AttractMode/marquee.png"_s; + static inline const QString MARQUEE_PATH = u":/launcher/AttractMode/marquee.png"_s; //-Instance Variables----------------------------------------------------------------------------------------------- private: @@ -97,20 +97,20 @@ class Install : public Fe::Install QString versionFromExecutable() const; // Image Processing - QString imageDestinationPath(Fp::ImageType imageType, const Fe::Game* game) const; + QString imageDestinationPath(Fp::ImageType imageType, const Lr::Game* game) const; // Doc handling - std::shared_ptr preparePlatformDocCheckout(std::unique_ptr& platformDoc, const QString& translatedName) override; - std::shared_ptr preparePlaylistDocCheckout(std::unique_ptr& playlistDoc, const QString& translatedName) override; - std::shared_ptr preparePlatformDocCommit(const std::unique_ptr& platformDoc) override; - std::shared_ptr preparePlaylistDocCommit(const std::unique_ptr& playlistDoc) override; - - Fe::DocHandlingError checkoutMainConfig(std::unique_ptr& returnBuffer); - Fe::DocHandlingError checkoutFlashpointRomlist(std::unique_ptr& returnBuffer); - Fe::DocHandlingError checkoutClifpEmulatorConfig(std::unique_ptr& returnBuffer); - Fe::DocHandlingError commitMainConfig(std::unique_ptr document); - Fe::DocHandlingError commitFlashpointRomlist(std::unique_ptr document); - Fe::DocHandlingError commitClifpEmulatorConfig(std::unique_ptr document); + std::shared_ptr preparePlatformDocCheckout(std::unique_ptr& platformDoc, const QString& translatedName) override; + std::shared_ptr preparePlaylistDocCheckout(std::unique_ptr& playlistDoc, const QString& translatedName) override; + std::shared_ptr preparePlatformDocCommit(const std::unique_ptr& platformDoc) override; + std::shared_ptr preparePlaylistDocCommit(const std::unique_ptr& playlistDoc) override; + + Lr::DocHandlingError checkoutMainConfig(std::unique_ptr& returnBuffer); + Lr::DocHandlingError checkoutFlashpointRomlist(std::unique_ptr& returnBuffer); + Lr::DocHandlingError checkoutClifpEmulatorConfig(std::unique_ptr& returnBuffer); + Lr::DocHandlingError commitMainConfig(std::unique_ptr document); + Lr::DocHandlingError commitFlashpointRomlist(std::unique_ptr document); + Lr::DocHandlingError commitClifpEmulatorConfig(std::unique_ptr document); public: // Install management @@ -118,22 +118,22 @@ class Install : public Fe::Install // Info QString name() const override; - QList preferredImageModeOrder() const override; + QList preferredImageModeOrder() const override; bool isRunning() const override; QString versionString() const override; - QString translateDocName(const QString& originalName, Fe::DataDoc::Type type) const override; + QString translateDocName(const QString& originalName, Lr::DataDoc::Type type) const override; // Import stage notifier hooks Qx::Error preImport(const ImportDetails& details) override; Qx::Error prePlatformsImport() override; Qx::Error postPlatformsImport() override; - Qx::Error preImageProcessing(QList& workerTransfers, const Fe::ImageSources& bulkSources) override; + Qx::Error preImageProcessing(QList& workerTransfers, const Lr::ImageSources& bulkSources) override; Qx::Error postImport() override; // Image handling - void processDirectGameImages(const Fe::Game* game, const Fe::ImageSources& imageSources) override; + void processDirectGameImages(const Lr::Game* game, const Lr::ImageSources& imageSources) override; }; -REGISTER_FRONTEND(Install::NAME, Install, &Install::ICON_PATH, &Install::HELP_URL); +REGISTER_LAUNCHER(Install::NAME, Install, &Install::ICON_PATH, &Install::HELP_URL); } #endif // ATTRACTMODE_INSTALL_H diff --git a/app/src/frontend/attractmode/am-install_linux.cpp b/app/src/launcher/attractmode/am-install_linux.cpp similarity index 100% rename from app/src/frontend/attractmode/am-install_linux.cpp rename to app/src/launcher/attractmode/am-install_linux.cpp diff --git a/app/src/frontend/attractmode/am-install_win.cpp b/app/src/launcher/attractmode/am-install_win.cpp similarity index 100% rename from app/src/frontend/attractmode/am-install_win.cpp rename to app/src/launcher/attractmode/am-install_win.cpp diff --git a/app/src/frontend/attractmode/am-items.cpp b/app/src/launcher/attractmode/am-items.cpp similarity index 97% rename from app/src/frontend/attractmode/am-items.cpp rename to app/src/launcher/attractmode/am-items.cpp index ca0e391..2eeeaa7 100644 --- a/app/src/frontend/attractmode/am-items.cpp +++ b/app/src/launcher/attractmode/am-items.cpp @@ -18,7 +18,7 @@ namespace Am RomEntry::RomEntry() {} RomEntry::RomEntry(const Fp::Game& flashpointGame) : - Fe::Game(flashpointGame.id(), ESCAPE(flashpointGame.title()), flashpointGame.platformName()), + Lr::Game(flashpointGame.id(), ESCAPE(flashpointGame.title()), flashpointGame.platformName()), mEmulator(Fp::NAME), mCloneOf(), mYear(flashpointGame.releaseDate().date()), @@ -42,7 +42,7 @@ RomEntry::RomEntry(const Fp::Game& flashpointGame) : {} RomEntry::RomEntry(const Fp::AddApp& flashpointAddApp, const Fp::Game& parentGame) : - Fe::Game(flashpointAddApp.id(), ESCAPE(addAppTitle(parentGame.title(), flashpointAddApp.name())), parentGame.platformName()), + Lr::Game(flashpointAddApp.id(), ESCAPE(addAppTitle(parentGame.title(), flashpointAddApp.name())), parentGame.platformName()), mEmulator(Fp::NAME), mCloneOf(flashpointAddApp.parentId().toString(QUuid::WithoutBraces)), mYear(parentGame.releaseDate().date()), @@ -83,13 +83,13 @@ QString RomEntry::addAppSortTitle(const QString& parentTitle, const QString& ori //-Instance Functions------------------------------------------------------------------------------------------------ //Public: -QUuid RomEntry::name() const { return mId; } // Alias for Fe::Game::Id -QString RomEntry::title() const { return mName; }; // Alias for Fe::Game::name +QUuid RomEntry::name() const { return mId; } // Alias for Lr::Game::Id +QString RomEntry::title() const { return mName; }; // Alias for Lr::Game::name QString RomEntry::emulator() const { return mEmulator; } QString RomEntry::cloneOf() const { return mCloneOf; } QDate RomEntry::year() const{ return mYear; } QString RomEntry::manufacturer() const { return mManufacturer; } -QString RomEntry::category() const { return mPlatform; } // Alias for Fe::Game::platform +QString RomEntry::category() const { return mPlatform; } // Alias for Lr::Game::platform QString RomEntry::players() const { return mPlayers; } quint8 RomEntry::rotation() const { return mRotation; } QString RomEntry::control() const { return mControl; } diff --git a/app/src/frontend/attractmode/am-items.h b/app/src/launcher/attractmode/am-items.h similarity index 88% rename from app/src/frontend/attractmode/am-items.h rename to app/src/launcher/attractmode/am-items.h index 8fb4029..1663ee6 100644 --- a/app/src/frontend/attractmode/am-items.h +++ b/app/src/launcher/attractmode/am-items.h @@ -9,12 +9,12 @@ #include // Project Includes -#include "frontend/fe-items.h" +#include "launcher/lr-items.h" namespace Am { -class RomEntry : public Fe::Game +class RomEntry : public Lr::Game { //-Inner Classes--------------------------------------------------------------------------------------------------- public: @@ -22,13 +22,13 @@ class RomEntry : public Fe::Game //-Instance Variables----------------------------------------------------------------------------------------------- private: - //mName - Handled as alias for Fe::Game::mId - //mTitle - Handled as alias for Fe::Game::mName + //mName - Handled as alias for Lr::Game::mId + //mTitle - Handled as alias for Lr::Game::mName QString mEmulator; QString mCloneOf; QDate mYear; QString mManufacturer; - //mCategory - Handled as alias for Fe::Game::mPlatform + //mCategory - Handled as alias for Lr::Game::mPlatform QString mPlayers; quint8 mRotation; QString mControl; @@ -57,13 +57,13 @@ class RomEntry : public Fe::Game //-Instance Functions------------------------------------------------------------------------------------------------------ public: - QUuid name() const; // Alias for Fe::Game::Id - QString title() const; // Alias for Fe::Game::name + QUuid name() const; // Alias for Lr::Game::Id + QString title() const; // Alias for Lr::Game::name QString emulator() const; QString cloneOf() const; QDate year() const; QString manufacturer() const; - QString category() const; // Alias for Fe::Game::platform + QString category() const; // Alias for Lr::Game::platform QString players() const; quint8 rotation() const; QString control() const; @@ -80,7 +80,7 @@ class RomEntry : public Fe::Game QString rating() const; }; -class RomEntry::Builder : public Fe::Game::Builder +class RomEntry::Builder : public Lr::Game::Builder { //-Constructor------------------------------------------------------------------------------------------------- public: @@ -111,7 +111,7 @@ class RomEntry::Builder : public Fe::Game::Builder Builder& wRating(const QString& rating); }; -class EmulatorArtworkEntry : public Fe::Item +class EmulatorArtworkEntry : public Lr::Item { //-Inner Classes--------------------------------------------------------------------------------------------------- public: @@ -132,7 +132,7 @@ class EmulatorArtworkEntry : public Fe::Item QStringList paths() const; }; -class EmulatorArtworkEntry::Builder : public Fe::Item::Builder +class EmulatorArtworkEntry::Builder : public Lr::Item::Builder { //-Constructor------------------------------------------------------------------------------------------------- public: diff --git a/app/src/frontend/attractmode/am-settings-data.cpp b/app/src/launcher/attractmode/am-settings-data.cpp similarity index 98% rename from app/src/frontend/attractmode/am-settings-data.cpp rename to app/src/launcher/attractmode/am-settings-data.cpp index 7d1a73e..53c119c 100644 --- a/app/src/frontend/attractmode/am-settings-data.cpp +++ b/app/src/launcher/attractmode/am-settings-data.cpp @@ -168,7 +168,7 @@ CrudeSettings::CrudeSettings(Install* const parent, const QString& filePath, con //-Instance Functions-------------------------------------------------------------------------------------------------- //Public: bool CrudeSettings::isEmpty() const { return mDisplays.isEmpty() && mOtherSettings.isEmpty(); } -Fe::DataDoc::Type CrudeSettings::type() const { return Type::Config; } +Lr::DataDoc::Type CrudeSettings::type() const { return Type::Config; } bool CrudeSettings::containsDisplay(const QString& name) { return mDisplays.contains(name); } void CrudeSettings::addDisplay(const Display& display) { mDisplays.insert(display.name(), display); } @@ -217,9 +217,9 @@ CrudeSettings* CrudeSettingsReader::targetCrudeSettings() const return static_cast(mTargetDocument); } -Fe::DocHandlingError CrudeSettingsReader::readTargetDoc() +Lr::DocHandlingError CrudeSettingsReader::readTargetDoc() { - Fe::DocHandlingError errorStatus; + Lr::DocHandlingError errorStatus; // Got through all entries while(!mStreamReader.atEnd()) @@ -253,7 +253,7 @@ Fe::DocHandlingError CrudeSettingsReader::readTargetDoc() if(!mCurrentSubSettingParser->parse(key, value, depth)) { QString setting = mCurrentSubSettingParser->settingName(); - errorStatus = Fe::DocHandlingError(*mTargetDocument, Fe::DocHandlingError::DocReadFailed, UNKNOWN_KEY_ERROR.arg(key, setting)); + errorStatus = Lr::DocHandlingError(*mTargetDocument, Lr::DocHandlingError::DocReadFailed, UNKNOWN_KEY_ERROR.arg(key, setting)); break; } } diff --git a/app/src/frontend/attractmode/am-settings-data.h b/app/src/launcher/attractmode/am-settings-data.h similarity index 96% rename from app/src/frontend/attractmode/am-settings-data.h rename to app/src/launcher/attractmode/am-settings-data.h index 67c580f..5d59430 100644 --- a/app/src/frontend/attractmode/am-settings-data.h +++ b/app/src/launcher/attractmode/am-settings-data.h @@ -2,13 +2,13 @@ #define AM_SETTINGS_DATA_H // Project Includes -#include "am-data.h" -#include "am-settings-items.h" +#include "launcher/attractmode/am-data.h" +#include "launcher/attractmode/am-settings-items.h" namespace Am { -/* This setup of AM config parsing basically inverts the approach of the standard Fe items in that instead of using a builder +/* This setup of AM config parsing basically inverts the approach of the standard Lr items in that instead of using a builder * to work on an in-progress item and then building the item when done and finally adding it to its destination, instead the * item is constructed in a default state first and immediately added to its destination and then what parses the item works * on it in-place. This is basically required due to the need for polymorphism, itself needed due to how the AM config is @@ -18,7 +18,7 @@ namespace Am * are really hacky/jank and should be avoided. * * In the end this approach is similar to how AM itself reads attract.cfg and so it is for the best despite the slight consistency - * break with the Fe defaults. It's probably a good thing that different frontend implementations can be this flexible anyway. + * break with the Lr defaults. It's probably a good thing that different launcher implementations can be this flexible anyway. */ class ISettingParser @@ -198,7 +198,7 @@ class CrudeSettingsReader : public ConfigDoc::Reader //-Instance Functions------------------------------------------------------------------------------------------------- private: CrudeSettings* targetCrudeSettings() const; - Fe::DocHandlingError readTargetDoc() override; + Lr::DocHandlingError readTargetDoc() override; void initializeGenericSubSetting(const QString& key, const QString& value); }; diff --git a/app/src/frontend/attractmode/am-settings-items.cpp b/app/src/launcher/attractmode/am-settings-items.cpp similarity index 100% rename from app/src/frontend/attractmode/am-settings-items.cpp rename to app/src/launcher/attractmode/am-settings-items.cpp diff --git a/app/src/frontend/attractmode/am-settings-items.h b/app/src/launcher/attractmode/am-settings-items.h similarity index 96% rename from app/src/frontend/attractmode/am-settings-items.h rename to app/src/launcher/attractmode/am-settings-items.h index 106d046..f1eff33 100644 --- a/app/src/frontend/attractmode/am-settings-items.h +++ b/app/src/launcher/attractmode/am-settings-items.h @@ -2,14 +2,14 @@ #define AM_SETTINGS_ITEMS_H // Project Includes -#include "frontend/fe-items.h" +#include "launcher/lr-items.h" using namespace Qt::Literals::StringLiterals; namespace Am { -class SettingsItem : public Fe::Item +class SettingsItem : public Lr::Item { //-Instance Variables----------------------------------------------------------------------------------------------- private: @@ -50,7 +50,7 @@ class DisplayGlobalFilter : public SettingsItem QStringList exceptions() const; }; -class DisplayGlobalFilter::Builder : public Fe::Item::Builder +class DisplayGlobalFilter::Builder : public Lr::Item::Builder { //-Constructor------------------------------------------------------------------------------------------------- public: @@ -129,7 +129,7 @@ class DisplayFilter : public SettingsItem int listLimit() const; }; -class DisplayFilter::Builder : public Fe::Item::Builder +class DisplayFilter::Builder : public Lr::Item::Builder { //-Constructor------------------------------------------------------------------------------------------------- public: @@ -183,7 +183,7 @@ class Display : public SettingsItem const QList& filters() const; }; -class Display::Builder : public Fe::Item::Builder +class Display::Builder : public Lr::Item::Builder { //-Constructor------------------------------------------------------------------------------------------------- public: @@ -243,7 +243,7 @@ class OtherSetting : public SettingsItem QList contents() const; }; -class OtherSetting::Builder : public Fe::Item::Builder +class OtherSetting::Builder : public Lr::Item::Builder { //-Constructor------------------------------------------------------------------------------------------------- public: diff --git a/app/src/frontend/launchbox/lb-data.cpp b/app/src/launcher/launchbox/lb-data.cpp similarity index 94% rename from app/src/frontend/launchbox/lb-data.cpp rename to app/src/launcher/launchbox/lb-data.cpp index e065744..980fa30 100644 --- a/app/src/frontend/launchbox/lb-data.cpp +++ b/app/src/launcher/launchbox/lb-data.cpp @@ -5,7 +5,7 @@ #include // Project Includes -#include "lb-install.h" +#include "launcher/launchbox/lb-install.h" namespace Xml { @@ -126,14 +126,14 @@ namespace Lb //-Constructor-------------------------------------------------------------------------------------------------------- //Public: -PlatformDoc::PlatformDoc(Install* const parent, const QString& xmlPath, QString docName, const Fe::UpdateOptions& updateOptions, +PlatformDoc::PlatformDoc(Install* const parent, const QString& xmlPath, QString docName, const Import::UpdateOptions& updateOptions, const DocKey&) : - Fe::BasicPlatformDoc(parent, xmlPath, docName, updateOptions) + Lr::BasicPlatformDoc(parent, xmlPath, docName, updateOptions) {} //-Instance Functions-------------------------------------------------------------------------------------------------- //Private: -std::shared_ptr PlatformDoc::prepareGame(const Fp::Game& game, const Fe::ImageSources& images) +std::shared_ptr PlatformDoc::prepareGame(const Fp::Game& game, const Lr::ImageSources& images) { Q_UNUSED(images); // LaunchBox doesn't store image info in its platform doc directly @@ -155,7 +155,7 @@ std::shared_ptr PlatformDoc::prepareGame(const Fp::Game& game, const F return lbGame; } -std::shared_ptr PlatformDoc::prepareAddApp(const Fp::AddApp& addApp) +std::shared_ptr PlatformDoc::prepareAddApp(const Fp::AddApp& addApp) { // Convert to LaunchBox add app const QString& clifpPath = static_cast(parent())->mImportDetails->clifpPath; @@ -174,7 +174,7 @@ void PlatformDoc::addCustomField(std::shared_ptr customField) //Public: bool PlatformDoc::isEmpty() const { - return mCustomFieldsFinal.isEmpty() && mCustomFieldsExisting.isEmpty() && Fe::BasicPlatformDoc::isEmpty(); + return mCustomFieldsFinal.isEmpty() && mCustomFieldsExisting.isEmpty() && Lr::BasicPlatformDoc::isEmpty(); } void PlatformDoc::finalize() @@ -192,7 +192,7 @@ void PlatformDoc::finalize() ++i; } - Fe::BasicPlatformDoc::finalize(); + Lr::BasicPlatformDoc::finalize(); } //=============================================================================================================== @@ -202,14 +202,14 @@ void PlatformDoc::finalize() //-Constructor-------------------------------------------------------------------------------------------------------- //Public: PlatformDoc::Reader::Reader(PlatformDoc* targetDoc) : - Fe::DataDoc::Reader(targetDoc), - Fe::BasicPlatformDoc::Reader(targetDoc), - Fe::XmlDocReader(targetDoc, Xml::ROOT_ELEMENT) + Lr::DataDoc::Reader(targetDoc), + Lr::BasicPlatformDoc::Reader(targetDoc), + Lr::XmlDocReader(targetDoc, Xml::ROOT_ELEMENT) {} //-Instance Functions------------------------------------------------------------------------------------------------- //Private: -Fe::DocHandlingError PlatformDoc::Reader::readTargetDoc() +Lr::DocHandlingError PlatformDoc::Reader::readTargetDoc() { while(mStreamReader.readNextStartElement()) { @@ -346,9 +346,9 @@ void PlatformDoc::Reader::parseCustomField() //-Constructor-------------------------------------------------------------------------------------------------------- //Public: PlatformDoc::Writer::Writer(PlatformDoc* sourceDoc) : - Fe::DataDoc::Writer(sourceDoc), - Fe::BasicPlatformDoc::Writer(sourceDoc), - Fe::XmlDocWriter(sourceDoc, Xml::ROOT_ELEMENT) + Lr::DataDoc::Writer(sourceDoc), + Lr::BasicPlatformDoc::Writer(sourceDoc), + Lr::XmlDocWriter(sourceDoc, Xml::ROOT_ELEMENT) {} //-Instance Functions------------------------------------------------------------------------------------------------- @@ -356,14 +356,14 @@ PlatformDoc::Writer::Writer(PlatformDoc* sourceDoc) : bool PlatformDoc::Writer::writeSourceDoc() { // Write all games - for(const std::shared_ptr& game : static_cast(mSourceDocument)->finalGames()) + for(const std::shared_ptr& game : static_cast(mSourceDocument)->finalGames()) { if(!writeGame(*std::static_pointer_cast(game))) return false; } // Write all additional apps - for(const std::shared_ptr& addApp : static_cast(mSourceDocument)->finalAddApps()) + for(const std::shared_ptr& addApp : static_cast(mSourceDocument)->finalAddApps()) { if(!writeAddApp(*std::static_pointer_cast(addApp))) return false; @@ -475,15 +475,15 @@ bool PlatformDoc::Writer::writeCustomField(const CustomField& customField) //-Constructor-------------------------------------------------------------------------------------------------------- //Public: -PlaylistDoc::PlaylistDoc(Install* const parent, const QString& xmlPath, QString docName, const Fe::UpdateOptions& updateOptions, +PlaylistDoc::PlaylistDoc(Install* const parent, const QString& xmlPath, QString docName, const Import::UpdateOptions& updateOptions, const DocKey&) : - Fe::BasicPlaylistDoc(parent, xmlPath, docName, updateOptions), + Lr::BasicPlaylistDoc(parent, xmlPath, docName, updateOptions), mLaunchBoxDatabaseIdTracker(&parent->mLbDatabaseIdTracker) {} //-Instance Functions-------------------------------------------------------------------------------------------------- //Private: -std::shared_ptr PlaylistDoc::preparePlaylistHeader(const Fp::Playlist& playlist) +std::shared_ptr PlaylistDoc::preparePlaylistHeader(const Fp::Playlist& playlist) { // Convert to LaunchBox playlist header std::shared_ptr lbPlaylist = std::make_shared(playlist); @@ -492,7 +492,7 @@ std::shared_ptr PlaylistDoc::preparePlaylistHeader(const Fp: return lbPlaylist; } -std::shared_ptr PlaylistDoc::preparePlaylistGame(const Fp::PlaylistGame& game) +std::shared_ptr PlaylistDoc::preparePlaylistGame(const Fp::PlaylistGame& game) { // Convert to LaunchBox playlist game std::shared_ptr lbPlaylistGame = std::make_shared(game, static_cast(parent())->mPlaylistGameDetailsCache); @@ -502,7 +502,7 @@ std::shared_ptr PlaylistDoc::preparePlaylistGame(const Fp::Pla if(mPlaylistGamesExisting.contains(key)) { // Move LB playlist ID if applicable - if(mUpdateOptions.importMode == Fe::ImportMode::NewAndExisting) + if(mUpdateOptions.importMode == Import::UpdateMode::NewAndExisting) lbPlaylistGame->setLBDatabaseId(std::static_pointer_cast(mPlaylistGamesExisting[key])->lbDatabaseId()); } else @@ -522,14 +522,14 @@ std::shared_ptr PlaylistDoc::preparePlaylistGame(const Fp::Pla //-Constructor-------------------------------------------------------------------------------------------------------- //Public: PlaylistDoc::Reader::Reader(PlaylistDoc* targetDoc) : - Fe::DataDoc::Reader(targetDoc), - Fe::BasicPlaylistDoc::Reader(targetDoc), - Fe::XmlDocReader(targetDoc, Xml::ROOT_ELEMENT) + Lr::DataDoc::Reader(targetDoc), + Lr::BasicPlaylistDoc::Reader(targetDoc), + Lr::XmlDocReader(targetDoc, Xml::ROOT_ELEMENT) {} //-Instance Functions------------------------------------------------------------------------------------------------- //Private: -Fe::DocHandlingError PlaylistDoc::Reader::readTargetDoc() +Lr::DocHandlingError PlaylistDoc::Reader::readTargetDoc() { while(mStreamReader.readNextStartElement()) { @@ -616,9 +616,9 @@ void PlaylistDoc::Reader::parsePlaylistGame() //-Constructor-------------------------------------------------------------------------------------------------------- //Public: PlaylistDoc::Writer::Writer(PlaylistDoc* sourceDoc) : - Fe::DataDoc::Writer(sourceDoc), - Fe::BasicPlaylistDoc::Writer(sourceDoc), - Fe::XmlDocWriter(sourceDoc, Xml::ROOT_ELEMENT) + Lr::DataDoc::Writer(sourceDoc), + Lr::BasicPlaylistDoc::Writer(sourceDoc), + Lr::XmlDocWriter(sourceDoc, Xml::ROOT_ELEMENT) {} //-Instance Functions------------------------------------------------------------------------------------------------- @@ -626,12 +626,12 @@ PlaylistDoc::Writer::Writer(PlaylistDoc* sourceDoc) : bool PlaylistDoc::Writer::writeSourceDoc() { // Write playlist header - std::shared_ptr playlistHeader = static_cast(mSourceDocument)->playlistHeader(); + std::shared_ptr playlistHeader = static_cast(mSourceDocument)->playlistHeader(); if(!writePlaylistHeader(*std::static_pointer_cast(playlistHeader))) return false; // Write all playlist games - for(const std::shared_ptr& playlistGame : static_cast(mSourceDocument)->finalPlaylistGames()) + for(const std::shared_ptr& playlistGame : static_cast(mSourceDocument)->finalPlaylistGames()) { if(!writePlaylistGame(*std::static_pointer_cast(playlistGame))) return false; @@ -690,14 +690,14 @@ bool PlaylistDoc::Writer::writePlaylistGame(const PlaylistGame& playlistGame) //-Constructor-------------------------------------------------------------------------------------------------------- //Public: -PlatformsConfigDoc::PlatformsConfigDoc(Install* const parent, const QString& xmlPath, const Fe::UpdateOptions& updateOptions, +PlatformsConfigDoc::PlatformsConfigDoc(Install* const parent, const QString& xmlPath, const Import::UpdateOptions& updateOptions, const DocKey&) : - Fe::UpdateableDoc(parent, xmlPath, STD_NAME, updateOptions) + Lr::UpdateableDoc(parent, xmlPath, STD_NAME, updateOptions) {} //-Instance Functions-------------------------------------------------------------------------------------------------- //Private: -Fe::DataDoc::Type PlatformsConfigDoc::type() const { return Fe::DataDoc::Type::Config; } +Lr::DataDoc::Type PlatformsConfigDoc::type() const { return Lr::DataDoc::Type::Config; } //Public: bool PlatformsConfigDoc::isEmpty() const @@ -758,7 +758,7 @@ void PlatformsConfigDoc::finalize() finalizeUpdateableItems(mPlatformCategoriesExisting, mPlatformCategoriesFinal); // Finalize base - Fe::UpdateableDoc::finalize(); + Lr::UpdateableDoc::finalize(); } //=============================================================================================================== @@ -768,13 +768,13 @@ void PlatformsConfigDoc::finalize() //-Constructor-------------------------------------------------------------------------------------------------------- //Public: PlatformsConfigDoc::Reader::Reader(PlatformsConfigDoc* targetDoc) : - Fe::DataDoc::Reader(targetDoc), - Fe::XmlDocReader(targetDoc, Xml::ROOT_ELEMENT) + Lr::DataDoc::Reader(targetDoc), + Lr::XmlDocReader(targetDoc, Xml::ROOT_ELEMENT) {} //-Instance Functions------------------------------------------------------------------------------------------------- //Private: -Fe::DocHandlingError PlatformsConfigDoc::Reader::readTargetDoc() +Lr::DocHandlingError PlatformsConfigDoc::Reader::readTargetDoc() { while(mStreamReader.readNextStartElement()) { @@ -782,7 +782,7 @@ Fe::DocHandlingError PlatformsConfigDoc::Reader::readTargetDoc() parsePlatform(); else if(mStreamReader.name() == Xml::Element_PlatformFolder::NAME) { - if(Fe::DocHandlingError dhe = parsePlatformFolder(); dhe.isValid()) + if(Lr::DocHandlingError dhe = parsePlatformFolder(); dhe.isValid()) return dhe; } else if (mStreamReader.name() == Xml::Element_PlatformCategory::NAME) @@ -816,7 +816,7 @@ void PlatformsConfigDoc::Reader::parsePlatform() static_cast(mTargetDocument)->mPlatformsExisting[existingPlatform.name()] = existingPlatform; } -Fe::DocHandlingError PlatformsConfigDoc::Reader::parsePlatformFolder() +Lr::DocHandlingError PlatformsConfigDoc::Reader::parsePlatformFolder() { // Platform Folder to Build PlatformFolder::Builder pfb; @@ -831,14 +831,14 @@ Fe::DocHandlingError PlatformsConfigDoc::Reader::parsePlatformFolder() else if(mStreamReader.name() == Xml::Element_PlatformFolder::ELEMENT_PLATFORM) pfb.wPlatform(mStreamReader.readElementText()); else - return Fe::DocHandlingError(*mTargetDocument, Fe::DocHandlingError::DocInvalidType); + return Lr::DocHandlingError(*mTargetDocument, Lr::DocHandlingError::DocInvalidType); } // Build PlatformFolder and add to document PlatformFolder existingPlatformFolder = pfb.build(); static_cast(mTargetDocument)->mPlatformFoldersExisting[existingPlatformFolder.identifier()] = existingPlatformFolder; - return Fe::DocHandlingError(); + return Lr::DocHandlingError(); } void PlatformsConfigDoc::Reader::parsePlatformCategory() @@ -871,8 +871,8 @@ void PlatformsConfigDoc::Reader::parsePlatformCategory() //-Constructor-------------------------------------------------------------------------------------------------------- //Public: PlatformsConfigDoc::Writer::Writer(PlatformsConfigDoc* sourceDoc) : - Fe::DataDoc::Writer(sourceDoc), - Fe::XmlDocWriter(sourceDoc, Xml::ROOT_ELEMENT) + Lr::DataDoc::Writer(sourceDoc), + Lr::XmlDocWriter(sourceDoc, Xml::ROOT_ELEMENT) {} //-Instance Functions------------------------------------------------------------------------------------------------- @@ -966,12 +966,12 @@ bool PlatformsConfigDoc::Writer::writePlatformCategory(const PlatformCategory& p //-Constructor-------------------------------------------------------------------------------------------------------- //Public: ParentsDoc::ParentsDoc(Install* const parent, const QString& xmlPath, const DocKey&) : - Fe::DataDoc(parent, xmlPath, STD_NAME) + Lr::DataDoc(parent, xmlPath, STD_NAME) {} //-Instance Functions-------------------------------------------------------------------------------------------------- //Private: -Fe::DataDoc::Type ParentsDoc::type() const { return Fe::DataDoc::Type::Config; } +Lr::DataDoc::Type ParentsDoc::type() const { return Lr::DataDoc::Type::Config; } bool ParentsDoc::removeIfPresent(qsizetype idx) { @@ -1042,13 +1042,13 @@ void ParentsDoc::addParent(const Parent& parent) { mParents.append(parent); } //-Constructor-------------------------------------------------------------------------------------------------------- //Public: ParentsDoc::Reader::Reader(ParentsDoc* targetDoc) : - Fe::DataDoc::Reader(targetDoc), - Fe::XmlDocReader(targetDoc, Xml::ROOT_ELEMENT) + Lr::DataDoc::Reader(targetDoc), + Lr::XmlDocReader(targetDoc, Xml::ROOT_ELEMENT) {} //-Instance Functions------------------------------------------------------------------------------------------------- //Private: -Fe::DocHandlingError ParentsDoc::Reader::readTargetDoc() +Lr::DocHandlingError ParentsDoc::Reader::readTargetDoc() { while(mStreamReader.readNextStartElement()) { @@ -1094,8 +1094,8 @@ void ParentsDoc::Reader::parseParent() //-Constructor-------------------------------------------------------------------------------------------------------- //Public: ParentsDoc::Writer::Writer(ParentsDoc* sourceDoc) : - Fe::DataDoc::Writer(sourceDoc), - Fe::XmlDocWriter(sourceDoc, Xml::ROOT_ELEMENT) + Lr::DataDoc::Writer(sourceDoc), + Lr::XmlDocWriter(sourceDoc, Xml::ROOT_ELEMENT) {} //-Instance Functions------------------------------------------------------------------------------------------------- diff --git a/app/src/frontend/launchbox/lb-data.h b/app/src/launcher/launchbox/lb-data.h similarity index 85% rename from app/src/frontend/launchbox/lb-data.h rename to app/src/launcher/launchbox/lb-data.h index 835073b..652cfcb 100644 --- a/app/src/frontend/launchbox/lb-data.h +++ b/app/src/launcher/launchbox/lb-data.h @@ -1,5 +1,5 @@ -#ifndef LAUNCHBOX_XML_H -#define LAUNCHBOX_XML_H +#ifndef LAUNCHBOX_DATA_H +#define LAUNCHBOX_DATA_H #pragma warning( disable : 4250 ) @@ -12,8 +12,8 @@ #include // Project Includes -#include "lb-items.h" -#include "frontend/fe-data.h" +#include "launcher/lr-data.h" +#include "launcher/launchbox/lb-items.h" // Reminder for virtual inheritance constructor mechanics if needed, // since some classes here use multiple virtual inheritance: @@ -32,7 +32,7 @@ class DocKey DocKey(const DocKey&) = default; }; -class PlatformDoc : public Fe::BasicPlatformDoc +class PlatformDoc : public Lr::BasicPlatformDoc { //-Inner Classes---------------------------------------------------------------------------------------------------- public: @@ -46,13 +46,13 @@ class PlatformDoc : public Fe::BasicPlatformDoc //-Constructor-------------------------------------------------------------------------------------------------------- public: - explicit PlatformDoc(Install* const parent, const QString& xmlPath, QString docName, const Fe::UpdateOptions& updateOptions, + explicit PlatformDoc(Install* const parent, const QString& xmlPath, QString docName, const Import::UpdateOptions& updateOptions, const DocKey&); //-Instance Functions-------------------------------------------------------------------------------------------------- private: - std::shared_ptr prepareGame(const Fp::Game& game, const Fe::ImageSources& images) override; - std::shared_ptr prepareAddApp(const Fp::AddApp& addApp) override; + std::shared_ptr prepareGame(const Fp::Game& game, const Lr::ImageSources& images) override; + std::shared_ptr prepareAddApp(const Fp::AddApp& addApp) override; void addCustomField(std::shared_ptr customField); @@ -62,7 +62,7 @@ class PlatformDoc : public Fe::BasicPlatformDoc void finalize() override; }; -class PlatformDoc::Reader : public Fe::BasicPlatformDoc::Reader, public Fe::XmlDocReader +class PlatformDoc::Reader : public Lr::BasicPlatformDoc::Reader, public Lr::XmlDocReader { //-Constructor-------------------------------------------------------------------------------------------------------- public: @@ -70,13 +70,13 @@ class PlatformDoc::Reader : public Fe::BasicPlatformDoc::Reader, public Fe::XmlD //-Instance Functions------------------------------------------------------------------------------------------------- private: - Fe::DocHandlingError readTargetDoc() override; + Lr::DocHandlingError readTargetDoc() override; void parseGame(); void parseAddApp(); void parseCustomField(); }; -class PlatformDoc::Writer : public Fe::BasicPlatformDoc::Writer, public Fe::XmlDocWriter +class PlatformDoc::Writer : public Lr::BasicPlatformDoc::Writer, public Lr::XmlDocWriter { //-Constructor-------------------------------------------------------------------------------------------------------- public: @@ -90,7 +90,7 @@ class PlatformDoc::Writer : public Fe::BasicPlatformDoc::Writer, public Fe::XmlD bool writeCustomField(const CustomField& customField); }; -class PlaylistDoc : public Fe::BasicPlaylistDoc +class PlaylistDoc : public Lr::BasicPlaylistDoc { //-Inner Classes---------------------------------------------------------------------------------------------------- public: @@ -103,16 +103,16 @@ class PlaylistDoc : public Fe::BasicPlaylistDoc //-Constructor-------------------------------------------------------------------------------------------------------- public: - explicit PlaylistDoc(Install* const parent, const QString& xmlPath, QString docName, const Fe::UpdateOptions& updateOptions, + explicit PlaylistDoc(Install* const parent, const QString& xmlPath, QString docName, const Import::UpdateOptions& updateOptions, const DocKey&); //-Instance Functions-------------------------------------------------------------------------------------------------- private: - std::shared_ptr preparePlaylistHeader(const Fp::Playlist& playlist) override; - std::shared_ptr preparePlaylistGame(const Fp::PlaylistGame& game) override; + std::shared_ptr preparePlaylistHeader(const Fp::Playlist& playlist) override; + std::shared_ptr preparePlaylistGame(const Fp::PlaylistGame& game) override; }; -class PlaylistDoc::Reader : public Fe::BasicPlaylistDoc::Reader, public Fe::XmlDocReader +class PlaylistDoc::Reader : public Lr::BasicPlaylistDoc::Reader, public Lr::XmlDocReader { //-Constructor-------------------------------------------------------------------------------------------------------- public: @@ -120,12 +120,12 @@ class PlaylistDoc::Reader : public Fe::BasicPlaylistDoc::Reader, public Fe::XmlD //-Instance Functions------------------------------------------------------------------------------------------------- private: - Fe::DocHandlingError readTargetDoc() override; + Lr::DocHandlingError readTargetDoc() override; void parsePlaylistHeader(); void parsePlaylistGame(); }; -class PlaylistDoc::Writer : public Fe::BasicPlaylistDoc::Writer, Fe::XmlDocWriter +class PlaylistDoc::Writer : public Lr::BasicPlaylistDoc::Writer, Lr::XmlDocWriter { //-Constructor-------------------------------------------------------------------------------------------------------- public: @@ -138,7 +138,7 @@ class PlaylistDoc::Writer : public Fe::BasicPlaylistDoc::Writer, Fe::XmlDocWrite bool writePlaylistGame(const PlaylistGame& playlistGame); }; -class PlatformsConfigDoc : public Fe::UpdateableDoc +class PlatformsConfigDoc : public Lr::UpdateableDoc { //-Inner Classes---------------------------------------------------------------------------------------------------- public: @@ -160,7 +160,7 @@ class PlatformsConfigDoc : public Fe::UpdateableDoc //-Constructor-------------------------------------------------------------------------------------------------------- public: - explicit PlatformsConfigDoc(Install* const parent, const QString& xmlPath, const Fe::UpdateOptions& updateOptions, + explicit PlatformsConfigDoc(Install* const parent, const QString& xmlPath, const Import::UpdateOptions& updateOptions, const DocKey&); //-Instance Functions-------------------------------------------------------------------------------------------------- @@ -186,7 +186,7 @@ class PlatformsConfigDoc : public Fe::UpdateableDoc void finalize() override; }; -class PlatformsConfigDoc::Reader : public Fe::XmlDocReader +class PlatformsConfigDoc::Reader : public Lr::XmlDocReader { //-Constructor-------------------------------------------------------------------------------------------------------- public: @@ -194,13 +194,13 @@ class PlatformsConfigDoc::Reader : public Fe::XmlDocReader //-Instance Functions------------------------------------------------------------------------------------------------- private: - Fe::DocHandlingError readTargetDoc() override; + Lr::DocHandlingError readTargetDoc() override; void parsePlatform(); - Fe::DocHandlingError parsePlatformFolder(); + Lr::DocHandlingError parsePlatformFolder(); void parsePlatformCategory(); }; -class PlatformsConfigDoc::Writer : public Fe::XmlDocWriter +class PlatformsConfigDoc::Writer : public Lr::XmlDocWriter { //-Constructor-------------------------------------------------------------------------------------------------------- public: @@ -214,7 +214,7 @@ class PlatformsConfigDoc::Writer : public Fe::XmlDocWriter bool writePlatformCategory(const PlatformCategory& platformCategory); }; -class ParentsDoc : public Fe::DataDoc +class ParentsDoc : public Lr::DataDoc { //-Inner Classes---------------------------------------------------------------------------------------------------- public: @@ -263,7 +263,7 @@ class ParentsDoc : public Fe::DataDoc void addParent(const Parent& parent); }; -class ParentsDoc::Reader : public Fe::XmlDocReader +class ParentsDoc::Reader : public Lr::XmlDocReader { //-Constructor-------------------------------------------------------------------------------------------------------- public: @@ -271,11 +271,11 @@ class ParentsDoc::Reader : public Fe::XmlDocReader //-Instance Functions------------------------------------------------------------------------------------------------- private: - Fe::DocHandlingError readTargetDoc() override; + Lr::DocHandlingError readTargetDoc() override; void parseParent(); }; -class ParentsDoc::Writer : public Fe::XmlDocWriter +class ParentsDoc::Writer : public Lr::XmlDocWriter { //-Constructor-------------------------------------------------------------------------------------------------------- public: @@ -288,4 +288,4 @@ class ParentsDoc::Writer : public Fe::XmlDocWriter }; } -#endif // LAUNCHBOX_XML_H +#endif // LAUNCHBOX_DATA_H diff --git a/app/src/frontend/launchbox/lb-install.cpp b/app/src/launcher/launchbox/lb-install.cpp similarity index 81% rename from app/src/frontend/launchbox/lb-install.cpp rename to app/src/launcher/launchbox/lb-install.cpp index a4769d7..64b7f30 100644 --- a/app/src/frontend/launchbox/lb-install.cpp +++ b/app/src/launcher/launchbox/lb-install.cpp @@ -24,7 +24,7 @@ namespace Lb //-Constructor------------------------------------------------------------------------------------------------ //Public: Install::Install(const QString& installPath) : - Fe::Install(installPath), + Lr::Install(installPath), mDataDirectory(installPath + '/' + DATA_PATH), mPlatformsDirectory(installPath + '/' + PLATFORMS_PATH), mPlaylistsDirectory(installPath + '/' + PLAYLISTS_PATH), @@ -51,7 +51,7 @@ Install::Install(const QString& installPath) : //Private: void Install::nullify() { - Fe::Install::nullify(); + Lr::Install::nullify(); mDataDirectory = QDir(); mPlatformsDirectory = QDir(); @@ -73,7 +73,7 @@ Qx::Error Install::populateExistingDocs() return existingCheck; for(const QFileInfo& platformFile : qAsConst(existingList)) - catalogueExistingDoc(Fe::DataDoc::Identifier(Fe::DataDoc::Type::Platform, platformFile.baseName())); + catalogueExistingDoc(Lr::DataDoc::Identifier(Lr::DataDoc::Type::Platform, platformFile.baseName())); // Check for playlists existingCheck = Qx::dirContentInfoList(existingList, mPlaylistsDirectory, {u"*."_s + XML_EXT}, QDir::NoFilter, QDirIterator::Subdirectories); @@ -81,7 +81,7 @@ Qx::Error Install::populateExistingDocs() return existingCheck; for(const QFileInfo& playlistFile : qAsConst(existingList)) - catalogueExistingDoc(Fe::DataDoc::Identifier(Fe::DataDoc::Type::Playlist, playlistFile.baseName())); + catalogueExistingDoc(Lr::DataDoc::Identifier(Lr::DataDoc::Type::Playlist, playlistFile.baseName())); // Check for config docs existingCheck = Qx::dirContentInfoList(existingList, mDataDirectory, {u"*."_s + XML_EXT}); @@ -89,13 +89,13 @@ Qx::Error Install::populateExistingDocs() return existingCheck; for(const QFileInfo& configDocFile : qAsConst(existingList)) - catalogueExistingDoc(Fe::DataDoc::Identifier(Fe::DataDoc::Type::Config, configDocFile.baseName())); + catalogueExistingDoc(Lr::DataDoc::Identifier(Lr::DataDoc::Type::Config, configDocFile.baseName())); // Return success return Qx::Error(); } -QString Install::imageDestinationPath(Fp::ImageType imageType, const Fe::Game* game) const +QString Install::imageDestinationPath(Fp::ImageType imageType, const Lr::Game* game) const { return mPlatformImagesDirectory.absolutePath() + '/' + game->platform() + '/' + @@ -104,7 +104,7 @@ QString Install::imageDestinationPath(Fp::ImageType imageType, const Fe::Game* g '.' + IMAGE_EXT; } -void Install::editBulkImageReferences(const Fe::ImageSources& imageSources) +void Install::editBulkImageReferences(const Lr::ImageSources& imageSources) { // Set media folder paths const QList affectedPlatforms = modifiedPlatforms(); @@ -130,68 +130,68 @@ void Install::editBulkImageReferences(const Fe::ImageSources& imageSources) } } -QString Install::dataDocPath(Fe::DataDoc::Identifier identifier) const +QString Install::dataDocPath(Lr::DataDoc::Identifier identifier) const { QString fileName = identifier.docName() + u"."_s + XML_EXT; switch(identifier.docType()) { - case Fe::DataDoc::Type::Platform: + case Lr::DataDoc::Type::Platform: return mPlatformsDirectory.absoluteFilePath(fileName); break; - case Fe::DataDoc::Type::Playlist: + case Lr::DataDoc::Type::Playlist: return mPlaylistsDirectory.absoluteFilePath(fileName); break; - case Fe::DataDoc::Type::Config: + case Lr::DataDoc::Type::Config: return mDataDirectory.absoluteFilePath(fileName); break; default: - throw new std::invalid_argument("Function argument was not of type Fe::DataDoc::Identifier"); + throw new std::invalid_argument("Function argument was not of type Lr::DataDoc::Identifier"); } } -std::shared_ptr Install::preparePlatformDocCheckout(std::unique_ptr& platformDoc, const QString& translatedName) +std::shared_ptr Install::preparePlatformDocCheckout(std::unique_ptr& platformDoc, const QString& translatedName) { // Create doc file reference - Fe::DataDoc::Identifier docId(Fe::DataDoc::Type::Platform, translatedName); + Lr::DataDoc::Identifier docId(Lr::DataDoc::Type::Platform, translatedName); // Construct unopened document platformDoc = std::make_unique(this, dataDocPath(docId), translatedName, mImportDetails->updateOptions, DocKey{}); // Construct doc reader (need to downcast pointer since doc pointer is upcasted after construction above) - std::shared_ptr docReader = std::make_shared(static_cast(platformDoc.get())); + std::shared_ptr docReader = std::make_shared(static_cast(platformDoc.get())); // Return reader and doc return docReader; } -std::shared_ptr Install::preparePlaylistDocCheckout(std::unique_ptr& playlistDoc, const QString& translatedName) +std::shared_ptr Install::preparePlaylistDocCheckout(std::unique_ptr& playlistDoc, const QString& translatedName) { // Create doc file reference - Fe::DataDoc::Identifier docId(Fe::DataDoc::Type::Playlist, translatedName); + Lr::DataDoc::Identifier docId(Lr::DataDoc::Type::Playlist, translatedName); // Construct unopened document playlistDoc = std::make_unique(this, dataDocPath(docId), translatedName, mImportDetails->updateOptions, DocKey{}); // Construct doc reader (need to downcast pointer since doc pointer is upcasted after construction above) - std::shared_ptr docReader = std::make_shared(static_cast(playlistDoc.get())); + std::shared_ptr docReader = std::make_shared(static_cast(playlistDoc.get())); // Return reader and doc return docReader; } -std::shared_ptr Install::preparePlatformDocCommit(const std::unique_ptr& platformDoc) +std::shared_ptr Install::preparePlatformDocCommit(const std::unique_ptr& platformDoc) { // Construct doc writer - std::shared_ptr docWriter = std::make_shared(static_cast(platformDoc.get())); + std::shared_ptr docWriter = std::make_shared(static_cast(platformDoc.get())); // Return writer return docWriter; } -std::shared_ptr Install::preparePlaylistDocCommit(const std::unique_ptr& playlistDoc) +std::shared_ptr Install::preparePlaylistDocCommit(const std::unique_ptr& playlistDoc) { // Work with native type auto lbPlaylistDoc = static_cast(playlistDoc.get()); @@ -201,26 +201,26 @@ std::shared_ptr Install::preparePlaylistDocCommit(const mModifiedPlaylistIds.insert(lbPlaylistDoc->playlistHeader()->id()); // Construct doc writer - std::shared_ptr docWriter = std::make_shared(lbPlaylistDoc); + std::shared_ptr docWriter = std::make_shared(lbPlaylistDoc); // Return writer return docWriter; } -Fe::DocHandlingError Install::checkoutPlatformsConfigDoc(std::unique_ptr& returnBuffer) +Lr::DocHandlingError Install::checkoutPlatformsConfigDoc(std::unique_ptr& returnBuffer) { // Create doc file reference - Fe::DataDoc::Identifier docId(Fe::DataDoc::Type::Config, PlatformsConfigDoc::STD_NAME); + Lr::DataDoc::Identifier docId(Lr::DataDoc::Type::Config, PlatformsConfigDoc::STD_NAME); // Construct unopened document - Fe::UpdateOptions uo{.importMode = Fe::ImportMode::NewAndExisting, .removeObsolete = false}; + Import::UpdateOptions uo{.importMode = Import::UpdateMode::NewAndExisting, .removeObsolete = false}; returnBuffer = std::make_unique(this, dataDocPath(docId), uo, DocKey{}); // Construct doc reader std::shared_ptr docReader = std::make_shared(returnBuffer.get()); // Open document - Fe::DocHandlingError readErrorStatus = checkoutDataDocument(returnBuffer.get(), docReader); + Lr::DocHandlingError readErrorStatus = checkoutDataDocument(returnBuffer.get(), docReader); // Set return null on failure if(readErrorStatus.isValid()) @@ -230,7 +230,7 @@ Fe::DocHandlingError Install::checkoutPlatformsConfigDoc(std::unique_ptr document) +Lr::DocHandlingError Install::commitPlatformsConfigDoc(std::unique_ptr document) { assert(document->parent() == this); @@ -238,7 +238,7 @@ Fe::DocHandlingError Install::commitPlatformsConfigDoc(std::unique_ptr docWriter = std::make_shared(document.get()); // Write - Fe::DocHandlingError writeErrorStatus = commitDataDocument(document.get(), docWriter); + Lr::DocHandlingError writeErrorStatus = commitDataDocument(document.get(), docWriter); // Ensure document is cleared document.reset(); @@ -247,10 +247,10 @@ Fe::DocHandlingError Install::commitPlatformsConfigDoc(std::unique_ptr& returnBuffer) +Lr::DocHandlingError Install::checkoutParentsDoc(std::unique_ptr& returnBuffer) { // Create doc file reference - Fe::DataDoc::Identifier docId(Fe::DataDoc::Type::Config, ParentsDoc::STD_NAME); + Lr::DataDoc::Identifier docId(Lr::DataDoc::Type::Config, ParentsDoc::STD_NAME); // Construct unopened document returnBuffer = std::make_unique(this, dataDocPath(docId), DocKey{}); @@ -259,7 +259,7 @@ Fe::DocHandlingError Install::checkoutParentsDoc(std::unique_ptr& re std::shared_ptr docReader = std::make_shared(returnBuffer.get()); // Open document - Fe::DocHandlingError readErrorStatus = checkoutDataDocument(returnBuffer.get(), docReader); + Lr::DocHandlingError readErrorStatus = checkoutDataDocument(returnBuffer.get(), docReader); // Set return null on failure if(readErrorStatus.isValid()) @@ -269,7 +269,7 @@ Fe::DocHandlingError Install::checkoutParentsDoc(std::unique_ptr& re return readErrorStatus; } -Fe::DocHandlingError Install::commitParentsDoc(std::unique_ptr document) +Lr::DocHandlingError Install::commitParentsDoc(std::unique_ptr document) { assert(document->parent() == this); @@ -277,7 +277,7 @@ Fe::DocHandlingError Install::commitParentsDoc(std::unique_ptr docum std::shared_ptr docWriter = std::make_shared(document.get()); // Write - Fe::DocHandlingError writeErrorStatus = commitDataDocument(document.get(), docWriter); + Lr::DocHandlingError writeErrorStatus = commitDataDocument(document.get(), docWriter); // Ensure document is cleared document.reset(); @@ -289,7 +289,7 @@ Fe::DocHandlingError Install::commitParentsDoc(std::unique_ptr docum //Public: void Install::softReset() { - Fe::Install::softReset(); + Lr::Install::softReset(); mLbDatabaseIdTracker = Qx::FreeIndexTracker(0, LB_DB_ID_TRACKER_MAX); mPlaylistGameDetailsCache.clear(); @@ -297,12 +297,12 @@ void Install::softReset() } QString Install::name() const { return NAME; } -QList Install::preferredImageModeOrder() const { return IMAGE_MODE_ORDER; } +QList Install::preferredImageModeOrder() const { return IMAGE_MODE_ORDER; } bool Install::isRunning() const { return Qx::processIsRunning(mExeFile.fileName()); } QString Install::versionString() const { - Qx::FileDetails exeDetails = Qx::FileDetails::readFileDetails(mExeFile.fileName()); + Qx::FileDetails exeDetails = Qx::FileDetails::readFileDetails(mExeFile.absoluteFilePath()); QString fileVersionStr = exeDetails.stringTable().fileVersion; QString productVersionStr = exeDetails.stringTable().productVersion; @@ -312,10 +312,10 @@ QString Install::versionString() const else if(!productVersionStr.isEmpty()) return productVersionStr; else - return Fe::Install::versionString(); + return Lr::Install::versionString(); } -QString Install::translateDocName(const QString& originalName, Fe::DataDoc::Type type) const +QString Install::translateDocName(const QString& originalName, Lr::DataDoc::Type type) const { Q_UNUSED(type); @@ -333,6 +333,7 @@ QString Install::translateDocName(const QString& originalName, Fe::DataDoc::Type QString translatedName = originalName; // LB matched changes (LB might replace all illegal characters with underscores, but these are is known for sure) + // TODO: Use Qx for this translatedName.replace(':','_'); translatedName.replace('#','_'); translatedName.replace('\'','_'); @@ -345,7 +346,7 @@ QString Install::translateDocName(const QString& originalName, Fe::DataDoc::Type Qx::Error Install::prePlatformsImport() { - if(Qx::Error superErr = Fe::Install::prePlatformsImport(); superErr.isValid()) + if(Qx::Error superErr = Lr::Install::prePlatformsImport(); superErr.isValid()) return superErr; // Open platforms document @@ -354,11 +355,11 @@ Qx::Error Install::prePlatformsImport() Qx::Error Install::postPlatformsImport() { - if(Qx::Error superErr = Fe::Install::postPlatformsImport(); superErr.isValid()) + if(Qx::Error superErr = Lr::Install::postPlatformsImport(); superErr.isValid()) return superErr; // Open Parents.xml - if(Fe::DocHandlingError dhe = checkoutParentsDoc(mParents); dhe.isValid()) + if(Lr::DocHandlingError dhe = checkoutParentsDoc(mParents); dhe.isValid()) return dhe; // Add PlatformCategories to Platforms.xml @@ -420,19 +421,19 @@ Qx::Error Install::postPlatformsImport() return Qx::Error(); } -Qx::Error Install::preImageProcessing(QList& workerTransfers, const Fe::ImageSources& bulkSources) +Qx::Error Install::preImageProcessing(QList& workerTransfers, const Lr::ImageSources& bulkSources) { - if(Qx::Error superErr = Fe::Install::preImageProcessing(workerTransfers, bulkSources); superErr.isValid()) + if(Qx::Error superErr = Lr::Install::preImageProcessing(workerTransfers, bulkSources); superErr.isValid()) return superErr; switch(mImportDetails->imageMode) { - case Fe::ImageMode::Link: - case Fe::ImageMode::Copy: + case Import::ImageMode::Link: + case Import::ImageMode::Copy: workerTransfers.swap(mWorkerImageJobs); editBulkImageReferences(bulkSources); break; - case Fe::ImageMode::Reference: + case Import::ImageMode::Reference: editBulkImageReferences(bulkSources); break; default: @@ -444,12 +445,12 @@ Qx::Error Install::preImageProcessing(QList& workerTransfers, const Fe Qx::Error Install::postImageProcessing() { - if(Qx::Error superErr = Fe::Install::postImageProcessing(); superErr.isValid()) + if(Qx::Error superErr = Lr::Install::postImageProcessing(); superErr.isValid()) return superErr; // Save platforms document since it's no longer needed at this point mPlatformsConfig->finalize(); - Fe::DocHandlingError saveError = commitPlatformsConfigDoc(std::move(mPlatformsConfig)); + Lr::DocHandlingError saveError = commitPlatformsConfigDoc(std::move(mPlatformsConfig)); return saveError; } @@ -475,10 +476,10 @@ Qx::Error Install::postPlaylistsImport() return commitParentsDoc(std::move(mParents)); } -void Install::processDirectGameImages(const Fe::Game* game, const Fe::ImageSources& imageSources) +void Install::processDirectGameImages(const Lr::Game* game, const Lr::ImageSources& imageSources) { - Fe::ImageMode mode = mImportDetails->imageMode; - if(mode == Fe::ImageMode::Link || mode == Fe::ImageMode::Copy) + Import::ImageMode mode = mImportDetails->imageMode; + if(mode == Import::ImageMode::Link || mode == Import::ImageMode::Copy) { if(!imageSources.logoPath().isEmpty()) { diff --git a/app/src/frontend/launchbox/lb-install.h b/app/src/launcher/launchbox/lb-install.h similarity index 74% rename from app/src/frontend/launchbox/lb-install.h rename to app/src/launcher/launchbox/lb-install.h index 48d7667..b662295 100644 --- a/app/src/frontend/launchbox/lb-install.h +++ b/app/src/launcher/launchbox/lb-install.h @@ -9,13 +9,13 @@ #include // Project Includes -#include "frontend/fe-install.h" -#include "lb-items.h" -#include "lb-data.h" +#include "launcher/lr-install.h" +#include "launcher/launchbox/lb-items.h" +#include "launcher/launchbox/lb-data.h" namespace Lb { -class Install : public Fe::Install +class Install : public Lr::Install { friend class PlatformDoc; // TODO: See about removing the need for these (CLIfp path would need public accessor here) friend class PlaylistDoc; @@ -23,7 +23,7 @@ class Install : public Fe::Install public: // Identity static inline const QString NAME = u"LaunchBox"_s; - static inline const QString ICON_PATH = u":/frontend/LaunchBox/icon.svg"_s; + static inline const QString ICON_PATH = u":/launcher/LaunchBox/icon.svg"_s; static inline const QUrl HELP_URL = QUrl(u"https://forums.launchbox-app.com/files/file/2652-obbys-flashpoint-importer-for-launchbox"_s); // Paths @@ -53,10 +53,10 @@ class Install : public Fe::Install static const quint64 LB_DB_ID_TRACKER_MAX = 100000; // Support - static inline const QList IMAGE_MODE_ORDER { - Fe::ImageMode::Link, - Fe::ImageMode::Copy, - Fe::ImageMode::Reference + static inline const QList IMAGE_MODE_ORDER { + Import::ImageMode::Link, + Import::ImageMode::Copy, + Import::ImageMode::Reference }; //-Instance Variables----------------------------------------------------------------------------------------------- @@ -97,20 +97,20 @@ class Install : public Fe::Install Qx::Error populateExistingDocs() override; // Image Processing - QString imageDestinationPath(Fp::ImageType imageType, const Fe::Game* game) const; - void editBulkImageReferences(const Fe::ImageSources& imageSources); + QString imageDestinationPath(Fp::ImageType imageType, const Lr::Game* game) const; + void editBulkImageReferences(const Lr::ImageSources& imageSources); // Doc handling - QString dataDocPath(Fe::DataDoc::Identifier identifier) const; - std::shared_ptr preparePlatformDocCheckout(std::unique_ptr& platformDoc, const QString& translatedName) override; - std::shared_ptr preparePlaylistDocCheckout(std::unique_ptr& playlistDoc, const QString& translatedName) override; - std::shared_ptr preparePlatformDocCommit(const std::unique_ptr& platformDoc) override; - std::shared_ptr preparePlaylistDocCommit(const std::unique_ptr& playlistDoc) override; + QString dataDocPath(Lr::DataDoc::Identifier identifier) const; + std::shared_ptr preparePlatformDocCheckout(std::unique_ptr& platformDoc, const QString& translatedName) override; + std::shared_ptr preparePlaylistDocCheckout(std::unique_ptr& playlistDoc, const QString& translatedName) override; + std::shared_ptr preparePlatformDocCommit(const std::unique_ptr& platformDoc) override; + std::shared_ptr preparePlaylistDocCommit(const std::unique_ptr& playlistDoc) override; - Fe::DocHandlingError checkoutPlatformsConfigDoc(std::unique_ptr& returnBuffer); - Fe::DocHandlingError commitPlatformsConfigDoc(std::unique_ptr document); - Fe::DocHandlingError checkoutParentsDoc(std::unique_ptr& returnBuffer); - Fe::DocHandlingError commitParentsDoc(std::unique_ptr document); + Lr::DocHandlingError checkoutPlatformsConfigDoc(std::unique_ptr& returnBuffer); + Lr::DocHandlingError commitPlatformsConfigDoc(std::unique_ptr document); + Lr::DocHandlingError checkoutParentsDoc(std::unique_ptr& returnBuffer); + Lr::DocHandlingError commitParentsDoc(std::unique_ptr document); public: // Install management @@ -118,26 +118,26 @@ class Install : public Fe::Install // Info QString name() const override; - QList preferredImageModeOrder() const override; + QList preferredImageModeOrder() const override; bool isRunning() const override; QString versionString() const override; - QString translateDocName(const QString& originalName, Fe::DataDoc::Type type) const override; + QString translateDocName(const QString& originalName, Lr::DataDoc::Type type) const override; // Import stage notifier hooks Qx::Error prePlatformsImport() override; Qx::Error postPlatformsImport() override; - Qx::Error preImageProcessing(QList& workerTransfers, const Fe::ImageSources& bulkSources) override; + Qx::Error preImageProcessing(QList& workerTransfers, const Lr::ImageSources& bulkSources) override; Qx::Error postImageProcessing() override; Qx::Error postPlaylistsImport() override; // Image handling - void processDirectGameImages(const Fe::Game* game, const Fe::ImageSources& imageSources) override; + void processDirectGameImages(const Lr::Game* game, const Lr::ImageSources& imageSources) override; QString platformCategoryIconPath() const override; std::optional platformIconsDirectory() const override; std::optional playlistIconsDirectory() const override; }; -REGISTER_FRONTEND(Install::NAME, Install, &Install::ICON_PATH, &Install::HELP_URL); +REGISTER_LAUNCHER(Install::NAME, Install, &Install::ICON_PATH, &Install::HELP_URL); } diff --git a/app/src/frontend/launchbox/lb-items.cpp b/app/src/launcher/launchbox/lb-items.cpp similarity index 98% rename from app/src/frontend/launchbox/lb-items.cpp rename to app/src/launcher/launchbox/lb-items.cpp index 205137c..2042a64 100644 --- a/app/src/frontend/launchbox/lb-items.cpp +++ b/app/src/launcher/launchbox/lb-items.cpp @@ -2,7 +2,7 @@ #include "lb-items.h" // Project Includes -#include "clifp.h" +#include "kernel/clifp.h" namespace Lb { @@ -15,7 +15,7 @@ namespace Lb Game::Game() {} Game::Game(const Fp::Game& flashpointGame, const QString& fullCLIFpPath) : - Fe::Game(flashpointGame.id(), flashpointGame.title(), flashpointGame.platformName()), + Lr::Game(flashpointGame.id(), flashpointGame.title(), flashpointGame.platformName()), mSeries(flashpointGame.series()), mDeveloper(flashpointGame.developer()), mPublisher(flashpointGame.publisher()), @@ -112,7 +112,7 @@ Game::Builder& Game::Builder::wReleaseType(const QString& releaseType) { mItemBl AddApp::AddApp() {} AddApp::AddApp(const Fp::AddApp& flashpointAddApp, const QString& fullCLIFpPath) : - Fe::AddApp(flashpointAddApp.id(), flashpointAddApp.name(), flashpointAddApp.parentId()), + Lr::AddApp(flashpointAddApp.id(), flashpointAddApp.name(), flashpointAddApp.parentId()), mAppPath(QDir::toNativeSeparators(fullCLIFpPath)), mCommandLine(flashpointAddApp.isPlayable() ? CLIFp::parametersFromStandard(mId) : CLIFp::parametersFromStandard(flashpointAddApp.appPath(), flashpointAddApp.launchCommand())), @@ -179,7 +179,7 @@ CustomField::Builder& CustomField::Builder::wValue(const QString& value) { mItem //Public: PlaylistHeader::PlaylistHeader() {} PlaylistHeader::PlaylistHeader(const Fp::Playlist& flashpointPlaylist) : - Fe::PlaylistHeader(flashpointPlaylist.id(), flashpointPlaylist.title()), + Lr::PlaylistHeader(flashpointPlaylist.id(), flashpointPlaylist.title()), mNestedName(flashpointPlaylist.title()), mNotes(flashpointPlaylist.description()) {} @@ -231,7 +231,7 @@ QString PlaylistGame::EntryDetails::platform() const { return mPlatform; } //-Constructor------------------------------------------------------------------------------------------------ //Public: PlaylistGame::PlaylistGame(const Fp::PlaylistGame& flashpointPlaylistGame, const QHash& playlistGameDetailsMap) : - Fe::PlaylistGame(flashpointPlaylistGame.gameId(), playlistGameDetailsMap.value(flashpointPlaylistGame.gameId()).title()), + Lr::PlaylistGame(flashpointPlaylistGame.gameId(), playlistGameDetailsMap.value(flashpointPlaylistGame.gameId()).title()), mLBDatabaseId(-1), mGameFilename(playlistGameDetailsMap.value(flashpointPlaylistGame.gameId()).filename()), mGamePlatform(playlistGameDetailsMap.value(flashpointPlaylistGame.gameId()).platform()), diff --git a/app/src/frontend/launchbox/lb-items.h b/app/src/launcher/launchbox/lb-items.h similarity index 93% rename from app/src/frontend/launchbox/lb-items.h rename to app/src/launcher/launchbox/lb-items.h index 685dd3e..cbea416 100644 --- a/app/src/frontend/launchbox/lb-items.h +++ b/app/src/launcher/launchbox/lb-items.h @@ -10,12 +10,12 @@ #include // Project Includes -#include "frontend/fe-items.h" +#include "launcher/lr-items.h" namespace Lb { -class Game : public Fe::Game +class Game : public Lr::Game { //-Inner Classes--------------------------------------------------------------------------------------------------- public: @@ -68,7 +68,7 @@ class Game : public Fe::Game QString releaseType() const; }; -class Game::Builder : public Fe::Game::Builder +class Game::Builder : public Lr::Game::Builder { //-Constructor------------------------------------------------------------------------------------------------- public: @@ -96,7 +96,7 @@ class Game::Builder : public Fe::Game::Builder Builder& wReleaseType(const QString& releaseType); }; -class AddApp : public Fe::AddApp +class AddApp : public Lr::AddApp { //-Inner Classes--------------------------------------------------------------------------------------------------- public: @@ -122,7 +122,7 @@ class AddApp : public Fe::AddApp bool isWaitForExit() const; }; -class AddApp::Builder : public Fe::AddApp::Builder +class AddApp::Builder : public Lr::AddApp::Builder { //-Constructor------------------------------------------------------------------------------------------------- public: @@ -136,7 +136,7 @@ class AddApp::Builder : public Fe::AddApp::Builder Builder& wWaitForExit(const QString& rawWaitForExit); }; -class CustomField : public Fe::Item +class CustomField : public Lr::Item { //-Inner Classes--------------------------------------------------------------------------------------------------- public: @@ -163,7 +163,7 @@ class CustomField : public Fe::Item QString value() const; }; -class CustomField::Builder : public Fe::Item::Builder +class CustomField::Builder : public Lr::Item::Builder { //-Constructor------------------------------------------------------------------------------------------------- public: @@ -177,7 +177,7 @@ class CustomField::Builder : public Fe::Item::Builder Builder& wValue(const QString& value); }; -class PlaylistHeader : public Fe::PlaylistHeader +class PlaylistHeader : public Lr::PlaylistHeader { //-Inner Classes--------------------------------------------------------------------------------------------------- public: @@ -200,7 +200,7 @@ class PlaylistHeader : public Fe::PlaylistHeader QString notes() const; }; -class PlaylistHeader::Builder : public Fe::PlaylistHeader::Builder +class PlaylistHeader::Builder : public Lr::PlaylistHeader::Builder { //-Constructor------------------------------------------------------------------------------------------------- public: @@ -213,7 +213,7 @@ class PlaylistHeader::Builder : public Fe::PlaylistHeader::Builder +class PlaylistGame::Builder : public Lr::PlaylistGame::Builder { //-Constructor------------------------------------------------------------------------------------------------- public: @@ -275,7 +275,7 @@ class PlaylistGame::Builder : public Fe::PlaylistGame::Builder Builder& wManualOrder(const QString& rawManualOrder); }; -class Platform : public Fe::Item +class Platform : public Lr::Item { //-Inner Classes--------------------------------------------------------------------------------------------------- public: @@ -296,7 +296,7 @@ class Platform : public Fe::Item // QString category() const; }; -class Platform::Builder : public Fe::Item::Builder +class Platform::Builder : public Lr::Item::Builder { //-Constructor------------------------------------------------------------------------------------------------- public: @@ -308,7 +308,7 @@ class Platform::Builder : public Fe::Item::Builder // Builder& wCategory(const QString& category); }; -class PlatformFolder : public Fe::Item +class PlatformFolder : public Lr::Item { //-Inner Classes--------------------------------------------------------------------------------------------------- public: @@ -332,7 +332,7 @@ class PlatformFolder : public Fe::Item QString identifier() const; }; -class PlatformFolder::Builder : public Fe::Item::Builder +class PlatformFolder::Builder : public Lr::Item::Builder { //-Constructor------------------------------------------------------------------------------------------------- public: @@ -345,7 +345,7 @@ class PlatformFolder::Builder : public Fe::Item::Builder Builder& wPlatform(const QString& platform); }; -class PlatformCategory : public Fe::Item +class PlatformCategory : public Lr::Item { //-Inner Classes--------------------------------------------------------------------------------------------------- public: @@ -366,7 +366,7 @@ class PlatformCategory : public Fe::Item QString nestedName() const; }; -class PlatformCategory::Builder : public Fe::Item::Builder +class PlatformCategory::Builder : public Lr::Item::Builder { //-Constructor------------------------------------------------------------------------------------------------- public: @@ -378,7 +378,7 @@ class PlatformCategory::Builder : public Fe::Item::Builder Builder& wNestedName(const QString& nestedName); }; -class Parent : public Fe::Item +class Parent : public Lr::Item { //-Inner Classes--------------------------------------------------------------------------------------------------- public: @@ -403,7 +403,7 @@ class Parent : public Fe::Item QUuid playlistId() const; }; -class Parent::Builder : public Fe::Item::Builder +class Parent::Builder : public Lr::Item::Builder { //-Constructor------------------------------------------------------------------------------------------------- public: diff --git a/app/src/frontend/fe-data.cpp b/app/src/launcher/lr-data.cpp similarity index 91% rename from app/src/frontend/fe-data.cpp rename to app/src/launcher/lr-data.cpp index 7d8377e..0f7e047 100644 --- a/app/src/frontend/fe-data.cpp +++ b/app/src/launcher/lr-data.cpp @@ -1,14 +1,14 @@ // Unit Include -#include "fe-data.h" +#include "lr-data.h" // Qx Includes #include #include // Project Includes -#include "fe-install.h" +#include "launcher/lr-install.h" -namespace Fe +namespace Lr { //=============================================================================================================== // DocHandlingError @@ -31,6 +31,7 @@ DocHandlingError::DocHandlingError(const DataDoc& doc, Type t, const QString& s) //Private: QString DocHandlingError::generatePrimaryString(const DataDoc& doc, Type t) { + // TODO: Use Qx for this QString formattedError = ERR_STRINGS[t]; formattedError.replace(M_DOC_TYPE, doc.identifier().docTypeString()); formattedError.replace(M_DOC_NAME, doc.identifier().docName()); @@ -178,7 +179,7 @@ Qx::Error Errorable::error() const { return mError; } //-Constructor----------------------------------------------------------------------------------------------------- //Protected: -UpdateableDoc::UpdateableDoc(Install* const parent, const QString& docPath, QString docName, const UpdateOptions& updateOptions) : +UpdateableDoc::UpdateableDoc(Install* const parent, const QString& docPath, QString docName, const Import::UpdateOptions& updateOptions) : DataDoc(parent, docPath, docName), mUpdateOptions(updateOptions) {} @@ -193,7 +194,7 @@ void UpdateableDoc::finalize() {} // Does nothing for base class //-Constructor-------------------------------------------------------------------------------------------------------- //Protected: -PlatformDoc::PlatformDoc(Install* const parent, const QString& docPath, QString docName, const UpdateOptions& updateOptions) : +PlatformDoc::PlatformDoc(Install* const parent, const QString& docPath, QString docName, const Import::UpdateOptions& updateOptions) : UpdateableDoc(parent, docPath, docName, updateOptions) {} @@ -225,7 +226,7 @@ PlatformDoc::Writer::Writer(DataDoc* sourceDoc) : //-Constructor-------------------------------------------------------------------------------------------------------- //Protected: -BasicPlatformDoc::BasicPlatformDoc(Install* const parent, const QString& docPath, QString docName, const UpdateOptions& updateOptions) : +BasicPlatformDoc::BasicPlatformDoc(Install* const parent, const QString& docPath, QString docName, const Import::UpdateOptions& updateOptions) : PlatformDoc(parent, docPath, docName, updateOptions) {} @@ -247,23 +248,23 @@ void BasicPlatformDoc::addSet(const Fp::Set& set, const ImageSources& images) if(!mError.isValid()) { // Prepare game - std::shared_ptr feGame = prepareGame(set.game(), images); + std::shared_ptr lrGame = prepareGame(set.game(), images); // Add game - addUpdateableItem(mGamesExisting, mGamesFinal, feGame); + addUpdateableItem(mGamesExisting, mGamesFinal, lrGame); // Handle additional apps for(const Fp::AddApp& addApp : set.addApps()) { // Prepare - std::shared_ptr feAddApp = prepareAddApp(addApp); + std::shared_ptr lrAddApp = prepareAddApp(addApp); // Add - addUpdateableItem(mAddAppsExisting, mAddAppsFinal, feAddApp); + addUpdateableItem(mAddAppsExisting, mAddAppsFinal, lrAddApp); } // Allow install to handle images if needed - parent()->processDirectGameImages(feGame.get(), images); + parent()->processDirectGameImages(lrGame.get(), images); } } @@ -329,7 +330,7 @@ BasicPlatformDoc::Writer::Writer(DataDoc* sourceDoc) : //-Constructor-------------------------------------------------------------------------------------------------------- //Public: -PlaylistDoc::PlaylistDoc(Install* const parent, const QString& docPath, QString docName, const UpdateOptions& updateOptions) : +PlaylistDoc::PlaylistDoc(Install* const parent, const QString& docPath, QString docName, const Import::UpdateOptions& updateOptions) : UpdateableDoc(parent, docPath, docName, updateOptions) {} @@ -370,7 +371,7 @@ PlaylistDoc::Writer::Writer(DataDoc* sourceDoc) : * it may be better to require a value for it in this base class' constructor so that all derivatives must provide * a default (likely null/empty) playlist header. */ -BasicPlaylistDoc::BasicPlaylistDoc(Install* const parent, const QString& docPath, QString docName, const UpdateOptions& updateOptions) : +BasicPlaylistDoc::BasicPlaylistDoc(Install* const parent, const QString& docPath, QString docName, const Import::UpdateOptions& updateOptions) : PlaylistDoc(parent, docPath, docName, updateOptions) {} @@ -392,22 +393,22 @@ void BasicPlaylistDoc::setPlaylistData(const Fp::Playlist& playlist) { if(!mError.isValid()) { - std::shared_ptr fePlaylistHeader = preparePlaylistHeader(playlist); + std::shared_ptr lrPlaylistHeader = preparePlaylistHeader(playlist); // Ensure doc already existed before transferring (null check) if(mPlaylistHeader) - fePlaylistHeader->transferOtherFields(mPlaylistHeader->otherFields()); + lrPlaylistHeader->transferOtherFields(mPlaylistHeader->otherFields()); // Set instance header to new one - mPlaylistHeader = fePlaylistHeader; + mPlaylistHeader = lrPlaylistHeader; for(const auto& plg : playlist.playlistGames()) { // Prepare playlist game - std::shared_ptr fePlaylistGame = preparePlaylistGame(plg); + std::shared_ptr lrPlaylistGame = preparePlaylistGame(plg); // Add playlist game - addUpdateableItem(mPlaylistGamesExisting, mPlaylistGamesFinal, fePlaylistGame); + addUpdateableItem(mPlaylistGamesExisting, mPlaylistGamesFinal, lrPlaylistGame); } } } @@ -462,8 +463,8 @@ BasicPlaylistDoc::Writer::Writer(DataDoc* sourceDoc) : //-Constructor-------------------------------------------------------------------------------------------------------- //Public: -XmlDocReader::XmlDocReader(Fe::DataDoc* targetDoc, const QString& root) : - Fe::DataDoc::Reader(targetDoc), +XmlDocReader::XmlDocReader(DataDoc* targetDoc, const QString& root) : + DataDoc::Reader(targetDoc), mXmlFile(targetDoc->path()), mStreamReader(&mXmlFile), mRootElement(root) @@ -471,32 +472,32 @@ XmlDocReader::XmlDocReader(Fe::DataDoc* targetDoc, const QString& root) : //-Instance Functions------------------------------------------------------------------------------------------------- //Protected: -Fe::DocHandlingError XmlDocReader::streamStatus() const +DocHandlingError XmlDocReader::streamStatus() const { if(mStreamReader.hasError()) { Qx::XmlStreamReaderError xmlError(mStreamReader); - return Fe::DocHandlingError(*mTargetDocument, Fe::DocHandlingError::DocReadFailed, xmlError.text()); + return DocHandlingError(*mTargetDocument, DocHandlingError::DocReadFailed, xmlError.text()); } - return Fe::DocHandlingError(); + return DocHandlingError(); } //Public: -Fe::DocHandlingError XmlDocReader::readInto() +DocHandlingError XmlDocReader::readInto() { // Open File if(!mXmlFile.open(QFile::ReadOnly)) - return Fe::DocHandlingError(*mTargetDocument, Fe::DocHandlingError::DocCantOpen, mXmlFile.errorString()); + return DocHandlingError(*mTargetDocument, DocHandlingError::DocCantOpen, mXmlFile.errorString()); if(!mStreamReader.readNextStartElement()) { Qx::XmlStreamReaderError xmlError(mStreamReader); - return Fe::DocHandlingError(*mTargetDocument, Fe::DocHandlingError::DocReadFailed, xmlError.text()); + return DocHandlingError(*mTargetDocument, DocHandlingError::DocReadFailed, xmlError.text()); } if(mStreamReader.name() != mRootElement) - return Fe::DocHandlingError(*mTargetDocument, Fe::DocHandlingError::NotParentDoc); + return DocHandlingError(*mTargetDocument, DocHandlingError::NotParentDoc); return readTargetDoc(); @@ -509,8 +510,8 @@ Fe::DocHandlingError XmlDocReader::readInto() //-Constructor-------------------------------------------------------------------------------------------------------- //Public: -XmlDocWriter::XmlDocWriter(Fe::DataDoc* sourceDoc, const QString& root) : - Fe::DataDoc::Writer(sourceDoc), +XmlDocWriter::XmlDocWriter(DataDoc* sourceDoc, const QString& root) : + DataDoc::Writer(sourceDoc), mXmlFile(sourceDoc->path()), mStreamWriter(&mXmlFile), mRootElement(root) @@ -532,18 +533,18 @@ void XmlDocWriter::writeOtherFields(const QHash& otherFields) writeCleanTextElement(i.key(), i.value()); } -Fe::DocHandlingError XmlDocWriter::streamStatus() const +DocHandlingError XmlDocWriter::streamStatus() const { - return mStreamWriter.hasError() ? Fe::DocHandlingError(*mSourceDocument, Fe::DocHandlingError::DocWriteFailed, mStreamWriter.device()->errorString()) : - Fe::DocHandlingError(); + return mStreamWriter.hasError() ? DocHandlingError(*mSourceDocument, DocHandlingError::DocWriteFailed, mStreamWriter.device()->errorString()) : + DocHandlingError(); } //Public: -Fe::DocHandlingError XmlDocWriter::writeOutOf() +DocHandlingError XmlDocWriter::writeOutOf() { // Open File if(!mXmlFile.open(QFile::WriteOnly | QFile::Truncate)) // Discard previous contents - return Fe::DocHandlingError(*mSourceDocument, Fe::DocHandlingError::DocCantSave, mXmlFile.errorString()); + return DocHandlingError(*mSourceDocument, DocHandlingError::DocCantSave, mXmlFile.errorString()); // Enable auto formatting mStreamWriter.setAutoFormatting(true); diff --git a/app/src/frontend/fe-data.h b/app/src/launcher/lr-data.h similarity index 93% rename from app/src/frontend/fe-data.h rename to app/src/launcher/lr-data.h index c6486cf..0bf36d2 100644 --- a/app/src/frontend/fe-data.h +++ b/app/src/launcher/lr-data.h @@ -1,5 +1,5 @@ -#ifndef FE_DATA -#define FE_DATA +#ifndef LR_DATA +#define LR_DATA // Standard Library Includes #include @@ -19,7 +19,8 @@ #include // Project Includes -#include "fe-items.h" +#include "launcher/lr-items.h" +#include "import/settings.h" /* TODO: Right now all docs that need to be constructed by an install have that install marked as their friend, * but they also are using the Passkey Idiom, a key class with a private constructor that they are also friends @@ -34,7 +35,7 @@ * having to do Passkey. */ -namespace Fe +namespace Lr { //-Concepts------------------------------------------------------------------------------------------------------ template @@ -66,18 +67,8 @@ concept updateable_basicitem_container = Qx::qassociative && basic_item @@ -333,7 +324,7 @@ T* itemPtr(std::shared_ptr item) { return item.get(); } if(existingItems.contains(key)) { // Replace if existing update is on, move existing otherwise - if(mUpdateOptions.importMode == ImportMode::NewAndExisting) + if(mUpdateOptions.importMode == Import::UpdateMode::NewAndExisting) { itemPtr(newItem)->transferOtherFields(itemPtr(existingItems[key])->otherFields()); finalItems[key] = newItem; @@ -375,7 +366,7 @@ class PlatformDoc : public UpdateableDoc, public Errorable //-Constructor-------------------------------------------------------------------------------------------------------- protected: - explicit PlatformDoc(Install* const parent, const QString& docPath, QString docName, const UpdateOptions& updateOptions); + explicit PlatformDoc(Install* const parent, const QString& docPath, QString docName, const Import::UpdateOptions& updateOptions); //-Instance Functions-------------------------------------------------------------------------------------------------- private: @@ -428,7 +419,7 @@ class BasicPlatformDoc : public PlatformDoc //-Constructor-------------------------------------------------------------------------------------------------------- protected: - explicit BasicPlatformDoc(Install* const parent, const QString& docPath, QString docName, const UpdateOptions& updateOptions); + explicit BasicPlatformDoc(Install* const parent, const QString& docPath, QString docName, const Import::UpdateOptions& updateOptions); //-Instance Functions-------------------------------------------------------------------------------------------------- protected: @@ -477,7 +468,7 @@ class PlaylistDoc : public UpdateableDoc, public Errorable //-Constructor-------------------------------------------------------------------------------------------------------- protected: - explicit PlaylistDoc(Install* const parent, const QString& docPath, QString docName, const UpdateOptions& updateOptions); + explicit PlaylistDoc(Install* const parent, const QString& docPath, QString docName, const Import::UpdateOptions& updateOptions); //-Instance Functions-------------------------------------------------------------------------------------------------- private: @@ -518,7 +509,7 @@ class BasicPlaylistDoc : public PlaylistDoc //-Constructor-------------------------------------------------------------------------------------------------------- protected: - explicit BasicPlaylistDoc(Install* const parent, const QString& docPath, QString docName, const UpdateOptions& updateOptions); + explicit BasicPlaylistDoc(Install* const parent, const QString& docPath, QString docName, const Import::UpdateOptions& updateOptions); //-Instance Functions-------------------------------------------------------------------------------------------------- protected: @@ -565,9 +556,9 @@ class BasicPlaylistDoc::Writer : public PlaylistDoc::Writer }; /* - * Not used by base implementation, but useful for multiple frontends + * Not used by base implementation, but useful for multiple launchers */ -class XmlDocReader : public virtual Fe::DataDoc::Reader +class XmlDocReader : public virtual DataDoc::Reader { //-Instance Variables-------------------------------------------------------------------------------------------------- protected: @@ -577,20 +568,20 @@ class XmlDocReader : public virtual Fe::DataDoc::Reader //-Constructor-------------------------------------------------------------------------------------------------------- public: - XmlDocReader(Fe::DataDoc* targetDoc, const QString& root); + XmlDocReader(DataDoc* targetDoc, const QString& root); //-Instance Functions------------------------------------------------------------------------------------------------- private: - virtual Fe::DocHandlingError readTargetDoc() = 0; + virtual DocHandlingError readTargetDoc() = 0; protected: - Fe::DocHandlingError streamStatus() const; + DocHandlingError streamStatus() const; public: - Fe::DocHandlingError readInto() override; + DocHandlingError readInto() override; }; -class XmlDocWriter : public virtual Fe::DataDoc::Writer +class XmlDocWriter : public virtual DataDoc::Writer { //-Instance Variables-------------------------------------------------------------------------------------------------- protected: @@ -600,18 +591,18 @@ class XmlDocWriter : public virtual Fe::DataDoc::Writer //-Constructor-------------------------------------------------------------------------------------------------------- public: - XmlDocWriter(Fe::DataDoc* sourceDoc, const QString& root); + XmlDocWriter(DataDoc* sourceDoc, const QString& root); //-Instance Functions------------------------------------------------------------------------------------------------- protected: virtual bool writeSourceDoc() = 0; void writeCleanTextElement(const QString& qualifiedName, const QString& text); void writeOtherFields(const QHash& otherFields); - Fe::DocHandlingError streamStatus() const; + DocHandlingError streamStatus() const; public: - Fe::DocHandlingError writeOutOf() override; + DocHandlingError writeOutOf() override; }; } -#endif // FE_DATA +#endif // LR_DATA diff --git a/app/src/frontend/fe-install.cpp b/app/src/launcher/lr-install.cpp similarity index 80% rename from app/src/frontend/fe-install.cpp rename to app/src/launcher/lr-install.cpp index 9a2a5dd..82c12d8 100644 --- a/app/src/frontend/fe-install.cpp +++ b/app/src/launcher/lr-install.cpp @@ -1,10 +1,10 @@ // Unit Include -#include "fe-install.h" +#include "lr-install.h" // Qt Includes #include -namespace Fe +namespace Lr { //=============================================================================================================== @@ -22,7 +22,7 @@ QMap& Install::registry() { static QMap void Install::registerInstall(const QString& name, const Entry& entry) { registry()[name] = entry; } -std::shared_ptr Install::acquireMatch(const QString& installPath) +std::unique_ptr Install::acquireMatch(const QString& installPath) { // Check all installs against path and return match if found QMap::const_iterator i; @@ -30,7 +30,7 @@ std::shared_ptr Install::acquireMatch(const QString& installPath) for(i = registry().constBegin(); i != registry().constEnd(); ++i) { Entry entry = i.value(); - std::shared_ptr possibleMatch = entry.factory->produce(installPath); + std::unique_ptr possibleMatch = entry.factory->produce(installPath); if(possibleMatch->isValid()) return possibleMatch; @@ -56,7 +56,7 @@ void Install::softReset() InstallFoundation::softReset(); } -bool Install::supportsImageMode(ImageMode imageMode) const { return preferredImageModeOrder().contains(imageMode); } +bool Install::supportsImageMode(Import::ImageMode imageMode) const { return preferredImageModeOrder().contains(imageMode); } QString Install::versionString() const { return u"Unknown Version"_s; } @@ -91,16 +91,16 @@ QString Install::translateDocName(const QString& originalName, DataDoc::Type typ return InstallFoundation::translateDocName(originalName, type); } -Fe::DocHandlingError Install::checkoutPlatformDoc(std::unique_ptr& returnBuffer, const QString& name) +DocHandlingError Install::checkoutPlatformDoc(std::unique_ptr& returnBuffer, const QString& name) { - // Translate to frontend doc name + // Translate to launcher doc name QString translatedName = translateDocName(name, DataDoc::Type::Platform); // Get initialized blank doc and reader std::shared_ptr docReader = preparePlatformDocCheckout(returnBuffer, translatedName); // Open document - Fe::DocHandlingError readErrorStatus = checkoutDataDocument(returnBuffer.get(), docReader); + DocHandlingError readErrorStatus = checkoutDataDocument(returnBuffer.get(), docReader); // Set return null on failure if(readErrorStatus.isValid()) @@ -110,16 +110,16 @@ Fe::DocHandlingError Install::checkoutPlatformDoc(std::unique_ptr& return readErrorStatus; } -Fe::DocHandlingError Install::checkoutPlaylistDoc(std::unique_ptr& returnBuffer, const QString& name) +DocHandlingError Install::checkoutPlaylistDoc(std::unique_ptr& returnBuffer, const QString& name) { - // Translate to frontend doc name + // Translate to launcher doc name QString translatedName = translateDocName(name, DataDoc::Type::Playlist); // Get initialized blank doc and reader std::shared_ptr docReader = preparePlaylistDocCheckout(returnBuffer, translatedName); // Open document - Fe::DocHandlingError readErrorStatus = checkoutDataDocument(returnBuffer.get(), docReader); + DocHandlingError readErrorStatus = checkoutDataDocument(returnBuffer.get(), docReader); // Set return null on failure if(readErrorStatus.isValid()) @@ -129,7 +129,7 @@ Fe::DocHandlingError Install::checkoutPlaylistDoc(std::unique_ptr& return readErrorStatus; } -Fe::DocHandlingError Install::commitPlatformDoc(std::unique_ptr document) +DocHandlingError Install::commitPlatformDoc(std::unique_ptr document) { // Doc should belong to this install assert(document->parent() == this); @@ -138,7 +138,7 @@ Fe::DocHandlingError Install::commitPlatformDoc(std::unique_ptr doc std::shared_ptr docWriter = preparePlatformDocCommit(document); // Write - Fe::DocHandlingError writeErrorStatus = commitDataDocument(document.get(), docWriter); + DocHandlingError writeErrorStatus = commitDataDocument(document.get(), docWriter); // Ensure document is cleared document.reset(); @@ -147,7 +147,7 @@ Fe::DocHandlingError Install::commitPlatformDoc(std::unique_ptr doc return writeErrorStatus; } -Fe::DocHandlingError Install::commitPlaylistDoc(std::unique_ptr document) +DocHandlingError Install::commitPlaylistDoc(std::unique_ptr document) { // Doc should belong to this install assert(document->parent() == this); @@ -156,7 +156,7 @@ Fe::DocHandlingError Install::commitPlaylistDoc(std::unique_ptr doc std::shared_ptr docWriter = preparePlaylistDocCommit(document); // Write - Fe::DocHandlingError writeErrorStatus = commitDataDocument(document.get(), docWriter); + DocHandlingError writeErrorStatus = commitDataDocument(document.get(), docWriter); // Ensure document is cleared document.reset(); diff --git a/app/src/frontend/fe-install.h b/app/src/launcher/lr-install.h similarity index 73% rename from app/src/frontend/fe-install.h rename to app/src/launcher/lr-install.h index 471ea9f..8e5226f 100644 --- a/app/src/frontend/fe-install.h +++ b/app/src/launcher/lr-install.h @@ -1,38 +1,38 @@ -#ifndef FE_INSTALL_H -#define FE_INSTALL_H +#ifndef LR_INSTALL_H +#define LR_INSTALL_H // Qt Includes #include // Project Includes -#include "fe-installfoundation.h" +#include "launcher/lr-installfoundation.h" //-Macros------------------------------------------------------------------------------------------------------------------- -#define REGISTER_FRONTEND(fe_name, fe_install, fe_icon_path, fe_helpUrl) \ - class fe_install##Factory : public Fe::InstallFactory \ +#define REGISTER_LAUNCHER(lr_name, lr_install, lr_icon_path, lr_helpUrl) \ + class lr_install##Factory : public Lr::InstallFactory \ { \ public: \ - fe_install##Factory() \ + lr_install##Factory() \ { \ Install::Entry entry { \ .factory = this, \ - .iconPath = fe_icon_path, \ - .helpUrl = fe_helpUrl \ + .iconPath = lr_icon_path, \ + .helpUrl = lr_helpUrl \ }; \ - Fe::Install::registerInstall(fe_name, entry); \ + Lr::Install::registerInstall(lr_name, entry); \ } \ - virtual std::shared_ptr produce(const QString& installPath) const { return std::make_shared(installPath); } \ + virtual std::unique_ptr produce(const QString& installPath) const { return std::make_unique(installPath); } \ }; \ - static fe_install##Factory _##install##Factory; + static lr_install##Factory _##install##Factory; -namespace Fe +namespace Lr { class InstallFactory { //-Instance Functions------------------------------------------------------------------------------------------------------ public: - virtual std::shared_ptr produce(const QString& installPath) const = 0; + virtual std::unique_ptr produce(const QString& installPath) const = 0; }; class Install : public InstallFoundation @@ -52,10 +52,10 @@ class Install : public InstallFoundation //-Class Functions------------------------------------------------------------------------------------------------------ public: - // NOTE: Registry put behind function call to avoid SIOF since otherwise initialization of static registry before calls to registerFrontend would not be guaranteed + // NOTE: Registry put behind function call to avoid SIOF since otherwise initialization of static registry before calls to registerLauncher would not be guaranteed static QMap& registry(); static void registerInstall(const QString& name, const Entry& entry); - static std::shared_ptr acquireMatch(const QString& installPath); + [[nodiscard]] static std::unique_ptr acquireMatch(const QString& installPath); //-Instance Functions--------------------------------------------------------------------------------------------------------- protected: @@ -75,8 +75,8 @@ class Install : public InstallFoundation // Info virtual QString name() const = 0; - virtual QList preferredImageModeOrder() const = 0; - bool supportsImageMode(ImageMode imageMode) const; + virtual QList preferredImageModeOrder() const = 0; + bool supportsImageMode(Import::ImageMode imageMode) const; virtual QString versionString() const; virtual bool isRunning() const = 0; @@ -92,14 +92,14 @@ class Install : public InstallFoundation // Doc handling virtual QString translateDocName(const QString& originalName, DataDoc::Type type) const override; - Fe::DocHandlingError checkoutPlatformDoc(std::unique_ptr& returnBuffer, const QString& name); - Fe::DocHandlingError checkoutPlaylistDoc(std::unique_ptr& returnBuffer, const QString& name); - Fe::DocHandlingError commitPlatformDoc(std::unique_ptr platformDoc); - Fe::DocHandlingError commitPlaylistDoc(std::unique_ptr playlistDoc); + DocHandlingError checkoutPlatformDoc(std::unique_ptr& returnBuffer, const QString& name); + DocHandlingError checkoutPlaylistDoc(std::unique_ptr& returnBuffer, const QString& name); + DocHandlingError commitPlatformDoc(std::unique_ptr platformDoc); + DocHandlingError commitPlaylistDoc(std::unique_ptr playlistDoc); // Image handling // NOTE: The image paths provided here can be null (i.e. images unavailable). Handle accordingly in derived. - virtual void processDirectGameImages(const Game* game, const Fe::ImageSources& imageSources) = 0; + virtual void processDirectGameImages(const Game* game, const ImageSources& imageSources) = 0; // TODO: These might need to be changed to support launchers where the platform images are tied closely to the platform documents, // but currently none do this so this works. @@ -109,4 +109,4 @@ class Install : public InstallFoundation }; } -#endif // FE_INSTALL_H +#endif // LR_INSTALL_H diff --git a/app/src/frontend/fe-installfoundation.cpp b/app/src/launcher/lr-installfoundation.cpp similarity index 90% rename from app/src/frontend/fe-installfoundation.cpp rename to app/src/launcher/lr-installfoundation.cpp index 1e6148e..f225bc4 100644 --- a/app/src/frontend/fe-installfoundation.cpp +++ b/app/src/launcher/lr-installfoundation.cpp @@ -1,7 +1,7 @@ // Unit Include -#include "fe-installfoundation.h" +#include "lr-installfoundation.h" -namespace Fe +namespace Lr { //=============================================================================================================== @@ -108,14 +108,14 @@ QString InstallFoundation::translateDocName(const QString& originalName, DataDoc void InstallFoundation::catalogueExistingDoc(DataDoc::Identifier existingDoc) { mExistingDocuments.insert(existingDoc); } -Fe::DocHandlingError InstallFoundation::checkoutDataDocument(DataDoc* docToOpen, std::shared_ptr docReader) +DocHandlingError InstallFoundation::checkoutDataDocument(DataDoc* docToOpen, std::shared_ptr docReader) { // Error report to return - Fe::DocHandlingError openReadError; // Defaults to no error + DocHandlingError openReadError; // Defaults to no error // Check if lease is already out if(mLeasedDocuments.contains(docToOpen->identifier())) - openReadError = Fe::DocHandlingError(*docToOpen, Fe::DocHandlingError::DocAlreadyOpen); + openReadError = DocHandlingError(*docToOpen, DocHandlingError::DocAlreadyOpen); else { // Read existing file if present and a reader was provided @@ -131,11 +131,13 @@ Fe::DocHandlingError InstallFoundation::checkoutDataDocument(DataDoc* docToOpen, return openReadError; } -Fe::DocHandlingError InstallFoundation::commitDataDocument(DataDoc* docToSave, std::shared_ptr docWriter) +DocHandlingError InstallFoundation::commitDataDocument(DataDoc* docToSave, std::shared_ptr docWriter) { DataDoc::Identifier id = docToSave->identifier(); + + // Check if the doc was saved previously to prevent double-backups bool wasDeleted = mDeletedDocuments.contains(id); - bool wasModified = mDeletedDocuments.contains(id); + bool wasModified = mModifiedDocuments.contains(id); bool wasUntouched = !wasDeleted && !wasModified; // Handle backup/revert prep @@ -152,16 +154,16 @@ Fe::DocHandlingError InstallFoundation::commitDataDocument(DataDoc* docToSave, s if(QFile::exists(backupPath) && QFileInfo(backupPath).isFile()) { if(!QFile::remove(backupPath)) - return Fe::DocHandlingError(*docToSave, Fe::DocHandlingError::CantRemoveBackup); + return DocHandlingError(*docToSave, DocHandlingError::CantRemoveBackup); } if(!QFile::copy(docPath, backupPath)) - return Fe::DocHandlingError(*docToSave, Fe::DocHandlingError::CantCreateBackup); + return DocHandlingError(*docToSave, DocHandlingError::CantCreateBackup); } } // Error State - Fe::DocHandlingError commitError; + DocHandlingError commitError; // Handle modification if(!docToSave->isEmpty()) @@ -188,7 +190,7 @@ Fe::DocHandlingError InstallFoundation::commitDataDocument(DataDoc* docToSave, s } QList InstallFoundation::modifiedPlatforms() const { return modifiedDataDocs(DataDoc::Type::Platform); } -QList InstallFoundation::modifiedPlaylists() const { return modifiedDataDocs(DataDoc::Type::Playlist);} +QList InstallFoundation::modifiedPlaylists() const { return modifiedDataDocs(DataDoc::Type::Playlist); } //Public: bool InstallFoundation::isValid() const { return mValid; } diff --git a/app/src/frontend/fe-installfoundation.h b/app/src/launcher/lr-installfoundation.h similarity index 87% rename from app/src/frontend/fe-installfoundation.h rename to app/src/launcher/lr-installfoundation.h index cc9e307..99c4b36 100644 --- a/app/src/frontend/fe-installfoundation.h +++ b/app/src/launcher/lr-installfoundation.h @@ -1,19 +1,17 @@ -#ifndef FE_INSTALLFOUNDATION_H -#define FE_INSTALLFOUNDATION_H +#ifndef LR_INSTALLFOUNDATION_H +#define LR_INSTALLFOUNDATION_H // Qt Includes #include // Project Includes -#include "fe-data.h" +#include "launcher/lr-data.h" +#include "import/settings.h" -namespace Fe +namespace Lr { -//-Enums---------------------------------------------------------------------------------------------------------- -enum class ImageMode {Copy, Reference, Link}; - -class QX_ERROR_TYPE(RevertError, "Fe::RevertError", 1301) +class QX_ERROR_TYPE(RevertError, "Lr::RevertError", 1301) { friend class InstallFoundation; //-Class Enums------------------------------------------------------------- @@ -67,8 +65,8 @@ class InstallFoundation public: struct ImportDetails { - UpdateOptions updateOptions; - ImageMode imageMode; + Import::UpdateOptions updateOptions; + Import::ImageMode imageMode; QString clifpPath; QList involvedPlatforms; QList involvedPlaylists; @@ -92,7 +90,7 @@ class InstallFoundation public: // Base errors // TODO: This is unused, should it be in-use somewhere? - static inline const QString ERR_UNSUPPORTED_FEATURE = u"A feature unsupported by the frontend was called upon!"_s; + static inline const QString ERR_UNSUPPORTED_FEATURE = u"A feature unsupported by the launcher was called upon!"_s; // Image Errors static inline const QString CAPTION_IMAGE_ERR = u"Error importing game image(s)"_s; @@ -147,8 +145,8 @@ class InstallFoundation virtual QString translateDocName(const QString& originalName, DataDoc::Type type) const; void catalogueExistingDoc(DataDoc::Identifier existingDoc); - Fe::DocHandlingError checkoutDataDocument(DataDoc* docToOpen, std::shared_ptr docReader); - Fe::DocHandlingError commitDataDocument(DataDoc* docToSave, std::shared_ptr docWriter); + DocHandlingError checkoutDataDocument(DataDoc* docToOpen, std::shared_ptr docReader); + DocHandlingError commitDataDocument(DataDoc* docToSave, std::shared_ptr docWriter); QList modifiedPlatforms() const; QList modifiedPlaylists() const; @@ -170,4 +168,4 @@ class InstallFoundation } -#endif // FE_INSTALLFOUNDATION_H +#endif // LR_INSTALLFOUNDATION_H diff --git a/app/src/frontend/fe-installfoundation_linux.cpp b/app/src/launcher/lr-installfoundation_linux.cpp similarity index 92% rename from app/src/frontend/fe-installfoundation_linux.cpp rename to app/src/launcher/lr-installfoundation_linux.cpp index b802867..5317c27 100644 --- a/app/src/frontend/fe-installfoundation_linux.cpp +++ b/app/src/launcher/lr-installfoundation_linux.cpp @@ -1,10 +1,10 @@ // Unit Include -#include "fe-installfoundation.h" +#include "lr-installfoundation.h" // Qt Includes #include -namespace Fe +namespace Lr { //=============================================================================================================== // InstallFoundation diff --git a/app/src/frontend/fe-installfoundation_win.cpp b/app/src/launcher/lr-installfoundation_win.cpp similarity index 97% rename from app/src/frontend/fe-installfoundation_win.cpp rename to app/src/launcher/lr-installfoundation_win.cpp index cc79457..168eb16 100644 --- a/app/src/frontend/fe-installfoundation_win.cpp +++ b/app/src/launcher/lr-installfoundation_win.cpp @@ -1,10 +1,10 @@ // Unit Include -#include "fe-installfoundation.h" +#include "lr-installfoundation.h" // Windows Includes (Specifically for changing file permissions) #include "Aclapi.h" -namespace Fe +namespace Lr { //=============================================================================================================== // InstallFoundation @@ -14,6 +14,7 @@ namespace Fe //Private: void InstallFoundation::ensureModifiable(const QString& filePath) { + Q_ASSERT(!filePath.isEmpty()); PACL pCurrentDacl = nullptr, pNewDACL = nullptr; PSECURITY_DESCRIPTOR pSecurityDescriptor = nullptr; PSID pOwnerId = nullptr; diff --git a/app/src/frontend/fe-items.cpp b/app/src/launcher/lr-items.cpp similarity index 99% rename from app/src/frontend/fe-items.cpp rename to app/src/launcher/lr-items.cpp index 646c3c3..3f546dd 100644 --- a/app/src/frontend/fe-items.cpp +++ b/app/src/launcher/lr-items.cpp @@ -1,7 +1,7 @@ // Unit Include -#include "fe-items.h" +#include "lr-items.h" -namespace Fe +namespace Lr { //=============================================================================================================== diff --git a/app/src/frontend/fe-items.h b/app/src/launcher/lr-items.h similarity index 96% rename from app/src/frontend/fe-items.h rename to app/src/launcher/lr-items.h index 8122e83..ba0d674 100644 --- a/app/src/frontend/fe-items.h +++ b/app/src/launcher/lr-items.h @@ -1,5 +1,5 @@ -#ifndef FE_ITEMS_H -#define FE_ITEMS_H +#ifndef LR_ITEMS_H +#define LR_ITEMS_H // Standard Library Includes #include @@ -10,19 +10,19 @@ using namespace Qt::Literals::StringLiterals; -namespace Fe +namespace Lr { /* - * TODO: Right now there is no Fe::Set or similar. This would be nice because it would set a standard for Platform + * TODO: Right now there is no Lr::Set or similar. This would be nice because it would set a standard for Platform * docs that store their add apps directly with their main games; however, with the current system this would make * the 'containsXXX' methods hairy and slow unless they're outright removed (which they can be, they're unused), and * the update situation is even rougher. The set couldn't really derive from Item because its not an item by itself, * and even if it was the other fields that would get transferred wouldn't actually be from the game/add apps of the set, * but just empty ones along side them. Likely an extra function would have to be added to UpdateableDoc like * "addUpdateableSet", and finalize be modified as well. Lastly there is the question of whether or not to have a fixed - * set that just contains pointers to Fe::Game and a list of pointers to Fe::AddApp, or intended for the set to - * be derived from as well. A fixed set is likely the better choice since any even remotely compatible frontend should + * set that just contains pointers to Lr::Game and a list of pointers to Lr::AddApp, or intended for the set to + * be derived from as well. A fixed set is likely the better choice since any even remotely compatible launcher should * be able to work with it */ @@ -250,4 +250,4 @@ class PlaylistGame::Builder : public BasicItem::Builder } -#endif // FE_ITEMS_H +#endif // LR_ITEMS_H diff --git a/app/src/main.cpp b/app/src/main.cpp index 5bfd2d3..c941d8d 100644 --- a/app/src/main.cpp +++ b/app/src/main.cpp @@ -1,17 +1,9 @@ -#include "ui/mainwindow.h" +#include "kernel/controller.h" #include int main(int argc, char *argv[]) { QApplication a(argc, argv); - MainWindow w; - - if(!w.initCompleted()) - { - QMessageBox::critical(nullptr, u"Cannot Start"_s, u"Initialization failed!"_s); - return 1; - } - - w.show(); + Controller c; return a.exec(); } diff --git a/app/src/ui/mainwindow.cpp b/app/src/ui/mainwindow.cpp index 723ffbb..8e405a8 100644 --- a/app/src/ui/mainwindow.cpp +++ b/app/src/ui/mainwindow.cpp @@ -1,13 +1,13 @@ -// Standard Library Includes -#include -#include +// Unit Include +#include "mainwindow.h" +#include "ui_mainwindow.h" // Qt Includes #include #include #include -#include #include +#include #include #include #include @@ -16,99 +16,114 @@ #include // Qx Includes -#include -#include #include -#include -#include +#include -// Project Includes -#include "mainwindow.h" -#include "ui_mainwindow.h" -#include "project_vars.h" -#include "clifp.h" +// Magic Enum Includes +#include -/* TODO: Consider having this tool deploy a .ini file (or the like) into the target launcher install - * (with the exact location probably being guided by the specific Install child) that saves the settings - * used for the import, so that they can be loaded again when that install is targeted by future versions - * of the tool. Would have to account for an initial import vs update (likely just leaving the update settings - * blank). Wouldn't be a huge difference but could be a nice little time saver. - */ +// Project Includes +#include "import/properties.h" +#include "launcher/lr-install.h" +#include "kernel/clifp.h" //=============================================================================================================== -// MAIN WINDOW +// MainWindow::SelectionList //=============================================================================================================== //-Constructor--------------------------------------------------------------------------------------------------- -MainWindow::MainWindow(QWidget *parent) : - QMainWindow(parent), - ui(new Ui::MainWindow), - mProgressPresenter(this) +MainWindow::SelectionList::SelectionList(QListWidget* widget) : + mWidget(widget) { - /*Register metatypes - * NOTE: Qt docs note these should be needed, as always, but since Qt6 signals/slots with these types seem to - * work fine without the following calls. - * See https://forum.qt.io/topic/136627/undocumented-automatic-metatype-registration-in-qt6 - */ - //qRegisterMetaType(); - //qRegisterMetaType(); - //qRegisterMetaType>(); + connect(mWidget, &QListWidget::itemChanged, mWidget, [this](QListWidgetItem* item){ handleCheckChange(item); }); +} - // Ensure built-in CLIFp version is valid - if(CLIFp::internalVersion().isNull()) +//-Instance Functions-------------------------------------------------------------------------------------------- +//Private: +void MainWindow::SelectionList::handleCheckChange(QListWidgetItem* item) +{ + bool checked = item->checkState() == Qt::Checked; + int selCount = mSelCount.valueBypassingBindings(); + if(checked){ ++selCount; } else { --selCount; } + mSelCount = selCount; + + // Handle existing special case + QVariant vExisting = item->data(USER_ROLE_EXISTING); + if(vExisting.isValid() && vExisting.toBool()) { - QMessageBox::critical(this, CAPTION_GENERAL_FATAL_ERROR, MSG_FATAL_NO_INTERNAL_CLIFP_VER); - mInitCompleted = false; - return; + int eSelCount = mExistSelCount.valueBypassingBindings(); + if(checked){ ++eSelCount; } else { --eSelCount; } + mExistSelCount = eSelCount; } - - // General setup - ui->setupUi(this); - QApplication::setApplicationName(PROJECT_FULL_NAME); - mHasLinkPermissions = testForLinkPermissions(); - setWindowTitle(PROJECT_FULL_NAME); - initializeEnableConditionMaps(); - initializeForms(); - initializeFrontendHelpActions(); - - // Check if Flashpoint is running - if(Qx::processIsRunning(Fp::Install::LAUNCHER_NAME)) - QMessageBox::warning(this, QApplication::applicationName(), MSG_FP_CLOSE_PROMPT); - - mInitCompleted = true; } -//-Destructor---------------------------------------------------------------------------------------------------- -MainWindow::~MainWindow() { delete ui; } - -//-Instance Functions-------------------------------------------------------------------------------------------- -//Private: -bool MainWindow::testForLinkPermissions() +//Public: +void MainWindow::SelectionList::fill(const QList& imps) { - QTemporaryDir testLinkDir; - if(testLinkDir.isValid()) + // Fill list widget + for(const auto& imp : imps) { - QFile testLinkTarget(testLinkDir.filePath(u"linktarget.tmp"_s)); + // Configure item before adding to avoid unwanted triggering of handleCheckChange() + auto* item = new QListWidgetItem(imp.name); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(Qt::Unchecked); - if(testLinkTarget.open(QIODevice::WriteOnly)) + if(imp.existing) { - testLinkTarget.close(); - std::error_code symlinkError; - std::filesystem::create_symlink(testLinkTarget.fileName().toStdString(), testLinkDir.filePath(u"testlink.tmp"_s).toStdString(), symlinkError); - - if(!symlinkError) - return true; + item->setBackground(QBrush(smExistingItemColor)); + item->setData(USER_ROLE_EXISTING, true); } + else + item->setData(USER_ROLE_EXISTING, false); + + mWidget->addItem(item); } +} + +void MainWindow::SelectionList::clear() +{ + mSelCount = 0; + mExistSelCount = 0; + mWidget->clear(); +} - // Default - return false; +int MainWindow::SelectionList::selectedCount() const { return mSelCount; } +int MainWindow::SelectionList::existingSelectedCount() const { return mExistSelCount; } + +//=============================================================================================================== +// MainWindow +//=============================================================================================================== + +//-Constructor--------------------------------------------------------------------------------------------------- +MainWindow::MainWindow(const Import::Properties& importProperties, QWidget* parent) : + QMainWindow(parent), + ui([this]{ auto ui = new Ui::MainWindow; ui->setupUi(this); return ui;}()), + mPlatformSelections(ui->listWidget_platformChoices), + mPlaylistSelections(ui->listWidget_playlistChoices), + mImportProperties(importProperties), + mImageModeMap(initializeImageModeMap()), + mPlaylistGameModeMap(initializePlaylistGameModeMap()) +{ + // Prepare tag model + mTagModel.setAutoTristate(true); + mTagModel.setSortRole(Qt::DisplayRole); + + // General setup + setWindowTitle(QApplication::applicationName()); + initializeForms(); + initializeLauncherHelpActions(); + initializeBindings(); } +//-Destructor---------------------------------------------------------------------------------------------------- +MainWindow::~MainWindow() { delete ui; } + +//-Instance Functions-------------------------------------------------------------------------------------------- +//Private: void MainWindow::initializeForms() { // Capture existing item color from label for use in platform/playlist selection lists - mExistingItemColor = ui->label_existingItemColor->palette().color(QPalette::Window); + smExistingItemColor = ui->label_existingItemColor->palette().color(QPalette::Window); // Add CLIFp version to deploy option ui->action_deployCLIFp->setText(ui->action_deployCLIFp->text() + ' ' + CLIFp::internalVersion().normalized(2).toString()); @@ -125,413 +140,194 @@ void MainWindow::initializeForms() ui->radioButton_reference->text(), ui->radioButton_link->text()); - // Setup main forms - ui->label_flashpointVersion->clear(); - ui->label_frontendVersion->clear(); - // If no link permissions, inform user - if(!mHasLinkPermissions) + if(!mImportProperties.hasLinkPermissions()) ui->radioButton_link->setText(ui->radioButton_link->text().append(REQUIRE_ELEV)); - // Perform standard widget updates - refreshEnableStates(); - refreshCheckStates(); - // NOTE: THIS IS FOR DEBUG PURPOSES //checkLaunchBoxInput("C:/Users/Player/Desktop/LBTest/LaunchBox"); //checkFlashpointInput("D:/FP/Flashpoint 8.1 Ultimate"); } -void MainWindow::initializeEnableConditionMaps() +Qx::Bimap MainWindow::initializeImageModeMap() const { - /* TODO: When Qt6 ports built-in widgets to use the C++ bindable properties system, it - * would be great to convert this approach to using those instead, though this gets - * tricky when checking for things that aren't easy to make a QProperty, such as when - * checking for if the Install pointers are assigned - */ - - // Populate hash-map of widget element enable conditions - mWidgetEnableConditionMap[ui->groupBox_importSelection] = [&](){ return mFrontendInstall && mFlashpointInstall; }; - mWidgetEnableConditionMap[ui->groupBox_playlistGameMode] = [&](){ return getSelectedPlaylists().count() > 0; }; - mWidgetEnableConditionMap[ui->groupBox_updateMode] = [&](){ return selectionsMayModify(); }; - mWidgetEnableConditionMap[ui->groupBox_imageMode] = [&](){ return mFrontendInstall && mFlashpointInstall; }; - - mWidgetEnableConditionMap[ui->radioButton_reference] = [&](){ - return mFrontendInstall && mFlashpointInstall && mFrontendInstall->supportsImageMode(Fe::ImageMode::Reference); - }; - mWidgetEnableConditionMap[ui->radioButton_link] = [&](){ - return mHasLinkPermissions && mFrontendInstall && mFlashpointInstall && mFrontendInstall->supportsImageMode(Fe::ImageMode::Link); + return{ + {Import::ImageMode::Link, ui->radioButton_link}, + {Import::ImageMode::Copy, ui->radioButton_copy}, + {Import::ImageMode::Reference, ui->radioButton_reference}, }; - mWidgetEnableConditionMap[ui->radioButton_copy] = [&](){ - return mFrontendInstall && mFlashpointInstall && mFrontendInstall->supportsImageMode(Fe::ImageMode::Copy); - }; - - mWidgetEnableConditionMap[ui->pushButton_startImport] = [&](){ return getSelectedPlatforms().count() > 0 || - (getSelectedPlaylistGameMode() == ImportWorker::ForceAll && getSelectedPlaylists().count() > 0); }; - - // Populate hash-map of action element enable conditions - mActionEnableConditionMap[ui->action_forceDownloadImages] = [&](){ return mFlashpointInstall && mFlashpointInstall->preferences().onDemandImages; }; - mActionEnableConditionMap[ui->action_editTagFilter] = [&](){ return mFrontendInstall && mFlashpointInstall; }; -} - -void MainWindow::initializeFrontendHelpActions() -{ - // Add install help link for each registered install - auto i = Fe::Install::registry().cbegin(); - auto end = Fe::Install::registry().cend(); - - for(; i != end; i++) - { - QAction* feHelpAction = new QAction(ui->menu_frontendHelp); - feHelpAction->setObjectName(MENU_FE_HELP_OBJ_NAME_TEMPLATE.arg(i.key())); - feHelpAction->setText(i.key()); - feHelpAction->setIcon(QIcon(*(i->iconPath))); - ui->menu_frontendHelp->addAction(feHelpAction); - } -} - -bool MainWindow::installMatchesTargetSeries(const Fp::Install& fpInstall) -{ - Qx::VersionNumber fpVersion = fpInstall.versionInfo()->version(); - return TARGET_FP_VERSION_PREFIX.isPrefixOf(fpVersion) || - TARGET_FP_VERSION_PREFIX.normalized() == fpVersion; // Accounts for if FP doesn't use a trailing zero for major releases } -void MainWindow::checkManualInstallInput(InstallType install) +QHash MainWindow::initializePlaylistGameModeMap() const { - QLineEdit* pathSource = install == InstallType::Frontend ? - ui->lineEdit_frontendPath : - ui->lineEdit_flashpointPath; - - QDir selectedDir = QDir::cleanPath(QDir::fromNativeSeparators(pathSource->text())); - if(!pathSource->text().isEmpty() && selectedDir.exists()) - validateInstall(selectedDir.absolutePath(), install); - else - invalidateInstall(install, false); -} - -void MainWindow::validateInstall(const QString& installPath, InstallType install) -{ - switch(install) - { - case InstallType::Frontend: - mFrontendInstall = Fe::Install::acquireMatch(installPath); - if(mFrontendInstall) - { - ui->icon_frontend_install_status->setPixmap(QPixmap(u":/ui/Valid_Install.png"_s)); - ui->label_frontendVersion->setText(mFrontendInstall->name() + ' ' + mFrontendInstall->versionString()); - } - else - invalidateInstall(install, true); - break; - - case InstallType::Flashpoint: - mFlashpointInstall = std::make_shared(installPath, true); - if(mFlashpointInstall->isValid()) - { - ui->label_flashpointVersion->setText(mFlashpointInstall->versionInfo()->fullString()); - if(installMatchesTargetSeries(*mFlashpointInstall)) - ui->icon_flashpoint_install_status->setPixmap(QPixmap(u":/ui/Valid_Install.png"_s)); - else - { - ui->icon_flashpoint_install_status->setPixmap(QPixmap(u":/ui/Mismatch_Install.png"_s)); - QMessageBox::warning(this, QApplication::applicationName(), MSG_FP_VER_NOT_TARGET); - } - } - else - invalidateInstall(install, true); - break; - } - - refreshEnableStates(); - - if(mFrontendInstall && mFlashpointInstall) - gatherInstallInfo(); + return{ + {ui->radioButton_selectedPlatformsOnly, Import::PlaylistGameMode::SelectedPlatform}, + {ui->radioButton_forceAll, Import::PlaylistGameMode::ForceAll} + }; } -void MainWindow::gatherInstallInfo() +QHash MainWindow::initializeUpdateModeMap() const { - // Get data in order but only continue if each step is successful - if(parseFrontendData()) - { - // Show selection options - populateImportSelectionBoxes(); - - // Generate tab selection model - generateTagSelectionOptions(); - - // Ensure valid image mode - refreshCheckStates(); - - // Advance to next input stage - refreshEnableStates(); - } - else - invalidateInstall(InstallType::Frontend, false); + return{ + {ui->radioButton_onlyAdd, Import::UpdateMode::OnlyNew}, + {ui->radioButton_updateExisting, Import::UpdateMode::NewAndExisting} + }; } -void MainWindow::populateImportSelectionBoxes() +void MainWindow::initializeBindings() { - // Populate import selection boxes - clearListWidgets(); - ui->listWidget_platformChoices->addItems(mFlashpointInstall->database()->platformNames()); - ui->listWidget_playlistChoices->addItems(mFlashpointInstall->playlistManager()->playlistTitles()); - - // Set item attributes - QListWidgetItem* currentItem; + Bindings& b = mBindings; - for(int i = 0; i < ui->listWidget_platformChoices->count(); i++) - { - currentItem = ui->listWidget_platformChoices->item(i); - currentItem->setFlags(currentItem->flags() | Qt::ItemIsUserCheckable); - currentItem->setCheckState(Qt::Unchecked); - - if(mFrontendInstall->containsPlatform(currentItem->text())) - currentItem->setBackground(QBrush(mExistingItemColor)); - } + // Import mode + b.forceAllModeChecked = Qx::Bindable(ui->radioButton_forceAll, "checked"); - for(int i = 0; i < ui->listWidget_playlistChoices->count(); i++) - { - currentItem = ui->listWidget_playlistChoices->item(i); - currentItem->setFlags(currentItem->flags() | Qt::ItemIsUserCheckable); - currentItem->setCheckState(Qt::Unchecked); + // Indirect Enabled + mImportProperties.bindableImageModeOrder().addLifetimeNotifier([&]{ + if(!mImportProperties.isLauncherReady()) + return; - if(mFrontendInstall->containsPlaylist(currentItem->text())) - currentItem->setBackground(QBrush(mExistingItemColor)); - } -} + auto validModes = mImportProperties.imageModeOrder(); + Q_ASSERT(!validModes.isEmpty()); -void MainWindow::generateTagSelectionOptions() -{ - // Ensure old options are dropped - mTagSelectionModel.reset(); + // Move selection to valid option if no longer valid + if(!validModes.contains(getSelectedImageMode())) + mImageModeMap.from(validModes.front())->setChecked(true); - // Get tag hierarchy - QMap tagMap = mFlashpointInstall->database()->tags(); + // Disable invalid mode buttons + magic_enum::enum_for_each([this, &validModes](auto val) { + constexpr Import::ImageMode im = val; + if(!validModes.contains(im)) + mImageModeMap.from(im)->setChecked(false); + }); + }); - // Create new model - mTagSelectionModel = std::make_unique(); - mTagSelectionModel->setAutoTristate(true); - mTagSelectionModel->setSortRole(Qt::DisplayRole); + // Enabled + b.importSelectionEnabled.setBinding([&]{ return mImportProperties.isLauncherReady() && mImportProperties.isFlashpointReady(); }); + b.importSelectionEnabled.subscribeLifetime([&]{ ui->groupBox_importSelection->setEnabled(b.importSelectionEnabled); }); + b.playlistGameModeEnabled.setBinding([&]{ return mPlaylistSelections.selectedCount() > 0; }); + b.playlistGameModeEnabled.subscribeLifetime([&]{ ui->groupBox_playlistGameMode->setEnabled(b.playlistGameModeEnabled); }); + b.updateModeEnabled.setBinding([&]{ return selectionsMayModify(); }); + b.updateModeEnabled.subscribeLifetime([&]{ ui->groupBox_updateMode->setEnabled(b.updateModeEnabled); }); + b.imageModeEnabled.setBinding([&]{ return mImportProperties.isLauncherReady() && mImportProperties.isFlashpointReady(); }); + b.imageModeEnabled.subscribeLifetime([&]{ ui->groupBox_imageMode->setEnabled(b.imageModeEnabled); }); + b.startImportEnabled.setBinding([&]{ + return mPlatformSelections.selectedCount() > 0 || (getSelectedPlaylistGameMode() == Import::PlaylistGameMode::ForceAll && mPlaylistSelections.selectedCount() > 0); + }); + b.startImportEnabled.subscribeLifetime([&]{ ui->pushButton_startImport->setEnabled(b.startImportEnabled); }); + b.forceDownloadImagesEnabled.setBinding([&]{ return mImportProperties.isImageDownloadable(); }); + b.forceDownloadImagesEnabled.subscribeLifetime([&]{ + bool e = b.forceDownloadImagesEnabled; + ui->action_forceDownloadImages->setEnabled(e); + if(!e) + ui->action_forceDownloadImages->setChecked(false); + }); + b.editTagFilterEnabled.setBinding([&]{ return mImportProperties.isFlashpointReady(); }); + b.editTagFilterEnabled.subscribeLifetime([&]{ ui->action_editTagFilter->setEnabled(b.editTagFilterEnabled); }); + + // Label text + b.launcherVersion.setBinding([&]{ return mImportProperties.launcherInfo(); }); + b.launcherVersion.subscribeLifetime([&]{ ui->label_launcherVersion->setText(b.launcherVersion); }); + b.flashpointVersion.setBinding([&]{ return mImportProperties.flashpointInfo(); }); + b.flashpointVersion.subscribeLifetime([&]{ ui->label_flashpointVersion->setText(b.flashpointVersion); }); + + // Icon + b.launcherStatus.setBinding([&]{ + return QPixmap(!b.launcherPathTouched ? u":/ui/No_Install.png"_s : + !mImportProperties.isLauncherReady() ? u":/ui/Invalid_Install.png"_s : + u":/ui/Valid_Install.png"_s); + }); + b.launcherStatus.subscribeLifetime([&]{ ui->icon_launcher_install_status->setPixmap(b.launcherStatus); }); - // Populate model - QStandardItem* modelRoot = mTagSelectionModel->invisibleRootItem(); - QMap::const_iterator i; + b.flashpointStatus.setBinding([&]{ + return QPixmap(!b.flashpointPathTouched ? u":/ui/No_Install.png"_s : + !mImportProperties.isFlashpointReady() ? u":/ui/Invalid_Install.png"_s : + !mImportProperties.isFlashpointTargetSeries() ? u":/ui/Mismatch_Install.png"_s : + u":/ui/Valid_Install.png"_s); + }); + b.flashpointStatus.subscribeLifetime([&]{ ui->icon_flashpoint_install_status->setPixmap(b.flashpointStatus); }); - // Add root tag categories - for(i = tagMap.constBegin(); i != tagMap.constEnd(); ++i) - { - QStandardItem* rootItem = new QStandardItem(QString(i->name)); - rootItem->setData(QBrush(i->color), Qt::BackgroundRole); - rootItem->setData(QBrush(Qx::Color::textFromBackground(i->color)), Qt::ForegroundRole); - rootItem->setCheckState(Qt::CheckState::Checked); - rootItem->setCheckable(true); - QMap::const_iterator j; - - // Add child tags - for(j = i->tags.constBegin(); j != i->tags.constEnd(); ++j) + // Tag map + mImportProperties.bindableTagMap().subscribeLifetime([&]{ + // Populate model + auto tagMap = mImportProperties.tagMap(); + if(tagMap.isEmpty()) { - QStandardItem* childItem = new QStandardItem(QString(j->primaryAlias)); - childItem->setData(j->id, USER_ROLE_TAG_ID); - childItem->setCheckState(Qt::CheckState::Checked); - childItem->setCheckable(true); - - rootItem->appendRow(childItem); + mTagModel.clear(); + return; } - modelRoot->appendRow(rootItem); - } - - // Sort - mTagSelectionModel->sort(0); -} + QStandardItem* modelRoot = mTagModel.invisibleRootItem(); -bool MainWindow::parseFrontendData() -{ - // IO Error check instance - Qx::Error existingCheck; - - // Get list of existing platforms and playlists - existingCheck = mFrontendInstall->refreshExistingDocs(); - - // IO Error Check - if(existingCheck.isValid()) - Qx::postBlockingError(existingCheck); - - // Return true on success - return !existingCheck.isValid(); -} - -bool MainWindow::installsHaveChanged() -{ - // TODO: Make this check more thorough + // Add root tag categories + for(const Fp::Db::TagCategory& tc : tagMap) + { + QStandardItem* rootItem = new QStandardItem(QString(tc.name)); + rootItem->setData(QBrush(tc.color), Qt::BackgroundRole); + rootItem->setData(QBrush(Qx::Color::textFromBackground(tc.color)), Qt::ForegroundRole); + rootItem->setCheckState(Qt::CheckState::Checked); + rootItem->setCheckable(true); + + // Add child tags + for(const Fp::Db::Tag& tag : tc.tags) + { + QStandardItem* childItem = new QStandardItem(QString(tag.primaryAlias)); + childItem->setData(tag.id, USER_ROLE_TAG_ID); + childItem->setCheckState(Qt::CheckState::Checked); + childItem->setCheckable(true); - // Check frontend existing items - bool changed = false; - mFrontendInstall->refreshExistingDocs(&changed); - return changed; -} + rootItem->appendRow(childItem); + } -void MainWindow::redoInputChecks() -{ - // Check existing locations again - validateInstall(mFrontendInstall->path(), InstallType::Frontend); - validateInstall(mFlashpointInstall->dir().absolutePath(), InstallType::Flashpoint); -} + modelRoot->appendRow(rootItem); + } -void MainWindow::invalidateInstall(InstallType install, bool informUser) -{ - clearListWidgets(); - mTagSelectionModel.reset(); // Void tag selection model + // Sort + mTagModel.sort(0); + }); - switch(install) - { - case InstallType::Frontend: - ui->icon_frontend_install_status->setPixmap(QPixmap(u":/ui/Invalid_Install.png"_s)); - ui->label_frontendVersion->clear(); - if(informUser) - QMessageBox::critical(this, QApplication::applicationName(), MSG_FE_INSTALL_INVALID); - mFrontendInstall.reset(); - break; - - case InstallType::Flashpoint: - ui->icon_flashpoint_install_status->setPixmap(QPixmap(u":/ui/Invalid_Install.png"_s)); - ui->label_flashpointVersion->clear(); - if(informUser) - Qx::postBlockingError(mFlashpointInstall->error(), QMessageBox::Ok); - mFlashpointInstall.reset(); - break; - } + // List widget items + mImportProperties.bindablePlatforms().subscribeLifetime([&]{ + // Always clear any existing + mPlatformSelections.clear(); - refreshEnableStates(); -} + auto platforms = mImportProperties.platforms(); + if(!platforms.isEmpty()) + mPlatformSelections.fill(platforms); + }); + mImportProperties.bindablePlaylists().subscribeLifetime([&]{ + // Always clear any existing + mPlaylistSelections.clear(); -void MainWindow::clearListWidgets() -{ - ui->listWidget_platformChoices->clear(); - ui->listWidget_playlistChoices->clear(); - mPlatformItemCheckStates.clear(); - mPlaylistItemCheckStates.clear(); + auto playlists = mImportProperties.playlists(); + if(!playlists.isEmpty()) + mPlaylistSelections.fill(playlists); + }); } -bool MainWindow::isExistingPlatformSelected() +void MainWindow::initializeLauncherHelpActions() { - // Check platform choices - for(int i = 0; i < ui->listWidget_platformChoices->count(); i++) - { - if(ui->listWidget_platformChoices->item(i)->checkState() == Qt::Checked && - mFrontendInstall->containsPlatform(ui->listWidget_platformChoices->item(i)->text())) - return true; - } - - // Return false if no match - return false; -} + // Add install help link for each registered install + auto i = Lr::Install::registry().cbegin(); + auto end = Lr::Install::registry().cend(); -bool MainWindow::isExistingPlaylistSelected() -{ - // Check platform choices - for(int i = 0; i < ui->listWidget_playlistChoices->count(); i++) + for(; i != end; i++) { - if(ui->listWidget_playlistChoices->item(i)->checkState() == Qt::Checked && - mFrontendInstall->containsPlaylist(ui->listWidget_playlistChoices->item(i)->text())) - return true; + QAction* lrHelpAction = new QAction(ui->menu_launcherHelp); + lrHelpAction->setObjectName(MENU_LR_HELP_OBJ_NAME_TEMPLATE.arg(i.key())); + lrHelpAction->setText(i.key()); + lrHelpAction->setIcon(QIcon(*(i->iconPath))); + ui->menu_launcherHelp->addAction(lrHelpAction); } - - // Return false if no match - return false; } - bool MainWindow::selectionsMayModify() { - return isExistingPlatformSelected() || isExistingPlaylistSelected() || - (getSelectedPlaylistGameMode() == ImportWorker::ForceAll && - mFrontendInstall->containsAnyPlatform(mFlashpointInstall->database()->platformNames())); -} - -void MainWindow::postSqlError(const QString& mainText, const QSqlError& sqlError) -{ - QMessageBox sqlErrorMsg; - sqlErrorMsg.setIcon(QMessageBox::Critical); - sqlErrorMsg.setText(mainText); - sqlErrorMsg.setInformativeText(sqlError.text()); - sqlErrorMsg.setStandardButtons(QMessageBox::Ok); - - sqlErrorMsg.exec(); -} - -void MainWindow::postListError(const QString& mainText, const QStringList& detailedItems) -{ - QMessageBox listError; - listError.setIcon(QMessageBox::Critical); - listError.setText(mainText); - listError.setDetailedText(detailedItems.join('\n')); - listError.setStandardButtons(QMessageBox::Ok); - - listError.exec(); -} - -void MainWindow::postIOError(const QString& mainText, const Qx::IoOpReport& report) -{ - QMessageBox ioErrorMsg; - ioErrorMsg.setIcon(QMessageBox::Critical); - ioErrorMsg.setText(mainText); - ioErrorMsg.setInformativeText(report.outcome()); - ioErrorMsg.setStandardButtons(QMessageBox::Ok); - - ioErrorMsg.exec(); -} - -void MainWindow::refreshEnableStates() -{ - QHash>::const_iterator i; - for(i = mWidgetEnableConditionMap.constBegin(); i != mWidgetEnableConditionMap.constEnd(); i++) - i.key()->setEnabled(i.value()()); - - QHash>::const_iterator j; - for(j = mActionEnableConditionMap.constBegin(); j != mActionEnableConditionMap.constEnd(); j++) - j.key()->setEnabled(j.value()()); -} - -void MainWindow::refreshCheckStates() -{ - // Determine allowed/preferred image mode order - QList modeOrder = mFrontendInstall ? mFrontendInstall->preferredImageModeOrder() : DEFAULT_IMAGE_MODE_ORDER; - - // Remove link as an option if user doesn't have permissions - if(!mHasLinkPermissions) - modeOrder.removeAll(Fe::ImageMode::Link); - - // Ensure an option remains - if(modeOrder.isEmpty()) - throw std::runtime_error("MainWindow::refreshCheckStates(): At least one image import mode must be available!"); - - // Move image mode selection to next preferred option if the current one is invalid - Fe::ImageMode im = getSelectedImageMode(); - if(!modeOrder.contains(im)) - { - Fe::ImageMode preferredMode = modeOrder.first(); - - switch(preferredMode) - { - case Fe::ImageMode::Link: - ui->radioButton_link->setChecked(true); - break; - case Fe::ImageMode::Reference: - ui->radioButton_reference->setChecked(true); - break; - case Fe::ImageMode::Copy: - ui->radioButton_copy->setChecked(true); - break; - default: - qCritical("MainWindow::refreshCheckStates(): Invalid preferred image mode."); - break; - } - } + if(mPlatformSelections.existingSelectedCount() > 0 || mPlaylistSelections.existingSelectedCount() > 0) + return true; - // Ensure that the force download images option is unchecked if not supported - if(ui->action_forceDownloadImages->isChecked() && mFlashpointInstall && !mFlashpointInstall->preferences().onDemandImages) - ui->action_forceDownloadImages->setChecked(false); + auto plats = mImportProperties.platforms(); + return std::any_of(plats.cbegin(), plats.cend(), [](const Import::Importee& i){ + return i.existing; // Checks if any platform is existing at all + }); } QStringList MainWindow::getSelectedPlatforms() const @@ -556,24 +352,39 @@ QStringList MainWindow::getSelectedPlaylists() const return selectedPlaylists; } -Fp::Db::InclusionOptions MainWindow::getSelectedInclusionOptions() const +Import::PlaylistGameMode MainWindow::getSelectedPlaylistGameMode() const { - return {generateTagExlusionSet(), ui->action_includeAnimations->isChecked()}; + /* This is a trick to make UI options that depend on which playlist game mode + * is selected dependent on the relevant button(s). Qx::ButtonGroup would be better + * but the UI designer cannot use custom button groups and we don't want to give that + * up. + */ + Q_UNUSED(mBindings.forceAllModeChecked.value()); + + QRadioButton* sel = static_cast(ui->buttonGroup_playlistGameMode->checkedButton()); + Q_ASSERT(sel); + return mPlaylistGameModeMap[sel]; } -Fe::UpdateOptions MainWindow::getSelectedUpdateOptions() const +Fp::Db::InclusionOptions MainWindow::getSelectedInclusionOptions() const { - return {ui->radioButton_onlyAdd->isChecked() ? Fe::ImportMode::OnlyNew : Fe::ImportMode::NewAndExisting, ui->checkBox_removeMissing->isChecked() }; + return {generateTagExlusionSet(), ui->action_includeAnimations->isChecked()}; } -Fe::ImageMode MainWindow::getSelectedImageMode() const +Import::UpdateOptions MainWindow::getSelectedUpdateOptions() const { - return ui->radioButton_copy->isChecked() ? Fe::ImageMode::Copy : ui->radioButton_reference->isChecked() ? Fe::ImageMode::Reference : Fe::ImageMode::Link; + return {ui->radioButton_onlyAdd->isChecked() ? Import::UpdateMode::OnlyNew : Import::UpdateMode::NewAndExisting, ui->checkBox_removeMissing->isChecked() }; + + QRadioButton* sel = static_cast(ui->buttonGroup_updateMode->checkedButton()); + Q_ASSERT(sel); + return mUpdateModeMap[sel]; } -ImportWorker::PlaylistGameMode MainWindow::getSelectedPlaylistGameMode() const +Import::ImageMode MainWindow::getSelectedImageMode() const { - return ui->radioButton_selectedPlatformsOnly->isChecked() ? ImportWorker::SelectedPlatform : ImportWorker::ForceAll; + QRadioButton* sel = static_cast(ui->buttonGroup_imageMode->checkedButton()); + Q_ASSERT(sel); + return mImageModeMap.from(sel); } bool MainWindow::getForceDownloadImages() const @@ -583,184 +394,50 @@ bool MainWindow::getForceDownloadImages() const void MainWindow::prepareImport() { - // Check that install contents haven't been altered - if(installsHaveChanged()) - { - QMessageBox::warning(this, QApplication::applicationName(), MSG_INSTALL_CONTENTS_CHANGED); - redoInputChecks(); - return; - } - - // Warn user if they are changing existing files - if(selectionsMayModify()) - if(QMessageBox::warning(this, QApplication::applicationName(), MSG_PRE_EXISTING_IMPORT, QMessageBox::Yes | QMessageBox::Cancel, QMessageBox::Cancel) == QMessageBox::Cancel) - return; - - // Warn user if Flashpoint is running - // Check if Flashpoint is running - if(Qx::processIsRunning(Fp::Install::LAUNCHER_NAME)) - QMessageBox::warning(this, QApplication::applicationName(), MSG_FP_CLOSE_PROMPT); - - // Only allow proceeding if frontend isn't running - bool feRunning; - while((feRunning = mFrontendInstall->isRunning())) - if(QMessageBox::critical(this, QApplication::applicationName(), MSG_FRONTEND_CLOSE_PROMPT, QMessageBox::Retry | QMessageBox::Cancel, QMessageBox::Retry) == QMessageBox::Cancel) - break; - - if(!feRunning) - { - // Start progress presentation - mProgressPresenter.setMinimum(0); - mProgressPresenter.setMaximum(0); - mProgressPresenter.setValue(0); - mProgressPresenter.setBusyState(); - mProgressPresenter.setLabelText(STEP_FP_DB_INITIAL_QUERY); - QApplication::processEvents(); // Force show progress immediately - - // Setup import worker - ImportWorker::ImportSelections impSel{.platforms = getSelectedPlatforms(), - .playlists =getSelectedPlaylists()}; - ImportWorker::OptionSet optSet{ - getSelectedUpdateOptions(), - getSelectedImageMode(), - getForceDownloadImages(), - getSelectedPlaylistGameMode(), - getSelectedInclusionOptions() - }; - ImportWorker importWorker(mFlashpointInstall, mFrontendInstall, impSel, optSet); - - // Setup blocking error connection - connect(&importWorker, &ImportWorker::blockingErrorOccured, this, &MainWindow::handleBlockingError); - - // Setup auth handler - connect(&importWorker, &ImportWorker::authenticationRequired, this, &MainWindow::handleAuthRequest); - - // Create process update connections - connect(&importWorker, &ImportWorker::progressStepChanged, &mProgressPresenter, &ProgressPresenter::setLabelText); - connect(&importWorker, &ImportWorker::progressValueChanged, &mProgressPresenter, &ProgressPresenter::setValue); - connect(&importWorker, &ImportWorker::progressMaximumChanged, &mProgressPresenter, &ProgressPresenter::setMaximum); - connect(&mProgressPresenter, &ProgressPresenter::canceled, &importWorker, &ImportWorker::notifyCanceled); - - // Import error tracker - Qx::Error importError; - - // Start import and forward result to handler - ImportWorker::ImportResult importResult = importWorker.doImport(importError); - handleImportResult(importResult, importError); - } -} - -void MainWindow::revertAllFrontendChanges() -{ - // Trackers - bool tempSkip = false; - bool alwaysSkip = false; - Fe::RevertError currentError; - int retryChoice; - - // Progress - QProgressDialog reversionProgress(CAPTION_REVERT, QString(), 0, mFrontendInstall->revertQueueCount(), this); - reversionProgress.setWindowModality(Qt::WindowModal); - reversionProgress.setAutoReset(false); - - while(mFrontendInstall->revertNextChange(currentError, alwaysSkip || tempSkip) != 0) - { - // Check for error - if(!currentError.isValid()) - { - tempSkip = false; - reversionProgress.setValue(reversionProgress.value() + 1); - } - else - { - retryChoice = Qx::postBlockingError(currentError, QMessageBox::Retry | QMessageBox::Ignore | QMessageBox::Abort, QMessageBox::Retry); - - if(retryChoice == QMessageBox::Ignore) - tempSkip = true; - else if(retryChoice == QMessageBox::Abort) - alwaysSkip = true; - } - } - - // Ensure progress dialog is closed - reversionProgress.close(); - - // Reset instance - mFrontendInstall->softReset(); -} - -void MainWindow::deployCLIFp(const Fp::Install& fp, QMessageBox::Button abandonButton) -{ - bool willDeploy = true; - - // Check for existing CLIFp - if(CLIFp::hasCLIFp(fp)) - { - // Notify user if this will be a downgrade - if(CLIFp::internalVersion() < CLIFp::installedVersion(fp)) - willDeploy = (QMessageBox::warning(this, CAPTION_CLIFP_DOWNGRADE, MSG_FP_CLFIP_WILL_DOWNGRADE, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes); - } + // Gather selection's and notify controller + Import::Selections impSel{.platforms = getSelectedPlatforms(), + .playlists =getSelectedPlaylists()}; + Import::OptionSet optSet{ + getSelectedUpdateOptions(), + getSelectedImageMode(), + getForceDownloadImages(), + getSelectedPlaylistGameMode(), + getSelectedInclusionOptions() + }; - // Deploy CLIFp if applicable - if(willDeploy) - { - // Deploy exe - QString deployError; - while(!CLIFp::deployCLIFp(deployError, fp)) - if(QMessageBox::critical(this, CAPTION_CLIFP_ERR, MSG_FP_CANT_DEPLOY_CLIFP.arg(deployError), QMessageBox::Retry | abandonButton, QMessageBox::Retry) == abandonButton) - break; - } + emit importTriggered(impSel, optSet, selectionsMayModify()); } -void MainWindow::standaloneCLIFpDeploy() -{ - // Browse for install - QString selectedDir = QFileDialog::getExistingDirectory(this, CAPTION_FLASHPOINT_BROWSE, QDir::currentPath()); - - if(!selectedDir.isEmpty()) - { - Fp::Install tempFlashpointInstall(selectedDir); - if(tempFlashpointInstall.isValid()) - { - if(!installMatchesTargetSeries(tempFlashpointInstall)) - QMessageBox::warning(this, QApplication::applicationName(), MSG_FP_VER_NOT_TARGET); - - deployCLIFp(tempFlashpointInstall, QMessageBox::Cancel); - } - else - Qx::postBlockingError(tempFlashpointInstall.error(), QMessageBox::Ok); - } -} void MainWindow::showTagSelectionDialog() { // Ensure tags have been populated - assert(mTagSelectionModel); + Q_ASSERT(mTagModel.rowCount() > 0); // Cache current selection states QHash originalCheckStates; - mTagSelectionModel->forEachItem([&](QStandardItem* item) { originalCheckStates[item] = item->checkState(); }); + mTagModel.forEachItem([&](QStandardItem* item) { originalCheckStates[item] = item->checkState(); }); // Create dialog Qx::TreeInputDialog tagSelectionDialog(this); - tagSelectionDialog.setModel(mTagSelectionModel.get()); + tagSelectionDialog.setModel(&mTagModel); tagSelectionDialog.setWindowFlags(tagSelectionDialog.windowFlags() & ~Qt::WindowContextHelpButtonHint); tagSelectionDialog.setWindowTitle(CAPTION_TAG_FILTER); - connect(&tagSelectionDialog, &Qx::TreeInputDialog::selectNoneClicked, mTagSelectionModel.get(), &Qx::StandardItemModel::selectNone); - connect(&tagSelectionDialog, &Qx::TreeInputDialog::selectAllClicked, mTagSelectionModel.get(), &Qx::StandardItemModel::selectAll); + connect(&tagSelectionDialog, &Qx::TreeInputDialog::selectNoneClicked, &mTagModel, &Qx::StandardItemModel::selectNone); + connect(&tagSelectionDialog, &Qx::TreeInputDialog::selectAllClicked, &mTagModel, &Qx::StandardItemModel::selectAll); // Present dialog and capture commitment choice int dc = tagSelectionDialog.exec(); // If new selections were canceled, restore previous ones if(dc == QDialog::Rejected) - mTagSelectionModel->forEachItem([&](QStandardItem* item) { item->setCheckState(originalCheckStates[item]); }); + mTagModel.forEachItem([&](QStandardItem* item) { item->setCheckState(originalCheckStates[item]); }); } QSet MainWindow::generateTagExlusionSet() const { QSet exclusionSet; - mTagSelectionModel->forEachItem([&exclusionSet](QStandardItem* item){ + mTagModel.forEachItem([&exclusionSet](QStandardItem* item){ if(item->data(USER_ROLE_TAG_ID).isValid() && item->checkState() == Qt::Unchecked) exclusionSet.insert(item->data(USER_ROLE_TAG_ID).toInt()); }); @@ -768,16 +445,6 @@ QSet MainWindow::generateTagExlusionSet() const return exclusionSet; } -//Protected: -void MainWindow::showEvent(QShowEvent* event) -{ - mProgressPresenter.attachWindow(windowHandle()); - QMainWindow::showEvent(event); -} - -//Public: -bool MainWindow::initCompleted() { return mInitCompleted; } - //-Slots--------------------------------------------------------------------------------------------------------- //Private: void MainWindow::all_on_action_triggered() @@ -793,7 +460,7 @@ void MainWindow::all_on_action_triggered() if(senderAction == ui->action_exit) QApplication::quit(); else if(senderAction == ui->action_deployCLIFp) - standaloneCLIFpDeploy(); + emit standaloneDeployTriggered(); else if(senderAction == ui->action_goToCLIFpGitHub) QDesktopServices::openUrl(URL_CLIFP_GITHUB); else if(senderAction == ui->action_goToFILGitHub) @@ -810,21 +477,27 @@ void MainWindow::all_on_pushButton_clicked() { // Get the object that called this slot QPushButton* senderPushButton = qobject_cast(sender()); - - // Ensure the signal that triggered this slot belongs to the above class by checking for null pointer - if(senderPushButton == nullptr) - throw std::runtime_error("Pointer conversion to push button failed"); + Q_ASSERT(senderPushButton); // Determine sender and take corresponding action - if(senderPushButton == ui->pushButton_frontendBrowse) + if(senderPushButton == ui->pushButton_launcherBrowse) { - QString selectedDir = QFileDialog::getExistingDirectory(this, CAPTION_FRONTEND_BROWSE, - (QFileInfo::exists(ui->lineEdit_frontendPath->text()) ? ui->lineEdit_frontendPath->text() : QDir::currentPath())); + QString selectedDir = QFileDialog::getExistingDirectory(this, CAPTION_LAUNCHER_BROWSE, + (QFileInfo::exists(ui->lineEdit_launcherPath->text()) ? ui->lineEdit_launcherPath->text() : QDir::currentPath())); if(!selectedDir.isEmpty()) { - ui->lineEdit_frontendPath->setText(QDir::toNativeSeparators(selectedDir)); - validateInstall(selectedDir, InstallType::Frontend); + /* TODO: Here, and below, we simulate the user entering this text manually into the box so that + * the text is placed there and the editingFinished() signal is emitted; however, this hinges on + * a quirk of QLineEdit that I'm looking to remove in a patch in that the editingFinished() signal + * is emitted on focus lost if the text is different from when the signal was last emitted, even if + * the text was put there programatically. IMO it should only include user edits, as QWidgets usually + * use the word "changed" to mean any change and "edited" to mean user changes only. So, when that patch + * goes through this will need tweaking + */ + ui->lineEdit_launcherPath->setFocus(); + ui->lineEdit_launcherPath->setText(QDir::toNativeSeparators(selectedDir)); + ui->pushButton_launcherBrowse->setFocus(); // Return focus to browse button } } else if(senderPushButton == ui->pushButton_flashpointBrowse) @@ -834,8 +507,9 @@ void MainWindow::all_on_pushButton_clicked() if(!selectedDir.isEmpty()) { + ui->lineEdit_flashpointPath->setFocus(); ui->lineEdit_flashpointPath->setText(QDir::toNativeSeparators(selectedDir)); - validateInstall(selectedDir, InstallType::Flashpoint); + ui->pushButton_launcherBrowse->setFocus(); // Return focus to browse button } } else if(senderPushButton == ui->pushButton_selectAll_platforms) @@ -869,168 +543,58 @@ void MainWindow::all_on_pushButton_clicked() else if(senderPushButton == ui->pushButton_exit) QApplication::quit(); else - throw std::runtime_error("Unhandled use of all_on_pushButton_clicked() slot"); + qFatal("Unhandled use of all_on_pushButton_clicked() slot"); } void MainWindow::all_on_lineEdit_editingFinished() { // Get the object that called this slot QLineEdit* senderLineEdit = qobject_cast(sender()); - - // Ensure the signal that triggered this slot belongs to the above class by checking for null pointer - if(senderLineEdit == nullptr) - throw std::runtime_error("Pointer conversion to line edit failed"); + Q_ASSERT(senderLineEdit); // Determine sender and take corresponding action - if(senderLineEdit == ui->lineEdit_frontendPath) - checkManualInstallInput(InstallType::Frontend); - else if(senderLineEdit == ui->lineEdit_flashpointPath) - checkManualInstallInput(InstallType::Flashpoint); - else - throw std::runtime_error("Unhandled use of all_on_linedEdit_textEdited() slot"); -} - -void MainWindow::all_on_listWidget_itemChanged(QListWidgetItem* item) // Proxy for u"onItemChecked"_s -{ - // Get the object that called this slot - QListWidget* senderListWidget = qobject_cast(sender()); - - // Ensure the signal that triggered this slot belongs to the above class by checking for null pointer - if(senderListWidget == nullptr) - throw std::runtime_error("Pointer conversion to list widget failed"); - - if(senderListWidget == ui->listWidget_platformChoices) + if(senderLineEdit == ui->lineEdit_launcherPath) { - // Check if change was change in check state - if(mPlatformItemCheckStates.contains(item) && item->checkState() != mPlatformItemCheckStates.value(item)) - refreshEnableStates(); - - // Add/update check state - mPlatformItemCheckStates[item] = item->checkState(); + mBindings.launcherPathTouched = true; + emit installPathChanged(ui->lineEdit_launcherPath->text(), Import::Install::Launcher); } - else if(senderListWidget == ui->listWidget_playlistChoices) + else if(senderLineEdit == ui->lineEdit_flashpointPath) { - // Check if change was change in check state - if(mPlaylistItemCheckStates.contains(item) && item->checkState() != mPlaylistItemCheckStates.value(item)) - refreshEnableStates(); - - // Add/update check state - mPlaylistItemCheckStates[item] = item->checkState(); + mBindings.flashpointPathTouched = true; + emit installPathChanged(ui->lineEdit_flashpointPath->text(), Import::Install::Flashpoint); } else - throw std::runtime_error("Unhandled use of all_on_listWidget_itemChanged() slot"); -} - -void MainWindow::all_on_radioButton_clicked() -{ - // Get the object that called this slot - QRadioButton* senderRadioButton = qobject_cast(sender()); - - // Ensure the signal that triggered this slot belongs to the above class by checking for null pointer - if(senderRadioButton == nullptr) - throw std::runtime_error("Pointer conversion to radio button failed"); - - if(senderRadioButton == ui->radioButton_selectedPlatformsOnly) - refreshEnableStates(); - else if(senderRadioButton == ui->radioButton_forceAll) - refreshEnableStates(); - else - throw std::runtime_error("Unhandled use of all_on_radioButton_clicked() slot"); + qFatal("Unhandled use of all_on_linedEdit_textEdited() slot"); } void MainWindow::all_on_menu_triggered(QAction *action) { // Get the object that called this slot QMenu* senderMenu = qobject_cast(sender()); + Q_ASSERT(senderMenu); - // Ensure the signal that triggered this slot belongs to the above class by checking for null pointer - if(senderMenu == nullptr) - throw std::runtime_error("Pointer conversion to menu failed"); - - if(senderMenu == ui->menu_frontendHelp) + if(senderMenu == ui->menu_launcherHelp) { // Get associated help URL and open it - QRegularExpressionMatch frontendMatch = MENU_FE_HELP_KEY_REGEX.match(action->objectName()); + QRegularExpressionMatch launcherMatch = MENU_LR_HELP_KEY_REGEX.match(action->objectName()); - if(frontendMatch.hasMatch()) + if(launcherMatch.hasMatch()) { - QString frontendName = frontendMatch.captured(u"frontend"_s); - if(!frontendName.isNull() && Fe::Install::registry().contains(frontendName)) + QString launcherName = launcherMatch.captured(u"launcher"_s); + if(!launcherName.isNull() && Lr::Install::registry().contains(launcherName)) { - const QUrl* helpUrl = Fe::Install::registry()[frontendName].helpUrl; + const QUrl* helpUrl = Lr::Install::registry()[launcherName].helpUrl; QDesktopServices::openUrl(*helpUrl); return; } } - qWarning("Frontend help action name could not be determined."); + qWarning("Launcher help action name could not be determined."); } else - throw std::runtime_error("Unhandled use of all_on_menu_triggered() slot"); -} - -void MainWindow::handleBlockingError(std::shared_ptr response, const Qx::Error& blockingError, QMessageBox::StandardButtons choices) -{ - mProgressPresenter.setErrorState(); - - // Post error and get response - int userChoice = Qx::postBlockingError(blockingError, choices); - - // If applicable return selection - if(response) - *response = userChoice; - - mProgressPresenter.resetState(); -} - -void MainWindow::handleAuthRequest(const QString& prompt, QAuthenticator* authenticator) -{ - Qx::LoginDialog ld; - ld.setPrompt(prompt); - - int choice = ld.exec(); - - if(choice == QDialog::Accepted) - { - authenticator->setUser(ld.username()); - authenticator->setPassword(ld.password()); - } + qFatal("Unhandled use of all_on_menu_triggered() slot"); } -void MainWindow::handleImportResult(ImportWorker::ImportResult importResult, const Qx::Error& errorReport) -{ - // Reset progress presenter - mProgressPresenter.reset(); - - // Post error report if present - if(errorReport.isValid()) - Qx::postBlockingError(errorReport, QMessageBox::Ok); - if(importResult == ImportWorker::Successful) - { - deployCLIFp(*mFlashpointInstall, QMessageBox::Ignore); - // Post-import message - QMessageBox::information(this, QApplication::applicationName(), MSG_POST_IMPORT); - // Update selection lists to reflect newly existing platforms - gatherInstallInfo(); - } - else if(importResult == ImportWorker::Taskless) - { - QMessageBox::warning(this, CAPTION_TASKLESS_IMPORT, MSG_NO_WORK); - } - else if(importResult == ImportWorker::Canceled) - { - QMessageBox::critical(this, CAPTION_REVERT, MSG_USER_CANCELED); - revertAllFrontendChanges(); - } - else if(importResult == ImportWorker::Failed) - { - // Show general next steps message - QMessageBox::warning(this, CAPTION_REVERT, MSG_HAVE_TO_REVERT); - revertAllFrontendChanges(); - } - else - qCritical("unhandled import worker result type."); -} diff --git a/app/src/ui/mainwindow.h b/app/src/ui/mainwindow.h index 76ed60b..cead72b 100644 --- a/app/src/ui/mainwindow.h +++ b/app/src/ui/mainwindow.h @@ -7,42 +7,75 @@ #include // Qx Includes -#include -#include #include - -// libfp Includes -#include +#include +#include +#include // Project Includes +#include "import/properties.h" #include "project_vars.h" -#include "ui/progresspresenter.h" -#include "frontend/fe-install.h" -#include "import-worker.h" -#include "clifp.h" QT_BEGIN_NAMESPACE namespace Ui { class MainWindow; } QT_END_NAMESPACE +class QRadioButton; + class MainWindow : public QMainWindow { - Q_OBJECT // Required for classes that use Qt elements + Q_OBJECT; +//-Class Structs------------------------------------------------------------------------------------------------ +private: + struct Bindings + { + /* Ideally these could all just be QBindable, but that only works for Q_PROPERTIES that have + * a notify signal, and most of these don't, so we have to use our own property and subscribe + * to it as a workaround instead. + */ + Qx::Bindable forceAllModeChecked; // Defaults to false + Qx::Property importSelectionEnabled; + Qx::Property playlistGameModeEnabled; + Qx::Property updateModeEnabled; + Qx::Property imageModeEnabled; + Qx::Property startImportEnabled; + Qx::Property forceDownloadImagesEnabled; + Qx::Property editTagFilterEnabled; + Qx::Property launcherVersion; + Qx::Property flashpointVersion; + Qx::Property launcherStatus; + Qx::Property flashpointStatus; + Qx::Property launcherPathTouched; // Defaults to false + Qx::Property flashpointPathTouched; // Defaults to false + }; -//-Class Enums------------------------------------------------------------------------------------------------ +//-Inner Classes------------------------------------------------------------------------------------------------ private: - enum class InputStage {Paths, Imports}; - enum class InstallType {Frontend, Flashpoint}; + class SelectionList + { + static constexpr int USER_ROLE_EXISTING = 0x333; // Value chosen arbitrarily (must be > 0x100) + + QListWidget* mWidget; + Qx::Property mSelCount; + Qx::Property mExistSelCount; + + void handleCheckChange(QListWidgetItem* item); + + public: + SelectionList(QListWidget* widget); + + void fill(const QList& imps); + void clear(); + + int selectedCount() const; + int existingSelectedCount() const; + }; //-Class Variables-------------------------------------------------------------------------------------------- private: // UI Text static inline const QString REQUIRE_ELEV = u" [Run as Admin/Dev Mode]"_s; - // Messages - General - static inline const QString MSG_FATAL_NO_INTERNAL_CLIFP_VER = u"Failed to get version information from the internal copy of CLIFp.exe!\n" - "\n" - "Execution cannot continue."_s; // Messages - Help static inline const QString MSG_ABOUT = PROJECT_FULL_NAME + u"\n\nVersion: "_s + PROJECT_VERSION_STR; static inline const QString MSG_PLAYLIST_GAME_MODE_HELP = u"%1 - Games found in the selected playlists that are not part of any selected platform will be excluded.
" @@ -60,137 +93,68 @@ class MainWindow : public QMainWindow "games if this option is unchecked, but this may be useful for archival purposes or in case you later want to revert to a previous version of Flashpoint and maintain the entries personal " "metadata. Note that this option will also remove any games that are not covered by your current import selections (i.e. platforms, playlists, tag filter, etc.), even if they still remain " "in Flashpoint"_s; - static inline const QString MSG_IMAGE_MODE_HELP = u"%1 - All relevant images from Flashpoint will be fully copied into your frontend installation. This causes zero overhead but will require additional storage space proportional to " + static inline const QString MSG_IMAGE_MODE_HELP = u"%1 - All relevant images from Flashpoint will be fully copied into your launcher installation. This causes zero overhead but will require additional storage space proportional to " "the number of games you end up importing, up to double if all platforms are selected.
" "Space Consumption: High
" "Import Speed: Very Slow
" - "Frontend Access Speed: Fast
" + "Launcher Access Speed: Fast
" "
" - "%2 - Your frontend platform configuration will be altered so that the relevant image folders within Flashpoint are directly referenced by its media scanner, requiring no " + "%2 - Your launcher platform configuration will be altered so that the relevant image folders within Flashpoint are directly referenced by its media scanner, requiring no " "extra space and causing no overhead.
" "Space Consumption: None
" "Import Speed: Fast
" - ">Frontend Access Speed:Varies, but usually slow
" + "Launcher Access Speed:Varies, but usually slow
" "
" - "%3 - A symbolic link to each relevant image from Flashpoint will be created in your frontend installation. These appear like the real files to the frontend, adding only a minuscule " + "%3 - A symbolic link to each relevant image from Flashpoint will be created in your launcher installation. These appear like the real files to the launcher, adding only a minuscule " "amount of overhead when it loads images and require almost no extra disk space to store.
" "Space Consumption: Near-zero
" "Import Speed: Slow
" - ">Frontend Access Speed: Fast
"_s; - - // Messages - Input - static inline const QString MSG_FE_INSTALL_INVALID = u"The specified directory either doesn't contain a valid frontend install, or it contains a version that is incompatible with this tool."_s; - static inline const QString MSG_FP_INSTALL_INVALID = u"The specified directory either doesn't contain a valid Flashpoint install, or it contains a version that is incompatible with this tool."_s; - static inline const QString MSG_FP_VER_NOT_TARGET = u"The selected Flashpoint install contains a version of Flashpoint that is different from the target version series (" PROJECT_TARGET_FP_VER_PFX_STR "), but appears to have a compatible structure. " - "You may proceed at your own risk as the tool is not guaranteed to work correctly in this circumstance. Please use a newer version of " PROJECT_SHORT_NAME " if available."_s; - - static inline const QString MSG_INSTALL_CONTENTS_CHANGED = u"The contents of your installs have been changed since the initial scan and therefore must be re-evaluated. You will need to make your selections again."_s; - - // Messages - General import procedure - static inline const QString MSG_PRE_EXISTING_IMPORT = u"One or more existing Platforms/Playlists may be affected by this import. These will be altered even if they did not originate from this program (i.e. if you " - "already happened to have a Platform/Playlist with the same name as one present in Flashpoint).\n" - "\n" - "Are you sure you want to proceed?"_s; - static inline const QString MSG_FRONTEND_CLOSE_PROMPT = u"The importer has detected that the selected frontend is running. It must be closed in order to continue. If recently closed, wait a few moments before trying to proceed again as it performs significant cleanup in the background."_s; - static inline const QString MSG_POST_IMPORT = u"The Flashpoint import has completed successfully. Next time you start the frontend it may take longer than usual as it may have to fill in some default fields for the imported Platforms/Playlists.\n" - "\n" - "If you wish to import further selections or update to a newer version of Flashpoint, simply re-run this procedure after pointing it to the desired Flashpoint installation."_s; - // Initial import status - static inline const QString STEP_FP_DB_INITIAL_QUERY = u"Making initial Flashpoint database queries..."_s; - - // Messages - FP General - static inline const QString MSG_FP_CLOSE_PROMPT = u"It is strongly recommended to close Flashpoint before proceeding as it can severely slow or interfere with the import process"_s; - - // Messages - FP Database read - static inline const QString MSG_FP_DB_MISSING_TABLE = u"The Flashpoint database is missing tables critical to the import process."_s; - static inline const QString MSG_FP_DB_TABLE_MISSING_COLUMN = u"The Flashpoint database tables are missing columns critical to the import process."_s; - static inline const QString MSG_FP_DB_UNEXPECTED_ERROR = u"An unexpected SQL error occurred while reading the Flashpoint database:"_s; - - // Messages - FP CLIFp - static inline const QString MSG_FP_CLFIP_WILL_DOWNGRADE = u"The existing version of "_s + CLIFp::EXE_NAME + u" in your Flashpoint install is newer than the version package with this tool.\n" - "\n" - "Replacing it with the packaged Version (downgrade) will likely cause compatibility issues unless you are specifically re-importing after downgrading your Flashpoint install to a previous version.\n" - "\n" - "Do you wish to downgrade "_s + CLIFp::EXE_NAME + u"?"_s; - - static inline const QString MSG_FP_CANT_DEPLOY_CLIFP = u"Failed to deploy "_s + CLIFp::EXE_NAME + u" to the selected Flashpoint install.\n" - "\n" - "%1\n" - "\n" - "If you choose to ignore this you will have to place CLIFp in your Flashpoint install directory manually."_s; - - // Messages - Import Result - static inline const QString MSG_HAVE_TO_REVERT = u"Due to previous unrecoverable errors, all changes that occurred during import will now be reverted (other than existing images that were replaced with newer versions).\n" - "\n" - "Afterwards, check to see if there is a newer version of " PROJECT_SHORT_NAME " and try again using that version. If not ask for help on the relevant forums where this tool was released (see Help).\n" - "\n" - "If you believe this to be due to a bug with this software, please submit an issue to its GitHub page (listed under help)"_s; - - static inline const QString MSG_USER_CANCELED = u"Import canceled by user, all changes that occurred during import will now be reverted (other than existing images that were replaced with newer versions)."_s; - static inline const QString MSG_NO_WORK = u"The provided import selections/options resulted in no tasks to perform. Double-check your settings."_s; + "Launcher Access Speed: Fast
"_s; // Dialog captions - static inline const QString CAPTION_GENERAL_FATAL_ERROR = u"Fatal Error!"_s; - static inline const QString CAPTION_FRONTEND_BROWSE = u"Select the root directory of your frontend install..."_s; + static inline const QString CAPTION_LAUNCHER_BROWSE = u"Select the root directory of your launcher install..."_s; static inline const QString CAPTION_FLASHPOINT_BROWSE = u"Select the root directory of your Flashpoint install..."_s; static inline const QString CAPTION_PLAYLIST_GAME_MODE_HELP = u"Playlist game mode options"_s; static inline const QString CAPTION_UPDATE_MODE_HELP = u"Update mode options"_s; static inline const QString CAPTION_IMAGE_MODE_HELP = u"Image mode options"_s; - static inline const QString CAPTION_TASKLESS_IMPORT = u"Nothing to do"_s; - static inline const QString CAPTION_REVERT = u"Reverting changes..."_s; - static inline const QString CAPTION_CLIFP_ERR = u"Error deploying CLIFp"_s; - static inline const QString CAPTION_CLIFP_DOWNGRADE = u"Downgrade CLIFp?"_s; static inline const QString CAPTION_TAG_FILTER = u"Tag Filter"_s; // Menus - static inline const QString MENU_FE_HELP_OBJ_NAME_TEMPLATE = u"action_%1Help"_s; - static inline const QRegularExpression MENU_FE_HELP_KEY_REGEX = QRegularExpression(u"action_(?.*?)Help"_s); + static inline const QString MENU_LR_HELP_OBJ_NAME_TEMPLATE = u"action_%1Help"_s; + static inline const QRegularExpression MENU_LR_HELP_KEY_REGEX = QRegularExpression(u"action_(?.*?)Help"_s); // URLs static inline const QUrl URL_CLIFP_GITHUB = QUrl(u"https://github.com/oblivioncth/CLIFp"_s); static inline const QUrl URL_FIL_GITHUB = QUrl(u"https://github.com/oblivioncth/FIL"_s); - // Flashpoint version check - static inline const Qx::VersionNumber TARGET_FP_VERSION_PREFIX = Qx::VersionNumber::fromString(PROJECT_TARGET_FP_VER_PFX_STR); - - // Selection defaults - static inline const QList DEFAULT_IMAGE_MODE_ORDER = { - Fe::ImageMode::Link, - Fe::ImageMode::Reference, - Fe::ImageMode::Copy - }; - // User Roles static inline const int USER_ROLE_TAG_ID = 0x200; // Value chosen arbitrarily (must be > 0x100) + // Color + static inline QColor smExistingItemColor; + //-Instance Variables-------------------------------------------------------------------------------------------- private: - Ui::MainWindow *ui; - bool mInitCompleted; - - QHash> mWidgetEnableConditionMap; - QHash> mActionEnableConditionMap; - QColor mExistingItemColor; - - std::shared_ptr mFrontendInstall; - std::shared_ptr mFlashpointInstall; - - QHash mPlatformItemCheckStates; - QHash mPlaylistItemCheckStates; - std::unique_ptr mTagSelectionModel; + Ui::MainWindow* ui; + SelectionList mPlatformSelections; + SelectionList mPlaylistSelections; + const Import::Properties& mImportProperties; - bool mHasLinkPermissions = false; + Qx::Bimap mImageModeMap; + QHash mPlaylistGameModeMap; + QHash mUpdateModeMap; + Qx::StandardItemModel mTagModel; QString mArgedPlaylistGameModeHelp; QString mArgedUpdateModeHelp; QString mArgedImageModeHelp; - // Process monitoring - ProgressPresenter mProgressPresenter; + // Behavior + Bindings mBindings; //-Constructor--------------------------------------------------------------------------------------------------- public: - MainWindow(QWidget *parent = nullptr); + MainWindow(const Import::Properties& importProperties, QWidget *parent = nullptr); //-Destructor---------------------------------------------------------------------------------------------------- public: @@ -198,68 +162,43 @@ class MainWindow : public QMainWindow //-Instance Functions-------------------------------------------------------------------------------------------- private: - bool testForLinkPermissions(); + // Init + Qx::Bimap initializeImageModeMap() const; + QHash initializePlaylistGameModeMap() const; + QHash initializeUpdateModeMap() const; + void initializeBindings(); void initializeForms(); - void initializeEnableConditionMaps(); - void initializeFrontendHelpActions(); - bool installMatchesTargetSeries(const Fp::Install& fpInstall); - void checkManualInstallInput(InstallType install); - void validateInstall(const QString& installPath, InstallType install); - void gatherInstallInfo(); - void populateImportSelectionBoxes(); - void generateTagSelectionOptions(); - bool parseFrontendData(); - bool installsHaveChanged(); - void redoInputChecks(); - - void invalidateInstall(InstallType install, bool informUser); - void clearListWidgets(); - bool isExistingPlatformSelected(); - bool isExistingPlaylistSelected(); - bool selectionsMayModify(); - - void postSqlError(const QString& mainText, const QSqlError& sqlError); - void postListError(const QString& mainText, const QStringList& detailedItems); - void postIOError(const QString& mainText, const Qx::IoOpReport& report); - - void refreshEnableStates(); - void refreshCheckStates(); + void initializeLauncherHelpActions(); + // Input querying + bool selectionsMayModify(); QStringList getSelectedPlatforms() const; QStringList getSelectedPlaylists() const; + Import::PlaylistGameMode getSelectedPlaylistGameMode() const; Fp::Db::InclusionOptions getSelectedInclusionOptions() const; - Fe::UpdateOptions getSelectedUpdateOptions() const; - Fe::ImageMode getSelectedImageMode() const; - ImportWorker::PlaylistGameMode getSelectedPlaylistGameMode() const; + Import::UpdateOptions getSelectedUpdateOptions() const; + Import::ImageMode getSelectedImageMode() const; bool getForceDownloadImages() const; + // Import void prepareImport(); - void revertAllFrontendChanges(); - void deployCLIFp(const Fp::Install& fp, QMessageBox::Button abandonButton); - void standaloneCLIFpDeploy(); + + // Tags (move to controller?) void showTagSelectionDialog(); QSet generateTagExlusionSet() const; -protected: - void showEvent(QShowEvent* event); - -public: - bool initCompleted(); - -//-Slots--------------------------------------------------------------------------------------------------------- +//-Signals & Slots---------------------------------------------------------------------------------------------------- private slots: // Direct UI, start with "all" to avoid Qt calling "connectSlotsByName" on these slots (slots that start with "on_") void all_on_action_triggered(); - void all_on_lineEdit_editingFinished(); void all_on_pushButton_clicked(); - void all_on_listWidget_itemChanged(QListWidgetItem* item); - void all_on_radioButton_clicked(); void all_on_menu_triggered(QAction *action); + void all_on_lineEdit_editingFinished(); - // Import Exception Handling - void handleBlockingError(std::shared_ptr response, const Qx::Error& blockingError, QMessageBox::StandardButtons choices); - void handleAuthRequest(const QString& prompt, QAuthenticator* authenticator); - void handleImportResult(ImportWorker::ImportResult importResult, const Qx::Error& errorReport); +signals: + void installPathChanged(const QString& installPath, Import::Install type); + void importTriggered(Import::Selections sel, Import::OptionSet opt, bool mayModify); + void standaloneDeployTriggered(); }; #endif // MAINWINDOW_H diff --git a/app/src/ui/mainwindow.ui b/app/src/ui/mainwindow.ui index ba50b3f..6b2dc25 100644 --- a/app/src/ui/mainwindow.ui +++ b/app/src/ui/mainwindow.ui @@ -31,7 +31,7 @@ 9 - + 20 @@ -52,7 +52,7 @@ - + 0 @@ -87,16 +87,16 @@ - Qt::PlainText + Qt::TextFormat::PlainText - :/ui/No_Install.png + :/ui/No_Install.png true - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignVCenter @@ -124,7 +124,7 @@ - :/ui/No_Install.png + :/ui/No_Install.png true @@ -153,7 +153,7 @@ - + 1 @@ -228,9 +228,9 @@ 0 - + - Frontend Install: + Launcher Install: 0 @@ -238,7 +238,7 @@ - + true @@ -277,7 +277,7 @@ Platforms - Qt::AlignCenter + Qt::AlignmentFlag::AlignCenter @@ -296,7 +296,7 @@ - QAbstractItemView::NoSelection + QAbstractItemView::SelectionMode::NoSelection @@ -347,10 +347,10 @@ - Qt::Horizontal + Qt::Orientation::Horizontal - QSizePolicy::Preferred + QSizePolicy::Policy::Preferred @@ -368,7 +368,7 @@ Playlists - Qt::AlignCenter + Qt::AlignmentFlag::AlignCenter @@ -381,14 +381,14 @@ - QAbstractItemView::NoSelection + QAbstractItemView::SelectionMode::NoSelection - QLayout::SetDefaultConstraint + QLayout::SizeConstraint::SetDefaultConstraint @@ -450,7 +450,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -484,12 +484,9 @@ Help - + :/ui/Update_Mode_Info.png:/ui/Update_Mode_Info.png - - buttonGroup_imageMode - @@ -506,6 +503,9 @@ false + + buttonGroup_playlistGameMode + @@ -522,6 +522,9 @@ true + + buttonGroup_playlistGameMode + @@ -542,10 +545,10 @@ - Qt::Horizontal + Qt::Orientation::Horizontal - QSizePolicy::Expanding + QSizePolicy::Policy::Expanding @@ -579,7 +582,7 @@ Help - + :/ui/Update_Mode_Info.png:/ui/Update_Mode_Info.png @@ -604,6 +607,9 @@ true + + buttonGroup_updateMode + @@ -648,6 +654,9 @@ false + + buttonGroup_updateMode + @@ -777,7 +786,7 @@ Existing Item - Qt::AlignCenter + Qt::AlignmentFlag::AlignCenter @@ -836,7 +845,7 @@ Help - + :/ui/Update_Mode_Info.png:/ui/Update_Mode_Info.png @@ -844,7 +853,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -953,14 +962,14 @@ Help - + - Frontends + Launchers - + @@ -969,7 +978,7 @@ - + :/ui/Exit.png:/ui/Exit.png @@ -978,7 +987,7 @@ - + :/ui/GitHub.png:/ui/GitHub.png @@ -987,7 +996,7 @@ - + :/ui/GitHub.png:/ui/GitHub.png @@ -1005,7 +1014,7 @@ - + :/ui/CLIFp.png:/ui/CLIFp.png @@ -1057,7 +1066,7 @@ - + :/ui/About.png:/ui/About.png @@ -1066,7 +1075,7 @@ - + @@ -1086,7 +1095,7 @@ - pushButton_frontendBrowse + pushButton_launcherBrowse clicked() MainWindow all_on_pushButton_clicked() @@ -1101,38 +1110,6 @@ - - lineEdit_frontendPath - editingFinished() - MainWindow - all_on_lineEdit_editingFinished() - - - 170 - 94 - - - 399 - 299 - - - - - lineEdit_flashpointPath - editingFinished() - MainWindow - all_on_lineEdit_editingFinished() - - - 170 - 144 - - - 399 - 299 - - - pushButton_selectAll_platforms clicked() @@ -1197,22 +1174,6 @@ - - listWidget_platformChoices - itemChanged(QListWidgetItem*) - MainWindow - all_on_listWidget_itemChanged(QListWidgetItem*) - - - 125 - 336 - - - 244 - 333 - - - pushButton_exit clicked() @@ -1358,30 +1319,14 @@ - listWidget_playlistChoices - itemChanged(QListWidgetItem*) - MainWindow - all_on_listWidget_itemChanged(QListWidgetItem*) - - - 486 - 354 - - - 323 - 479 - - - - - radioButton_selectedPlatformsOnly - clicked() + action_editTagFilter + triggered() MainWindow - all_on_radioButton_clicked() + all_on_action_triggered() - 219 - 568 + -1 + -1 323 @@ -1390,14 +1335,14 @@ - radioButton_forceAll - clicked() + action_about + triggered() MainWindow - all_on_radioButton_clicked() + all_on_action_triggered() - 219 - 596 + -1 + -1 323 @@ -1406,14 +1351,14 @@ - action_editTagFilter - triggered() + lineEdit_launcherPath + editingFinished() MainWindow - all_on_action_triggered() + all_on_lineEdit_editingFinished() - -1 - -1 + 307 + 98 323 @@ -1422,14 +1367,14 @@ - action_about - triggered() + lineEdit_flashpointPath + editingFinished() MainWindow - all_on_action_triggered() + all_on_lineEdit_editingFinished() - -1 - -1 + 307 + 155 323 @@ -1449,6 +1394,8 @@ all_on_menu_triggered(QAction*) + + diff --git a/app/src/ui/progresspresenter.cpp b/app/src/ui/progresspresenter.cpp index 5ff3c9e..595e01e 100644 --- a/app/src/ui/progresspresenter.cpp +++ b/app/src/ui/progresspresenter.cpp @@ -24,7 +24,6 @@ void ProgressPresenter::setupProgressDialog() // Initialize dialog mDialog.setCancelButtonText(u"Cancel"_s); mDialog.setWindowModality(Qt::WindowModal); - mDialog.setWindowTitle(CAPTION_IMPORTING); mDialog.setWindowFlags(mDialog.windowFlags() & ~Qt::WindowContextHelpButtonHint); mDialog.setAutoReset(false); mDialog.setAutoClose(false); @@ -59,6 +58,8 @@ void ProgressPresenter::setBusyState() #endif } +void ProgressPresenter::setCaption(const QString& caption) { mDialog.setWindowTitle(caption); } + void ProgressPresenter::resetState() { #ifdef _WIN32 @@ -76,6 +77,8 @@ void ProgressPresenter::reset() #endif } +int ProgressPresenter::value() const { return mDialog.value(); } + //-Slots--------------------------------------------------------------------------------------------------------- //Public: void ProgressPresenter::setLabelText(const QString& text) { mDialog.setLabelText(text); } diff --git a/app/src/ui/progresspresenter.h b/app/src/ui/progresspresenter.h index 2ba9c46..1e5ed26 100644 --- a/app/src/ui/progresspresenter.h +++ b/app/src/ui/progresspresenter.h @@ -14,10 +14,6 @@ using namespace Qt::StringLiterals; class ProgressPresenter : public QObject { Q_OBJECT -//-Class Variables-------------------------------------------------------------------------------------------- -private: - static inline const QString CAPTION_IMPORTING = u"FP Import"_s; - //-Instance Variables-------------------------------------------------------------------------------------------- private: #ifdef _WIN32 @@ -37,8 +33,10 @@ class ProgressPresenter : public QObject void attachWindow(QWindow* window); void setErrorState(); void setBusyState(); + void setCaption(const QString& caption); void resetState(); void reset(); + int value() const; //-Slots--------------------------------------------------------------------------------------------------------- public slots: From ac56edca6bce5c7ff1bd1c937224df0eff9879b3 Mon Sep 17 00:00:00 2001 From: Christian Heimlich Date: Sun, 26 Jan 2025 10:51:00 -0500 Subject: [PATCH 03/20] Add templated interum classes for easier Launcher implementation Avoids the need for manual static_casts in launcher install classes and also allows for some further automation for the same. This allows for the install registry to be part of a template traits system and reduce the degree to which a macro is needed for registration. Also, move the responsibility of image task storage entirely to Worker, and have IPlatformDoc implement a sensible default for addSet() which forwards image processing to Install if the right mode is selected. Move item related concepts to lr-items-interface.h and use inline constraints. --- CMakeLists.txt | 2 +- app/CMakeLists.txt | 65 +- app/src/import/properties.cpp | 6 +- app/src/import/properties.h | 8 +- app/src/import/worker.cpp | 67 +- app/src/import/worker.h | 11 +- app/src/kernel/controller.cpp | 7 +- app/src/launcher/abstract/lr-data.h | 304 +++++++++ app/src/launcher/abstract/lr-data.tpp | 412 ++++++++++++ app/src/launcher/abstract/lr-install.h | 79 +++ app/src/launcher/abstract/lr-install.tpp | 134 ++++ app/src/launcher/abstract/lr-registration.cpp | 42 ++ app/src/launcher/abstract/lr-registration.h | 191 ++++++ .../attractmode/am-data.cpp | 352 +++------- .../attractmode/am-data.h | 123 ++-- .../implementation/attractmode/am-data.tpp | 193 ++++++ .../attractmode/am-install.cpp | 117 +--- .../attractmode/am-install.h | 34 +- .../attractmode/am-install_linux.cpp | 0 .../attractmode/am-install_win.cpp | 0 .../attractmode/am-items.cpp | 0 .../attractmode/am-items.h | 2 +- .../attractmode/am-registration.cpp | 4 + .../attractmode/am-registration.h | 33 + .../attractmode/am-settings-data.cpp | 34 +- .../attractmode/am-settings-data.h | 12 +- .../attractmode/am-settings-items.cpp | 0 .../attractmode/am-settings-items.h | 2 +- .../launchbox/lb-data.cpp | 165 +++-- .../{ => implementation}/launchbox/lb-data.h | 104 ++- .../launchbox/lb-install.cpp | 136 ++-- .../launchbox/lb-install.h | 39 +- .../launchbox/lb-items.cpp | 0 .../{ => implementation}/launchbox/lb-items.h | 2 +- .../launchbox/lb-registration.cpp | 4 + .../launchbox/lb-registration.h | 38 ++ .../launcher/interface/lr-data-interface.cpp | 219 +++++++ .../launcher/interface/lr-data-interface.h | 362 +++++++++++ .../lr-install-interface.cpp} | 134 ++-- .../lr-install-interface.h} | 83 ++- .../lr-install-interface_linux.cpp} | 6 +- .../lr-install-interface_win.cpp} | 6 +- .../lr-items-interface.cpp} | 2 +- .../lr-items-interface.h} | 82 ++- app/src/launcher/lr-data.cpp | 576 ----------------- app/src/launcher/lr-data.h | 608 ------------------ app/src/launcher/lr-install.cpp | 172 ----- app/src/launcher/lr-install.h | 112 ---- app/src/ui/mainwindow.cpp | 20 +- 49 files changed, 2710 insertions(+), 2394 deletions(-) create mode 100644 app/src/launcher/abstract/lr-data.h create mode 100644 app/src/launcher/abstract/lr-data.tpp create mode 100644 app/src/launcher/abstract/lr-install.h create mode 100644 app/src/launcher/abstract/lr-install.tpp create mode 100644 app/src/launcher/abstract/lr-registration.cpp create mode 100644 app/src/launcher/abstract/lr-registration.h rename app/src/launcher/{ => implementation}/attractmode/am-data.cpp (66%) rename app/src/launcher/{ => implementation}/attractmode/am-data.h (84%) create mode 100644 app/src/launcher/implementation/attractmode/am-data.tpp rename app/src/launcher/{ => implementation}/attractmode/am-install.cpp (78%) rename app/src/launcher/{ => implementation}/attractmode/am-install.h (73%) rename app/src/launcher/{ => implementation}/attractmode/am-install_linux.cpp (100%) rename app/src/launcher/{ => implementation}/attractmode/am-install_win.cpp (100%) rename app/src/launcher/{ => implementation}/attractmode/am-items.cpp (100%) rename app/src/launcher/{ => implementation}/attractmode/am-items.h (98%) create mode 100644 app/src/launcher/implementation/attractmode/am-registration.cpp create mode 100644 app/src/launcher/implementation/attractmode/am-registration.h rename app/src/launcher/{ => implementation}/attractmode/am-settings-data.cpp (92%) rename app/src/launcher/{ => implementation}/attractmode/am-settings-data.h (96%) rename app/src/launcher/{ => implementation}/attractmode/am-settings-items.cpp (100%) rename app/src/launcher/{ => implementation}/attractmode/am-settings-items.h (99%) rename app/src/launcher/{ => implementation}/launchbox/lb-data.cpp (86%) rename app/src/launcher/{ => implementation}/launchbox/lb-data.h (68%) rename app/src/launcher/{ => implementation}/launchbox/lb-install.cpp (72%) rename app/src/launcher/{ => implementation}/launchbox/lb-install.h (73%) rename app/src/launcher/{ => implementation}/launchbox/lb-items.cpp (100%) rename app/src/launcher/{ => implementation}/launchbox/lb-items.h (99%) create mode 100644 app/src/launcher/implementation/launchbox/lb-registration.cpp create mode 100644 app/src/launcher/implementation/launchbox/lb-registration.h create mode 100644 app/src/launcher/interface/lr-data-interface.cpp create mode 100644 app/src/launcher/interface/lr-data-interface.h rename app/src/launcher/{lr-installfoundation.cpp => interface/lr-install-interface.cpp} (61%) rename app/src/launcher/{lr-installfoundation.h => interface/lr-install-interface.h} (59%) rename app/src/launcher/{lr-installfoundation_linux.cpp => interface/lr-install-interface_linux.cpp} (81%) rename app/src/launcher/{lr-installfoundation_win.cpp => interface/lr-install-interface_win.cpp} (95%) rename app/src/launcher/{lr-items.cpp => interface/lr-items-interface.cpp} (99%) rename app/src/launcher/{lr-items.h => interface/lr-items-interface.h} (86%) delete mode 100644 app/src/launcher/lr-data.cpp delete mode 100644 app/src/launcher/lr-data.h delete mode 100644 app/src/launcher/lr-install.cpp delete mode 100644 app/src/launcher/lr-install.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 8c59f0a..34475e7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -74,7 +74,7 @@ endif() include(OB/FetchQx) ob_fetch_qx( - REF "6db968d73e681eac7b687b0f9105cde934e4a6b2" + REF "a7ff0ecbabc51c89b99b93812145a0fa6b2f7283" COMPONENTS ${FIL_QX_COMPONENTS} ) diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 5dc495c..c10bbe8 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -28,28 +28,35 @@ set(FIL_SOURCE import/settings.h import/worker.h import/worker.cpp - launcher/lr-data.h - launcher/lr-data.cpp - launcher/lr-installfoundation.h - launcher/lr-installfoundation.cpp - launcher/lr-installfoundation_win.cpp - launcher/lr-installfoundation_linux.cpp - launcher/lr-install.h - launcher/lr-install.cpp - launcher/lr-items.h - launcher/lr-items.cpp - launcher/attractmode/am-data.h - launcher/attractmode/am-data.cpp - launcher/attractmode/am-install.h - launcher/attractmode/am-install.cpp - launcher/attractmode/am-install_win.cpp - launcher/attractmode/am-install_linux.cpp - launcher/attractmode/am-items.h - launcher/attractmode/am-items.cpp - launcher/attractmode/am-settings-data.h - launcher/attractmode/am-settings-data.cpp - launcher/attractmode/am-settings-items.h - launcher/attractmode/am-settings-items.cpp + launcher/abstract/lr-data.h + launcher/abstract/lr-data.tpp + launcher/abstract/lr-install.h + launcher/abstract/lr-install.tpp + launcher/abstract/lr-registration.h + launcher/abstract/lr-registration.cpp + launcher/implementation/attractmode/am-data.h + launcher/implementation/attractmode/am-data.tpp + launcher/implementation/attractmode/am-data.cpp + launcher/implementation/attractmode/am-install.h + launcher/implementation/attractmode/am-install.cpp + launcher/implementation/attractmode/am-install_win.cpp + launcher/implementation/attractmode/am-install_linux.cpp + launcher/implementation/attractmode/am-items.h + launcher/implementation/attractmode/am-items.cpp + launcher/implementation/attractmode/am-settings-data.h + launcher/implementation/attractmode/am-settings-data.cpp + launcher/implementation/attractmode/am-settings-items.h + launcher/implementation/attractmode/am-settings-items.cpp + launcher/implementation/attractmode/am-registration.h + launcher/implementation/attractmode/am-registration.cpp + launcher/interface/lr-data-interface.h + launcher/interface/lr-data-interface.cpp + launcher/interface/lr-install-interface.h + launcher/interface/lr-install-interface.cpp + launcher/interface/lr-install-interface_win.cpp + launcher/interface/lr-install-interface_linux.cpp + launcher/interface/lr-items-interface.h + launcher/interface/lr-items-interface.cpp ui/mainwindow.h ui/mainwindow.cpp ui/mainwindow.ui @@ -64,12 +71,14 @@ set(FIL_SOURCE if(CMAKE_SYSTEM_NAME STREQUAL Windows) list(APPEND FIL_SOURCE - launcher/launchbox/lb-data.h - launcher/launchbox/lb-data.cpp - launcher/launchbox/lb-install.h - launcher/launchbox/lb-install.cpp - launcher/launchbox/lb-items.h - launcher/launchbox/lb-items.cpp + launcher/implementation/launchbox/lb-data.h + launcher/implementation/launchbox/lb-data.cpp + launcher/implementation/launchbox/lb-install.h + launcher/implementation/launchbox/lb-install.cpp + launcher/implementation/launchbox/lb-items.h + launcher/implementation/launchbox/lb-items.cpp + launcher/implementation/launchbox/lb-registration.h + launcher/implementation/launchbox/lb-registration.cpp ) endif() diff --git a/app/src/import/properties.cpp b/app/src/import/properties.cpp index 91dae9c..4c9b3d9 100644 --- a/app/src/import/properties.cpp +++ b/app/src/import/properties.cpp @@ -8,7 +8,7 @@ #include // Project Includes -#include "launcher/lr-install.h" +#include "launcher/interface/lr-install-interface.h" namespace Import { @@ -141,11 +141,11 @@ QList Properties::platforms() const { return mPlatforms; } const Qx::Bindable> Properties::bindablePlaylists() const { return mPlaylists; } QList Properties::playlists() const { return mPlaylists; } -void Properties::setLauncher(std::unique_ptr&& launcher) { mLauncher = std::move(launcher); } +void Properties::setLauncher(std::unique_ptr&& launcher) { mLauncher = std::move(launcher); } void Properties::setFlashpoint(std::unique_ptr&& flashpoint) { mFlashpoint = std::move(flashpoint); } void Properties::refreshInstallData() { gatherTargetData(); } -Lr::Install* Properties::launcher() { Q_ASSERT(*mLauncher); return (*mLauncher).get(); } +Lr::IInstall* Properties::launcher() { Q_ASSERT(*mLauncher); return (*mLauncher).get(); } Fp::Install* Properties::flashpoint() { Q_ASSERT(*mFlashpoint); return (*mFlashpoint).get(); }; } diff --git a/app/src/import/properties.h b/app/src/import/properties.h index 6a4d830..b6b0c77 100644 --- a/app/src/import/properties.h +++ b/app/src/import/properties.h @@ -22,7 +22,7 @@ * have control of the instances. */ -namespace Lr { class Install; } +namespace Lr { class IInstall; } namespace Fp { class Install; } namespace Import @@ -38,7 +38,7 @@ class Properties //-Instance Variables------------------------------------------------------------- private: bool mHasLinkPerms; - Qx::Property> mLauncher; + Qx::Property> mLauncher; Qx::Property> mFlashpoint; Qx::Property mLauncherReady; Qx::Property mFlashpointReady; @@ -85,11 +85,11 @@ class Properties const Qx::Bindable> bindablePlaylists() const; QList playlists() const; - void setLauncher(std::unique_ptr&& launcher); + void setLauncher(std::unique_ptr&& launcher); void setFlashpoint(std::unique_ptr&& flashpoint); void refreshInstallData(); - Lr::Install* launcher(); + Lr::IInstall* launcher(); Fp::Install* flashpoint(); }; diff --git a/app/src/import/worker.cpp b/app/src/import/worker.cpp index 0eb26a9..5043ae8 100644 --- a/app/src/import/worker.cpp +++ b/app/src/import/worker.cpp @@ -62,7 +62,7 @@ QString ImageTransferError::deriveCaption() const { return CAPTION_IMAGE_ERR; } //=============================================================================================================== //-Constructor--------------------------------------------------------------------------------------------------- -Worker::Worker(Fp::Install* flashpoint, Lr::Install* launcher, Selections importSelections, OptionSet optionSet) : +Worker::Worker(Fp::Install* flashpoint, Lr::IInstall* launcher, Selections importSelections, OptionSet optionSet) : mFlashpointInstall(flashpoint), mLauncherInstall(launcher), mImportSelections(importSelections), @@ -156,7 +156,7 @@ ImageTransferError Worker::transferImage(bool symlink, QString sourcePath, QStri return ImageTransferError(ImageTransferError::CantCreateDirectory, QString(), destinationDir.absolutePath()); // Determine backup path - QString backupPath = Lr::Install::filePathToBackupPath(destinationInfo.absoluteFilePath()); + QString backupPath = Lr::IInstall::filePathToBackupPath(destinationInfo.absoluteFilePath()); // Temporarily backup image if it already exists (also acts as deletion marking in case images for the title were removed in an update) if(destinationOccupied) @@ -197,14 +197,14 @@ ImageTransferError Worker::transferImage(bool symlink, QString sourcePath, QStri return ImageTransferError(); } -bool Worker::performImageJobs(const QList& jobs, bool symlink, Qx::ProgressGroup* pg) +bool Worker::performImageJobs(const QList& jobs, bool symlink, Qx::ProgressGroup* pg) { // Setup for image transfers ImageTransferError imageTransferError; // Error return reference *mBlockingErrorResponse = QMessageBox::NoToAll; // Default to choice "NoToAll" in case the signal is not correctly connected using Qt::BlockingQueuedConnection bool ignoreAllTransferErrors = false; // NoToAll response tracker - for(const Lr::Install::ImageMap& imageJob : jobs) + for(const Lr::IInstall::ImageMap& imageJob : jobs) { while((imageTransferError = transferImage(symlink, imageJob.sourcePath, imageJob.destPath)).isValid() && !ignoreAllTransferErrors) { @@ -229,7 +229,7 @@ bool Worker::performImageJobs(const QList& jobs, bool sym return true; } -Worker::Result Worker::processPlatformGames(Qx::Error& errorReport, std::unique_ptr& platformDoc, Fp::Db::QueryBuffer& gameQueryResult) +Worker::Result Worker::processPlatformGames(Qx::Error& errorReport, std::unique_ptr& platformDoc, Fp::Db::QueryBuffer& gameQueryResult) { const Fp::Toolkit* tk = mFlashpointInstall->toolkit(); @@ -276,11 +276,14 @@ Worker::Result Worker::processPlatformGames(Qx::Error& errorReport, std::unique_ // Get image information QFileInfo logoLocalInfo(tk->entryImageLocalPath(Fp::ImageType::Logo, builtGame.id())); QFileInfo ssLocalInfo(tk->entryImageLocalPath(Fp::ImageType::Screenshot, builtGame.id())); - - // Add set to doc QString checkedLogoPath = (logoLocalInfo.exists() || mOptionSet.downloadImages) ? logoLocalInfo.absoluteFilePath() : QString(); QString checkedScreenshotPath = (ssLocalInfo.exists() || mOptionSet.downloadImages) ? ssLocalInfo.absoluteFilePath() : QString(); - platformDoc->addSet(builtSet, Lr::ImageSources(checkedLogoPath, checkedScreenshotPath)); + Lr::ImagePaths imagePaths(checkedLogoPath, checkedScreenshotPath); + Lr::IInstall::ImageMap logoMap{.sourcePath = imagePaths.logoPath(), .destPath = ""}; + Lr::IInstall::ImageMap screenshotMap{.sourcePath = imagePaths.screenshotPath(), .destPath = ""}; + + // Add set to doc + platformDoc->addSet(builtSet, imagePaths); // Add ID to imported game cache mImportedGameIdsCache.insert(builtGame.id()); @@ -305,13 +308,20 @@ Worker::Result Worker::processPlatformGames(Qx::Error& errorReport, std::unique_ mProgressManager.group(Pg::ImageDownload)->decrementMaximum(); // Already exists, remove download step from progress bar } - // Handle image transfer progress + // Handle image transfer if(mOptionSet.imageMode == ImageMode::Copy || mOptionSet.imageMode == ImageMode::Link) { - // Adjust progress if images aren't available - if(checkedLogoPath.isEmpty()) + logoMap.destPath = imagePaths.logoPath(); + screenshotMap.destPath = imagePaths.screenshotPath(); + + if(!logoMap.destPath.isEmpty()) + mImageTransferJobs.append(logoMap); + else mProgressManager.group(Pg::ImageTransfer)->decrementMaximum(); // Can't transfer image that doesn't/won't exist - if(checkedScreenshotPath.isEmpty()) + + if(!screenshotMap.destPath.isEmpty()) + mImageTransferJobs.append(screenshotMap); + else mProgressManager.group(Pg::ImageTransfer)->decrementMaximum(); // Can't transfer image that doesn't/won't exist } @@ -397,7 +407,7 @@ Worker::Result Worker::processGames(Qx::Error& errorReport, QList currentPlatformDoc; + std::unique_ptr currentPlatformDoc; Lr::DocHandlingError platformReadError = mLauncherInstall->checkoutPlatformDoc(currentPlatformDoc, currentQueryResult.source); // Stop import if error occurred @@ -461,7 +471,7 @@ Worker::Result Worker::processPlaylists(Qx::Error& errorReport, const QList currentPlaylistDoc; + std::unique_ptr currentPlaylistDoc; Lr::DocHandlingError playlistReadError = mLauncherInstall->checkoutPlaylistDoc(currentPlaylistDoc, currentPlaylist.title()); // Stop import if error occurred @@ -559,15 +569,14 @@ Worker::Result Worker::processImages(Qx::Error& errorReport) emit progressStepChanged(STEP_IMPORTING_IMAGES); // Provide launcher with bulk reference locations and acquire any transfer tasks - QList imageTransferJobs; - Lr::ImageSources bulkSources; + Lr::ImagePaths bulkSources; if(mOptionSet.imageMode == ImageMode::Reference) { bulkSources.setLogoPath(QDir::toNativeSeparators(mFlashpointInstall->entryLogosDirectory().absolutePath())); bulkSources.setScreenshotPath(QDir::toNativeSeparators(mFlashpointInstall->entryScreenshotsDirectory().absolutePath())); } - Qx::Error imageExchangeError = mLauncherInstall->preImageProcessing(imageTransferJobs, bulkSources); + Qx::Error imageExchangeError = mLauncherInstall->preImageProcessing(bulkSources); if(imageExchangeError.isValid()) { @@ -584,14 +593,16 @@ Worker::Result Worker::processImages(Qx::Error& errorReport) * For example, this may happen with infinity if a game hasn't been clicked on, as the logo * will have been downloaded but not the screenshot */ - if(static_cast(imageTransferJobs.size()) != mProgressManager.group(Pg::ImageTransfer)->maximum()) - mProgressManager.group(Pg::ImageTransfer)->setMaximum(imageTransferJobs.size()); + if(static_cast(mImageTransferJobs.size()) != mProgressManager.group(Pg::ImageTransfer)->maximum()) + mProgressManager.group(Pg::ImageTransfer)->setMaximum(mImageTransferJobs.size()); - if(!performImageJobs(imageTransferJobs, mOptionSet.imageMode == ImageMode::Link, mProgressManager.group(Pg::ImageTransfer))) + if(!performImageJobs(mImageTransferJobs, mOptionSet.imageMode == ImageMode::Link, mProgressManager.group(Pg::ImageTransfer))) return Canceled; + + mImageTransferJobs.clear(); } - else if(!imageTransferJobs.isEmpty()) - qWarning("the launcher provided image transfers when the mode wasn't link/copy"); + else if(!mImageTransferJobs.isEmpty()) + qFatal("the launcher provided image transfers when the mode wasn't link/copy"); // Handle launcher specific actions mLauncherInstall->postImageProcessing(); @@ -603,7 +614,7 @@ Worker::Result Worker::processImages(Qx::Error& errorReport) Worker::Result Worker::processIcons(Qx::Error& errorReport, const QStringList& platforms, const QList& playlists) { - QList jobs; + QList jobs; QString mainDest = mLauncherInstall->platformCategoryIconPath(); std::optional platformDestDir = mLauncherInstall->platformIconsDirectory(); std::optional playlistDestDir = mLauncherInstall->playlistIconsDirectory(); @@ -612,7 +623,7 @@ Worker::Result Worker::processIcons(Qx::Error& errorReport, const QStringList& p // Main Job if(!mainDest.isEmpty()) - jobs.emplace_back(Lr::Install::ImageMap{.sourcePath = u":/flashpoint/icon.png"_s, .destPath = mainDest}); + jobs.emplace_back(Lr::IInstall::ImageMap{.sourcePath = u":/flashpoint/icon.png"_s, .destPath = mainDest}); // Platform jobs if(platformDestDir) @@ -622,7 +633,7 @@ Worker::Result Worker::processIcons(Qx::Error& errorReport, const QStringList& p { QString src = tk->platformLogoPath(p); if(QFile::exists(src)) - jobs.emplace_back(Lr::Install::ImageMap{.sourcePath = src, + jobs.emplace_back(Lr::IInstall::ImageMap{.sourcePath = src, .destPath = pdd.absoluteFilePath(p + ".png")}); } } @@ -662,7 +673,7 @@ Worker::Result Worker::processIcons(Qx::Error& errorReport, const QStringList& p * Use translated name for destination since that's what the launcher is expecting */ QString sFilename = p.title() + ".png"; - QString dFilename = mLauncherInstall->translateDocName(p.title(), Lr::DataDoc::Type::Playlist) + ".png";; + QString dFilename = mLauncherInstall->translateDocName(p.title(), Lr::IDataDoc::Type::Playlist) + ".png";; QString source = iconInflateDir.filePath(sFilename); QString dest = pdd.absoluteFilePath(dFilename); @@ -673,7 +684,7 @@ Worker::Result Worker::processIcons(Qx::Error& errorReport, const QStringList& p return Failed; } - jobs.emplace_back(Lr::Install::ImageMap{.sourcePath = source, .destPath = dest}); + jobs.emplace_back(Lr::IInstall::ImageMap{.sourcePath = source, .destPath = dest}); } } @@ -831,7 +842,7 @@ Worker::Result Worker::doImport(Qx::Error& errorReport) connect(&mProgressManager, &Qx::GroupedProgressManager::progressUpdated, this, &Worker::pmProgressUpdated); //-Handle Launcher Specific Import Setup------------------------------ - Lr::Install::ImportDetails details{ + Lr::IInstall::ImportDetails details{ .updateOptions = mOptionSet.updateOptions, .imageMode = mOptionSet.imageMode, .clifpPath = CLIFp::standardCLIFpPath(*mFlashpointInstall), diff --git a/app/src/import/worker.h b/app/src/import/worker.h index 8d0444e..96369d2 100644 --- a/app/src/import/worker.h +++ b/app/src/import/worker.h @@ -13,7 +13,7 @@ #include // Project Includes -#include "launcher/lr-install.h" +#include "launcher/interface/lr-install-interface.h" namespace Import { @@ -108,10 +108,11 @@ class Worker : public QObject private: // Install links Fp::Install* mFlashpointInstall; - Lr::Install* mLauncherInstall; + Lr::IInstall* mLauncherInstall; // Image processing Qx::SyncDownloadManager mImageDownloadManager; + QList mImageTransferJobs; // Job details Selections mImportSelections; @@ -133,7 +134,7 @@ class Worker : public QObject //-Constructor--------------------------------------------------------------------------------------------------- public: - Worker(Fp::Install* flashpoint, Lr::Install* launcher, Selections importSelections, OptionSet optionSet); + Worker(Fp::Install* flashpoint, Lr::IInstall* launcher, Selections importSelections, OptionSet optionSet); //-Instance Functions--------------------------------------------------------------------------------------------------------- private: @@ -141,8 +142,8 @@ class Worker : public QObject Qx::Error preloadPlaylists(QList& targetPlaylists); QList getPlaylistSpecificGameIds(const QList& playlists); ImageTransferError transferImage(bool symlink, QString sourcePath, QString destPath); - bool performImageJobs(const QList& jobs, bool symlink, Qx::ProgressGroup* pg = nullptr); - Result processPlatformGames(Qx::Error& errorReport, std::unique_ptr& platformDoc, Fp::Db::QueryBuffer& gameQueryResult); + bool performImageJobs(const QList& jobs, bool symlink, Qx::ProgressGroup* pg = nullptr); + Result processPlatformGames(Qx::Error& errorReport, std::unique_ptr& platformDoc, Fp::Db::QueryBuffer& gameQueryResult); void cullUnimportedPlaylistGames(QList& playlists); Result preloadAddApps(Qx::Error& errorReport, Fp::Db::QueryBuffer& addAppQuery); diff --git a/app/src/kernel/controller.cpp b/app/src/kernel/controller.cpp index cbdee59..47cdc06 100644 --- a/app/src/kernel/controller.cpp +++ b/app/src/kernel/controller.cpp @@ -10,6 +10,9 @@ #include #include +// Project Includes +#include "launcher/abstract/lr-registration.h" + /* TODO: Consider having this tool deploy a .ini file (or the like) into the target launcher install * (with the exact location probably being guided by the specific Install child) that saves the settings * used for the import, so that they can be loaded again when that install is targeted by future versions @@ -213,8 +216,8 @@ void Controller::updateInstallPath(const QString& installPath, Import::Install t mImportProperties.setLauncher(nullptr); else { - std::unique_ptr launcher; - launcher = Lr::Install::acquireMatch(checkedPath); + std::unique_ptr launcher; + launcher = Lr::Registry::acquireMatch(checkedPath); if(!launcher->isValid()) { QMessageBox::critical(&mMainWindow, QApplication::applicationName(), MSG_LR_INSTALL_INVALID); diff --git a/app/src/launcher/abstract/lr-data.h b/app/src/launcher/abstract/lr-data.h new file mode 100644 index 0000000..befde5a --- /dev/null +++ b/app/src/launcher/abstract/lr-data.h @@ -0,0 +1,304 @@ +#ifndef LR_DATA_H +#define LR_DATA_H + +// Project Includes +#include "launcher/interface/lr-data-interface.h" +#include "launcher/abstract/lr-registration.h" + +/* NOTE: These classes are convenience versions of the ones in the 'interface' folder that are templated + * so that some of the types involved will be the launcher specific versions instead of a generic base + * pointer (meaning less manual casting is required while using them), and so that a few of their methods + * are automatically implemented according to the types at play. + * + * Generally, these are the classes that should be derived form to add a frontend + */ + +namespace Lr +{ + +/* We just repeat the templated 'parent' methods here in order to avoid needing virtual inheritance for IDataDoc if + * only DataDoc had it and then all derived templates types derived from DataDoc as well as the specific derived + * interface type. Maybe its not worth the clutter, but doing it this way for now. + */ + + +/* This was going to have another parameter for Type (doc type), but it created trouble when using + * the readers/writers and dealing with diamond inheritance where one of the branches already defined + * type(). + */ +template +class DataDoc : public IDataDoc +{ +protected: + using InstallT = Id::InstallT; +//-Constructor------------------------------------------------------------------------------------------------------ +protected: + DataDoc(InstallT* install, const QString& docPath, QString docName); + +//-Destructor------------------------------------------------------------------------------------------------- +public: + virtual ~DataDoc() = default; + +//-Instance Functions----------------------------------------------------------------------------------------------- +public: + InstallT* install() const; + + // IMPLEMENT + using IDataDoc::type; + using IDataDoc::isEmpty; +}; + +template +class DataDocReader : public IDataDoc::Reader +{ +//-Constructor-------------------------------------------------------------------------------------------------------- +protected: + DataDocReader(DocT* targetDoc); + +//-Destructor------------------------------------------------------------------------------------------------- +public: + virtual ~DataDocReader() = default; + +//-Instance Functions------------------------------------------------------------------------------------------------- +protected: + DocT* target() const; + +public: + // IMPLEMENT + using IDataDoc::Reader::readInto; +}; + +template +class DataDocWriter : public IDataDoc::Writer +{ +//-Constructor-------------------------------------------------------------------------------------------------------- +protected: + DataDocWriter(DocT* sourceDoc); + +//-Destructor------------------------------------------------------------------------------------------------- +public: + virtual ~DataDocWriter() = default; + +//-Instance Functions------------------------------------------------------------------------------------------------- +protected: + DocT* source() const; + +public: + // IMPLEMENT + using IDataDoc::Writer::writeOutOf; +}; + +template +class UpdateableDoc : public IUpdateableDoc +{ +protected: + using InstallT = Id::InstallT; +//-Constructor-------------------------------------------------------------------------------------------------------- +protected: + explicit UpdateableDoc(InstallT* install, const QString& docPath, QString docName, const Import::UpdateOptions& updateOptions); + +//-Instance Functions-------------------------------------------------------------------------------------------------- +public: + InstallT* install() const; + + // IMPLEMENT + using IUpdateableDoc::isEmpty; + + // OPTIONALLY RE-IMPELEMENT + using IUpdateableDoc::finalize; +}; + +template +class PlatformDoc : public IPlatformDoc +{ +protected: + using InstallT = Id::InstallT; + using GameT = Id::GameT; +//-Constructor-------------------------------------------------------------------------------------------------------- +protected: + explicit PlatformDoc(InstallT* install, const QString& docPath, QString docName, const Import::UpdateOptions& updateOptions); + +//-Instance Functions-------------------------------------------------------------------------------------------------- +private: + // IMPLEMENT + virtual std::shared_ptr processSet(const Fp::Set& set) = 0; + +public: + InstallT* install() const; + + void addSet(const Fp::Set& set, ImagePaths& images) override; + + // IMPLEMENT + using IPlatformDoc::isEmpty; + using IPlatformDoc::containsGame; // NOTE: UNUSED + using IPlatformDoc::containsAddApp; // NOTE: UNUSED + + // OPTIONALLY RE-IMPELEMENT + using IPlatformDoc::finalize; +}; + +template +class PlaylistDoc : public IPlaylistDoc +{ +protected: + using InstallT = Id::InstallT; +//-Constructor-------------------------------------------------------------------------------------------------------- +protected: + explicit PlaylistDoc(InstallT* install, const QString& docPath, QString docName, const Import::UpdateOptions& updateOptions); + +//-Instance Functions-------------------------------------------------------------------------------------------------- +public: + InstallT* install() const; + + // IMPLEMENT + using IPlaylistDoc::isEmpty; + using IPlaylistDoc::containsPlaylistGame; // NOTE: UNUSED + using IPlaylistDoc::setPlaylistData; + + // OPTIONALLY RE-IMPELEMENT + using IPlaylistDoc::finalize; +}; + + +template +class BasicPlatformDoc : public PlatformDoc +{ +protected: + using InstallT = Id::InstallT; + using GameT = Id::GameT; + using AddAppT = Id::AddAppT; + using IErrorable::mError; + using IUpdateableDoc::finalizeUpdateableItems; + using IUpdateableDoc::addUpdateableItem; + +//-Instance Variables-------------------------------------------------------------------------------------------------- +protected: + QHash> mGamesFinal; + QHash> mGamesExisting; + QHash> mAddAppsFinal; + QHash> mAddAppsExisting; + +//-Constructor-------------------------------------------------------------------------------------------------------- +protected: + explicit BasicPlatformDoc(InstallT* install, const QString& docPath, QString docName, const Import::UpdateOptions& updateOptions); + +//-Instance Functions-------------------------------------------------------------------------------------------------- +protected: + // IMPLEMENT + virtual std::shared_ptr prepareGame(const Fp::Game& game) = 0; + virtual std::shared_ptr prepareAddApp(const Fp::AddApp& game) = 0; + +public: + InstallT* install() const; + + const QHash>& finalGames() const; + const QHash>& finalAddApps() const; + bool containsGame(QUuid gameId) const override; // NOTE: UNUSED + bool containsAddApp(QUuid addAppId) const override; // NOTE: UNUSED + + std::shared_ptr processSet(const Fp::Set& set) override; + void finalize() override; + + // OPTIONALLY RE-IMPELEMENT + virtual bool isEmpty() const override; +}; + +template +class BasicPlaylistDoc : public PlaylistDoc +{ +protected: + using InstallT = Id::InstallT; + using PlaylistHeaderT = Id::PlaylistHeaderT; + using PlaylistGameT = Id::PlaylistGameT; + using IErrorable::mError; + using IUpdateableDoc::finalizeUpdateableItems; + using IUpdateableDoc::addUpdateableItem; + +//-Instance Variables-------------------------------------------------------------------------------------------------- +protected: + std::shared_ptr mPlaylistHeader; + QHash> mPlaylistGamesFinal; + QHash> mPlaylistGamesExisting; + +//-Constructor-------------------------------------------------------------------------------------------------------- +protected: + explicit BasicPlaylistDoc(InstallT* install, const QString& docPath, QString docName, const Import::UpdateOptions& updateOptions); + +//-Instance Functions-------------------------------------------------------------------------------------------------- +protected: + // IMPLEMENT + virtual std::shared_ptr preparePlaylistHeader(const Fp::Playlist& playlist) = 0; + virtual std::shared_ptr preparePlaylistGame(const Fp::PlaylistGame& game) = 0; + +public: + InstallT* install() const; + + const std::shared_ptr& playlistHeader() const; + const QHash>& finalPlaylistGames() const; + bool containsPlaylistGame(QUuid gameId) const override; + void setPlaylistData(const Fp::Playlist& playlist) override; + void finalize() override; + + // OPTIONALLY RE-IMPELEMENT + virtual bool isEmpty() const override; +}; + +template +class XmlDocReader : public DataDocReader +{ +protected: + using DataDocReader::target; +//-Instance Variables-------------------------------------------------------------------------------------------------- +protected: + QFile mXmlFile; + QXmlStreamReader mStreamReader; + QString mRootElement; + +//-Constructor-------------------------------------------------------------------------------------------------------- +public: + XmlDocReader(DocT* targetDoc, const QString& root); + +//-Instance Functions------------------------------------------------------------------------------------------------- +private: + // IMPLEMENT + virtual DocHandlingError readTargetDoc() = 0; + +protected: + DocHandlingError streamStatus() const; + +public: + DocHandlingError readInto() override; +}; + +template +class XmlDocWriter : public DataDocWriter +{ +protected: + using DataDocWriter::source; +//-Instance Variables-------------------------------------------------------------------------------------------------- +protected: + QFile mXmlFile; + QXmlStreamWriter mStreamWriter; + QString mRootElement; + +//-Constructor-------------------------------------------------------------------------------------------------------- +public: + XmlDocWriter(DocT* sourceDoc, const QString& root); + +//-Instance Functions------------------------------------------------------------------------------------------------- +protected: + void writeCleanTextElement(const QString& qualifiedName, const QString& text); + void writeOtherFields(const QHash& otherFields); + DocHandlingError streamStatus() const; + + // IMPLEMENT + virtual bool writeSourceDoc() = 0; + +public: + DocHandlingError writeOutOf() override; +}; + +} + +#include "lr-data.tpp" +#endif // LR_DATA_H diff --git a/app/src/launcher/abstract/lr-data.tpp b/app/src/launcher/abstract/lr-data.tpp new file mode 100644 index 0000000..d3ae6db --- /dev/null +++ b/app/src/launcher/abstract/lr-data.tpp @@ -0,0 +1,412 @@ +#ifndef LR_DATA_TPP +#define LR_DATA_TPP + +#include "lr-data.h" // Ignore recursive error, doesn't actually cause problem + +// Qx Includes +#include +#include + +// Project Includes +#include "launcher/abstract/lr-install.h" + +namespace Lr +{ + +//=============================================================================================================== +// DataDoc +//=============================================================================================================== + +//-Constructor-------------------------------------------------------------------------------------------------------- +//Protected: +template +DataDoc::DataDoc(InstallT* install, const QString& docPath, QString docName) : + IDataDoc(install, docPath, docName) +{} + +//-Instance Functions-------------------------------------------------------------------------------------------------- +//Public: +template +Id::InstallT* DataDoc::install() const { return static_cast(IDataDoc::install()); } + +//=============================================================================================================== +// DataDoc::Reader +//=============================================================================================================== + +//-Constructor-------------------------------------------------------------------------------------------------------- +//Protected: +template +DataDocReader::DataDocReader(DocT* targetDoc) : + IDataDoc::Reader(targetDoc) +{} + +//-Instance Functions-------------------------------------------------------------------------------------------------- +//Protected: +template +DocT* DataDocReader::target() const { return static_cast(IDataDoc::Reader::target()); } + +//=============================================================================================================== +// DataDoc::Writer +//=============================================================================================================== + +//-Constructor-------------------------------------------------------------------------------------------------------- +//Protected: +template +DataDocWriter::DataDocWriter(DocT* sourceDoc) : + IDataDoc::Writer(sourceDoc) +{} + +//-Instance Functions-------------------------------------------------------------------------------------------------- +//Protected: +template +DocT* DataDocWriter::source() const { return static_cast(IDataDoc::Writer::source()); } + +//=============================================================================================================== +// UpdateableDoc +//=============================================================================================================== + +//-Constructor-------------------------------------------------------------------------------------------------------- +//Protected: +template +UpdateableDoc::UpdateableDoc(InstallT* install, const QString& docPath, QString docName, const Import::UpdateOptions& updateOptions) : + IUpdateableDoc(install, docPath, docName, updateOptions) +{} + +//-Instance Functions-------------------------------------------------------------------------------------------------- +//Public: +template +Id::InstallT* UpdateableDoc::install() const { return static_cast(IDataDoc::install()); } + +//=============================================================================================================== +// PlatformDoc +//=============================================================================================================== + +//-Constructor-------------------------------------------------------------------------------------------------------- +//Protected: +template +PlatformDoc::PlatformDoc(InstallT* install, const QString& docPath, QString docName, const Import::UpdateOptions& updateOptions) : + IPlatformDoc(install, docPath, docName, updateOptions) +{} + +//-Instance Functions-------------------------------------------------------------------------------------------------- +//Public: +template +Id::InstallT* PlatformDoc::install() const { return static_cast(IDataDoc::install()); } + +template +void PlatformDoc::addSet(const Fp::Set& set, ImagePaths& images) +{ + if(!mError.isValid()) + { + // Process set (technically this can fail and return nullptr) + std::shared_ptr game = processSet(set); + + /* Process single image if applicable. + * + * The derived install type will not be defined at this point so we must access install() via + * the abstract base type. + */ + auto install = static_cast*>(IPlatformDoc::install()); + if(game && install->importDetails().imageMode != Import::ImageMode::Reference) + install->convertToDestinationImages(*game, images); + } +} + +//=============================================================================================================== +// PlaylistDoc +//=============================================================================================================== + +//-Constructor-------------------------------------------------------------------------------------------------------- +//Protected: +template +PlaylistDoc::PlaylistDoc(InstallT* install, const QString& docPath, QString docName, const Import::UpdateOptions& updateOptions) : + IPlaylistDoc(install, docPath, docName, updateOptions) +{} + +//-Instance Functions-------------------------------------------------------------------------------------------------- +//Public: +template +Id::InstallT* PlaylistDoc::install() const { return static_cast(IDataDoc::install()); } + +//=============================================================================================================== +// BasicPlatformDoc +//=============================================================================================================== + +//-Constructor-------------------------------------------------------------------------------------------------------- +//Protected: +template +BasicPlatformDoc::BasicPlatformDoc(InstallT* install, const QString& docPath, QString docName, const Import::UpdateOptions& updateOptions) : + PlatformDoc(install, docPath, docName, updateOptions) +{} + +//-Instance Functions-------------------------------------------------------------------------------------------------- +//Public: +template +Id::InstallT* BasicPlatformDoc::install() const { return static_cast(IDataDoc::install()); } + +template +const QHash>& BasicPlatformDoc::finalGames() const { return mGamesFinal; } + +template +const QHash>& BasicPlatformDoc::finalAddApps() const { return mAddAppsFinal; } + +template +bool BasicPlatformDoc::containsGame(QUuid gameId) const { return mGamesFinal.contains(gameId) || mGamesExisting.contains(gameId); } + +template +bool BasicPlatformDoc::containsAddApp(QUuid addAppId) const { return mAddAppsFinal.contains(addAppId) || mAddAppsExisting.contains(addAppId); } + +template +std::shared_ptr BasicPlatformDoc::processSet(const Fp::Set& set) +{ + std::shared_ptr game; + if(!mError.isValid()) + { + // Prepare game + game = prepareGame(set.game()); + + // Add game + addUpdateableItem(mGamesExisting, mGamesFinal, game); + + // Handle additional apps + for(const Fp::AddApp& addApp : set.addApps()) + { + // Prepare + std::shared_ptr lrAddApp = prepareAddApp(addApp); + + // Add + addUpdateableItem(mAddAppsExisting, mAddAppsFinal, lrAddApp); + } + } + return game; +} + +template +void BasicPlatformDoc::finalize() +{ + if(!mError.isValid()) + { + /* TODO: Have this (and all other implementations of finalize() do something like return + * the IDs of titles that were removed, or otherwise populate an internal variable so that afterwards + * the list can be used to purge all images or other title related files (like overviews with AM). + * Right now only the data portion of old games is removed) + */ + + // Finalize item stores + finalizeUpdateableItems(mGamesExisting, mGamesFinal); + finalizeUpdateableItems(mAddAppsExisting, mAddAppsFinal); + + // Perform base finalization + IUpdateableDoc::finalize(); + } +} + +template +bool BasicPlatformDoc::isEmpty() const +{ + return mGamesFinal.isEmpty() && mGamesExisting.isEmpty() && mAddAppsFinal.isEmpty() && mAddAppsExisting.isEmpty(); +} + +//=============================================================================================================== +// BasicPlaylistDoc +//=============================================================================================================== + +//-Constructor-------------------------------------------------------------------------------------------------------- +//Public: +/* NOTE: Right now mPlaylistHeader is left uninitialized (unless done so explicitly by a derivative). This is fine, + * as currently 'void PlaylistDoc::setPlaylistHeader(Fp::Playlist playlist)' checks to see if an existing header + * is present before performing a field transfer (i.e. in case the playlist doc didn't already exist); however, + * if more parts of the process end up needing to interact with a doc that has a potentially null playlist header, + * it may be better to require a value for it in this base class' constructor so that all derivatives must provide + * a default (likely null/empty) playlist header. + */ +template +BasicPlaylistDoc::BasicPlaylistDoc(InstallT* install, const QString& docPath, QString docName, const Import::UpdateOptions& updateOptions) : + PlaylistDoc(install, docPath, docName, updateOptions) +{} + +//-Instance Functions-------------------------------------------------------------------------------------------------- +//Public: +template +Id::InstallT* BasicPlaylistDoc::install() const { return static_cast(IDataDoc::install()); } + +template +const std::shared_ptr& BasicPlaylistDoc::playlistHeader() const { return mPlaylistHeader; } + +template +const QHash>& BasicPlaylistDoc::finalPlaylistGames() const { return mPlaylistGamesFinal; } + +template +bool BasicPlaylistDoc::containsPlaylistGame(QUuid gameId) const { return mPlaylistGamesFinal.contains(gameId) || mPlaylistGamesExisting.contains(gameId); } + +template +void BasicPlaylistDoc::setPlaylistData(const Fp::Playlist& playlist) +{ + if(!mError.isValid()) + { + std::shared_ptr lrPlaylistHeader = preparePlaylistHeader(playlist); + + // Ensure doc already existed before transferring (null check) + if(mPlaylistHeader) + lrPlaylistHeader->transferOtherFields(mPlaylistHeader->otherFields()); + + // Set instance header to new one + mPlaylistHeader = lrPlaylistHeader; + + for(const auto& plg : playlist.playlistGames()) + { + // Prepare playlist game + std::shared_ptr lrPlaylistGame = preparePlaylistGame(plg); + + // Add playlist game + addUpdateableItem(mPlaylistGamesExisting, mPlaylistGamesFinal, lrPlaylistGame); + } + } +} + +template +void BasicPlaylistDoc::finalize() +{ + if(!mError.isValid()) + { + // Finalize item stores + finalizeUpdateableItems(mPlaylistGamesExisting, mPlaylistGamesFinal); + + // Perform base finalization + IUpdateableDoc::finalize(); + } +} + +template +bool BasicPlaylistDoc::isEmpty() const +{ + // The playlist header doesn't matter if there are no games + return mPlaylistGamesFinal.isEmpty() && mPlaylistGamesExisting.isEmpty(); +} + +//=============================================================================================================== +// XmlDocReader +//=============================================================================================================== + +//-Constructor-------------------------------------------------------------------------------------------------------- +//Public: +template +XmlDocReader::XmlDocReader(DocT* targetDoc, const QString& root) : + DataDocReader(targetDoc), + mXmlFile(targetDoc->path()), + mStreamReader(&mXmlFile), + mRootElement(root) +{} + +//-Instance Functions------------------------------------------------------------------------------------------------- +//Protected: +template +DocHandlingError XmlDocReader::streamStatus() const +{ + if(mStreamReader.hasError()) + { + Qx::XmlStreamReaderError xmlError(mStreamReader); + return DocHandlingError(*target(), DocHandlingError::DocReadFailed, xmlError.text()); + } + + return DocHandlingError(); +} + +//Public: +template +DocHandlingError XmlDocReader::readInto() +{ + // Open File + if(!mXmlFile.open(QFile::ReadOnly)) + return DocHandlingError(*target(), DocHandlingError::DocCantOpen, mXmlFile.errorString()); + + if(!mStreamReader.readNextStartElement()) + { + Qx::XmlStreamReaderError xmlError(mStreamReader); + return DocHandlingError(*target(), DocHandlingError::DocReadFailed, xmlError.text()); + } + + if(mStreamReader.name() != mRootElement) + return DocHandlingError(*target(), DocHandlingError::NotParentDoc); + + return readTargetDoc(); + + // File is automatically closed when reader is destroyed... +} + +//=============================================================================================================== +// XmlDocWriter +//=============================================================================================================== + +//-Constructor-------------------------------------------------------------------------------------------------------- +//Public: +template +XmlDocWriter::XmlDocWriter(DocT* sourceDoc, const QString& root) : + DataDocWriter(sourceDoc), + mXmlFile(sourceDoc->path()), + mStreamWriter(&mXmlFile), + mRootElement(root) +{} + +//-Instance Functions------------------------------------------------------------------------------------------------- +//Protected: +template +void XmlDocWriter::writeCleanTextElement(const QString& qualifiedName, const QString& text) +{ + if(text.isEmpty()) + mStreamWriter.writeEmptyElement(qualifiedName); + else + mStreamWriter.writeTextElement(qualifiedName, Qx::xmlSanitized(text)); +} + +template +void XmlDocWriter::writeOtherFields(const QHash& otherFields) +{ + for(QHash::const_iterator i = otherFields.constBegin(); i != otherFields.constEnd(); ++i) + writeCleanTextElement(i.key(), i.value()); +} + +template +DocHandlingError XmlDocWriter::streamStatus() const +{ + return mStreamWriter.hasError() ? DocHandlingError(*source(), DocHandlingError::DocWriteFailed, mStreamWriter.device()->errorString()) : + DocHandlingError(); +} + +//Public: +template +DocHandlingError XmlDocWriter::writeOutOf() +{ + // Open File + if(!mXmlFile.open(QFile::WriteOnly | QFile::Truncate)) // Discard previous contents + return DocHandlingError(*source(), DocHandlingError::DocCantSave, mXmlFile.errorString()); + + // Enable auto formatting + mStreamWriter.setAutoFormatting(true); + mStreamWriter.setAutoFormattingIndent(2); + + // Write standard XML header + mStreamWriter.writeStartDocument(u"1.0"_s, true); + + // Write main LaunchBox tag + mStreamWriter.writeStartElement(mRootElement); + + // Write main body + if(!writeSourceDoc()) + return streamStatus(); + + // Close main LaunchBox tag + mStreamWriter.writeEndElement(); + + // Finish document + mStreamWriter.writeEndDocument(); + + // Return null string on success + return streamStatus(); + + // File is automatically closed when writer is destroyed... +} + +} + +#endif // LR_DATA_TPP diff --git a/app/src/launcher/abstract/lr-install.h b/app/src/launcher/abstract/lr-install.h new file mode 100644 index 0000000..177c118 --- /dev/null +++ b/app/src/launcher/abstract/lr-install.h @@ -0,0 +1,79 @@ +#ifndef LR_INSTALL_H +#define LR_INSTALL_H + +// Qt Includes +#include + +// Project Includes +#include "launcher/interface/lr-install-interface.h" +#include "launcher/abstract/lr-registration.h" + +namespace Lr +{ + +template +class Install : public IInstall +{ +//-Aliases------------------------------------------------------------------------------------------------------------- +protected: + using PlatformT = Id::PlatformT; + using PlatformReaderT = Id::PlatformReaderT; + using PlatformWriterT = Id::PlatformWriterT; + using PlaylistT = Id::PlaylistT; + using PlaylistReaderT = Id::PlaylistReaderT; + using PlaylistWriterT = Id::PlaylistWriterT; + using GameT = Id::GameT; + +//-Constructor---------------------------------------------------------------------------------------------------------- +public: + Install(const QString& installPath); + +//-Instance Functions--------------------------------------------------------------------------------------------------- +protected: + // RE-IMPLEMENT + using IInstall::nullify; + + // OPTIONALLY RE-IMPLEMENT + virtual void preparePlatformDocCommit(const PlatformT& document); // Does nothing by default + virtual void preparePlaylistDocCommit(const PlaylistT& document); // Does nothing by default + + // IMPLEMENT + using IInstall::populateExistingDocs; + virtual std::unique_ptr preparePlatformDocCheckout(const QString& translatedName) = 0; + virtual std::unique_ptr preparePlaylistDocCheckout(const QString& translatedName) = 0; + +public: + QString name() const override; + + DocHandlingError checkoutPlatformDoc(std::unique_ptr& returnBuffer, const QString& name) override; + DocHandlingError checkoutPlaylistDoc(std::unique_ptr& returnBuffer, const QString& name) override; + DocHandlingError commitPlatformDoc(std::unique_ptr platformDoc) override; + DocHandlingError commitPlaylistDoc(std::unique_ptr playlistDoc) override; + + // RE-IMPLEMENT + using IInstall::softReset; + + // IMPLEMENT + using IInstall::preferredImageModeOrder; + using IInstall::isRunning; + virtual void convertToDestinationImages(const GameT& game, ImagePaths& images) = 0; // NOTE: The image paths provided here can be null (i.e. images unavailable). + + // OPTIONALLY RE-IMPLEMENT + using IInstall::preImport; + using IInstall::postImport; + using IInstall::prePlatformsImport; + using IInstall::postPlatformsImport; + using IInstall::preImageProcessing; + using IInstall::postImageProcessing; + using IInstall::prePlaylistsImport; + using IInstall::postPlaylistsImport; + using IInstall::translateDocName; + using IInstall::platformCategoryIconPath; + using IInstall::platformIconsDirectory; + using IInstall::playlistIconsDirectory; +}; + +} + +#include "lr-install.tpp" +#endif // LR_INSTALL_H diff --git a/app/src/launcher/abstract/lr-install.tpp b/app/src/launcher/abstract/lr-install.tpp new file mode 100644 index 0000000..d01ff22 --- /dev/null +++ b/app/src/launcher/abstract/lr-install.tpp @@ -0,0 +1,134 @@ +#ifndef LR_INSTALL_TPP +#define LR_INSTALL_TPP + +// Helpful for previewing code in IDE, but needs to be commented out when compiling +//#include "lr-install.h" + +// Qx Includes +#include +#include + +#ifndef LR_INSTALL_H +#error __FILE__ should only be included from lr-install.h. +#endif // LR_INSTALL_H + +namespace Lr +{ + +//=============================================================================================================== +// Install +//=============================================================================================================== + +//-Constructor--------------------------------------------------------------------------------------------------- +template +Install::Install(const QString& installPath) : + IInstall(installPath) +{} + +//-Instance Functions-------------------------------------------------------------------------------------------- +//Protected: +template +void Install::preparePlatformDocCommit(const PlatformT& document) +{ + Q_UNUSED(document); +} + +template +void Install::preparePlaylistDocCommit(const PlaylistT& document) +{ + Q_UNUSED(document); +} + +//Public: +template +QString Install::name() const { return StaticRegistry::name().toString(); } + +template +DocHandlingError Install::checkoutPlatformDoc(std::unique_ptr& returnBuffer, const QString& name) +{ + // Translate to launcher doc name + QString translatedName = translateDocName(name, IDataDoc::Type::Platform); + + // Get initialized blank doc and create reader if present + std::unique_ptr platformDoc = preparePlatformDocCheckout(translatedName); + std::shared_ptr docReader; + if constexpr(HasPlatformReader) + docReader = std::make_shared(platformDoc.get()); + + // Open document + DocHandlingError readErrorStatus = checkoutDataDocument(platformDoc.get(), docReader); + + // Fill return buffer on success + if(!readErrorStatus.isValid()) + returnBuffer = std::move(platformDoc); + + // Return status + return readErrorStatus; +} + +template +DocHandlingError Install::checkoutPlaylistDoc(std::unique_ptr& returnBuffer, const QString& name) +{ + // Translate to launcher doc name + QString translatedName = translateDocName(name, IDataDoc::Type::Playlist); + + // Get initialized blank doc and create reader if present + std::unique_ptr playlistDoc = preparePlaylistDocCheckout(translatedName); + std::shared_ptr docReader; + if constexpr(HasPlaylistReader) + docReader = std::make_shared(playlistDoc.get()); + + // Open document + DocHandlingError readErrorStatus = checkoutDataDocument(playlistDoc.get(), docReader); + + // Fill return buffer on success + if(!readErrorStatus.isValid()) + returnBuffer = std::move(playlistDoc); + + // Return status + return readErrorStatus; +} + +template +DocHandlingError Install::commitPlatformDoc(std::unique_ptr document) +{ + // Doc should belong to this install + Q_ASSERT(document->install() == this); + + // Work with native type + auto nativeDoc = static_cast(document.get()); + + // Handle any preparations + preparePlatformDocCommit(*nativeDoc); + + // Write + std::shared_ptr docWriter = std::make_shared(nativeDoc); + DocHandlingError writeErrorStatus = commitDataDocument(nativeDoc, docWriter); + + // Return write status and let document ptr auto delete + return writeErrorStatus; +} + +template +DocHandlingError Install::commitPlaylistDoc(std::unique_ptr document) +{ + // Doc should belong to this install + Q_ASSERT(document->install() == this); + + // Work with native type + auto nativeDoc = static_cast(document.get()); + + // Handle any preparations + preparePlaylistDocCommit(*nativeDoc); + + // Write + std::shared_ptr docWriter = std::make_shared(nativeDoc); + DocHandlingError writeErrorStatus = commitDataDocument(nativeDoc, docWriter); + + // Return write status and let document ptr auto delete + return writeErrorStatus; +} + +} + +#endif // LR_INSTALL_TPP diff --git a/app/src/launcher/abstract/lr-registration.cpp b/app/src/launcher/abstract/lr-registration.cpp new file mode 100644 index 0000000..de8ff97 --- /dev/null +++ b/app/src/launcher/abstract/lr-registration.cpp @@ -0,0 +1,42 @@ +// Unit Include +#include "lr-registration.h" + +namespace Lr +{ +//=============================================================================================================== +// Registry +//=============================================================================================================== + +//-Class Functions-------------------------------------------------- +//Protected: +Registry::Entry* Registry::registerInstall(Entry&& entry) +{ + Q_ASSERT(!smRegistry.contains(entry.name)); + return &smRegistry.insert(entry.name, std::move(entry)).value(); +} + +//Public: +std::unique_ptr Registry::acquireMatch(const QString& installPath) +{ + // Check all installs against path and return match if found + for(const auto& entry : std::as_const(smRegistry)) + { + std::unique_ptr possibleMatch = entry.make(installPath); + + if(possibleMatch->isValid()) + return possibleMatch; + } + + // Return nullptr on failure to find match + return nullptr; +} + +QUrl Registry::helpUrl(QStringView name) +{ + auto rItr = smRegistry.constFind(name); + return rItr != smRegistry.cend() ? rItr->helpUrl : QUrl(); +} + +QMapIterator Registry::entries() { return smRegistry; } + +} diff --git a/app/src/launcher/abstract/lr-registration.h b/app/src/launcher/abstract/lr-registration.h new file mode 100644 index 0000000..a175dd4 --- /dev/null +++ b/app/src/launcher/abstract/lr-registration.h @@ -0,0 +1,191 @@ +#ifndef LR_REGISTRATION_H +#define LR_REGISTRATION_H + +// Qx Includes +#include +#include + +// Project Includes +#include "launcher/interface/lr-data-interface.h" +#include "launcher/interface/lr-install-interface.h" + +#define REGISTER_LAUNCHER(LauncherId) static Lr::Register reg; + +namespace Lr +{ +template< + class Install, + class Platform, + class PlatformReader, // Voidable, if platforms are always overwritten in their entirety + class PlatformWriter, + class Playlist, + class PlaylistReader, // Voidable, if playlists are always overwritten in their entirety + class PlaylistWriter, + class Game, + class AddApp, // Voidable, if not using the BasicXXXDoc types + class PlaylistHeader, // Voidable, if not using the BasicXXXDoc types + class PlaylistGame, // Voidable, if not using the BasicXXXDoc types + Qx::StringLiteral16 NameS, + Qx::StringLiteral16 IconPathS, + Qx::StringLiteral16 HelpUrlS +> +struct Registrar +{ + using InstallT = Install; + using PlatformT = Platform; + using PlatformReaderT = PlatformReader; + using PlatformWriterT = PlatformWriter; + using PlaylistT = Playlist; + using PlaylistReaderT = PlaylistReader; + using PlaylistWriterT = PlaylistWriter; + using GameT = Game; + using AddAppT = AddApp; + using PlaylistHeaderT = PlaylistHeader; + using PlaylistGameT = PlaylistGame; + static constexpr QStringView Name = NameS.value; + static constexpr QStringView IconPath = IconPathS.value; + static constexpr QStringView HelpUrl = HelpUrlS.value; +}; + +/* This is shitty, but there is no good way to check if a type is a specialization of a template when + * that template has non-type parameters, so here we just see if its equivalent based on its aliases + */ +template +concept LauncherId = requires{ + typename T::InstallT; + typename T::PlatformT; + typename T::PlatformReaderT; + typename T::PlatformWriterT; + typename T::PlaylistT; + typename T::PlaylistReaderT; + typename T::PlaylistWriterT; + typename T::GameT; + typename T::AddAppT; + typename T::PlaylistHeaderT; + typename T::PlaylistGameT; + T::Name; + T::IconPath; + T::HelpUrl; +}; + +namespace _detail +{ + +template +concept _valid_install = std::derived_from; + +template +concept _valid_platform = std::derived_from; + +template +concept _valid_playlist = std::derived_from; + +template +concept _valid_reader = (std::derived_from && std::constructible_from) || std::same_as; + +template +concept _valid_writer = std::derived_from && std::constructible_from; + +template +concept _valid_game = std::derived_from; + +template +concept _valid_addapp = std::derived_from || std::same_as; + +template +concept _valid_playlistheader = std::derived_from || std::same_as; + +template +concept _valid_playlistgame = std::derived_from || std::same_as; + +} + +// This needs a full examination anyway +template +concept CompleteLauncherId = + _detail::_valid_install && + _detail::_valid_platform && + _detail::_valid_reader && + _detail::_valid_writer && + _detail::_valid_playlist && + _detail::_valid_reader && + _detail::_valid_writer && + _detail::_valid_game && + _detail::_valid_addapp && + _detail::_valid_playlistheader && + _detail::_valid_playlistgame; + +template +concept HasPlatformReader = !std::same_as; + +template +concept HasPlaylistReader = !std::same_as; + +class Registry +{ + template + friend class Register; +//-Structs--------------------------------------------------- +public: + struct Entry + { + QStringView name; + std::unique_ptr (*make)(const QString&); + QStringView iconPath; + QUrl helpUrl; + }; + +//-Class Variables------------------------------------------------- +private: + static inline constinit QMap smRegistry; + +//-Class Functions-------------------------------------------------- +protected: + static Entry* registerInstall(Entry&& entry); + +public: + [[nodiscard]] static std::unique_ptr acquireMatch(const QString& installPath); + static QUrl helpUrl(QStringView name); + static QMapIterator entries(); +}; + +template +class Register; + +template +class StaticRegistry +{ + template + friend class Register; + static inline Registry::Entry* smEntry; + +public: + static inline QStringView name() { Q_ASSERT(smEntry); return smEntry->name; } + static inline QStringView iconPath() { Q_ASSERT(smEntry); return smEntry->iconPath; } + static inline QUrl helpUrl() { Q_ASSERT(smEntry); return smEntry->helpUrl; } +}; + +template +class Register +{ +private: + static std::unique_ptr createInstall(const QString& path) { + return std::make_unique(path); + } + +public: + Register() + { + // TODO: MAKE NOTE HERE ABOUT USAGE AND WHY THIS HAS TO EXIST + StaticRegistry::smEntry = Registry::registerInstall(Registry::Entry{ + .name = Id::Name, + .make = createInstall, + .iconPath = Id::IconPath, + .helpUrl = QUrl(Id::HelpUrl.toString()) + }); + } +}; + +} + +#endif // LR_REGISTRATION_H diff --git a/app/src/launcher/attractmode/am-data.cpp b/app/src/launcher/implementation/attractmode/am-data.cpp similarity index 66% rename from app/src/launcher/attractmode/am-data.cpp rename to app/src/launcher/implementation/attractmode/am-data.cpp index 794d843..9d85fef 100644 --- a/app/src/launcher/attractmode/am-data.cpp +++ b/app/src/launcher/implementation/attractmode/am-data.cpp @@ -8,96 +8,11 @@ #include // Project Includes -#include "launcher/attractmode/am-install.h" +#include "launcher/implementation/attractmode/am-install.h" namespace Am { -//=============================================================================================================== -// CommonDocReader -//=============================================================================================================== - -//-Constructor-------------------------------------------------------------------------------------------------------- -//Protected: -CommonDocReader::CommonDocReader(Lr::DataDoc* targetDoc) : - Lr::DataDoc::Reader(targetDoc), - mStreamReader(targetDoc->path()) -{} - -//-Instance Functions------------------------------------------------------------------------------------------------- -//Protected: -bool CommonDocReader::lineIsComment(const QString& line) { return line.front() == '#'; } - -QString CommonDocReader::readLineIgnoringComments(qint64 maxlen) -{ - QString line; - - do - line = mStreamReader.readLine(maxlen); - while(!line.isEmpty() && line.front() == '#'); // Must check for empty string due to QString::front() constraints - - return line; -} - -//Public: -Lr::DocHandlingError CommonDocReader::readInto() -{ - // Open file - Qx::IoOpReport openError = mStreamReader.openFile(); - if(openError.isFailure()) - return Lr::DocHandlingError(*mTargetDocument, Lr::DocHandlingError::DocCantOpen, openError.outcomeInfo()); - - // Check that doc is valid - bool isValid = false; - if(!checkDocValidity(isValid)) - return Lr::DocHandlingError(*mTargetDocument, Lr::DocHandlingError::DocWriteFailed, mStreamReader.status().outcomeInfo()); - else if(!isValid) - return Lr::DocHandlingError(*mTargetDocument, Lr::DocHandlingError::DocInvalidType); - - // Read doc - Lr::DocHandlingError parseError = readTargetDoc(); - - // Close file - mStreamReader.closeFile(); - - // Return outcome - if(parseError.isValid()) - return parseError; - else if(mStreamReader.hasError()) - return Lr::DocHandlingError(*mTargetDocument, Lr::DocHandlingError::DocWriteFailed, mStreamReader.status().outcomeInfo()); - else - return Lr::DocHandlingError(); -} - -//=============================================================================================================== -// CommonDocWriter -//=============================================================================================================== - -//-Constructor-------------------------------------------------------------------------------------------------------- -//Protected: -CommonDocWriter::CommonDocWriter(Lr::DataDoc* sourceDoc) : - Lr::DataDoc::Writer(sourceDoc), - mStreamWriter(sourceDoc->path(), Qx::WriteMode::Truncate) -{} - -//-Instance Functions------------------------------------------------------------------------------------------------- -//Public: -Lr::DocHandlingError CommonDocWriter::writeOutOf() -{ - // Open file - Qx::IoOpReport openError = mStreamWriter.openFile(); - if(openError.isFailure()) - return Lr::DocHandlingError(*mSourceDocument, Lr::DocHandlingError::DocCantOpen, openError.outcomeInfo()); - - // Write doc - bool writeSuccess = writeSourceDoc(); - - // Close file - mStreamWriter.closeFile(); - // Return outcome - return writeSuccess ? Lr::DocHandlingError() : - Lr::DocHandlingError(*mSourceDocument, Lr::DocHandlingError::DocWriteFailed, mStreamWriter.status().outcomeInfo()); -} //=============================================================================================================== // ConfigDoc @@ -105,97 +20,18 @@ Lr::DocHandlingError CommonDocWriter::writeOutOf() //-Constructor-------------------------------------------------------------------------------------------------------- //Public: -ConfigDoc::ConfigDoc(Install* const parent, const QString& filePath, QString docName) : - Lr::DataDoc(parent, filePath, docName) +ConfigDoc::ConfigDoc(Install* install, const QString& filePath, QString docName) : + Lr::DataDoc(install, filePath, docName) {} //-Instance Functions-------------------------------------------------------------------------------------------------- //Protected: QString ConfigDoc::versionedTagline() { - QString verString = static_cast(parent())->versionString(); - return TAGLINE + verString; + return TAGLINE + install()->versionString(); } -//=============================================================================================================== -// ConfigDoc::Reader -//=============================================================================================================== -//-Constructor-------------------------------------------------------------------------------------------------------- -//Protected: -ConfigDoc::Reader::Reader(ConfigDoc* targetDoc) : - CommonDocReader(targetDoc) -{} - -//-Class Functions------------------------------------------------------------------------------------------------- -//Protected: -bool ConfigDoc::Reader::splitKeyValue(const QString& line, QString& key, QString& value) -{ - /* TODO: The result from this function is currently unused due to no easy way to raise a custom - * error with the stream reader in this class (and how the current paradigm is to return bools - * for each step and then use the reader status if one is found). If used properly this should - * never error, but ideally it should be checked for anyway. Might need to have all read functions - * return Qx::GenericError to allow non stream related errors to be returned. - */ - - // Null out return buffers - key = QString(); - value = QString(); - - QRegularExpressionMatch keyValueCheck = KEY_VALUE_REGEX.match(line); - if(keyValueCheck.hasMatch()) - { - key = keyValueCheck.captured(u"key"_s); - value = keyValueCheck.captured(u"value"_s); - return true; - } - else - { - qWarning("Invalid key value string"); - return false; - } -} - -//-Instance Functions------------------------------------------------------------------------------------------------- -//Protected: -bool ConfigDoc::Reader::checkDocValidity(bool& isValid) -{ - // Check for config "header" - QString firstLine = mStreamReader.readLine(); - QString secondLine = mStreamReader.readLine(); - - bool hasTagline = firstLine.left(ConfigDoc::TAGLINE.length()) == ConfigDoc::TAGLINE; - - isValid = hasTagline && lineIsComment(secondLine); - - // Return status - return !mStreamReader.hasError(); -} - -//=============================================================================================================== -// ConfigDoc::Writer -//=============================================================================================================== - -//-Constructor-------------------------------------------------------------------------------------------------------- -//Public: -ConfigDoc::Writer::Writer(ConfigDoc* sourceDoc) : - CommonDocWriter(sourceDoc) -{} - -//-Instance Functions-------------------------------------------------------------------------------------------------- -//Public: -bool ConfigDoc::Writer::writeSourceDoc() -{ - // Write config doc "header" - mStreamWriter.writeLine(static_cast(mSourceDocument)->versionedTagline()); - mStreamWriter.writeLine(u"#"_s); - - if(mStreamWriter.hasError()) - return false; - - // Perform custom writing - return writeConfigDoc(); -} //=============================================================================================================== // Taglist @@ -203,8 +39,8 @@ bool ConfigDoc::Writer::writeSourceDoc() //-Constructor-------------------------------------------------------------------------------------------------------- //Public: -Taglist::Taglist(Install* const parent, const QString& listPath, QString docName) : - Lr::DataDoc(parent, listPath, docName) +Taglist::Taglist(Install* install, const QString& listPath, QString docName) : + Lr::DataDoc(install, listPath, docName) {} //-Instance Functions-------------------------------------------------------------------------------------------------- @@ -231,11 +67,8 @@ Taglist::Writer::Writer(Taglist* sourceDoc) : //Public: bool Taglist::Writer::writeSourceDoc() { - // Get actual tag list - Taglist* sourceList = static_cast(mSourceDocument); - // Write tags - for(const QString& tag : qAsConst(sourceList->mTags)) + for(const QString& tag : qAsConst(source()->mTags)) mStreamWriter << tag << '\n'; // Return error status @@ -248,13 +81,13 @@ bool Taglist::Writer::writeSourceDoc() //-Constructor-------------------------------------------------------------------------------------------------------- //Private: -PlatformTaglist::PlatformTaglist(Install* const parent, const QString& listPath, QString docName) : - Am::Taglist(parent, listPath, docName) +PlatformTaglist::PlatformTaglist(Install* install, const QString& listPath, QString docName) : + Am::Taglist(install, listPath, docName) {} //-Instance Functions-------------------------------------------------------------------------------------------------- //Public: -Lr::DataDoc::Type PlatformTaglist::type() const { return Lr::DataDoc::Type::Platform; } +Lr::IDataDoc::Type PlatformTaglist::type() const { return Lr::IDataDoc::Type::Platform; } //=============================================================================================================== // PlaylistTaglist @@ -262,13 +95,13 @@ Lr::DataDoc::Type PlatformTaglist::type() const { return Lr::DataDoc::Type::Plat //-Constructor-------------------------------------------------------------------------------------------------------- //Private: -PlaylistTaglist::PlaylistTaglist(Install* const parent, const QString& listPath, QString docName) : - Am::Taglist(parent, listPath, docName) +PlaylistTaglist::PlaylistTaglist(Install* install, const QString& listPath, QString docName) : + Am::Taglist(install, listPath, docName) {} //-Instance Functions-------------------------------------------------------------------------------------------------- //Public: -Lr::DataDoc::Type PlaylistTaglist::type() const { return Lr::DataDoc::Type::Playlist; } +Lr::IDataDoc::Type PlaylistTaglist::type() const { return Lr::IDataDoc::Type::Playlist; } //=============================================================================================================== // Romlist @@ -276,14 +109,13 @@ Lr::DataDoc::Type PlaylistTaglist::type() const { return Lr::DataDoc::Type::Play //-Constructor-------------------------------------------------------------------------------------------------------- //Public: -Romlist::Romlist(Install* const parent, const QString& listPath, QString docName, const Import::UpdateOptions& updateOptions, - const DocKey&) : - Lr::UpdateableDoc(parent, listPath, docName, updateOptions) +Romlist::Romlist(Install* install, const QString& listPath, QString docName, const Import::UpdateOptions& updateOptions) : + Lr::UpdateableDoc(install, listPath, docName, updateOptions) {} //-Instance Functions-------------------------------------------------------------------------------------------------- //Public: -Lr::DataDoc::Type Romlist::type() const { return Lr::DataDoc::Type::Config; } +Lr::IDataDoc::Type Romlist::type() const { return Lr::IDataDoc::Type::Config; } bool Romlist::isEmpty() const { @@ -295,7 +127,7 @@ const QHash>& Romlist::finalEntries() const { r bool Romlist::containsGame(QUuid gameId) const { return mEntriesExisting.contains(gameId) || mEntriesFinal.contains(gameId); } bool Romlist::containsAddApp(QUuid addAppId) const { return mEntriesExisting.contains(addAppId) || mEntriesFinal.contains(addAppId); } -void Romlist::addSet(const Fp::Set& set, const Lr::ImageSources& images) +std::shared_ptr Romlist::processSet(const Fp::Set& set) { // Convert to romlist entry std::shared_ptr mainRomEntry = std::make_shared(set.game()); @@ -317,15 +149,14 @@ void Romlist::addSet(const Fp::Set& set, const Lr::ImageSources& images) } } - // Allow install to process images as necessary - parent()->processDirectGameImages(mainRomEntry.get(), images); + return mainRomEntry; } void Romlist::finalize() { finalizeUpdateableItems(mEntriesExisting, mEntriesFinal); - Lr::UpdateableDoc::finalize(); + Lr::IUpdateableDoc::finalize(); } //=============================================================================================================== @@ -335,16 +166,11 @@ void Romlist::finalize() //-Constructor-------------------------------------------------------------------------------------------------------- //Public: Romlist::Reader::Reader(Romlist* targetDoc) : - CommonDocReader(targetDoc) + CommonDocReader(targetDoc) {} //-Instance Functions-------------------------------------------------------------------------------------------------- //Private: -QHash>& Romlist::Reader::targetDocExistingRomEntries() -{ - return static_cast(mTargetDocument)->mEntriesExisting; -} - bool Romlist::Reader::checkDocValidity(bool& isValid) { // See if first line is the romlist header @@ -408,7 +234,7 @@ void Romlist::Reader::parseRomEntry(const QString& rawEntry) // Build Entry and add to document std::shared_ptr existingEntry = reb.buildShared(); - targetDocExistingRomEntries()[existingEntry->id()] = existingEntry; + target()->mEntriesExisting[existingEntry->id()] = existingEntry; } void Romlist::Reader::addFieldToBuilder(RomEntry::Builder& builder, QString field, quint8 index) @@ -501,7 +327,7 @@ bool Romlist::Writer::writeSourceDoc() mStreamWriter.writeLine(Romlist::HEADER); // Write all rom entries - for(const std::shared_ptr& entry : qAsConst(static_cast(mSourceDocument)->finalEntries())) + for(const std::shared_ptr& entry : qAsConst(source()->finalEntries())) { if(!writeRomEntry(*entry)) return false; @@ -584,37 +410,19 @@ bool BulkOverviewWriter::writeOverview(const QUuid& gameId, const QString& overv //-Constructor-------------------------------------------------------------------------------------------------------- //Public: -PlatformInterface::PlatformInterface(Install* const parent, const QString& platformTaglistPath, QString platformName, - const QDir& overviewDir,const DocKey&) : - Lr::PlatformDoc(parent, {}, platformName, {}), - mPlatformTaglist(parent, platformTaglistPath, platformName), +PlatformInterface::PlatformInterface(Install* install, const QString& platformTaglistPath, QString platformName, + const QDir& overviewDir) : + Lr::PlatformDoc(install, {}, platformName, {}), + mPlatformTaglist(install, platformTaglistPath, platformName), mOverviewWriter(overviewDir) {} //-Instance Functions-------------------------------------------------------------------------------------------------- -//Public: -bool PlatformInterface::isEmpty() const { return mPlatformTaglist.isEmpty(); } - -bool PlatformInterface::containsGame(QUuid gameId) const +//Private: +std::shared_ptr PlatformInterface::processSet(const Fp::Set& set) { - /* Check main romlist for ID. Could check the taglist instead, which would be more "correct" since only the current - * platform should contain the ID, but this doesn't matter given correct design and the lookup is performed via - * the romlist's internal hash, which is faster than checking a list for the presence of the ID. - */ - return static_cast(parent())->mRomlist->containsGame(gameId); -} + std::shared_ptr romEntry; -bool PlatformInterface::containsAddApp(QUuid addAppId) const -{ - /* Check main romlist for ID. Could check the taglist instead, which would be more "correct" since only the current - * platform should contain the ID, but this doesn't matter given correct design and the lookup is performed via - * the romlist's internal hash, which is faster than checking a list for the presence of the ID. - */ - return static_cast(parent())->mRomlist->containsAddApp(addAppId); -}; - -void PlatformInterface::addSet(const Fp::Set& set, const Lr::ImageSources& images) -{ if(!hasError()) { //-Handle game---------------------------------------------------------- @@ -631,7 +439,7 @@ void PlatformInterface::addSet(const Fp::Set& set, const Lr::ImageSources& image bool written = mOverviewWriter.writeOverview(game.id(), overview); if(written) - parent()->addRevertableFile(mOverviewWriter.currentFilePath()); + install()->addRevertableFile(mOverviewWriter.currentFilePath()); else mError = Lr::DocHandlingError(*this, Lr::DocHandlingError::DocWriteFailed, mOverviewWriter.fileErrorString()); } @@ -651,25 +459,47 @@ void PlatformInterface::addSet(const Fp::Set& set, const Lr::ImageSources& image } //-Forward game insertion to main Romlist-------------------------------- - static_cast(parent())->mRomlist->addSet(set, images); + romEntry = install()->mRomlist->processSet(set); } + + return romEntry; +} + +//Public: +bool PlatformInterface::isEmpty() const { return mPlatformTaglist.isEmpty(); } + +bool PlatformInterface::containsGame(QUuid gameId) const +{ + /* Check main romlist for ID. Could check the taglist instead, which would be more "correct" since only the current + * platform should contain the ID, but this doesn't matter given correct design and the lookup is performed via + * the romlist's internal hash, which is faster than checking a list for the presence of the ID. + */ + return install()->mRomlist->containsGame(gameId); } +bool PlatformInterface::containsAddApp(QUuid addAppId) const +{ + /* Check main romlist for ID. Could check the taglist instead, which would be more "correct" since only the current + * platform should contain the ID, but this doesn't matter given correct design and the lookup is performed via + * the romlist's internal hash, which is faster than checking a list for the presence of the ID. + */ + return install()->mRomlist->containsAddApp(addAppId); +}; + //=============================================================================================================== -// PlatformInterface::Writer +// PlatformInterfaceWriter //=============================================================================================================== //-Constructor-------------------------------------------------------------------------------------------------------- //Public: -PlatformInterface::Writer::Writer(PlatformInterface* sourceDoc) : - Lr::DataDoc::Writer(sourceDoc), - Lr::PlatformDoc::Writer(sourceDoc), +PlatformInterfaceWriter::PlatformInterfaceWriter(PlatformInterface* sourceDoc) : + Lr::DataDocWriter(sourceDoc), mTaglistWriter(&sourceDoc->mPlatformTaglist) {} //-Instance Functions-------------------------------------------------------------------------------------------------- //Public: -Lr::DocHandlingError PlatformInterface::Writer::writeOutOf() { return mTaglistWriter.writeOutOf(); } +Lr::DocHandlingError PlatformInterfaceWriter::writeOutOf() { return mTaglistWriter.writeOutOf(); } //=============================================================================================================== // PlaylistInterface @@ -677,10 +507,9 @@ Lr::DocHandlingError PlatformInterface::Writer::writeOutOf() { return mTaglistWr //-Constructor-------------------------------------------------------------------------------------------------------- //Public: -PlaylistInterface::PlaylistInterface(Install* const parent, const QString& playlistTaglistPath, QString playlistName, - const DocKey&) : - Lr::PlaylistDoc(parent, {}, playlistName, {}), - mPlaylistTaglist(parent, playlistTaglistPath, playlistName) +PlaylistInterface::PlaylistInterface(Install* install, const QString& playlistTaglistPath, QString playlistName) : + Lr::PlaylistDoc(install, {}, playlistName, {}), + mPlaylistTaglist(install, playlistTaglistPath, playlistName) {} //-Instance Functions-------------------------------------------------------------------------------------------------- @@ -699,20 +528,19 @@ void PlaylistInterface::setPlaylistData(const Fp::Playlist& playlist) } //=============================================================================================================== -// PlaylistInterface::Writer +// PlaylistInterfaceWriter //=============================================================================================================== //-Constructor-------------------------------------------------------------------------------------------------------- //Public: -PlaylistInterface::Writer::Writer(PlaylistInterface* sourceDoc) : - Lr::DataDoc::Writer(sourceDoc), - Lr::PlaylistDoc::Writer(sourceDoc), +PlaylistInterfaceWriter::PlaylistInterfaceWriter(PlaylistInterface* sourceDoc) : + Lr::DataDocWriter(sourceDoc), mTaglistWriter(&sourceDoc->mPlaylistTaglist) {} //-Instance Functions-------------------------------------------------------------------------------------------------- //Public: -Lr::DocHandlingError PlaylistInterface::Writer::writeOutOf() { return mTaglistWriter.writeOutOf(); } +Lr::DocHandlingError PlaylistInterfaceWriter::writeOutOf() { return mTaglistWriter.writeOutOf(); } //=============================================================================================================== // Emulator @@ -720,14 +548,14 @@ Lr::DocHandlingError PlaylistInterface::Writer::writeOutOf() { return mTaglistWr //-Constructor-------------------------------------------------------------------------------------------------------- //Public: -Emulator::Emulator(Install* const parent, const QString& filePath, const DocKey&) : - ConfigDoc(parent, filePath, STD_NAME) +Emulator::Emulator(Install* install, const QString& filePath) : + ConfigDoc(install, filePath, STD_NAME) {} //-Instance Functions-------------------------------------------------------------------------------------------------- //Public: bool Emulator::isEmpty() const { return false; } // Can have blank fields, but always has field keys -Lr::DataDoc::Type Emulator::type() const { return Lr::DataDoc::Type::Config; } +Lr::IDataDoc::Type Emulator::type() const { return Lr::IDataDoc::Type::Config; } QString Emulator::executable() const { return mExecutable; } QString Emulator::args() const { return mArgs; } @@ -757,7 +585,7 @@ void Emulator::setArtworkEntry(const EmulatorArtworkEntry& entry) { mArtworkEntr //-Constructor-------------------------------------------------------------------------------------------------------- //Public: EmulatorReader::EmulatorReader(Emulator* targetDoc) : - ConfigDoc::Reader(targetDoc) + ConfigDoc::Reader(targetDoc) {} //-Instance Functions-------------------------------------------------------------------------------------------------- @@ -799,20 +627,20 @@ void EmulatorReader::parseKeyValue(const QString& key, const QString& value) parseArtwork(value); } -void EmulatorReader::parseExecutable(const QString& value) { targetEmulator()->setExecutable(value); } -void EmulatorReader::parseArgs(const QString& value) { targetEmulator()->setArgs(value); } -void EmulatorReader::parseWorkDir(const QString& value) { targetEmulator()->setWorkDir(value); } +void EmulatorReader::parseExecutable(const QString& value) { target()->setExecutable(value); } +void EmulatorReader::parseArgs(const QString& value) { target()->setArgs(value); } +void EmulatorReader::parseWorkDir(const QString& value) { target()->setWorkDir(value); } void EmulatorReader::parseRomPath(const QString& value) { - targetEmulator()->setRomPath(value == uR"("")"_s ? u""_s : value); + target()->setRomPath(value == uR"("")"_s ? u""_s : value); } void EmulatorReader::parseRomExt(const QString& value) { - targetEmulator()->setRomPath(value == uR"("")"_s ? u""_s : value); + target()->setRomPath(value == uR"("")"_s ? u""_s : value); } -void EmulatorReader::parseSystem(const QString& value) { targetEmulator()->setSystem(value); } -void EmulatorReader::parseInfoSource(const QString& value) { targetEmulator()->setInfoSource(value); } -void EmulatorReader::parseExitHotkey(const QString& value) { targetEmulator()->setExitHotkey(value); } +void EmulatorReader::parseSystem(const QString& value) { target()->setSystem(value); } +void EmulatorReader::parseInfoSource(const QString& value) { target()->setInfoSource(value); } +void EmulatorReader::parseExitHotkey(const QString& value) { target()->setExitHotkey(value); } void EmulatorReader::parseArtwork(const QString& value) { QString type; @@ -823,11 +651,9 @@ void EmulatorReader::parseArtwork(const QString& value) eaeb.wType(type); eaeb.wPaths(!rawPaths.isEmpty() ? rawPaths.split(';') : QStringList()); - targetEmulator()->setArtworkEntry(eaeb.build()); + target()->setArtworkEntry(eaeb.build()); } -Emulator* EmulatorReader::targetEmulator() { return static_cast(mTargetDocument); } - //=============================================================================================================== // Emulator::Writer //=============================================================================================================== @@ -835,7 +661,7 @@ Emulator* EmulatorReader::targetEmulator() { return static_cast(mTarg //-Constructor-------------------------------------------------------------------------------------------------------- //Public: Emulator::Writer::Writer(Emulator* sourceDoc) : - ConfigDoc::Writer(sourceDoc) + ConfigDoc::Writer(sourceDoc) {} //-Instance Functions-------------------------------------------------------------------------------------------------- @@ -846,17 +672,17 @@ bool Emulator::Writer::writeConfigDoc() mStreamWriter.setFieldAlignment(QTextStream::AlignLeft); // Write main key/values - writeStandardKeyValue(Emulator::Keys::EXECUTABLE, sourceEmulator()->executable()); - writeStandardKeyValue(Emulator::Keys::ARGS, sourceEmulator()->args()); - writeStandardKeyValue(Emulator::Keys::WORK_DIR, sourceEmulator()->workDir()); - writeStandardKeyValue(Emulator::Keys::ROM_PATH, sourceEmulator()->romPath()); - writeStandardKeyValue(Emulator::Keys::ROM_EXT, sourceEmulator()->romExt()); - writeStandardKeyValue(Emulator::Keys::SYSTEM, sourceEmulator()->system()); - writeStandardKeyValue(Emulator::Keys::INFO_SOURCE, sourceEmulator()->infoSource()); - writeStandardKeyValue(Emulator::Keys::EXIT_HOTKEY, sourceEmulator()->exitHotkey()); + writeStandardKeyValue(Emulator::Keys::EXECUTABLE, source()->executable()); + writeStandardKeyValue(Emulator::Keys::ARGS, source()->args()); + writeStandardKeyValue(Emulator::Keys::WORK_DIR, source()->workDir()); + writeStandardKeyValue(Emulator::Keys::ROM_PATH, source()->romPath()); + writeStandardKeyValue(Emulator::Keys::ROM_EXT, source()->romExt()); + writeStandardKeyValue(Emulator::Keys::SYSTEM, source()->system()); + writeStandardKeyValue(Emulator::Keys::INFO_SOURCE, source()->infoSource()); + writeStandardKeyValue(Emulator::Keys::EXIT_HOTKEY, source()->exitHotkey()); // Write artwork entries - const QList artworkEntries = sourceEmulator()->artworkEntries(); + const QList artworkEntries = source()->artworkEntries(); for(const EmulatorArtworkEntry& entry : artworkEntries) writeArtworkEntry(entry); @@ -883,6 +709,4 @@ void Emulator::Writer::writeArtworkEntry(const EmulatorArtworkEntry& entry) mStreamWriter << '\n'; } -Emulator* Emulator::Writer::sourceEmulator() { return static_cast(mSourceDocument); } - } diff --git a/app/src/launcher/attractmode/am-data.h b/app/src/launcher/implementation/attractmode/am-data.h similarity index 84% rename from app/src/launcher/attractmode/am-data.h rename to app/src/launcher/implementation/attractmode/am-data.h index c891c28..cd2f76d 100644 --- a/app/src/launcher/attractmode/am-data.h +++ b/app/src/launcher/implementation/attractmode/am-data.h @@ -9,31 +9,26 @@ #include // Project Includes -#include "launcher/lr-data.h" -#include "launcher/attractmode/am-items.h" -namespace Am -{ - -class Install; +#include "launcher/abstract/lr-data.h" +#include "launcher/implementation/attractmode/am-registration.h" +#include "launcher/implementation/attractmode/am-items.h" -class DocKey +namespace Am { - friend class Install; -private: - DocKey() {} - DocKey(const DocKey&) = default; -}; -class CommonDocReader : public Lr::DataDoc::Reader +template +class CommonDocReader : public Lr::DataDocReader { +protected: + using Lr::DataDocReader::target; //-Instance Variables-------------------------------------------------------------------------------------------------- protected: Qx::TextStreamReader mStreamReader; //-Constructor-------------------------------------------------------------------------------------------------------- protected: - CommonDocReader(Lr::DataDoc* targetDoc); + CommonDocReader(DocT* targetDoc); //-Instance Functions------------------------------------------------------------------------------------------------- protected: @@ -46,15 +41,18 @@ class CommonDocReader : public Lr::DataDoc::Reader Lr::DocHandlingError readInto() override; }; -class CommonDocWriter : public Lr::DataDoc::Writer +template +class CommonDocWriter : public Lr::DataDocWriter { +protected: + using Lr::DataDocWriter::source; //-Instance Variables-------------------------------------------------------------------------------------------------- protected: Qx::TextStreamWriter mStreamWriter; //-Constructor-------------------------------------------------------------------------------------------------------- protected: - CommonDocWriter(Lr::DataDoc* sourceDoc); + CommonDocWriter(DocT* sourceDoc); //-Instance Functions------------------------------------------------------------------------------------------------- protected: @@ -64,11 +62,14 @@ class CommonDocWriter : public Lr::DataDoc::Writer Lr::DocHandlingError writeOutOf() override; }; -class ConfigDoc : public Lr::DataDoc +class ConfigDoc : public Lr::DataDoc { //-Inner Classes---------------------------------------------------------------------------------------------------- public: + template class Reader; + + template class Writer; //-Class Variables-------------------------------------------------------------------------------------------------- @@ -77,7 +78,7 @@ class ConfigDoc : public Lr::DataDoc //-Constructor-------------------------------------------------------------------------------------------------------- protected: - ConfigDoc(Install* const parent, const QString& filePath, QString docName); + ConfigDoc(Install* install, const QString& filePath, QString docName); //-Instance Functions-------------------------------------------------------------------------------------------------- protected: @@ -85,8 +86,13 @@ class ConfigDoc : public Lr::DataDoc }; -class ConfigDoc::Reader : public CommonDocReader +template +class ConfigDoc::Reader : public CommonDocReader { +protected: + using CommonDocReader::mStreamReader; + using CommonDocReader::lineIsComment; + using Lr::DataDocReader::target; //-Class Variables---------------------------------------------------------------------------------------------------- protected: static inline const QRegularExpression KEY_VALUE_REGEX = @@ -94,7 +100,7 @@ class ConfigDoc::Reader : public CommonDocReader //-Constructor-------------------------------------------------------------------------------------------------------- protected: - Reader(ConfigDoc* targetDoc); + Reader(DocT* targetDoc); //-Class Functions------------------------------------------------------------------------------------------------- protected: @@ -105,11 +111,15 @@ class ConfigDoc::Reader : public CommonDocReader bool checkDocValidity(bool& isValid) override; }; -class ConfigDoc::Writer : public CommonDocWriter +template +class ConfigDoc::Writer : public CommonDocWriter { +protected: + using CommonDocWriter::mStreamWriter; + using Lr::DataDocWriter::source; //-Constructor-------------------------------------------------------------------------------------------------------- protected: - Writer(ConfigDoc* targetDoc); + Writer(DocT* targetDoc); //-Instance Functions------------------------------------------------------------------------------------------------- protected: @@ -117,7 +127,7 @@ class ConfigDoc::Writer : public CommonDocWriter bool writeSourceDoc() override; }; -class Taglist : public Lr::DataDoc +class Taglist : public Lr::DataDoc { //-Inner Classes---------------------------------------------------------------------------------------------------- public: @@ -129,7 +139,7 @@ class Taglist : public Lr::DataDoc //-Constructor-------------------------------------------------------------------------------------------------------- protected: - Taglist(Install* const parent, const QString& listPath, QString docName); + Taglist(Install* install, const QString& listPath, QString docName); //-Instance Functions-------------------------------------------------------------------------------------------------- public: @@ -139,7 +149,7 @@ class Taglist : public Lr::DataDoc void appendTag(const QString& tag); }; -class Taglist::Writer : public CommonDocWriter +class Taglist::Writer : public CommonDocWriter { //-Constructor-------------------------------------------------------------------------------------------------------- public: @@ -155,7 +165,7 @@ class PlatformTaglist : public Taglist friend class PlatformInterface; //-Constructor-------------------------------------------------------------------------------------------------------- private: - PlatformTaglist(Install* const parent, const QString& listPath, QString docName); + PlatformTaglist(Install* install, const QString& listPath, QString docName); //-Instance Functions-------------------------------------------------------------------------------------------------- public: @@ -167,14 +177,14 @@ class PlaylistTaglist : public Taglist friend class PlaylistInterface; //-Constructor-------------------------------------------------------------------------------------------------------- private: - PlaylistTaglist(Install* const parent, const QString& listPath, QString docName); + PlaylistTaglist(Install* install, const QString& listPath, QString docName); //-Instance Functions-------------------------------------------------------------------------------------------------- public: Type type() const override; }; -class Romlist : public Lr::UpdateableDoc +class Romlist : public Lr::UpdateableDoc { /* This class looks like it should inherit PlatformDoc, but it isn't truly one in the context of an Am install * since those are represented by tag lists, and if it did there would be the issue that once modified it would @@ -199,12 +209,11 @@ class Romlist : public Lr::UpdateableDoc //-Constructor-------------------------------------------------------------------------------------------------------- public: - explicit Romlist(Install* const parent, const QString& listPath, QString docName, const Import::UpdateOptions& updateOptions, - const DocKey&); + explicit Romlist(Install* install, const QString& listPath, QString docName, const Import::UpdateOptions& updateOptions); //-Instance Functions-------------------------------------------------------------------------------------------------- public: - DataDoc::Type type() const override; + IDataDoc::Type type() const override; bool isEmpty() const override; const QHash>& finalEntries() const; @@ -212,12 +221,12 @@ class Romlist : public Lr::UpdateableDoc bool containsGame(QUuid gameId) const; bool containsAddApp(QUuid addAppId) const; - void addSet(const Fp::Set& set, const Lr::ImageSources& images); + std::shared_ptr processSet(const Fp::Set& set); void finalize() override; }; -class Romlist::Reader : public CommonDocReader +class Romlist::Reader : public CommonDocReader { //-Constructor-------------------------------------------------------------------------------------------------------- public: @@ -225,14 +234,13 @@ class Romlist::Reader : public CommonDocReader //-Instance Functions------------------------------------------------------------------------------------------------- private: - QHash>& targetDocExistingRomEntries(); bool checkDocValidity(bool& isValid) override; Lr::DocHandlingError readTargetDoc() override; void parseRomEntry(const QString& rawEntry); void addFieldToBuilder(RomEntry::Builder& builder, QString field, quint8 index); }; -class Romlist::Writer : public CommonDocWriter +class Romlist::Writer : public CommonDocWriter { //-Constructor-------------------------------------------------------------------------------------------------------- public: @@ -263,12 +271,9 @@ class BulkOverviewWriter bool writeOverview(const QUuid& gameId, const QString& overview); }; -class PlatformInterface : public Lr::PlatformDoc +class PlatformInterface : public Lr::PlatformDoc { -//-Inner Classes---------------------------------------------------------------------------------------------------- -public: - class Writer; - + friend class PlatformInterfaceWriter; //-Instance Variables-------------------------------------------------------------------------------------------------- private: PlatformTaglist mPlatformTaglist; @@ -279,20 +284,21 @@ class PlatformInterface : public Lr::PlatformDoc //-Constructor-------------------------------------------------------------------------------------------------------- public: - explicit PlatformInterface(Install* const parent, const QString& platformTaglistPath, QString platformName, - const QDir& overviewDir, const DocKey&); + explicit PlatformInterface(Install* install, const QString& platformTaglistPath, QString platformName, + const QDir& overviewDir); //-Instance Functions-------------------------------------------------------------------------------------------------- +private: + std::shared_ptr processSet(const Fp::Set& set) override; + public: bool isEmpty() const override; bool containsGame(QUuid gameId) const override; bool containsAddApp(QUuid addAppId) const override; - - void addSet(const Fp::Set& set, const Lr::ImageSources& images) override; }; -class PlatformInterface::Writer : public Lr::PlatformDoc::Writer +class PlatformInterfaceWriter : public Lr::DataDocWriter { // Shell for writing the taglist of the interface @@ -302,27 +308,23 @@ class PlatformInterface::Writer : public Lr::PlatformDoc::Writer //-Constructor-------------------------------------------------------------------------------------------------------- public: - Writer(PlatformInterface* sourceDoc); + PlatformInterfaceWriter(PlatformInterface* sourceDoc); //-Instance Functions------------------------------------------------------------------------------------------------- public: Lr::DocHandlingError writeOutOf() override; }; -class PlaylistInterface : public Lr::PlaylistDoc +class PlaylistInterface : public Lr::PlaylistDoc { -//-Inner Classes---------------------------------------------------------------------------------------------------- -public: - class Writer; - + friend class PlaylistInterfaceWriter; //-Instance Variables-------------------------------------------------------------------------------------------------- private: PlaylistTaglist mPlaylistTaglist; //-Constructor-------------------------------------------------------------------------------------------------------- public: - explicit PlaylistInterface(Install* const parent, const QString& playlistTaglistPath, QString playlistName, - const DocKey&); + explicit PlaylistInterface(Install* install, const QString& playlistTaglistPath, QString playlistName); //-Instance Functions-------------------------------------------------------------------------------------------------- public: @@ -333,7 +335,7 @@ class PlaylistInterface : public Lr::PlaylistDoc void setPlaylistData(const Fp::Playlist& playlist) override; }; -class PlaylistInterface::Writer : public Lr::PlaylistDoc::Writer +class PlaylistInterfaceWriter : public Lr::DataDocWriter { // Shell for writing the taglist of the interface @@ -343,7 +345,7 @@ class PlaylistInterface::Writer : public Lr::PlaylistDoc::Writer //-Constructor-------------------------------------------------------------------------------------------------------- public: - Writer(PlaylistInterface* sourceDoc); + PlaylistInterfaceWriter(PlaylistInterface* sourceDoc); //-Instance Functions------------------------------------------------------------------------------------------------- private: @@ -400,7 +402,7 @@ class Emulator : public ConfigDoc //-Constructor-------------------------------------------------------------------------------------------------------- public: - explicit Emulator(Install * const parent, const QString& filePath, const DocKey&); + explicit Emulator(Install * const install, const QString& filePath); //-Instance Functions-------------------------------------------------------------------------------------------------- public: @@ -429,7 +431,7 @@ class Emulator : public ConfigDoc void setArtworkEntry(const EmulatorArtworkEntry& entry); }; -class EmulatorReader : public ConfigDoc::Reader +class EmulatorReader : public ConfigDoc::Reader { //-Constructor-------------------------------------------------------------------------------------------------------- public: @@ -448,11 +450,9 @@ class EmulatorReader : public ConfigDoc::Reader void parseInfoSource(const QString& value); void parseExitHotkey(const QString& value); void parseArtwork(const QString& value); - - Emulator* targetEmulator(); // TODO: Example of what isn't needed if readers/writers are made into templates }; -class Emulator::Writer : public ConfigDoc::Writer +class Emulator::Writer : public ConfigDoc::Writer { //-Class Values------------------------------------------------------------------------------------------------------- private: @@ -469,10 +469,9 @@ class Emulator::Writer : public ConfigDoc::Writer bool writeConfigDoc() override; void writeStandardKeyValue(const QString& key, const QString& value); void writeArtworkEntry(const EmulatorArtworkEntry& entry); - - Emulator* sourceEmulator(); // TODO: Example of what isn't needed if readers/writers are made into templates }; } +#include "am-data.tpp" #endif // ATTRACTMODE_DATA_H diff --git a/app/src/launcher/implementation/attractmode/am-data.tpp b/app/src/launcher/implementation/attractmode/am-data.tpp new file mode 100644 index 0000000..0859e74 --- /dev/null +++ b/app/src/launcher/implementation/attractmode/am-data.tpp @@ -0,0 +1,193 @@ +#ifndef ATTRACTMODE_DATA_TPP +#define ATTRACTMODE_DATA_TPP + +#include "am-data.h" // Ignore recursive error, doesn't actually cause problem + +#ifndef ATTRACTMODE_DATA_H +#error __FILE__ should only be included from am-data.h. +#endif // ATTRACTMODE_DATA_H + +namespace Am +{ + +//=============================================================================================================== +// CommonDocReader +//=============================================================================================================== + +//-Constructor-------------------------------------------------------------------------------------------------------- +//Protected: +template +CommonDocReader::CommonDocReader(DocT* targetDoc) : + Lr::DataDocReader(targetDoc), + mStreamReader(targetDoc->path()) +{} + +//-Instance Functions------------------------------------------------------------------------------------------------- +//Protected: +template +bool CommonDocReader::lineIsComment(const QString& line) { return line.front() == '#'; } + +template +QString CommonDocReader::readLineIgnoringComments(qint64 maxlen) +{ + QString line; + + do + line = mStreamReader.readLine(maxlen); + while(!line.isEmpty() && line.front() == '#'); // Must check for empty string due to QString::front() constraints + + return line; +} + +//Public: +template +Lr::DocHandlingError CommonDocReader::readInto() +{ + // Open file + Qx::IoOpReport openError = mStreamReader.openFile(); + if(openError.isFailure()) + return Lr::DocHandlingError(*target(), Lr::DocHandlingError::DocCantOpen, openError.outcomeInfo()); + + // Check that doc is valid + bool isValid = false; + if(!checkDocValidity(isValid)) + return Lr::DocHandlingError(*target(), Lr::DocHandlingError::DocWriteFailed, mStreamReader.status().outcomeInfo()); + else if(!isValid) + return Lr::DocHandlingError(*target(), Lr::DocHandlingError::DocInvalidType); + + // Read doc + Lr::DocHandlingError parseError = readTargetDoc(); + + // Close file + mStreamReader.closeFile(); + + // Return outcome + if(parseError.isValid()) + return parseError; + else if(mStreamReader.hasError()) + return Lr::DocHandlingError(*target(), Lr::DocHandlingError::DocWriteFailed, mStreamReader.status().outcomeInfo()); + else + return Lr::DocHandlingError(); +} + +//=============================================================================================================== +// CommonDocWriter +//=============================================================================================================== + +//-Constructor-------------------------------------------------------------------------------------------------------- +//Protected: +template +CommonDocWriter::CommonDocWriter(DocT* sourceDoc) : + Lr::DataDocWriter(sourceDoc), + mStreamWriter(sourceDoc->path(), Qx::WriteMode::Truncate) +{} + +//-Instance Functions------------------------------------------------------------------------------------------------- +//Public: +template +Lr::DocHandlingError CommonDocWriter::writeOutOf() +{ + // Open file + Qx::IoOpReport openError = mStreamWriter.openFile(); + if(openError.isFailure()) + return Lr::DocHandlingError(*source(), Lr::DocHandlingError::DocCantOpen, openError.outcomeInfo()); + + // Write doc + bool writeSuccess = writeSourceDoc(); + + // Close file + mStreamWriter.closeFile(); + + // Return outcome + return writeSuccess ? Lr::DocHandlingError() : + Lr::DocHandlingError(*source(), Lr::DocHandlingError::DocWriteFailed, mStreamWriter.status().outcomeInfo()); +} + +//=============================================================================================================== +// ConfigDoc::Reader +//=============================================================================================================== + +//-Constructor-------------------------------------------------------------------------------------------------------- +//Protected: +template +ConfigDoc::Reader::Reader(DocT* targetDoc) : + CommonDocReader(targetDoc) +{} + +//-Class Functions------------------------------------------------------------------------------------------------- +//Protected: +template +bool ConfigDoc::Reader::splitKeyValue(const QString& line, QString& key, QString& value) +{ + /* TODO: The result from this function is currently unused due to no easy way to raise a custom + * error with the stream reader in this class (and how the current paradigm is to return bools + * for each step and then use the reader status if one is found). If used properly this should + * never error, but ideally it should be checked for anyway. Might need to have all read functions + * return Qx::GenericError to allow non stream related errors to be returned. + */ + + // Null out return buffers + key = QString(); + value = QString(); + + QRegularExpressionMatch keyValueCheck = KEY_VALUE_REGEX.match(line); + if(keyValueCheck.hasMatch()) + { + key = keyValueCheck.captured(u"key"_s); + value = keyValueCheck.captured(u"value"_s); + return true; + } + else + { + qWarning("Invalid key value string"); + return false; + } +} + +//-Instance Functions------------------------------------------------------------------------------------------------- +//Protected: +template +bool ConfigDoc::Reader::checkDocValidity(bool& isValid) +{ + // Check for config "header" + QString firstLine = mStreamReader.readLine(); + QString secondLine = mStreamReader.readLine(); + + bool hasTagline = firstLine.left(ConfigDoc::TAGLINE.length()) == ConfigDoc::TAGLINE; + + isValid = hasTagline && lineIsComment(secondLine); + + // Return status + return !mStreamReader.hasError(); +} + +//=============================================================================================================== +// ConfigDoc::Writer +//=============================================================================================================== + +//-Constructor-------------------------------------------------------------------------------------------------------- +//Public: +template +ConfigDoc::Writer::Writer(DocT* sourceDoc) : + CommonDocWriter(sourceDoc) +{} + +//-Instance Functions-------------------------------------------------------------------------------------------------- +//Public: +template +bool ConfigDoc::Writer::writeSourceDoc() +{ + // Write config doc "header" + mStreamWriter.writeLine(source()->versionedTagline()); + mStreamWriter.writeLine(u"#"_s); + + if(mStreamWriter.hasError()) + return false; + + // Perform custom writing + return writeConfigDoc(); +} + +} + +#endif // ATTRACTMODE_DATA_TPP diff --git a/app/src/launcher/attractmode/am-install.cpp b/app/src/launcher/implementation/attractmode/am-install.cpp similarity index 78% rename from app/src/launcher/attractmode/am-install.cpp rename to app/src/launcher/implementation/attractmode/am-install.cpp index 636175d..3faf0a5 100644 --- a/app/src/launcher/attractmode/am-install.cpp +++ b/app/src/launcher/implementation/attractmode/am-install.cpp @@ -20,7 +20,7 @@ namespace Am //-Constructor------------------------------------------------------------------------------------------------ //Public: Install::Install(const QString& installPath) : - Lr::Install(installPath), + Lr::Install(installPath), mEmulatorsDirectory(installPath + '/' + EMULATORS_PATH), mRomlistsDirectory(installPath + '/' + ROMLISTS_PATH), mMainConfigFile(installPath + '/' + MAIN_CFG_PATH), @@ -55,7 +55,7 @@ Install::Install(const QString& installPath) : //Private: void Install::nullify() { - Lr::Install::nullify(); + Lr::IInstall::nullify(); mEmulatorsDirectory = QDir(); mRomlistsDirectory = QDir(); @@ -89,7 +89,7 @@ Qx::Error Install::populateExistingDocs() return existingCheck; for(const QFileInfo& platformFile : qAsConst(existingList)) - catalogueExistingDoc(Lr::DataDoc::Identifier(Lr::DataDoc::Type::Platform, platformFile.baseName())); + catalogueExistingDoc(Lr::IDataDoc::Identifier(Lr::IDataDoc::Type::Platform, platformFile.baseName())); // Check for playlists existingCheck = Qx::dirContentInfoList(existingList, mFpTagDirectory, {u"[[]Playlist[]] *."_s + TAG_EXT}, @@ -98,35 +98,35 @@ Qx::Error Install::populateExistingDocs() return existingCheck; for(const QFileInfo& playlistFile : qAsConst(existingList)) - catalogueExistingDoc(Lr::DataDoc::Identifier(Lr::DataDoc::Type::Playlist, playlistFile.baseName())); + catalogueExistingDoc(Lr::IDataDoc::Identifier(Lr::IDataDoc::Type::Playlist, playlistFile.baseName())); // Check for special "Flashpoint" platform (more like a config doc but OK for now) QFileInfo mainRomlistInfo(mFpRomlist); if(mainRomlistInfo.exists()) - catalogueExistingDoc(Lr::DataDoc::Identifier(Lr::DataDoc::Type::Platform, mainRomlistInfo.baseName())); + catalogueExistingDoc(Lr::IDataDoc::Identifier(Lr::IDataDoc::Type::Platform, mainRomlistInfo.baseName())); } // Check for config docs QFileInfo mainCfgInfo(mMainConfigFile); - catalogueExistingDoc(Lr::DataDoc::Identifier(Lr::DataDoc::Type::Config, mainCfgInfo.baseName())); // Must exist + catalogueExistingDoc(Lr::IDataDoc::Identifier(Lr::IDataDoc::Type::Config, mainCfgInfo.baseName())); // Must exist QFileInfo emulatorCfgInfo(mEmulatorConfigFile); if(emulatorCfgInfo.exists()) - catalogueExistingDoc(Lr::DataDoc::Identifier(Lr::DataDoc::Type::Config, emulatorCfgInfo.baseName())); + catalogueExistingDoc(Lr::IDataDoc::Identifier(Lr::IDataDoc::Type::Config, emulatorCfgInfo.baseName())); // Return success return Qx::Error(); } -QString Install::imageDestinationPath(Fp::ImageType imageType, const Lr::Game* game) const +QString Install::imageDestinationPath(Fp::ImageType imageType, const Lr::Game& game) const { return mFpScraperDirectory.absolutePath() + '/' + (imageType == Fp::ImageType::Logo ? LOGO_FOLDER_NAME : SCREENSHOT_FOLDER_NAME) + '/' + - game->id().toString(QUuid::WithoutBraces) + + game.id().toString(QUuid::WithoutBraces) + '.' + IMAGE_EXT; } -std::shared_ptr Install::preparePlatformDocCheckout(std::unique_ptr& platformDoc, const QString& translatedName) +std::unique_ptr Install::preparePlatformDocCheckout(const QString& translatedName) { // Determine path to the taglist that corresponds with the interface QString taglistPath = mFpTagDirectory.absoluteFilePath(translatedName + u"."_s + TAG_EXT) ; @@ -135,46 +135,22 @@ std::shared_ptr Install::preparePlatformDocCheckout(std QDir overviewDir(mFpScraperDirectory.absoluteFilePath(OVERVIEW_FOLDER_NAME)); // Not a file, but works // Construct unopened document - platformDoc = std::make_unique(this, taglistPath, translatedName, overviewDir, DocKey{}); - - // No reading to be done for this interface (tag lists are always overwritten) - return std::shared_ptr(); + return std::make_unique(this, taglistPath, translatedName, overviewDir); } -std::shared_ptr Install::preparePlaylistDocCheckout(std::unique_ptr& playlistDoc, const QString& translatedName) +std::unique_ptr Install::preparePlaylistDocCheckout(const QString& translatedName) { // Determine path to the taglist that corresponds with the interface QString taglistPath = mFpTagDirectory.absoluteFilePath(translatedName + u"."_s + TAG_EXT) ; // Construct unopened document - playlistDoc = std::make_unique(this, taglistPath, translatedName, DocKey{}); - - // No reading to be done for this interface (tag lists are always overwritten) - return std::shared_ptr(); -} - -std::shared_ptr Install::preparePlatformDocCommit(const std::unique_ptr& platformDoc) -{ - // Construct doc writer - std::shared_ptr docWriter = std::make_shared(static_cast(platformDoc.get())); - - // Return writer - return docWriter; -} - -std::shared_ptr Install::preparePlaylistDocCommit(const std::unique_ptr& playlistDoc) -{ - // Construct doc writer - std::shared_ptr docWriter = std::make_shared(static_cast(playlistDoc.get())); - - // Return writer - return docWriter; + return std::make_unique(this, taglistPath, translatedName); } Lr::DocHandlingError Install::checkoutMainConfig(std::unique_ptr& returnBuffer) { // Construct unopened document - returnBuffer = std::make_unique(this, mMainConfigFile.fileName(), DocKey{}); + returnBuffer = std::make_unique(this, mMainConfigFile.fileName()); // Construct doc reader std::shared_ptr docReader = std::make_shared(returnBuffer.get()); @@ -193,7 +169,7 @@ Lr::DocHandlingError Install::checkoutMainConfig(std::unique_ptr& Lr::DocHandlingError Install::checkoutFlashpointRomlist(std::unique_ptr& returnBuffer) { // Construct unopened document - returnBuffer = std::make_unique(this, mFpRomlist.fileName(), Fp::NAME, mImportDetails->updateOptions, DocKey{}); + returnBuffer = std::make_unique(this, mFpRomlist.fileName(), Fp::NAME, importDetails().updateOptions); // Construct doc reader std::shared_ptr docReader = std::make_shared(returnBuffer.get()); @@ -212,7 +188,7 @@ Lr::DocHandlingError Install::checkoutFlashpointRomlist(std::unique_ptr Lr::DocHandlingError Install::checkoutClifpEmulatorConfig(std::unique_ptr& returnBuffer) { // Construct unopened document - returnBuffer = std::make_unique(this, mEmulatorConfigFile.fileName(), DocKey{}); + returnBuffer = std::make_unique(this, mEmulatorConfigFile.fileName()); // Construct doc reader std::shared_ptr docReader = std::make_shared(returnBuffer.get()); @@ -230,7 +206,7 @@ Lr::DocHandlingError Install::checkoutClifpEmulatorConfig(std::unique_ptr document) { - assert(document->parent() == this); + Q_ASSERT(document->install() == this); // Prepare writer std::shared_ptr docWriter = std::make_shared(document.get()); @@ -247,7 +223,7 @@ Lr::DocHandlingError Install::commitMainConfig(std::unique_ptr do Lr::DocHandlingError Install::commitFlashpointRomlist(std::unique_ptr document) { - assert(document->parent() == this); + Q_ASSERT(document->install() == this); // Prepare writer std::shared_ptr docWriter = std::make_shared(document.get()); @@ -265,7 +241,7 @@ Lr::DocHandlingError Install::commitFlashpointRomlist(std::unique_ptr d Lr::DocHandlingError Install::commitClifpEmulatorConfig(std::unique_ptr document) { - assert(document->parent() == this); + Q_ASSERT(document->install() == this); // Prepare writer std::shared_ptr docWriter = std::make_shared(document.get()); @@ -283,14 +259,12 @@ Lr::DocHandlingError Install::commitClifpEmulatorConfig(std::unique_ptr Install::preferredImageModeOrder() const { return IMAGE_MODE_ORDER; } bool Install::isRunning() const @@ -324,18 +298,18 @@ QString Install::versionString() const } // Can't determine version - return Lr::Install::versionString(); + return Lr::IInstall::versionString(); } -QString Install::translateDocName(const QString& originalName, Lr::DataDoc::Type type) const +QString Install::translateDocName(const QString& originalName, Lr::IDataDoc::Type type) const { // Perform general kosherization QString translatedName = Qx::kosherizeFileName(originalName); // Prefix platforms/playlists - if(type == Lr::DataDoc::Type::Platform) + if(type == Lr::IDataDoc::Type::Platform) translatedName.prepend(PLATFORM_TAG_PREFIX); - else if(type == Lr::DataDoc::Type::Playlist) + else if(type == Lr::IDataDoc::Type::Playlist) translatedName.prepend(PLAYLIST_TAG_PREFIX); return translatedName; @@ -371,7 +345,7 @@ Qx::Error Install::preImport(const ImportDetails& details) } // Perform base tasks - return Lr::Install::preImport(details); + return Lr::IInstall::preImport(details); } Qx::Error Install::prePlatformsImport() @@ -389,23 +363,10 @@ Qx::Error Install::postPlatformsImport() return commitFlashpointRomlist(std::move(mRomlist)); } -Qx::Error Install::preImageProcessing(QList& workerTransfers, const Lr::ImageSources& bulkSources) +Qx::Error Install::preImageProcessing(const Lr::ImagePaths& bulkSources) { Q_UNUSED(bulkSources); - - switch(mImportDetails->imageMode) - { - case Import::ImageMode::Link: - case Import::ImageMode::Copy: - workerTransfers.swap(mWorkerImageJobs); - return Qx::Error(); - case Import::ImageMode::Reference: - qWarning("unsupported image mode"); - return Qx::Error(); - default: - qWarning("unhandled image mode"); - return Qx::Error(); - } + return {}; } Qx::Error Install::postImport() @@ -421,7 +382,7 @@ Qx::Error Install::postImport() return emulatorConfigReadError; // General emulator setup - QString workingDir = QDir::toNativeSeparators(QFileInfo(mImportDetails->clifpPath).absolutePath()); + QString workingDir = QDir::toNativeSeparators(QFileInfo(importDetails().clifpPath).absolutePath()); emulatorConfig->setExecutable(CLIFp::EXE_NAME); emulatorConfig->setArgs(uR"(play -i u"[romfilename]"_s)"_s); emulatorConfig->setWorkDir(workingDir); @@ -541,25 +502,13 @@ Qx::Error Install::postImport() return Qx::Error(); } -void Install::processDirectGameImages(const Lr::Game* game, const Lr::ImageSources& imageSources) +void Install::convertToDestinationImages(const RomEntry& game, Lr::ImagePaths& images) { - Import::ImageMode mode = mImportDetails->imageMode; - if(mode == Import::ImageMode::Link || mode == Import::ImageMode::Copy) - { - if(!imageSources.logoPath().isEmpty()) - { - ImageMap logoMap{.sourcePath = imageSources.logoPath(), - .destPath = imageDestinationPath(Fp::ImageType::Logo, game)}; - mWorkerImageJobs.append(logoMap); - } + if(!images.logoPath().isEmpty()) + images.setLogoPath(imageDestinationPath(Fp::ImageType::Logo, game)); - if(!imageSources.screenshotPath().isEmpty()) - { - ImageMap ssMap{.sourcePath = imageSources.screenshotPath(), - .destPath = imageDestinationPath(Fp::ImageType::Screenshot, game)}; - mWorkerImageJobs.append(ssMap); - } - } + if(!images.screenshotPath().isEmpty()) + images.setScreenshotPath(imageDestinationPath(Fp::ImageType::Screenshot, game)); } } diff --git a/app/src/launcher/attractmode/am-install.h b/app/src/launcher/implementation/attractmode/am-install.h similarity index 73% rename from app/src/launcher/attractmode/am-install.h rename to app/src/launcher/implementation/attractmode/am-install.h index a9c4058..88a0996 100644 --- a/app/src/launcher/attractmode/am-install.h +++ b/app/src/launcher/implementation/attractmode/am-install.h @@ -5,25 +5,20 @@ #include // Project Includes -#include "launcher/lr-install.h" -#include "launcher/attractmode/am-data.h" -#include "launcher/attractmode/am-settings-data.h" +#include "launcher/abstract/lr-install.h" +#include "launcher/implementation/attractmode/am-data.h" +#include "launcher/implementation/attractmode/am-settings-data.h" namespace Am { -class Install : public Lr::Install +class Install : public Lr::Install { friend class PlatformInterface; friend class PlaylistInterface; //-Class Variables-------------------------------------------------------------------------------------------------- -public: - // Identity - static inline const QString NAME = u"AttractMode"_s; - static inline const QString ICON_PATH = u":/launcher/AttractMode/icon.png"_s; - static inline const QUrl HELP_URL = QUrl(u""_s); - +private: // Naming static inline const QString PLATFORM_TAG_PREFIX = u"[Platform] "_s; static inline const QString PLAYLIST_TAG_PREFIX = u"[Playlist] "_s; @@ -79,9 +74,6 @@ class Install : public Lr::Install QFile mFpRomlist; QFile mEmulatorConfigFile; - // Image transfers for import worker - QList mWorkerImageJobs; - // Main romlist std::unique_ptr mRomlist; @@ -97,13 +89,11 @@ class Install : public Lr::Install QString versionFromExecutable() const; // Image Processing - QString imageDestinationPath(Fp::ImageType imageType, const Lr::Game* game) const; + QString imageDestinationPath(Fp::ImageType imageType, const Lr::Game& game) const; // Doc handling - std::shared_ptr preparePlatformDocCheckout(std::unique_ptr& platformDoc, const QString& translatedName) override; - std::shared_ptr preparePlaylistDocCheckout(std::unique_ptr& playlistDoc, const QString& translatedName) override; - std::shared_ptr preparePlatformDocCommit(const std::unique_ptr& platformDoc) override; - std::shared_ptr preparePlaylistDocCommit(const std::unique_ptr& playlistDoc) override; + std::unique_ptr preparePlatformDocCheckout(const QString& translatedName) override; + std::unique_ptr preparePlaylistDocCheckout(const QString& translatedName) override; Lr::DocHandlingError checkoutMainConfig(std::unique_ptr& returnBuffer); Lr::DocHandlingError checkoutFlashpointRomlist(std::unique_ptr& returnBuffer); @@ -117,23 +107,21 @@ class Install : public Lr::Install void softReset() override; // Info - QString name() const override; QList preferredImageModeOrder() const override; bool isRunning() const override; QString versionString() const override; - QString translateDocName(const QString& originalName, Lr::DataDoc::Type type) const override; + QString translateDocName(const QString& originalName, Lr::IDataDoc::Type type) const override; // Import stage notifier hooks Qx::Error preImport(const ImportDetails& details) override; Qx::Error prePlatformsImport() override; Qx::Error postPlatformsImport() override; - Qx::Error preImageProcessing(QList& workerTransfers, const Lr::ImageSources& bulkSources) override; + Qx::Error preImageProcessing(const Lr::ImagePaths& bulkSources) override; Qx::Error postImport() override; // Image handling - void processDirectGameImages(const Lr::Game* game, const Lr::ImageSources& imageSources) override; + void convertToDestinationImages(const RomEntry& game, Lr::ImagePaths& images) override; }; -REGISTER_LAUNCHER(Install::NAME, Install, &Install::ICON_PATH, &Install::HELP_URL); } #endif // ATTRACTMODE_INSTALL_H diff --git a/app/src/launcher/attractmode/am-install_linux.cpp b/app/src/launcher/implementation/attractmode/am-install_linux.cpp similarity index 100% rename from app/src/launcher/attractmode/am-install_linux.cpp rename to app/src/launcher/implementation/attractmode/am-install_linux.cpp diff --git a/app/src/launcher/attractmode/am-install_win.cpp b/app/src/launcher/implementation/attractmode/am-install_win.cpp similarity index 100% rename from app/src/launcher/attractmode/am-install_win.cpp rename to app/src/launcher/implementation/attractmode/am-install_win.cpp diff --git a/app/src/launcher/attractmode/am-items.cpp b/app/src/launcher/implementation/attractmode/am-items.cpp similarity index 100% rename from app/src/launcher/attractmode/am-items.cpp rename to app/src/launcher/implementation/attractmode/am-items.cpp diff --git a/app/src/launcher/attractmode/am-items.h b/app/src/launcher/implementation/attractmode/am-items.h similarity index 98% rename from app/src/launcher/attractmode/am-items.h rename to app/src/launcher/implementation/attractmode/am-items.h index 1663ee6..38877f9 100644 --- a/app/src/launcher/attractmode/am-items.h +++ b/app/src/launcher/implementation/attractmode/am-items.h @@ -9,7 +9,7 @@ #include // Project Includes -#include "launcher/lr-items.h" +#include "launcher/interface/lr-items-interface.h" namespace Am { diff --git a/app/src/launcher/implementation/attractmode/am-registration.cpp b/app/src/launcher/implementation/attractmode/am-registration.cpp new file mode 100644 index 0000000..c61e56a --- /dev/null +++ b/app/src/launcher/implementation/attractmode/am-registration.cpp @@ -0,0 +1,4 @@ +#include "am-registration.h" +#include "am-install.h" + +REGISTER_LAUNCHER(Am::LauncherId); diff --git a/app/src/launcher/implementation/attractmode/am-registration.h b/app/src/launcher/implementation/attractmode/am-registration.h new file mode 100644 index 0000000..65643aa --- /dev/null +++ b/app/src/launcher/implementation/attractmode/am-registration.h @@ -0,0 +1,33 @@ +#ifndef ATTRACTMODE_REGISTRATION_H +#define ATTRACTMODE_REGISTRATION_H + +#include "launcher/abstract/lr-registration.h" + +namespace Am { + +class Install; +class PlatformInterface; +class PlatformInterfaceWriter; +class PlaylistInterface; +class PlaylistInterfaceWriter; +class RomEntry; + +using LauncherId = Lr::Registrar< + Install, + PlatformInterface, + void, // No reading to be done for this interface (tag lists are always overwritten) + PlatformInterfaceWriter, + PlaylistInterface, + void, // No reading to be done for this interface (tag lists are always overwritten) + PlaylistInterfaceWriter, + RomEntry, + void, + void, + void, + u"AttractMode", + u":/launcher/AttractMode/icon.png", + u"" // TODO: Add url +>; + +} +#endif // ATTRACTMODE_REGISTRATION_H diff --git a/app/src/launcher/attractmode/am-settings-data.cpp b/app/src/launcher/implementation/attractmode/am-settings-data.cpp similarity index 92% rename from app/src/launcher/attractmode/am-settings-data.cpp rename to app/src/launcher/implementation/attractmode/am-settings-data.cpp index 53c119c..069231d 100644 --- a/app/src/launcher/attractmode/am-settings-data.cpp +++ b/app/src/launcher/implementation/attractmode/am-settings-data.cpp @@ -161,14 +161,14 @@ bool OtherSetting::Parser::parse(QStringView key, const QString& value, int dept //-Constructor-------------------------------------------------------------------------------------------------------- //Public: -CrudeSettings::CrudeSettings(Install* const parent, const QString& filePath, const DocKey&) : - ConfigDoc(parent, filePath, STD_NAME) +CrudeSettings::CrudeSettings(Install* install, const QString& filePath) : + ConfigDoc(install, filePath, STD_NAME) {} //-Instance Functions-------------------------------------------------------------------------------------------------- //Public: bool CrudeSettings::isEmpty() const { return mDisplays.isEmpty() && mOtherSettings.isEmpty(); } -Lr::DataDoc::Type CrudeSettings::type() const { return Type::Config; } +Lr::IDataDoc::Type CrudeSettings::type() const { return Type::Config; } bool CrudeSettings::containsDisplay(const QString& name) { return mDisplays.contains(name); } void CrudeSettings::addDisplay(const Display& display) { mDisplays.insert(display.name(), display); } @@ -192,7 +192,7 @@ void CrudeSettings::addOtherSetting(const OtherSetting& setting) //-Constructor-------------------------------------------------------------------------------------------------------- //Public: CrudeSettingsReader::CrudeSettingsReader(CrudeSettings* targetDoc) : - ConfigDoc::Reader(targetDoc) + ConfigDoc::Reader(targetDoc) {} //-Class Functions-------------------------------------------------------------------------------------------------- @@ -212,11 +212,6 @@ int CrudeSettingsReader::checkTabDepth(const QString& line) //-Instance Functions-------------------------------------------------------------------------------------------------- //Private: -CrudeSettings* CrudeSettingsReader::targetCrudeSettings() const -{ - return static_cast(mTargetDocument); -} - Lr::DocHandlingError CrudeSettingsReader::readTargetDoc() { Lr::DocHandlingError errorStatus; @@ -242,8 +237,8 @@ Lr::DocHandlingError CrudeSettingsReader::readTargetDoc() if(key == CrudeSettings::Keys::DISPLAY) { // Add empty display to doc - targetCrudeSettings()->mDisplays[value] = Display(value); - Display* addedDisplay = &targetCrudeSettings()->mDisplays[value]; + target()->mDisplays[value] = Display(value); + Display* addedDisplay = &target()->mDisplays[value]; // Create parser and set to current mCurrentSubSettingParser = std::make_unique(addedDisplay); @@ -253,7 +248,7 @@ Lr::DocHandlingError CrudeSettingsReader::readTargetDoc() if(!mCurrentSubSettingParser->parse(key, value, depth)) { QString setting = mCurrentSubSettingParser->settingName(); - errorStatus = Lr::DocHandlingError(*mTargetDocument, Lr::DocHandlingError::DocReadFailed, UNKNOWN_KEY_ERROR.arg(key, setting)); + errorStatus = Lr::DocHandlingError(*target(), Lr::DocHandlingError::DocReadFailed, UNKNOWN_KEY_ERROR.arg(key, setting)); break; } } @@ -273,8 +268,8 @@ void CrudeSettingsReader::initializeGenericSubSetting(const QString& key, const { // Add empty generic QUuid id = OtherSetting::equivalentId(key, value); - targetCrudeSettings()->mOtherSettings[id] = OtherSetting(key, value); - OtherSetting* addedSetting = &targetCrudeSettings()->mOtherSettings[id]; + target()->mOtherSettings[id] = OtherSetting(key, value); + OtherSetting* addedSetting = &target()->mOtherSettings[id]; // Create parser and set to current mCurrentSubSettingParser = std::make_unique(addedSetting); @@ -287,7 +282,7 @@ void CrudeSettingsReader::initializeGenericSubSetting(const QString& key, const //-Constructor-------------------------------------------------------------------------------------------------------- //Public: CrudeSettingsWriter::CrudeSettingsWriter(CrudeSettings* sourceDoc) : - ConfigDoc::Writer(sourceDoc), + ConfigDoc::Writer(sourceDoc), mTabDepth(0) { // Global alignment @@ -296,11 +291,6 @@ CrudeSettingsWriter::CrudeSettingsWriter(CrudeSettings* sourceDoc) : //-Instance Functions-------------------------------------------------------------------------------------------------- //Private: -CrudeSettings* CrudeSettingsWriter::sourceCrudeSettings() const -{ - return static_cast(mSourceDocument); -} - void CrudeSettingsWriter::writeKeyValue(const QString& key, const QString& value) { mStreamWriter << QString(mTabDepth, '\t'); @@ -313,14 +303,14 @@ void CrudeSettingsWriter::writeKeyValue(const QString& key, const QString& value bool CrudeSettingsWriter::writeConfigDoc() { // Write all display entries - for(const Display& display : qAsConst(sourceCrudeSettings()->mDisplays)) + for(const Display& display : qAsConst(source()->mDisplays)) { if(!writeDisplay(display)) return false; } // Write all other settings - for(const OtherSetting& setting : qAsConst(sourceCrudeSettings()->mOtherSettings)) + for(const OtherSetting& setting : qAsConst(source()->mOtherSettings)) { if(!writeOtherSetting(setting)) return false; diff --git a/app/src/launcher/attractmode/am-settings-data.h b/app/src/launcher/implementation/attractmode/am-settings-data.h similarity index 96% rename from app/src/launcher/attractmode/am-settings-data.h rename to app/src/launcher/implementation/attractmode/am-settings-data.h index 5d59430..a01836b 100644 --- a/app/src/launcher/attractmode/am-settings-data.h +++ b/app/src/launcher/implementation/attractmode/am-settings-data.h @@ -2,8 +2,8 @@ #define AM_SETTINGS_DATA_H // Project Includes -#include "launcher/attractmode/am-data.h" -#include "launcher/attractmode/am-settings-items.h" +#include "launcher/implementation/attractmode/am-data.h" +#include "launcher/implementation/attractmode/am-settings-items.h" namespace Am { @@ -162,7 +162,7 @@ class CrudeSettings : public ConfigDoc //-Constructor-------------------------------------------------------------------------------------------------------- public: - explicit CrudeSettings(Install* const parent, const QString& filePath, const DocKey&); + explicit CrudeSettings(Install* install, const QString& filePath); //-Instance Functions-------------------------------------------------------------------------------------------------- public: @@ -177,7 +177,7 @@ class CrudeSettings : public ConfigDoc void addOtherSetting(const OtherSetting& setting); }; -class CrudeSettingsReader : public ConfigDoc::Reader +class CrudeSettingsReader : public ConfigDoc::Reader { //-Class Variables-------------------------------------------------------------------------------------------------- private: @@ -197,12 +197,11 @@ class CrudeSettingsReader : public ConfigDoc::Reader //-Instance Functions------------------------------------------------------------------------------------------------- private: - CrudeSettings* targetCrudeSettings() const; Lr::DocHandlingError readTargetDoc() override; void initializeGenericSubSetting(const QString& key, const QString& value); }; -class CrudeSettingsWriter : public ConfigDoc::Writer +class CrudeSettingsWriter : public ConfigDoc::Writer { //-Class Variables------------------------------------------------------------------------------------------------- private: @@ -218,7 +217,6 @@ class CrudeSettingsWriter : public ConfigDoc::Writer //-Instance Functions------------------------------------------------------------------------------------------------- private: - CrudeSettings* sourceCrudeSettings() const; void writeKeyValue(const QString& key, const QString& value = QString()); bool writeConfigDoc() override; diff --git a/app/src/launcher/attractmode/am-settings-items.cpp b/app/src/launcher/implementation/attractmode/am-settings-items.cpp similarity index 100% rename from app/src/launcher/attractmode/am-settings-items.cpp rename to app/src/launcher/implementation/attractmode/am-settings-items.cpp diff --git a/app/src/launcher/attractmode/am-settings-items.h b/app/src/launcher/implementation/attractmode/am-settings-items.h similarity index 99% rename from app/src/launcher/attractmode/am-settings-items.h rename to app/src/launcher/implementation/attractmode/am-settings-items.h index f1eff33..8abcbb5 100644 --- a/app/src/launcher/attractmode/am-settings-items.h +++ b/app/src/launcher/implementation/attractmode/am-settings-items.h @@ -2,7 +2,7 @@ #define AM_SETTINGS_ITEMS_H // Project Includes -#include "launcher/lr-items.h" +#include "launcher/interface/lr-items-interface.h" using namespace Qt::Literals::StringLiterals; diff --git a/app/src/launcher/launchbox/lb-data.cpp b/app/src/launcher/implementation/launchbox/lb-data.cpp similarity index 86% rename from app/src/launcher/launchbox/lb-data.cpp rename to app/src/launcher/implementation/launchbox/lb-data.cpp index 980fa30..735f42f 100644 --- a/app/src/launcher/launchbox/lb-data.cpp +++ b/app/src/launcher/implementation/launchbox/lb-data.cpp @@ -5,7 +5,7 @@ #include // Project Includes -#include "launcher/launchbox/lb-install.h" +#include "launcher/implementation/launchbox/lb-install.h" namespace Xml { @@ -126,23 +126,20 @@ namespace Lb //-Constructor-------------------------------------------------------------------------------------------------------- //Public: -PlatformDoc::PlatformDoc(Install* const parent, const QString& xmlPath, QString docName, const Import::UpdateOptions& updateOptions, - const DocKey&) : - Lr::BasicPlatformDoc(parent, xmlPath, docName, updateOptions) +PlatformDoc::PlatformDoc(Install* install, const QString& xmlPath, QString docName, const Import::UpdateOptions& updateOptions) : + Lr::BasicPlatformDoc(install, xmlPath, docName, updateOptions) {} //-Instance Functions-------------------------------------------------------------------------------------------------- //Private: -std::shared_ptr PlatformDoc::prepareGame(const Fp::Game& game, const Lr::ImageSources& images) +std::shared_ptr PlatformDoc::prepareGame(const Fp::Game& game) { - Q_UNUSED(images); // LaunchBox doesn't store image info in its platform doc directly - // Convert to LaunchBox game - const QString& clifpPath = static_cast(parent())->mImportDetails->clifpPath; + const QString& clifpPath = install()->importDetails().clifpPath; std::shared_ptr lbGame = std::make_shared(game, clifpPath); // Add details to cache - static_cast(parent())->mPlaylistGameDetailsCache.insert(game.id(), PlaylistGame::EntryDetails(*lbGame)); + install()->mPlaylistGameDetailsCache.insert(game.id(), PlaylistGame::EntryDetails(*lbGame)); // Add language as custom field CustomField::Builder cfb; @@ -155,10 +152,10 @@ std::shared_ptr PlatformDoc::prepareGame(const Fp::Game& game, const L return lbGame; } -std::shared_ptr PlatformDoc::prepareAddApp(const Fp::AddApp& addApp) +std::shared_ptr PlatformDoc::prepareAddApp(const Fp::AddApp& addApp) { // Convert to LaunchBox add app - const QString& clifpPath = static_cast(parent())->mImportDetails->clifpPath; + const QString& clifpPath = install()->importDetails().clifpPath; std::shared_ptr lbAddApp = std::make_shared(addApp, clifpPath); // Return converted game @@ -174,7 +171,7 @@ void PlatformDoc::addCustomField(std::shared_ptr customField) //Public: bool PlatformDoc::isEmpty() const { - return mCustomFieldsFinal.isEmpty() && mCustomFieldsExisting.isEmpty() && Lr::BasicPlatformDoc::isEmpty(); + return mCustomFieldsFinal.isEmpty() && mCustomFieldsExisting.isEmpty() && Lr::BasicPlatformDoc::isEmpty(); } void PlatformDoc::finalize() @@ -192,24 +189,22 @@ void PlatformDoc::finalize() ++i; } - Lr::BasicPlatformDoc::finalize(); + Lr::BasicPlatformDoc::finalize(); } //=============================================================================================================== -// PlatformDoc::Reader +// PlatformDocReader //=============================================================================================================== //-Constructor-------------------------------------------------------------------------------------------------------- //Public: -PlatformDoc::Reader::Reader(PlatformDoc* targetDoc) : - Lr::DataDoc::Reader(targetDoc), - Lr::BasicPlatformDoc::Reader(targetDoc), - Lr::XmlDocReader(targetDoc, Xml::ROOT_ELEMENT) +PlatformDocReader::PlatformDocReader(PlatformDoc* targetDoc) : + Lr::XmlDocReader(targetDoc, Xml::ROOT_ELEMENT) {} //-Instance Functions------------------------------------------------------------------------------------------------- //Private: -Lr::DocHandlingError PlatformDoc::Reader::readTargetDoc() +Lr::DocHandlingError PlatformDocReader::readTargetDoc() { while(mStreamReader.readNextStartElement()) { @@ -227,7 +222,7 @@ Lr::DocHandlingError PlatformDoc::Reader::readTargetDoc() return streamStatus(); } -void PlatformDoc::Reader::parseGame() +void PlatformDocReader::parseGame() { // Game to build Game::Builder gb; @@ -281,10 +276,10 @@ void PlatformDoc::Reader::parseGame() // Build Game and add to document std::shared_ptr existingGame = gb.buildShared(); - targetDocExistingGames()[existingGame->id()] = existingGame; + target()->mGamesExisting[existingGame->id()] = existingGame; } -void PlatformDoc::Reader::parseAddApp() +void PlatformDocReader::parseAddApp() { // Additional App to Build AddApp::Builder aab; @@ -312,10 +307,10 @@ void PlatformDoc::Reader::parseAddApp() // Build Additional App and add to document std::shared_ptr existingAddApp = aab.buildShared(); - targetDocExistingAddApps()[existingAddApp->id()] = existingAddApp; + target()->mAddAppsExisting[existingAddApp->id()] = existingAddApp; } -void PlatformDoc::Reader::parseCustomField() +void PlatformDocReader::parseCustomField() { // Custom Field to Build CustomField::Builder cfb; @@ -336,41 +331,39 @@ void PlatformDoc::Reader::parseCustomField() // Build Custom Field and add to document std::shared_ptr existingCustomField = cfb.buildShared(); QString key = existingCustomField->gameId().toString() + existingCustomField->name(); - static_cast(mTargetDocument)->mCustomFieldsExisting[key] = existingCustomField; + target()->mCustomFieldsExisting[key] = existingCustomField; } //=============================================================================================================== -// PlatformDoc::Writer +// PlatformDocWriter //=============================================================================================================== //-Constructor-------------------------------------------------------------------------------------------------------- //Public: -PlatformDoc::Writer::Writer(PlatformDoc* sourceDoc) : - Lr::DataDoc::Writer(sourceDoc), - Lr::BasicPlatformDoc::Writer(sourceDoc), - Lr::XmlDocWriter(sourceDoc, Xml::ROOT_ELEMENT) +PlatformDocWriter::PlatformDocWriter(PlatformDoc* sourceDoc) : + Lr::XmlDocWriter(sourceDoc, Xml::ROOT_ELEMENT) {} //-Instance Functions------------------------------------------------------------------------------------------------- //Private: -bool PlatformDoc::Writer::writeSourceDoc() +bool PlatformDocWriter::writeSourceDoc() { // Write all games - for(const std::shared_ptr& game : static_cast(mSourceDocument)->finalGames()) + for(const std::shared_ptr& game : source()->finalGames()) { if(!writeGame(*std::static_pointer_cast(game))) return false; } // Write all additional apps - for(const std::shared_ptr& addApp : static_cast(mSourceDocument)->finalAddApps()) + for(const std::shared_ptr& addApp : source()->finalAddApps()) { if(!writeAddApp(*std::static_pointer_cast(addApp))) return false; } // Write all custom fields - for(const std::shared_ptr& customField : qAsConst(static_cast(mSourceDocument)->mCustomFieldsFinal)) + for(const std::shared_ptr& customField : qAsConst(source()->mCustomFieldsFinal)) { if(!writeCustomField(*customField)) return false; @@ -380,7 +373,7 @@ bool PlatformDoc::Writer::writeSourceDoc() return true; } -bool PlatformDoc::Writer::writeGame(const Game& game) +bool PlatformDocWriter::writeGame(const Game& game) { // Write opening tag mStreamWriter.writeStartElement(Xml::Element_Game::NAME); @@ -425,7 +418,7 @@ bool PlatformDoc::Writer::writeGame(const Game& game) return !mStreamWriter.hasError(); } -bool PlatformDoc::Writer::writeAddApp(const AddApp& addApp) +bool PlatformDocWriter::writeAddApp(const AddApp& addApp) { // Write opening tag mStreamWriter.writeStartElement(Xml::Element_AddApp::NAME); @@ -449,7 +442,7 @@ bool PlatformDoc::Writer::writeAddApp(const AddApp& addApp) return !mStreamWriter.hasError(); } -bool PlatformDoc::Writer::writeCustomField(const CustomField& customField) +bool PlatformDocWriter::writeCustomField(const CustomField& customField) { // Write opening tag mStreamWriter.writeStartElement(Xml::Element_CustomField::NAME); @@ -475,15 +468,14 @@ bool PlatformDoc::Writer::writeCustomField(const CustomField& customField) //-Constructor-------------------------------------------------------------------------------------------------------- //Public: -PlaylistDoc::PlaylistDoc(Install* const parent, const QString& xmlPath, QString docName, const Import::UpdateOptions& updateOptions, - const DocKey&) : - Lr::BasicPlaylistDoc(parent, xmlPath, docName, updateOptions), - mLaunchBoxDatabaseIdTracker(&parent->mLbDatabaseIdTracker) +PlaylistDoc::PlaylistDoc(Install* install, const QString& xmlPath, QString docName, const Import::UpdateOptions& updateOptions) : + Lr::BasicPlaylistDoc(install, xmlPath, docName, updateOptions), + mLaunchBoxDatabaseIdTracker(&install->mLbDatabaseIdTracker) {} //-Instance Functions-------------------------------------------------------------------------------------------------- //Private: -std::shared_ptr PlaylistDoc::preparePlaylistHeader(const Fp::Playlist& playlist) +std::shared_ptr PlaylistDoc::preparePlaylistHeader(const Fp::Playlist& playlist) { // Convert to LaunchBox playlist header std::shared_ptr lbPlaylist = std::make_shared(playlist); @@ -492,10 +484,10 @@ std::shared_ptr PlaylistDoc::preparePlaylistHeader(const Fp: return lbPlaylist; } -std::shared_ptr PlaylistDoc::preparePlaylistGame(const Fp::PlaylistGame& game) +std::shared_ptr PlaylistDoc::preparePlaylistGame(const Fp::PlaylistGame& game) { // Convert to LaunchBox playlist game - std::shared_ptr lbPlaylistGame = std::make_shared(game, static_cast(parent())->mPlaylistGameDetailsCache); + std::shared_ptr lbPlaylistGame = std::make_shared(game, install()->mPlaylistGameDetailsCache); // Set LB Database ID appropriately before hand-off QUuid key = lbPlaylistGame->id(); @@ -516,20 +508,18 @@ std::shared_ptr PlaylistDoc::preparePlaylistGame(const Fp::Pla } //=============================================================================================================== -// PlaylistDoc::Reader +// PlaylistDocReader //=============================================================================================================== //-Constructor-------------------------------------------------------------------------------------------------------- //Public: -PlaylistDoc::Reader::Reader(PlaylistDoc* targetDoc) : - Lr::DataDoc::Reader(targetDoc), - Lr::BasicPlaylistDoc::Reader(targetDoc), - Lr::XmlDocReader(targetDoc, Xml::ROOT_ELEMENT) +PlaylistDocReader::PlaylistDocReader(PlaylistDoc* targetDoc) : + Lr::XmlDocReader(targetDoc, Xml::ROOT_ELEMENT) {} //-Instance Functions------------------------------------------------------------------------------------------------- //Private: -Lr::DocHandlingError PlaylistDoc::Reader::readTargetDoc() +Lr::DocHandlingError PlaylistDocReader::readTargetDoc() { while(mStreamReader.readNextStartElement()) { @@ -545,7 +535,7 @@ Lr::DocHandlingError PlaylistDoc::Reader::readTargetDoc() return streamStatus(); } -void PlaylistDoc::Reader::parsePlaylistHeader() +void PlaylistDocReader::parsePlaylistHeader() { // Playlist Header to Build PlaylistHeader::Builder phb; @@ -566,10 +556,10 @@ void PlaylistDoc::Reader::parsePlaylistHeader() } // Build Playlist Header and add to document - targetDocPlaylistHeader() = phb.buildShared(); + target()->mPlaylistHeader = phb.buildShared(); } -void PlaylistDoc::Reader::parsePlaylistGame() +void PlaylistDocReader::parsePlaylistGame() { // Playlist Game to Build PlaylistGame::Builder pgb; @@ -599,39 +589,37 @@ void PlaylistDoc::Reader::parsePlaylistGame() // Correct LB ID if it is invalid and then add it to tracker if(existingPlaylistGame->lbDatabaseId() < 0) { - auto optIdx = static_cast(mTargetDocument)->mLaunchBoxDatabaseIdTracker->reserveFirstFree(); + auto optIdx = target()->mLaunchBoxDatabaseIdTracker->reserveFirstFree(); existingPlaylistGame->setLBDatabaseId(optIdx.value_or(0)); } else - static_cast(mTargetDocument)->mLaunchBoxDatabaseIdTracker->reserve(existingPlaylistGame->lbDatabaseId()); + target()->mLaunchBoxDatabaseIdTracker->reserve(existingPlaylistGame->lbDatabaseId()); // Add to document - targetDocExistingPlaylistGames()[existingPlaylistGame->gameId()] = existingPlaylistGame; + target()->mPlaylistGamesExisting[existingPlaylistGame->gameId()] = existingPlaylistGame; } //=============================================================================================================== -// PlaylistDoc::Writer +// PlaylistDocWriter //=============================================================================================================== //-Constructor-------------------------------------------------------------------------------------------------------- //Public: -PlaylistDoc::Writer::Writer(PlaylistDoc* sourceDoc) : - Lr::DataDoc::Writer(sourceDoc), - Lr::BasicPlaylistDoc::Writer(sourceDoc), - Lr::XmlDocWriter(sourceDoc, Xml::ROOT_ELEMENT) +PlaylistDocWriter::PlaylistDocWriter(PlaylistDoc* sourceDoc) : + Lr::XmlDocWriter(sourceDoc, Xml::ROOT_ELEMENT) {} //-Instance Functions------------------------------------------------------------------------------------------------- //Private: -bool PlaylistDoc::Writer::writeSourceDoc() +bool PlaylistDocWriter::writeSourceDoc() { // Write playlist header - std::shared_ptr playlistHeader = static_cast(mSourceDocument)->playlistHeader(); + std::shared_ptr playlistHeader = source()->playlistHeader(); if(!writePlaylistHeader(*std::static_pointer_cast(playlistHeader))) return false; // Write all playlist games - for(const std::shared_ptr& playlistGame : static_cast(mSourceDocument)->finalPlaylistGames()) + for(const std::shared_ptr& playlistGame : source()->finalPlaylistGames()) { if(!writePlaylistGame(*std::static_pointer_cast(playlistGame))) return false; @@ -641,7 +629,7 @@ bool PlaylistDoc::Writer::writeSourceDoc() return true; } -bool PlaylistDoc::Writer::writePlaylistHeader(const PlaylistHeader& playlistHeader) +bool PlaylistDocWriter::writePlaylistHeader(const PlaylistHeader& playlistHeader) { // Write opening tag mStreamWriter.writeStartElement(Xml::Element_PlaylistHeader::NAME); @@ -662,7 +650,7 @@ bool PlaylistDoc::Writer::writePlaylistHeader(const PlaylistHeader& playlistHead return !mStreamWriter.hasError(); } -bool PlaylistDoc::Writer::writePlaylistGame(const PlaylistGame& playlistGame) +bool PlaylistDocWriter::writePlaylistGame(const PlaylistGame& playlistGame) { // Write opening tag mStreamWriter.writeStartElement(Xml::Element_PlaylistGame::NAME); @@ -690,14 +678,13 @@ bool PlaylistDoc::Writer::writePlaylistGame(const PlaylistGame& playlistGame) //-Constructor-------------------------------------------------------------------------------------------------------- //Public: -PlatformsConfigDoc::PlatformsConfigDoc(Install* const parent, const QString& xmlPath, const Import::UpdateOptions& updateOptions, - const DocKey&) : - Lr::UpdateableDoc(parent, xmlPath, STD_NAME, updateOptions) +PlatformsConfigDoc::PlatformsConfigDoc(Install* install, const QString& xmlPath, const Import::UpdateOptions& updateOptions) : + Lr::UpdateableDoc(install, xmlPath, STD_NAME, updateOptions) {} //-Instance Functions-------------------------------------------------------------------------------------------------- //Private: -Lr::DataDoc::Type PlatformsConfigDoc::type() const { return Lr::DataDoc::Type::Config; } +Lr::IDataDoc::Type PlatformsConfigDoc::type() const { return Lr::IDataDoc::Type::Config; } //Public: bool PlatformsConfigDoc::isEmpty() const @@ -758,7 +745,7 @@ void PlatformsConfigDoc::finalize() finalizeUpdateableItems(mPlatformCategoriesExisting, mPlatformCategoriesFinal); // Finalize base - Lr::UpdateableDoc::finalize(); + Lr::IUpdateableDoc::finalize(); } //=============================================================================================================== @@ -768,8 +755,7 @@ void PlatformsConfigDoc::finalize() //-Constructor-------------------------------------------------------------------------------------------------------- //Public: PlatformsConfigDoc::Reader::Reader(PlatformsConfigDoc* targetDoc) : - Lr::DataDoc::Reader(targetDoc), - Lr::XmlDocReader(targetDoc, Xml::ROOT_ELEMENT) + Lr::XmlDocReader(targetDoc, Xml::ROOT_ELEMENT) {} //-Instance Functions------------------------------------------------------------------------------------------------- @@ -813,7 +799,7 @@ void PlatformsConfigDoc::Reader::parsePlatform() // Build Platform and add to document Platform existingPlatform = pb.build(); - static_cast(mTargetDocument)->mPlatformsExisting[existingPlatform.name()] = existingPlatform; + target()->mPlatformsExisting[existingPlatform.name()] = existingPlatform; } Lr::DocHandlingError PlatformsConfigDoc::Reader::parsePlatformFolder() @@ -831,12 +817,12 @@ Lr::DocHandlingError PlatformsConfigDoc::Reader::parsePlatformFolder() else if(mStreamReader.name() == Xml::Element_PlatformFolder::ELEMENT_PLATFORM) pfb.wPlatform(mStreamReader.readElementText()); else - return Lr::DocHandlingError(*mTargetDocument, Lr::DocHandlingError::DocInvalidType); + return Lr::DocHandlingError(*target(), Lr::DocHandlingError::DocInvalidType); } // Build PlatformFolder and add to document PlatformFolder existingPlatformFolder = pfb.build(); - static_cast(mTargetDocument)->mPlatformFoldersExisting[existingPlatformFolder.identifier()] = existingPlatformFolder; + target()->mPlatformFoldersExisting[existingPlatformFolder.identifier()] = existingPlatformFolder; return Lr::DocHandlingError(); } @@ -861,7 +847,7 @@ void PlatformsConfigDoc::Reader::parsePlatformCategory() PlatformCategory pc = pcb.build(); // Build Playlist Header and add to document - static_cast(mTargetDocument)->mPlatformCategoriesExisting[pc.name()] = pc; + target()->mPlatformCategoriesExisting[pc.name()] = pc; } //=============================================================================================================== @@ -871,8 +857,7 @@ void PlatformsConfigDoc::Reader::parsePlatformCategory() //-Constructor-------------------------------------------------------------------------------------------------------- //Public: PlatformsConfigDoc::Writer::Writer(PlatformsConfigDoc* sourceDoc) : - Lr::DataDoc::Writer(sourceDoc), - Lr::XmlDocWriter(sourceDoc, Xml::ROOT_ELEMENT) + Lr::XmlDocWriter(sourceDoc, Xml::ROOT_ELEMENT) {} //-Instance Functions------------------------------------------------------------------------------------------------- @@ -880,21 +865,21 @@ PlatformsConfigDoc::Writer::Writer(PlatformsConfigDoc* sourceDoc) : bool PlatformsConfigDoc::Writer::writeSourceDoc() { // Write all platforms - for(const Platform& platform : static_cast(mSourceDocument)->finalPlatforms()) + for(const Platform& platform : source()->finalPlatforms()) { if(!writePlatform(platform)) return false; } // Write all platform folders - for(const PlatformFolder& platformFolder : static_cast(mSourceDocument)->finalPlatformFolders()) + for(const PlatformFolder& platformFolder : source()->finalPlatformFolders()) { if(!writePlatformFolder(platformFolder)) return false; } // Write all platform categories - for(const PlatformCategory& platformCategory : static_cast(mSourceDocument)->finalPlatformCategories()) + for(const PlatformCategory& platformCategory : source()->finalPlatformCategories()) { if(!writePlatformCategory(platformCategory)) return false; @@ -965,13 +950,13 @@ bool PlatformsConfigDoc::Writer::writePlatformCategory(const PlatformCategory& p //-Constructor-------------------------------------------------------------------------------------------------------- //Public: -ParentsDoc::ParentsDoc(Install* const parent, const QString& xmlPath, const DocKey&) : - Lr::DataDoc(parent, xmlPath, STD_NAME) +ParentsDoc::ParentsDoc(Install* install, const QString& xmlPath) : + Lr::DataDoc(install, xmlPath, STD_NAME) {} //-Instance Functions-------------------------------------------------------------------------------------------------- //Private: -Lr::DataDoc::Type ParentsDoc::type() const { return Lr::DataDoc::Type::Config; } +Lr::IDataDoc::Type ParentsDoc::type() const { return Lr::IDataDoc::Type::Config; } bool ParentsDoc::removeIfPresent(qsizetype idx) { @@ -1042,8 +1027,7 @@ void ParentsDoc::addParent(const Parent& parent) { mParents.append(parent); } //-Constructor-------------------------------------------------------------------------------------------------------- //Public: ParentsDoc::Reader::Reader(ParentsDoc* targetDoc) : - Lr::DataDoc::Reader(targetDoc), - Lr::XmlDocReader(targetDoc, Xml::ROOT_ELEMENT) + Lr::XmlDocReader(targetDoc, Xml::ROOT_ELEMENT) {} //-Instance Functions------------------------------------------------------------------------------------------------- @@ -1084,7 +1068,7 @@ void ParentsDoc::Reader::parseParent() // Build Platform and add to document Parent existingParent = pb.build(); - static_cast(mTargetDocument)->mParents.append(existingParent); + target()->mParents.append(existingParent); } //=============================================================================================================== @@ -1094,8 +1078,7 @@ void ParentsDoc::Reader::parseParent() //-Constructor-------------------------------------------------------------------------------------------------------- //Public: ParentsDoc::Writer::Writer(ParentsDoc* sourceDoc) : - Lr::DataDoc::Writer(sourceDoc), - Lr::XmlDocWriter(sourceDoc, Xml::ROOT_ELEMENT) + Lr::XmlDocWriter(sourceDoc, Xml::ROOT_ELEMENT) {} //-Instance Functions------------------------------------------------------------------------------------------------- @@ -1103,7 +1086,7 @@ ParentsDoc::Writer::Writer(ParentsDoc* sourceDoc) : bool ParentsDoc::Writer::writeSourceDoc() { // Write all parents - for(const Parent& parent : static_cast(mSourceDocument)->parents()) + for(const Parent& parent : source()->parents()) { if(!writeParent(parent)) return false; diff --git a/app/src/launcher/launchbox/lb-data.h b/app/src/launcher/implementation/launchbox/lb-data.h similarity index 68% rename from app/src/launcher/launchbox/lb-data.h rename to app/src/launcher/implementation/launchbox/lb-data.h index 652cfcb..26b3d13 100644 --- a/app/src/launcher/launchbox/lb-data.h +++ b/app/src/launcher/implementation/launchbox/lb-data.h @@ -12,33 +12,17 @@ #include // Project Includes -#include "launcher/lr-data.h" -#include "launcher/launchbox/lb-items.h" - -// Reminder for virtual inheritance constructor mechanics if needed, -// since some classes here use multiple virtual inheritance: -// https://stackoverflow.com/questions/70746451/ +#include "launcher/abstract/lr-data.h" +#include "launcher/implementation/launchbox/lb-registration.h" +#include "launcher/implementation/launchbox/lb-items.h" namespace Lb { -class Install; - -class DocKey +class PlatformDoc : public Lr::BasicPlatformDoc { - friend class Install; -private: - DocKey() {} - DocKey(const DocKey&) = default; -}; - -class PlatformDoc : public Lr::BasicPlatformDoc -{ -//-Inner Classes---------------------------------------------------------------------------------------------------- -public: - class Reader; - class Writer; - + friend PlatformDocReader; + friend PlatformDocWriter; //-Instance Variables-------------------------------------------------------------------------------------------------- private: QHash> mCustomFieldsFinal; @@ -46,13 +30,12 @@ class PlatformDoc : public Lr::BasicPlatformDoc //-Constructor-------------------------------------------------------------------------------------------------------- public: - explicit PlatformDoc(Install* const parent, const QString& xmlPath, QString docName, const Import::UpdateOptions& updateOptions, - const DocKey&); + explicit PlatformDoc(Install* install, const QString& xmlPath, QString docName, const Import::UpdateOptions& updateOptions); //-Instance Functions-------------------------------------------------------------------------------------------------- -private: - std::shared_ptr prepareGame(const Fp::Game& game, const Lr::ImageSources& images) override; - std::shared_ptr prepareAddApp(const Fp::AddApp& addApp) override; +private: + std::shared_ptr prepareGame(const Fp::Game& game) override; + std::shared_ptr prepareAddApp(const Fp::AddApp& addApp) override; void addCustomField(std::shared_ptr customField); @@ -62,11 +45,11 @@ class PlatformDoc : public Lr::BasicPlatformDoc void finalize() override; }; -class PlatformDoc::Reader : public Lr::BasicPlatformDoc::Reader, public Lr::XmlDocReader +class PlatformDocReader : public Lr::XmlDocReader { //-Constructor-------------------------------------------------------------------------------------------------------- public: - Reader(PlatformDoc* targetDoc); + PlatformDocReader(PlatformDoc* targetDoc); //-Instance Functions------------------------------------------------------------------------------------------------- private: @@ -76,11 +59,11 @@ class PlatformDoc::Reader : public Lr::BasicPlatformDoc::Reader, public Lr::XmlD void parseCustomField(); }; -class PlatformDoc::Writer : public Lr::BasicPlatformDoc::Writer, public Lr::XmlDocWriter +class PlatformDocWriter : public Lr::XmlDocWriter { //-Constructor-------------------------------------------------------------------------------------------------------- public: - Writer(PlatformDoc* sourceDoc); + PlatformDocWriter(PlatformDoc* sourceDoc); //-Instance Functions-------------------------------------------------------------------------------------------------- private: @@ -90,33 +73,29 @@ class PlatformDoc::Writer : public Lr::BasicPlatformDoc::Writer, public Lr::XmlD bool writeCustomField(const CustomField& customField); }; -class PlaylistDoc : public Lr::BasicPlaylistDoc +class PlaylistDoc : public Lr::BasicPlaylistDoc { -//-Inner Classes---------------------------------------------------------------------------------------------------- -public: - class Reader; - class Writer; - + friend class PlaylistDocReader; + friend class PlaylistDocWriter; //-Instance Variables-------------------------------------------------------------------------------------------------- private: Qx::FreeIndexTracker* mLaunchBoxDatabaseIdTracker; //-Constructor-------------------------------------------------------------------------------------------------------- public: - explicit PlaylistDoc(Install* const parent, const QString& xmlPath, QString docName, const Import::UpdateOptions& updateOptions, - const DocKey&); + explicit PlaylistDoc(Install* install, const QString& xmlPath, QString docName, const Import::UpdateOptions& updateOptions); //-Instance Functions-------------------------------------------------------------------------------------------------- private: - std::shared_ptr preparePlaylistHeader(const Fp::Playlist& playlist) override; - std::shared_ptr preparePlaylistGame(const Fp::PlaylistGame& game) override; + std::shared_ptr preparePlaylistHeader(const Fp::Playlist& playlist) override; + std::shared_ptr preparePlaylistGame(const Fp::PlaylistGame& game) override; }; -class PlaylistDoc::Reader : public Lr::BasicPlaylistDoc::Reader, public Lr::XmlDocReader +class PlaylistDocReader : public Lr::XmlDocReader { //-Constructor-------------------------------------------------------------------------------------------------------- public: - Reader(PlaylistDoc* targetDoc); + PlaylistDocReader(PlaylistDoc* targetDoc); //-Instance Functions------------------------------------------------------------------------------------------------- private: @@ -125,11 +104,11 @@ class PlaylistDoc::Reader : public Lr::BasicPlaylistDoc::Reader, public Lr::XmlD void parsePlaylistGame(); }; -class PlaylistDoc::Writer : public Lr::BasicPlaylistDoc::Writer, Lr::XmlDocWriter +class PlaylistDocWriter : public Lr::XmlDocWriter { //-Constructor-------------------------------------------------------------------------------------------------------- public: - Writer(PlaylistDoc* sourceDoc); + PlaylistDocWriter(PlaylistDoc* sourceDoc); //-Instance Functions------------------------------------------------------------------------------------------------- private: @@ -138,7 +117,7 @@ class PlaylistDoc::Writer : public Lr::BasicPlaylistDoc::Writer, Lr::XmlDocWrite bool writePlaylistGame(const PlaylistGame& playlistGame); }; -class PlatformsConfigDoc : public Lr::UpdateableDoc +class PlatformsConfigDoc : public Lr::UpdateableDoc { //-Inner Classes---------------------------------------------------------------------------------------------------- public: @@ -160,8 +139,7 @@ class PlatformsConfigDoc : public Lr::UpdateableDoc //-Constructor-------------------------------------------------------------------------------------------------------- public: - explicit PlatformsConfigDoc(Install* const parent, const QString& xmlPath, const Import::UpdateOptions& updateOptions, - const DocKey&); + explicit PlatformsConfigDoc(Install* install, const QString& xmlPath, const Import::UpdateOptions& updateOptions); //-Instance Functions-------------------------------------------------------------------------------------------------- private: @@ -186,7 +164,7 @@ class PlatformsConfigDoc : public Lr::UpdateableDoc void finalize() override; }; -class PlatformsConfigDoc::Reader : public Lr::XmlDocReader +class PlatformsConfigDoc::Reader : public Lr::XmlDocReader { //-Constructor-------------------------------------------------------------------------------------------------------- public: @@ -200,7 +178,7 @@ class PlatformsConfigDoc::Reader : public Lr::XmlDocReader void parsePlatformCategory(); }; -class PlatformsConfigDoc::Writer : public Lr::XmlDocWriter +class PlatformsConfigDoc::Writer : public Lr::XmlDocWriter { //-Constructor-------------------------------------------------------------------------------------------------------- public: @@ -214,26 +192,26 @@ class PlatformsConfigDoc::Writer : public Lr::XmlDocWriter bool writePlatformCategory(const PlatformCategory& platformCategory); }; -class ParentsDoc : public Lr::DataDoc +class ParentsDoc : public Lr::DataDoc { - //-Inner Classes---------------------------------------------------------------------------------------------------- +//-Inner Classes---------------------------------------------------------------------------------------------------- public: class Reader; class Writer; - //-Class Variables----------------------------------------------------------------------------------------------------- +//-Class Variables----------------------------------------------------------------------------------------------------- public: static inline const QString STD_NAME = u"Parents"_s; - //-Instance Variables-------------------------------------------------------------------------------------------------- +//-Instance Variables-------------------------------------------------------------------------------------------------- private: QList mParents; - //-Constructor-------------------------------------------------------------------------------------------------------- +//-Constructor-------------------------------------------------------------------------------------------------------- public: - explicit ParentsDoc(Install* const parent, const QString& xmlPath, const DocKey&); + explicit ParentsDoc(Install* install, const QString& xmlPath); - //-Instance Functions-------------------------------------------------------------------------------------------------- +//-Instance Functions-------------------------------------------------------------------------------------------------- private: Type type() const override; bool removeIfPresent(qsizetype idx); @@ -263,25 +241,25 @@ class ParentsDoc : public Lr::DataDoc void addParent(const Parent& parent); }; -class ParentsDoc::Reader : public Lr::XmlDocReader +class ParentsDoc::Reader : public Lr::XmlDocReader { - //-Constructor-------------------------------------------------------------------------------------------------------- +//-Constructor-------------------------------------------------------------------------------------------------------- public: Reader(ParentsDoc* targetDoc); - //-Instance Functions------------------------------------------------------------------------------------------------- +//-Instance Functions------------------------------------------------------------------------------------------------- private: Lr::DocHandlingError readTargetDoc() override; void parseParent(); }; -class ParentsDoc::Writer : public Lr::XmlDocWriter +class ParentsDoc::Writer : public Lr::XmlDocWriter { - //-Constructor-------------------------------------------------------------------------------------------------------- +//-Constructor-------------------------------------------------------------------------------------------------------- public: Writer(ParentsDoc* sourceDoc); - //-Instance Functions------------------------------------------------------------------------------------------------- +//-Instance Functions------------------------------------------------------------------------------------------------- private: bool writeSourceDoc() override; bool writeParent(const Parent& parent); diff --git a/app/src/launcher/launchbox/lb-install.cpp b/app/src/launcher/implementation/launchbox/lb-install.cpp similarity index 72% rename from app/src/launcher/launchbox/lb-install.cpp rename to app/src/launcher/implementation/launchbox/lb-install.cpp index 64b7f30..86618aa 100644 --- a/app/src/launcher/launchbox/lb-install.cpp +++ b/app/src/launcher/implementation/launchbox/lb-install.cpp @@ -24,7 +24,7 @@ namespace Lb //-Constructor------------------------------------------------------------------------------------------------ //Public: Install::Install(const QString& installPath) : - Lr::Install(installPath), + Lr::Install(installPath), mDataDirectory(installPath + '/' + DATA_PATH), mPlatformsDirectory(installPath + '/' + PLATFORMS_PATH), mPlaylistsDirectory(installPath + '/' + PLAYLISTS_PATH), @@ -51,7 +51,7 @@ Install::Install(const QString& installPath) : //Private: void Install::nullify() { - Lr::Install::nullify(); + Lr::IInstall::nullify(); mDataDirectory = QDir(); mPlatformsDirectory = QDir(); @@ -73,7 +73,7 @@ Qx::Error Install::populateExistingDocs() return existingCheck; for(const QFileInfo& platformFile : qAsConst(existingList)) - catalogueExistingDoc(Lr::DataDoc::Identifier(Lr::DataDoc::Type::Platform, platformFile.baseName())); + catalogueExistingDoc(Lr::IDataDoc::Identifier(Lr::IDataDoc::Type::Platform, platformFile.baseName())); // Check for playlists existingCheck = Qx::dirContentInfoList(existingList, mPlaylistsDirectory, {u"*."_s + XML_EXT}, QDir::NoFilter, QDirIterator::Subdirectories); @@ -81,7 +81,7 @@ Qx::Error Install::populateExistingDocs() return existingCheck; for(const QFileInfo& playlistFile : qAsConst(existingList)) - catalogueExistingDoc(Lr::DataDoc::Identifier(Lr::DataDoc::Type::Playlist, playlistFile.baseName())); + catalogueExistingDoc(Lr::IDataDoc::Identifier(Lr::IDataDoc::Type::Playlist, playlistFile.baseName())); // Check for config docs existingCheck = Qx::dirContentInfoList(existingList, mDataDirectory, {u"*."_s + XML_EXT}); @@ -89,22 +89,22 @@ Qx::Error Install::populateExistingDocs() return existingCheck; for(const QFileInfo& configDocFile : qAsConst(existingList)) - catalogueExistingDoc(Lr::DataDoc::Identifier(Lr::DataDoc::Type::Config, configDocFile.baseName())); + catalogueExistingDoc(Lr::IDataDoc::Identifier(Lr::IDataDoc::Type::Config, configDocFile.baseName())); // Return success return Qx::Error(); } -QString Install::imageDestinationPath(Fp::ImageType imageType, const Lr::Game* game) const +QString Install::imageDestinationPath(Fp::ImageType imageType, const Lr::Game& game) const { return mPlatformImagesDirectory.absolutePath() + '/' + - game->platform() + '/' + + game.platform() + '/' + (imageType == Fp::ImageType::Logo ? LOGO_PATH : SCREENSHOT_PATH) + '/' + - game->id().toString(QUuid::WithoutBraces) + + game.id().toString(QUuid::WithoutBraces) + '.' + IMAGE_EXT; } -void Install::editBulkImageReferences(const Lr::ImageSources& imageSources) +void Install::editBulkImageReferences(const Lr::ImagePaths& imageSources) { // Set media folder paths const QList affectedPlatforms = modifiedPlatforms(); @@ -130,91 +130,54 @@ void Install::editBulkImageReferences(const Lr::ImageSources& imageSources) } } -QString Install::dataDocPath(Lr::DataDoc::Identifier identifier) const +QString Install::dataDocPath(Lr::IDataDoc::Identifier identifier) const { QString fileName = identifier.docName() + u"."_s + XML_EXT; switch(identifier.docType()) { - case Lr::DataDoc::Type::Platform: + case Lr::IDataDoc::Type::Platform: return mPlatformsDirectory.absoluteFilePath(fileName); break; - case Lr::DataDoc::Type::Playlist: + case Lr::IDataDoc::Type::Playlist: return mPlaylistsDirectory.absoluteFilePath(fileName); break; - case Lr::DataDoc::Type::Config: + case Lr::IDataDoc::Type::Config: return mDataDirectory.absoluteFilePath(fileName); break; default: - throw new std::invalid_argument("Function argument was not of type Lr::DataDoc::Identifier"); + throw new std::invalid_argument("Function argument was not of type Lr::IDataDoc::Identifier"); } } -std::shared_ptr Install::preparePlatformDocCheckout(std::unique_ptr& platformDoc, const QString& translatedName) +std::unique_ptr Install::preparePlatformDocCheckout(const QString& translatedName) { // Create doc file reference - Lr::DataDoc::Identifier docId(Lr::DataDoc::Type::Platform, translatedName); + Lr::IDataDoc::Identifier docId(Lr::IDataDoc::Type::Platform, translatedName); - // Construct unopened document - platformDoc = std::make_unique(this, dataDocPath(docId), translatedName, mImportDetails->updateOptions, DocKey{}); - - // Construct doc reader (need to downcast pointer since doc pointer is upcasted after construction above) - std::shared_ptr docReader = std::make_shared(static_cast(platformDoc.get())); - - // Return reader and doc - return docReader; + // Construct unopened document and return + return std::make_unique(this, dataDocPath(docId), translatedName, importDetails().updateOptions); } -std::shared_ptr Install::preparePlaylistDocCheckout(std::unique_ptr& playlistDoc, const QString& translatedName) +std::unique_ptr Install::preparePlaylistDocCheckout(const QString& translatedName) { // Create doc file reference - Lr::DataDoc::Identifier docId(Lr::DataDoc::Type::Playlist, translatedName); + Lr::IDataDoc::Identifier docId(Lr::IDataDoc::Type::Playlist, translatedName); // Construct unopened document - playlistDoc = std::make_unique(this, dataDocPath(docId), translatedName, mImportDetails->updateOptions, DocKey{}); - - // Construct doc reader (need to downcast pointer since doc pointer is upcasted after construction above) - std::shared_ptr docReader = std::make_shared(static_cast(playlistDoc.get())); - - // Return reader and doc - return docReader; -} - -std::shared_ptr Install::preparePlatformDocCommit(const std::unique_ptr& platformDoc) -{ - // Construct doc writer - std::shared_ptr docWriter = std::make_shared(static_cast(platformDoc.get())); - - // Return writer - return docWriter; -} - -std::shared_ptr Install::preparePlaylistDocCommit(const std::unique_ptr& playlistDoc) -{ - // Work with native type - auto lbPlaylistDoc = static_cast(playlistDoc.get()); - - // Store playlist ID (if playlist will remain - if(!playlistDoc->isEmpty()) - mModifiedPlaylistIds.insert(lbPlaylistDoc->playlistHeader()->id()); - - // Construct doc writer - std::shared_ptr docWriter = std::make_shared(lbPlaylistDoc); - - // Return writer - return docWriter; + return std::make_unique(this, dataDocPath(docId), translatedName, importDetails().updateOptions); } Lr::DocHandlingError Install::checkoutPlatformsConfigDoc(std::unique_ptr& returnBuffer) { // Create doc file reference - Lr::DataDoc::Identifier docId(Lr::DataDoc::Type::Config, PlatformsConfigDoc::STD_NAME); + Lr::IDataDoc::Identifier docId(Lr::IDataDoc::Type::Config, PlatformsConfigDoc::STD_NAME); // Construct unopened document Import::UpdateOptions uo{.importMode = Import::UpdateMode::NewAndExisting, .removeObsolete = false}; - returnBuffer = std::make_unique(this, dataDocPath(docId), uo, DocKey{}); + returnBuffer = std::make_unique(this, dataDocPath(docId), uo); // Construct doc reader std::shared_ptr docReader = std::make_shared(returnBuffer.get()); @@ -232,7 +195,7 @@ Lr::DocHandlingError Install::checkoutPlatformsConfigDoc(std::unique_ptr document) { - assert(document->parent() == this); + Q_ASSERT(document->install() == this); // Prepare writer std::shared_ptr docWriter = std::make_shared(document.get()); @@ -250,10 +213,10 @@ Lr::DocHandlingError Install::commitPlatformsConfigDoc(std::unique_ptr& returnBuffer) { // Create doc file reference - Lr::DataDoc::Identifier docId(Lr::DataDoc::Type::Config, ParentsDoc::STD_NAME); + Lr::IDataDoc::Identifier docId(Lr::IDataDoc::Type::Config, ParentsDoc::STD_NAME); // Construct unopened document - returnBuffer = std::make_unique(this, dataDocPath(docId), DocKey{}); + returnBuffer = std::make_unique(this, dataDocPath(docId)); // Construct doc reader std::shared_ptr docReader = std::make_shared(returnBuffer.get()); @@ -271,7 +234,7 @@ Lr::DocHandlingError Install::checkoutParentsDoc(std::unique_ptr& re Lr::DocHandlingError Install::commitParentsDoc(std::unique_ptr document) { - assert(document->parent() == this); + Q_ASSERT(document->install() == this); // Prepare writer std::shared_ptr docWriter = std::make_shared(document.get()); @@ -289,14 +252,12 @@ Lr::DocHandlingError Install::commitParentsDoc(std::unique_ptr docum //Public: void Install::softReset() { - Lr::Install::softReset(); + Lr::IInstall::softReset(); mLbDatabaseIdTracker = Qx::FreeIndexTracker(0, LB_DB_ID_TRACKER_MAX); mPlaylistGameDetailsCache.clear(); - mWorkerImageJobs.clear(); } -QString Install::name() const { return NAME; } QList Install::preferredImageModeOrder() const { return IMAGE_MODE_ORDER; } bool Install::isRunning() const { return Qx::processIsRunning(mExeFile.fileName()); } @@ -312,10 +273,10 @@ QString Install::versionString() const else if(!productVersionStr.isEmpty()) return productVersionStr; else - return Lr::Install::versionString(); + return Lr::IInstall::versionString(); } -QString Install::translateDocName(const QString& originalName, Lr::DataDoc::Type type) const +QString Install::translateDocName(const QString& originalName, Lr::IDataDoc::Type type) const { Q_UNUSED(type); @@ -346,7 +307,7 @@ QString Install::translateDocName(const QString& originalName, Lr::DataDoc::Type Qx::Error Install::prePlatformsImport() { - if(Qx::Error superErr = Lr::Install::prePlatformsImport(); superErr.isValid()) + if(Qx::Error superErr = Lr::IInstall::prePlatformsImport(); superErr.isValid()) return superErr; // Open platforms document @@ -355,7 +316,7 @@ Qx::Error Install::prePlatformsImport() Qx::Error Install::postPlatformsImport() { - if(Qx::Error superErr = Lr::Install::postPlatformsImport(); superErr.isValid()) + if(Qx::Error superErr = Lr::IInstall::postPlatformsImport(); superErr.isValid()) return superErr; // Open Parents.xml @@ -421,16 +382,17 @@ Qx::Error Install::postPlatformsImport() return Qx::Error(); } -Qx::Error Install::preImageProcessing(QList& workerTransfers, const Lr::ImageSources& bulkSources) +Qx::Error Install::preImageProcessing(const Lr::ImagePaths& bulkSources) { - if(Qx::Error superErr = Lr::Install::preImageProcessing(workerTransfers, bulkSources); superErr.isValid()) + if(Qx::Error superErr = Lr::IInstall::preImageProcessing(bulkSources); superErr.isValid()) return superErr; - switch(mImportDetails->imageMode) + //TODO Deal with the fact that when we drop bulk sources we need to still have the part of editBulkImageRefernces happen that purges old references + + switch(importDetails().imageMode) { case Import::ImageMode::Link: case Import::ImageMode::Copy: - workerTransfers.swap(mWorkerImageJobs); editBulkImageReferences(bulkSources); break; case Import::ImageMode::Reference: @@ -445,7 +407,7 @@ Qx::Error Install::preImageProcessing(QList& workerTransfers, const Lr Qx::Error Install::postImageProcessing() { - if(Qx::Error superErr = Lr::Install::postImageProcessing(); superErr.isValid()) + if(Qx::Error superErr = Lr::IInstall::postImageProcessing(); superErr.isValid()) return superErr; // Save platforms document since it's no longer needed at this point @@ -476,25 +438,13 @@ Qx::Error Install::postPlaylistsImport() return commitParentsDoc(std::move(mParents)); } -void Install::processDirectGameImages(const Lr::Game* game, const Lr::ImageSources& imageSources) +void Install::convertToDestinationImages(const Game& game, Lr::ImagePaths& images) { - Import::ImageMode mode = mImportDetails->imageMode; - if(mode == Import::ImageMode::Link || mode == Import::ImageMode::Copy) - { - if(!imageSources.logoPath().isEmpty()) - { - ImageMap logoMap{.sourcePath = imageSources.logoPath(), - .destPath = imageDestinationPath(Fp::ImageType::Logo, game)}; - mWorkerImageJobs.append(logoMap); - } + if(!images.logoPath().isEmpty()) + images.setLogoPath(imageDestinationPath(Fp::ImageType::Logo, game)); - if(!imageSources.screenshotPath().isEmpty()) - { - ImageMap ssMap{.sourcePath = imageSources.screenshotPath(), - .destPath = imageDestinationPath(Fp::ImageType::Screenshot, game)}; - mWorkerImageJobs.append(ssMap); - } - } + if(!images.screenshotPath().isEmpty()) + images.setScreenshotPath(imageDestinationPath(Fp::ImageType::Screenshot, game)); } QString Install::platformCategoryIconPath() const { return mPlatformCategoryIconsDirectory.absoluteFilePath(u"Flashpoint.png"_s); } diff --git a/app/src/launcher/launchbox/lb-install.h b/app/src/launcher/implementation/launchbox/lb-install.h similarity index 73% rename from app/src/launcher/launchbox/lb-install.h rename to app/src/launcher/implementation/launchbox/lb-install.h index b662295..215a9ff 100644 --- a/app/src/launcher/launchbox/lb-install.h +++ b/app/src/launcher/implementation/launchbox/lb-install.h @@ -9,23 +9,17 @@ #include // Project Includes -#include "launcher/lr-install.h" -#include "launcher/launchbox/lb-items.h" -#include "launcher/launchbox/lb-data.h" +#include "launcher/abstract/lr-install.h" +#include "launcher/implementation/launchbox/lb-data.h" namespace Lb { -class Install : public Lr::Install +class Install : public Lr::Install { friend class PlatformDoc; // TODO: See about removing the need for these (CLIfp path would need public accessor here) friend class PlaylistDoc; //-Class Variables-------------------------------------------------------------------------------------------------- -public: - // Identity - static inline const QString NAME = u"LaunchBox"_s; - static inline const QString ICON_PATH = u":/launcher/LaunchBox/icon.svg"_s; - static inline const QUrl HELP_URL = QUrl(u"https://forums.launchbox-app.com/files/file/2652-obbys-flashpoint-importer-for-launchbox"_s); - +private: // Paths static inline const QString PLATFORMS_PATH = u"Data/Platforms"_s; static inline const QString PLAYLISTS_PATH = u"Data/Playlists"_s; @@ -72,9 +66,6 @@ class Install : public Lr::Install QDir mCoreDirectory; QFileInfo mExeFile; - // Image transfers for import worker - QList mWorkerImageJobs; - // Persistent config handles std::unique_ptr mPlatformsConfig; std::unique_ptr mParents; @@ -97,15 +88,13 @@ class Install : public Lr::Install Qx::Error populateExistingDocs() override; // Image Processing - QString imageDestinationPath(Fp::ImageType imageType, const Lr::Game* game) const; - void editBulkImageReferences(const Lr::ImageSources& imageSources); + QString imageDestinationPath(Fp::ImageType imageType, const Lr::Game& game) const; + void editBulkImageReferences(const Lr::ImagePaths& imageSources); // Doc handling - QString dataDocPath(Lr::DataDoc::Identifier identifier) const; - std::shared_ptr preparePlatformDocCheckout(std::unique_ptr& platformDoc, const QString& translatedName) override; - std::shared_ptr preparePlaylistDocCheckout(std::unique_ptr& playlistDoc, const QString& translatedName) override; - std::shared_ptr preparePlatformDocCommit(const std::unique_ptr& platformDoc) override; - std::shared_ptr preparePlaylistDocCommit(const std::unique_ptr& playlistDoc) override; + QString dataDocPath(Lr::IDataDoc::Identifier identifier) const; + std::unique_ptr preparePlatformDocCheckout(const QString& translatedName) override; + std::unique_ptr preparePlaylistDocCheckout(const QString& translatedName) override; Lr::DocHandlingError checkoutPlatformsConfigDoc(std::unique_ptr& returnBuffer); Lr::DocHandlingError commitPlatformsConfigDoc(std::unique_ptr document); @@ -117,27 +106,25 @@ class Install : public Lr::Install void softReset() override; // Info - QString name() const override; QList preferredImageModeOrder() const override; bool isRunning() const override; QString versionString() const override; - QString translateDocName(const QString& originalName, Lr::DataDoc::Type type) const override; + QString translateDocName(const QString& originalName, Lr::IDataDoc::Type type) const override; // Import stage notifier hooks Qx::Error prePlatformsImport() override; Qx::Error postPlatformsImport() override; - Qx::Error preImageProcessing(QList& workerTransfers, const Lr::ImageSources& bulkSources) override; + Qx::Error preImageProcessing(const Lr::ImagePaths& bulkSources) override; Qx::Error postImageProcessing() override; Qx::Error postPlaylistsImport() override; // Image handling - void processDirectGameImages(const Lr::Game* game, const Lr::ImageSources& imageSources) override; + void convertToDestinationImages(const Game& game, Lr::ImagePaths& images) override; QString platformCategoryIconPath() const override; std::optional platformIconsDirectory() const override; std::optional playlistIconsDirectory() const override; - }; -REGISTER_LAUNCHER(Install::NAME, Install, &Install::ICON_PATH, &Install::HELP_URL); + } diff --git a/app/src/launcher/launchbox/lb-items.cpp b/app/src/launcher/implementation/launchbox/lb-items.cpp similarity index 100% rename from app/src/launcher/launchbox/lb-items.cpp rename to app/src/launcher/implementation/launchbox/lb-items.cpp diff --git a/app/src/launcher/launchbox/lb-items.h b/app/src/launcher/implementation/launchbox/lb-items.h similarity index 99% rename from app/src/launcher/launchbox/lb-items.h rename to app/src/launcher/implementation/launchbox/lb-items.h index cbea416..1ccb5c2 100644 --- a/app/src/launcher/launchbox/lb-items.h +++ b/app/src/launcher/implementation/launchbox/lb-items.h @@ -10,7 +10,7 @@ #include // Project Includes -#include "launcher/lr-items.h" +#include "launcher/interface/lr-items-interface.h" namespace Lb { diff --git a/app/src/launcher/implementation/launchbox/lb-registration.cpp b/app/src/launcher/implementation/launchbox/lb-registration.cpp new file mode 100644 index 0000000..44d7536 --- /dev/null +++ b/app/src/launcher/implementation/launchbox/lb-registration.cpp @@ -0,0 +1,4 @@ +#include "lb-registration.h" +#include "lb-install.h" + +REGISTER_LAUNCHER(Lb::LauncherId); diff --git a/app/src/launcher/implementation/launchbox/lb-registration.h b/app/src/launcher/implementation/launchbox/lb-registration.h new file mode 100644 index 0000000..978d762 --- /dev/null +++ b/app/src/launcher/implementation/launchbox/lb-registration.h @@ -0,0 +1,38 @@ +#ifndef LAUNCHBOX_REGISTRATION_H +#define LAUNCHBOX_REGISTRATION_H + +#include "launcher/abstract/lr-registration.h" + +namespace Lb { + +class Install; +class PlatformDoc; +class PlatformDocReader; +class PlatformDocWriter; +class PlaylistDoc; +class PlaylistDocReader; +class PlaylistDocWriter; +class Game; +class AddApp; +class PlaylistHeader; +class PlaylistGame; + +using LauncherId = Lr::Registrar< + Install, + PlatformDoc, + PlatformDocReader, + PlatformDocWriter, + PlaylistDoc, + PlaylistDocReader, + PlaylistDocWriter, + Game, + AddApp, + PlaylistHeader, + PlaylistGame, + u"LaunchBox", + u":/launcher/LaunchBox/icon.svg", + u"https://forums.launchbox-app.com/files/file/2652-obbys-flashpoint-importer-for-launchbox" +>; + +} +#endif // LAUNCHBOX_REGISTRATION_H diff --git a/app/src/launcher/interface/lr-data-interface.cpp b/app/src/launcher/interface/lr-data-interface.cpp new file mode 100644 index 0000000..9cf7e91 --- /dev/null +++ b/app/src/launcher/interface/lr-data-interface.cpp @@ -0,0 +1,219 @@ +// Unit Include +#include "lr-data-interface.h" + +// Project Includes +#include "launcher/interface/lr-install-interface.h" + +namespace Lr +{ +//=============================================================================================================== +// DocHandlingError +//=============================================================================================================== + +//-Constructor------------------------------------------------------------- +//Public: +DocHandlingError::DocHandlingError() : + mType(NoError) +{} + +DocHandlingError::DocHandlingError(const IDataDoc& doc, Type t, const QString& s) : + mType(t), + mErrorStr(generatePrimaryString(doc, t)), + mSpecific(s) +{} + +//-Class Functions------------------------------------------------------------- +//Private: +QString DocHandlingError::generatePrimaryString(const IDataDoc& doc, Type t) +{ + // TODO: Use Qx for this + QString formattedError = ERR_STRINGS[t]; + formattedError.replace(M_DOC_TYPE, doc.identifier().docTypeString()); + formattedError.replace(M_DOC_NAME, doc.identifier().docName()); + formattedError.replace(M_DOC_PARENT, doc.install()->name()); + + return formattedError; +} + +//-Instance Functions------------------------------------------------------------- +//Public: +bool DocHandlingError::isValid() const { return mType != NoError; } +QString DocHandlingError::errorString() const { return mErrorStr; } +QString DocHandlingError::specific() const { return mSpecific; } +DocHandlingError::Type DocHandlingError::type() const { return mType; } + +//Private: +Qx::Severity DocHandlingError::deriveSeverity() const { return Qx::Critical; } +quint32 DocHandlingError::deriveValue() const { return mType; } +QString DocHandlingError::derivePrimary() const { return mErrorStr; } +QString DocHandlingError::deriveSecondary() const { return mSpecific; } + +//=============================================================================================================== +// ImageSources +//=============================================================================================================== + +//-Constructor-------------------------------------------------------------------------------------------------------- +//Public: +ImagePaths::ImagePaths() {} +ImagePaths::ImagePaths(const QString& logoPath, const QString& screenshotPath) : + mLogoPath(logoPath), + mScreenshotPath(screenshotPath) +{} + +//-Instance Functions-------------------------------------------------------------------------------------------------- +//Public: +bool ImagePaths::isNull() const { return mLogoPath.isEmpty() && mScreenshotPath.isEmpty(); } +QString ImagePaths::logoPath() const { return mLogoPath; } +QString ImagePaths::screenshotPath() const { return mScreenshotPath; } +void ImagePaths::setLogoPath(const QString& path) { mLogoPath = path; } +void ImagePaths::setScreenshotPath(const QString& path) { mScreenshotPath = path; } + +//=============================================================================================================== +// IDataDoc::Identifier +//=============================================================================================================== + +//-Operators---------------------------------------------------------------------------------------------------- +//Public: +bool operator== (const IDataDoc::Identifier& lhs, const IDataDoc::Identifier& rhs) noexcept +{ + return lhs.mDocType == rhs.mDocType && lhs.mDocName == rhs.mDocName; +} + +//-Hashing------------------------------------------------------------------------------------------------------ +uint qHash(const IDataDoc::Identifier& key, uint seed) noexcept +{ + seed = qHash(key.mDocType, seed); + seed = qHash(key.mDocName, seed); + + return seed; +} + +//-Constructor-------------------------------------------------------------------------------------------------------- +//Public: +IDataDoc::Identifier::Identifier(Type docType, QString docName) : + mDocType(docType), + mDocName(docName) +{} + +//-Instance Functions-------------------------------------------------------------------------------------------------- +//Public: +IDataDoc::Type IDataDoc::Identifier::docType() const { return mDocType; } +QString IDataDoc::Identifier::docTypeString() const { return TYPE_STRINGS.value(mDocType); } +QString IDataDoc::Identifier::docName() const { return mDocName; } + +//=============================================================================================================== +// IDataDoc +//=============================================================================================================== + +//-Constructor-------------------------------------------------------------------------------------------------------- +//Public: +IDataDoc::IDataDoc(IInstall* install, const QString& docPath, QString docName) : + mInstall(install), + mDocumentPath(docPath), + mName(docName) +{} + +//-Instance Functions-------------------------------------------------------------------------------------------------- +//Public: +IInstall* IDataDoc::install() const { return mInstall; } +QString IDataDoc::path() const { return mDocumentPath; } +IDataDoc::Identifier IDataDoc::identifier() const { return Identifier(type(), mName); } + +//=============================================================================================================== +// IDataDoc::Reader +//=============================================================================================================== + +//-Constructor----------------------------------------------------------------------------------------------------- +//Protected: +IDataDoc::Reader::Reader(IDataDoc* targetDoc) : + mTargetDocument(targetDoc) +{} + +//-Destructor------------------------------------------------------------------------------------------------ +//Public: +IDataDoc::Reader::~Reader() {} + +//-Instance Functions------------------------------------------------------------------------------------------------- +//Protected: +IDataDoc* IDataDoc::Reader::target() const { return mTargetDocument; } + +//=============================================================================================================== +// IDataDoc::Writer +//=============================================================================================================== + +//-Constructor----------------------------------------------------------------------------------------------------- +//Protected: +IDataDoc::Writer::Writer(IDataDoc* sourceDoc) : + mSourceDocument(sourceDoc) +{} + +//-Destructor------------------------------------------------------------------------------------------------ +//Public: +IDataDoc::Writer::~Writer() {} + +//-Instance Functions------------------------------------------------------------------------------------------------- +//Protected: +IDataDoc* IDataDoc::Writer::source() const { return mSourceDocument; } + +//=============================================================================================================== +// Errorable +//=============================================================================================================== + +//-Constructor----------------------------------------------------------------------------------------------------- +//Protected: +IErrorable::IErrorable() {} + +//-Destructor------------------------------------------------------------------------------------------------ +//Public: +IErrorable::~IErrorable() {} + +//-Instance Functions------------------------------------------------------------------------------------------------- +//Protected: +bool IErrorable::hasError() const { return mError.isValid(); } +Qx::Error IErrorable::error() const { return mError; } + +//=============================================================================================================== +// UpdateableDoc +//=============================================================================================================== + +//-Constructor----------------------------------------------------------------------------------------------------- +//Protected: +IUpdateableDoc::IUpdateableDoc(IInstall* install, const QString& docPath, QString docName, const Import::UpdateOptions& updateOptions) : + IDataDoc(install, docPath, docName), + mUpdateOptions(updateOptions) +{} + +//-Instance Functions-------------------------------------------------------------------------------------------------- +//Public: +void IUpdateableDoc::finalize() {} // Does nothing for base class + +//=============================================================================================================== +// PlatformDoc +//=============================================================================================================== + +//-Constructor-------------------------------------------------------------------------------------------------------- +//Protected: +IPlatformDoc::IPlatformDoc(IInstall* install, const QString& docPath, QString docName, const Import::UpdateOptions& updateOptions) : + IUpdateableDoc(install, docPath, docName, updateOptions) +{} + +//-Instance Functions-------------------------------------------------------------------------------------------------- +//Private: +IDataDoc::Type IPlatformDoc::type() const { return Type::Platform; } + +//=============================================================================================================== +// PlaylistDoc +//=============================================================================================================== + +//-Constructor-------------------------------------------------------------------------------------------------------- +//Public: +IPlaylistDoc::IPlaylistDoc(IInstall* install, const QString& docPath, QString docName, const Import::UpdateOptions& updateOptions) : + IUpdateableDoc(install, docPath, docName, updateOptions) +{} + +//-Instance Functions-------------------------------------------------------------------------------------------------- +//Private: +IDataDoc::Type IPlaylistDoc::type() const { return Type::Playlist; } + +} + diff --git a/app/src/launcher/interface/lr-data-interface.h b/app/src/launcher/interface/lr-data-interface.h new file mode 100644 index 0000000..85dddc0 --- /dev/null +++ b/app/src/launcher/interface/lr-data-interface.h @@ -0,0 +1,362 @@ +#ifndef LR_DATA_INTERFACE_H +#define LR_DATA_INTERFACE_H + +// Standard Library Includes +#include +#include + +// Qt Includes +#include +#include + +// Qx Includes +#include +#include +#include + +// libfp Includes +#include + +// Project Includes +#include "launcher/interface/lr-items-interface.h" +#include "import/settings.h" + +namespace Lr +{ +class IInstall; +class IDataDoc; + +//-Concepts------------------------------------------------------------------------------------------------------ + +template +concept updateable_item_container = Qx::qassociative && item; + +template +concept updateable_basicitem_container = Qx::qassociative && basic_item && + std::same_as; + +//-Classes----------------------------------------------------------------------------------------------------------- +class QX_ERROR_TYPE(DocHandlingError, "Lr::DocHandlingError", 1310) +{ +//-Class Enums------------------------------------------------------------- +public: + enum Type + { + NoError = 0, + DocAlreadyOpen = 1, + DocCantOpen = 2, + DocCantSave = 3, + NotParentDoc = 4, + CantRemoveBackup = 5, + CantCreateBackup = 6, + DocInvalidType = 7, + DocReadFailed = 8, + DocWriteFailed = 9 + }; + +//-Class Variables------------------------------------------------------------- +private: + // Message Macros + static inline const QString M_DOC_TYPE = u""_s; + static inline const QString M_DOC_NAME = u""_s; + static inline const QString M_DOC_PARENT = u""_s; + + static inline const QHash ERR_STRINGS{ + {NoError, u""_s}, + {DocAlreadyOpen, u"The target document ("_s + M_DOC_TYPE + u" | "_s + M_DOC_NAME + u") is already open."_s}, + {DocCantOpen, u"The target document ("_s + M_DOC_TYPE + u" | "_s + M_DOC_NAME + u") cannot be opened."_s}, + {DocCantSave, u"The target document ("_s + M_DOC_TYPE + u" | "_s + M_DOC_NAME + u") cannot be saved."_s}, + {NotParentDoc, u"The target document ("_s + M_DOC_TYPE + u" | "_s + M_DOC_NAME + u") is not a"_s + M_DOC_PARENT + u"document."_s}, + {CantRemoveBackup, u"The existing backup of the target document ("_s + M_DOC_TYPE + u" | "_s + M_DOC_NAME + u") could not be removed."_s}, + {CantCreateBackup, u"Could not create a backup of the target document ("_s + M_DOC_TYPE + u" | "_s + M_DOC_NAME + u")."_s}, + {DocInvalidType, u"The document ("_s + M_DOC_TYPE + u" | "_s + M_DOC_NAME + u") is invalid or of the wrong type."_s}, + {DocReadFailed, u"Reading the target document ("_s + M_DOC_TYPE + u" | "_s + M_DOC_NAME + u") failed."_s}, + {DocWriteFailed, u"Writing to the target document ("_s + M_DOC_TYPE + u" | "_s + M_DOC_NAME + u") failed."_s} + }; + +//-Instance Variables------------------------------------------------------------- +private: + Type mType; + QString mErrorStr; + QString mSpecific; + +//-Constructor------------------------------------------------------------- +public: + DocHandlingError(); + DocHandlingError(const IDataDoc& doc, Type t, const QString& s = {}); + +//-Class Functions------------------------------------------------------------- +private: + static QString generatePrimaryString(const IDataDoc& doc, Type t); + +//-Instance Functions------------------------------------------------------------- +public: + bool isValid() const; + Type type() const; + + QString errorString() const; + QString specific() const; + +private: + Qx::Severity deriveSeverity() const override; + quint32 deriveValue() const override; + QString derivePrimary() const override; + QString deriveSecondary() const override; +}; + +class ImagePaths +{ +//-Instance Members-------------------------------------------------------------------------------------------------- +private: + QString mLogoPath; + QString mScreenshotPath; + +//-Constructor-------------------------------------------------------------------------------------------------------- +public: + ImagePaths(); + ImagePaths(const QString& logoPath, const QString& screenshotPath); + +//-Instance Functions-------------------------------------------------------------------------------------------------- +public: + bool isNull() const; + QString logoPath() const; + QString screenshotPath() const; + void setLogoPath(const QString& path); + void setScreenshotPath(const QString& path); +}; + +class IDataDoc +{ +//-Class Enums--------------------------------------------------------------------------------------------------------- +public: + enum class Type {Platform, Playlist, Config}; + +//-Inner Classes---------------------------------------------------------------------------------------------------- +public: + class Reader; + class Writer; + + class Identifier + { + friend bool operator== (const Identifier& lhs, const Identifier& rhs) noexcept; + friend uint qHash(const Identifier& key, uint seed) noexcept; + + private: + Type mDocType; + QString mDocName; + + public: + Identifier(Type docType, QString docName); + + Type docType() const; + QString docTypeString() const; + QString docName() const; + }; + +//-Class Variables----------------------------------------------------------------------------------------------------- +private: + static inline const QHash TYPE_STRINGS = { + {Type::Platform, u"Platform"_s}, + {Type::Playlist, u"Playlist"_s}, + {Type::Config, u"Config"_s} + }; + +//-Instance Variables-------------------------------------------------------------------------------------------------- +private: + IInstall* mInstall; + const QString mDocumentPath; + const QString mName; + +//-Constructor-------------------------------------------------------------------------------------------------------- +protected: + IDataDoc(IInstall* install, const QString& docPath, QString docName); + +//-Destructor------------------------------------------------------------------------------------------------- +public: + virtual ~IDataDoc() = default; + +//-Instance Functions-------------------------------------------------------------------------------------------------- +protected: + virtual Type type() const = 0; + +public: + IInstall* install() const; + QString path() const; + Identifier identifier() const; + virtual bool isEmpty() const = 0; +}; +QX_SCOPED_ENUM_HASH_FUNC(IDataDoc::Type); + +class IDataDoc::Reader +{ +//-Instance Variables-------------------------------------------------------------------------------------------------- +private: + IDataDoc* mTargetDocument; + +//-Constructor-------------------------------------------------------------------------------------------------------- +protected: + Reader(IDataDoc* targetDoc); + +//-Destructor------------------------------------------------------------------------------------------------- +public: + virtual ~Reader(); + +//-Instance Functions------------------------------------------------------------------------------------------------- +protected: + IDataDoc* target() const; + +public: + virtual DocHandlingError readInto() = 0; +}; + +class IDataDoc::Writer +{ +//-Instance Variables-------------------------------------------------------------------------------------------------- +private: + IDataDoc* mSourceDocument; + +//-Constructor-------------------------------------------------------------------------------------------------------- +protected: + Writer(IDataDoc* sourceDoc); + +//-Destructor------------------------------------------------------------------------------------------------- +public: + virtual ~Writer(); + +//-Instance Functions------------------------------------------------------------------------------------------------- +protected: + IDataDoc* source() const; + +public: + virtual DocHandlingError writeOutOf() = 0; +}; + +class IErrorable +{ +//-Instance Variables-------------------------------------------------------------------------------------------------- +protected: + Qx::Error mError; + +//-Constructor-------------------------------------------------------------------------------------------------------- +protected: + IErrorable(); + +//-Destructor------------------------------------------------------------------------------------------------- +public: + virtual ~IErrorable(); + +//-Instance Functions-------------------------------------------------------------------------------------------------- +public: + bool hasError() const; + Qx::Error error() const; +}; + +class IUpdateableDoc : public IDataDoc +{ +//-Instance Variables-------------------------------------------------------------------------------------------------- +protected: + Import::UpdateOptions mUpdateOptions; + +//-Constructor-------------------------------------------------------------------------------------------------------- +protected: + explicit IUpdateableDoc(IInstall* install, const QString& docPath, QString docName, const Import::UpdateOptions& updateOptions); + +//-Class Functions----------------------------------------------------------------------------------------------------- +template +static T* itemPtr(T& item) { return &item; } + +template +static T* itemPtr(std::shared_ptr item) { return item.get(); } + +//-Instance Functions-------------------------------------------------------------------------------------------------- +protected: + template + requires updateable_item_container + void finalizeUpdateableItems(C& existingItems, + C& finalItems) + { + // Copy items to final list if obsolete entries are to be kept + if(!mUpdateOptions.removeObsolete) + finalItems.insert(existingItems); + + // Clear existing lists + existingItems.clear(); + } + + template + requires updateable_item_container + void addUpdateableItem(C& existingItems, + C& finalItems, + typename C::key_type key, + typename C::mapped_type newItem) + { + // Check if item exists + if(existingItems.contains(key)) + { + // Replace if existing update is on, move existing otherwise + if(mUpdateOptions.importMode == Import::UpdateMode::NewAndExisting) + { + itemPtr(newItem)->transferOtherFields(itemPtr(existingItems[key])->otherFields()); + finalItems[key] = newItem; + existingItems.remove(key); + } + else + { + finalItems[key] = std::move(existingItems[key]); + existingItems.remove(key); + } + + } + else + finalItems[key] = newItem; + } + + template + requires updateable_basicitem_container + void addUpdateableItem(C& existingItems, + C& finalItems, + typename C::mapped_type newItem) + { + addUpdateableItem(existingItems, + finalItems, + std::static_pointer_cast(newItem)->id(), + newItem); + } + +public: + virtual void finalize(); +}; + +class IPlatformDoc : public IUpdateableDoc, public IErrorable +{ +//-Constructor-------------------------------------------------------------------------------------------------------- +protected: + explicit IPlatformDoc(IInstall* install, const QString& docPath, QString docName, const Import::UpdateOptions& updateOptions); + +//-Instance Functions-------------------------------------------------------------------------------------------------- +private: + Type type() const override; + +public: + virtual bool containsGame(QUuid gameId) const = 0; // NOTE: UNUSED + virtual bool containsAddApp(QUuid addAppId) const = 0; // NOTE: UNUSED + virtual void addSet(const Fp::Set& set, ImagePaths& images) = 0; +}; + +class IPlaylistDoc : public IUpdateableDoc, public IErrorable +{ +//-Constructor-------------------------------------------------------------------------------------------------------- +protected: + explicit IPlaylistDoc(IInstall* install, const QString& docPath, QString docName, const Import::UpdateOptions& updateOptions); + +//-Instance Functions-------------------------------------------------------------------------------------------------- +private: + Type type() const override; + +public: + virtual bool containsPlaylistGame(QUuid gameId) const = 0; // NOTE: UNUSED + virtual void setPlaylistData(const Fp::Playlist& playlist) = 0; +}; + +} +#endif // LR_DATA_INTERFACE_H diff --git a/app/src/launcher/lr-installfoundation.cpp b/app/src/launcher/interface/lr-install-interface.cpp similarity index 61% rename from app/src/launcher/lr-installfoundation.cpp rename to app/src/launcher/interface/lr-install-interface.cpp index f225bc4..588a9d5 100644 --- a/app/src/launcher/lr-installfoundation.cpp +++ b/app/src/launcher/interface/lr-install-interface.cpp @@ -1,5 +1,5 @@ // Unit Include -#include "lr-installfoundation.h" +#include "lr-install-interface.h" namespace Lr { @@ -34,43 +34,45 @@ QString RevertError::deriveSecondary() const { return mSpecific; } QString RevertError::deriveCaption() const { return CAPTION_REVERT_ERR; } //=============================================================================================================== -// InstallFoundation +// IInstall //=============================================================================================================== //-Constructor--------------------------------------------------------------------------------------------------- -InstallFoundation::InstallFoundation(const QString& installPath) : +IInstall::IInstall(const QString& installPath) : mValid(false), // Path is invalid until proven otherwise mRootDirectory(installPath) {} //-Destructor------------------------------------------------------------------------------------------------ //Public: -InstallFoundation::~InstallFoundation() {} +IInstall::~IInstall() {} //Public: -QString InstallFoundation::filePathToBackupPath(const QString& filePath) +QString IInstall::filePathToBackupPath(const QString& filePath) { return filePath + '.' + BACKUP_FILE_EXT; } //-Instance Functions-------------------------------------------------------------------------------------------- //Private: -bool InstallFoundation::containsAnyDataDoc(DataDoc::Type type, const QList& names) const +bool IInstall::containsAnyDataDoc(IDataDoc::Type type, const QList& names) const { // Create identifier set of names - QSet searchSet; + QSet searchSet; for(const QString& docName : names) - searchSet << DataDoc::Identifier(type, translateDocName(docName, type)); + searchSet << IDataDoc::Identifier(type, translateDocName(docName, type)); // Cross reference with existing documents return mExistingDocuments.intersects(searchSet); } -QList InstallFoundation::modifiedDataDocs(DataDoc::Type type) const +bool IInstall::supportsImageMode(Import::ImageMode imageMode) const { return preferredImageModeOrder().contains(imageMode); } + +QList IInstall::modifiedDataDocs(IDataDoc::Type type) const { QList modList; - for(const DataDoc::Identifier& dataDocId : mModifiedDocuments) + for(const IDataDoc::Identifier& dataDocId : mModifiedDocuments) if(dataDocId.docType() == type) modList.append(dataDocId.docName()); @@ -78,37 +80,22 @@ QList InstallFoundation::modifiedDataDocs(DataDoc::Type type) const } //Protected: -void InstallFoundation::nullify() +void IInstall::nullify() { mValid = false; mRootDirectory = QDir(); } -void InstallFoundation::softReset() -{ - mRevertableFilePaths.clear(); - mModifiedDocuments.clear(); - mDeletedDocuments.clear(); - mLeasedDocuments.clear(); - mImportDetails.reset(); -} - -void InstallFoundation::declareValid(bool valid) +void IInstall::declareValid(bool valid) { mValid = valid; if(!valid) nullify(); } -QString InstallFoundation::translateDocName(const QString& originalName, DataDoc::Type type) const -{ - Q_UNUSED(type); - return originalName; -} - -void InstallFoundation::catalogueExistingDoc(DataDoc::Identifier existingDoc) { mExistingDocuments.insert(existingDoc); } +void IInstall::catalogueExistingDoc(IDataDoc::Identifier existingDoc) { mExistingDocuments.insert(existingDoc); } -DocHandlingError InstallFoundation::checkoutDataDocument(DataDoc* docToOpen, std::shared_ptr docReader) +DocHandlingError IInstall::checkoutDataDocument(IDataDoc* docToOpen, std::shared_ptr docReader) { // Error report to return DocHandlingError openReadError; // Defaults to no error @@ -131,9 +118,9 @@ DocHandlingError InstallFoundation::checkoutDataDocument(DataDoc* docToOpen, std return openReadError; } -DocHandlingError InstallFoundation::commitDataDocument(DataDoc* docToSave, std::shared_ptr docWriter) +DocHandlingError IInstall::commitDataDocument(IDataDoc* docToSave, std::shared_ptr docWriter) { - DataDoc::Identifier id = docToSave->identifier(); + IDataDoc::Identifier id = docToSave->identifier(); // Check if the doc was saved previously to prevent double-backups bool wasDeleted = mDeletedDocuments.contains(id); @@ -189,16 +176,39 @@ DocHandlingError InstallFoundation::commitDataDocument(DataDoc* docToSave, std:: return commitError; } -QList InstallFoundation::modifiedPlatforms() const { return modifiedDataDocs(DataDoc::Type::Platform); } -QList InstallFoundation::modifiedPlaylists() const { return modifiedDataDocs(DataDoc::Type::Playlist); } +QList IInstall::modifiedPlatforms() const { return modifiedDataDocs(IDataDoc::Type::Platform); } +QList IInstall::modifiedPlaylists() const { return modifiedDataDocs(IDataDoc::Type::Playlist); } //Public: -bool InstallFoundation::isValid() const { return mValid; } -QString InstallFoundation::path() const { return mRootDirectory.absolutePath(); } +QString IInstall::versionString() const { return u"Unknown Version"_s; } +bool IInstall::isValid() const { return mValid; } +QString IInstall::path() const { return mRootDirectory.absolutePath(); } -Qx::Error InstallFoundation::refreshExistingDocs(bool* changed) +void IInstall::softReset() { - QSet oldDocSet; + mRevertableFilePaths.clear(); + mModifiedDocuments.clear(); + mDeletedDocuments.clear(); + mLeasedDocuments.clear(); + mImportDetails.reset(); +} + +IInstall::ImportDetails IInstall::importDetails() const +{ + // We just assert here because no install should ever use this before it's available + Q_ASSERT(mImportDetails); + return mImportDetails.value(); +} + +QString IInstall::translateDocName(const QString& originalName, IDataDoc::Type type) const +{ + Q_UNUSED(type); + return originalName; +} + +Qx::Error IInstall::refreshExistingDocs(bool* changed) +{ + QSet oldDocSet; oldDocSet.swap(mExistingDocuments); Qx::Error error = populateExistingDocs(); if(changed) @@ -206,30 +216,30 @@ Qx::Error InstallFoundation::refreshExistingDocs(bool* changed) return error; } -bool InstallFoundation::containsPlatform(const QString& name) const +bool IInstall::containsPlatform(const QString& name) const { - return containsAnyDataDoc(DataDoc::Type::Platform, {name}); + return containsAnyDataDoc(IDataDoc::Type::Platform, {name}); } -bool InstallFoundation::containsPlaylist(const QString& name) const +bool IInstall::containsPlaylist(const QString& name) const { - return containsAnyDataDoc(DataDoc::Type::Playlist, {name}); + return containsAnyDataDoc(IDataDoc::Type::Playlist, {name}); } -bool InstallFoundation::containsAnyPlatform(const QList& names) const +bool IInstall::containsAnyPlatform(const QList& names) const { - return containsAnyDataDoc(DataDoc::Type::Platform, names); + return containsAnyDataDoc(IDataDoc::Type::Platform, names); } -bool InstallFoundation::containsAnyPlaylist(const QList& names) const +bool IInstall::containsAnyPlaylist(const QList& names) const { - return containsAnyDataDoc(DataDoc::Type::Playlist, names); + return containsAnyDataDoc(IDataDoc::Type::Playlist, names); } -void InstallFoundation::addRevertableFile(const QString& filePath) { mRevertableFilePaths.append(filePath); } -int InstallFoundation::revertQueueCount() const { return mRevertableFilePaths.size(); } +void IInstall::addRevertableFile(const QString& filePath) { mRevertableFilePaths.append(filePath); } +int IInstall::revertQueueCount() const { return mRevertableFilePaths.size(); } -int InstallFoundation::revertNextChange(RevertError& error, bool skipOnFail) +int IInstall::revertNextChange(RevertError& error, bool skipOnFail) { // Ensure error message is null error = RevertError(); @@ -263,4 +273,32 @@ int InstallFoundation::revertNextChange(RevertError& error, bool skipOnFail) return 0; } +/* These functions can be overridden by children as needed. + * Work within them should be kept as minimal as possible since they are not accounted + * for by the import progress indicator. + */ +Qx::Error IInstall::preImport(const ImportDetails& details) +{ + mImportDetails = details; + return Qx::Error(); +} + +Qx::Error IInstall::postImport() { return {}; } +Qx::Error IInstall::prePlatformsImport() { return {}; } +Qx::Error IInstall::postPlatformsImport() { return {}; } + +Qx::Error IInstall::preImageProcessing(const ImagePaths& bulkSources) +{ + Q_UNUSED(bulkSources); + return {}; +} + +Qx::Error IInstall::postImageProcessing() { return {}; } +Qx::Error IInstall::prePlaylistsImport() { return {}; } +Qx::Error IInstall::postPlaylistsImport() { return {}; } + +QString IInstall::platformCategoryIconPath() const { return QString(); } // Unsupported in default implementation +std::optional IInstall::platformIconsDirectory() const { return std::nullopt; } // Unsupported in default implementation +std::optional IInstall::playlistIconsDirectory() const { return std::nullopt; } // Unsupported in default implementation + } diff --git a/app/src/launcher/lr-installfoundation.h b/app/src/launcher/interface/lr-install-interface.h similarity index 59% rename from app/src/launcher/lr-installfoundation.h rename to app/src/launcher/interface/lr-install-interface.h index 99c4b36..da9d41d 100644 --- a/app/src/launcher/lr-installfoundation.h +++ b/app/src/launcher/interface/lr-install-interface.h @@ -1,11 +1,11 @@ -#ifndef LR_INSTALLFOUNDATION_H -#define LR_INSTALLFOUNDATION_H +#ifndef LR_INSTALL_INTERFACE_H +#define LR_INSTALL_INTERFACE_H // Qt Includes #include // Project Includes -#include "launcher/lr-data.h" +#include "launcher/interface/lr-data-interface.h" #include "import/settings.h" namespace Lr @@ -13,7 +13,7 @@ namespace Lr class QX_ERROR_TYPE(RevertError, "Lr::RevertError", 1301) { - friend class InstallFoundation; + friend class IInstall; //-Class Enums------------------------------------------------------------- public: enum Type @@ -59,7 +59,7 @@ class QX_ERROR_TYPE(RevertError, "Lr::RevertError", 1301) QString deriveCaption() const override; }; -class InstallFoundation +class IInstall { //-Class Structs------------------------------------------------------------------------------------------------------ public: @@ -104,68 +104,103 @@ class InstallFoundation QDir mRootDirectory; // Document tracking - QSet mExistingDocuments; - QSet mModifiedDocuments; - QSet mDeletedDocuments; - QSet mLeasedDocuments; + QSet mExistingDocuments; + QSet mModifiedDocuments; + QSet mDeletedDocuments; + QSet mLeasedDocuments; // Backup/Deletion tracking QStringList mRevertableFilePaths; -protected: // Import details - std::unique_ptr mImportDetails; + std::optional mImportDetails; //-Constructor--------------------------------------------------------------------------------------------------- public: - InstallFoundation(const QString& installPath); + IInstall(const QString& installPath); //-Destructor------------------------------------------------------------------------------------------------- public: - virtual ~InstallFoundation(); + virtual ~IInstall(); //-Class Functions------------------------------------------------------------------------------------------------------ private: static void ensureModifiable(const QString& filePath); public: + // TODO: Improve the backup system so that its more encapsulated and this doesn't need to be public static QString filePathToBackupPath(const QString& filePath); //-Instance Functions--------------------------------------------------------------------------------------------------------- private: - bool containsAnyDataDoc(DataDoc::Type type, const QList& names) const; - QList modifiedDataDocs(DataDoc::Type type) const; + // Support + bool containsAnyDataDoc(IDataDoc::Type type, const QList& names) const; + bool supportsImageMode(Import::ImageMode imageMode) const; // TODO: UNUSED + QList modifiedDataDocs(IDataDoc::Type type) const; protected: + // Validity virtual void nullify(); - virtual void softReset(); void declareValid(bool valid); - virtual Qx::Error populateExistingDocs() = 0; // Stated redundantly again in Install to make it clear its part of the main interface - - virtual QString translateDocName(const QString& originalName, DataDoc::Type type) const; - void catalogueExistingDoc(DataDoc::Identifier existingDoc); - - DocHandlingError checkoutDataDocument(DataDoc* docToOpen, std::shared_ptr docReader); - DocHandlingError commitDataDocument(DataDoc* docToSave, std::shared_ptr docWriter); + // Docs + virtual Qx::Error populateExistingDocs() = 0; + void catalogueExistingDoc(IDataDoc::Identifier existingDoc); + DocHandlingError checkoutDataDocument(IDataDoc* docToOpen, std::shared_ptr docReader); + DocHandlingError commitDataDocument(IDataDoc* docToSave, std::shared_ptr docWriter); QList modifiedPlatforms() const; QList modifiedPlaylists() const; public: + // Details + virtual QString name() const = 0; + virtual QList preferredImageModeOrder() const = 0; + virtual QString versionString() const; + virtual bool isRunning() const = 0; + bool isValid() const; QString path() const; + virtual void softReset(); + + // Import + ImportDetails importDetails() const; + // Docs + virtual QString translateDocName(const QString& originalName, IDataDoc::Type type) const; Qx::Error refreshExistingDocs(bool* changed = nullptr); bool containsPlatform(const QString& name) const; bool containsPlaylist(const QString& name) const; bool containsAnyPlatform(const QList& names) const; bool containsAnyPlaylist(const QList& names) const; + virtual DocHandlingError checkoutPlatformDoc(std::unique_ptr& returnBuffer, const QString& name) = 0; + virtual DocHandlingError checkoutPlaylistDoc(std::unique_ptr& returnBuffer, const QString& name) = 0; + virtual DocHandlingError commitPlatformDoc(std::unique_ptr platformDoc) = 0; + virtual DocHandlingError commitPlaylistDoc(std::unique_ptr playlistDoc) = 0; + + // Reversion void addRevertableFile(const QString& filePath); int revertQueueCount() const; int revertNextChange(RevertError& error, bool skipOnFail); + + // Import stage notifier hooks + virtual Qx::Error preImport(const ImportDetails& details); + virtual Qx::Error postImport(); + virtual Qx::Error prePlatformsImport(); + virtual Qx::Error postPlatformsImport(); + virtual Qx::Error preImageProcessing(const ImagePaths& bulkSources); + virtual Qx::Error postImageProcessing(); + virtual Qx::Error prePlaylistsImport(); + virtual Qx::Error postPlaylistsImport(); + + // Images + virtual QString platformCategoryIconPath() const; // Unsupported in default implementation, needs to return path with .png extension + virtual std::optional platformIconsDirectory() const; // Unsupported in default implementation + virtual std::optional playlistIconsDirectory() const; // Unsupported in default implementation + // TODO: These might need to be changed to support launchers where the platform images are tied closely to the platform documents, + // but currently none do this so this works. }; } -#endif // LR_INSTALLFOUNDATION_H +#endif // LR_INSTALL_INTERFACE_H diff --git a/app/src/launcher/lr-installfoundation_linux.cpp b/app/src/launcher/interface/lr-install-interface_linux.cpp similarity index 81% rename from app/src/launcher/lr-installfoundation_linux.cpp rename to app/src/launcher/interface/lr-install-interface_linux.cpp index 5317c27..aefd230 100644 --- a/app/src/launcher/lr-installfoundation_linux.cpp +++ b/app/src/launcher/interface/lr-install-interface_linux.cpp @@ -1,5 +1,5 @@ // Unit Include -#include "lr-installfoundation.h" +#include "lr-install-interface.h" // Qt Includes #include @@ -7,12 +7,12 @@ namespace Lr { //=============================================================================================================== -// InstallFoundation +// IInstall //=============================================================================================================== //-Class Functions-------------------------------------------------------------------------------------------- //Private: -void InstallFoundation::ensureModifiable(const QString& filePath) +void IInstall::ensureModifiable(const QString& filePath) { QFile f(filePath);; f.setPermissions(f.permissions() | QFile::WriteOwner | QFile::WriteGroup); diff --git a/app/src/launcher/lr-installfoundation_win.cpp b/app/src/launcher/interface/lr-install-interface_win.cpp similarity index 95% rename from app/src/launcher/lr-installfoundation_win.cpp rename to app/src/launcher/interface/lr-install-interface_win.cpp index 168eb16..d528d3a 100644 --- a/app/src/launcher/lr-installfoundation_win.cpp +++ b/app/src/launcher/interface/lr-install-interface_win.cpp @@ -1,5 +1,5 @@ // Unit Include -#include "lr-installfoundation.h" +#include "lr-install-interface.h" // Windows Includes (Specifically for changing file permissions) #include "Aclapi.h" @@ -7,12 +7,12 @@ namespace Lr { //=============================================================================================================== -// InstallFoundation +// IInstall //=============================================================================================================== //-Class Functions-------------------------------------------------------------------------------------------- //Private: -void InstallFoundation::ensureModifiable(const QString& filePath) +void IInstall::ensureModifiable(const QString& filePath) { Q_ASSERT(!filePath.isEmpty()); PACL pCurrentDacl = nullptr, pNewDACL = nullptr; diff --git a/app/src/launcher/lr-items.cpp b/app/src/launcher/interface/lr-items-interface.cpp similarity index 99% rename from app/src/launcher/lr-items.cpp rename to app/src/launcher/interface/lr-items-interface.cpp index 3f546dd..5c62e73 100644 --- a/app/src/launcher/lr-items.cpp +++ b/app/src/launcher/interface/lr-items-interface.cpp @@ -1,5 +1,5 @@ // Unit Include -#include "lr-items.h" +#include "lr-items-interface.h" namespace Lr { diff --git a/app/src/launcher/lr-items.h b/app/src/launcher/interface/lr-items-interface.h similarity index 86% rename from app/src/launcher/lr-items.h rename to app/src/launcher/interface/lr-items-interface.h index ba0d674..10961d3 100644 --- a/app/src/launcher/lr-items.h +++ b/app/src/launcher/interface/lr-items-interface.h @@ -1,5 +1,5 @@ -#ifndef LR_ITEMS_H -#define LR_ITEMS_H +#ifndef LR_ITEMS_INTERFACE_H +#define LR_ITEMS_INTERFACE_H // Standard Library Includes #include @@ -8,6 +8,9 @@ #include #include +// Qx Includes +#include + using namespace Qt::Literals::StringLiterals; namespace Lr @@ -26,13 +29,49 @@ namespace Lr * be able to work with it */ +class Item; +class BasicItem; +class Game; +class AddApp; +class PlaylistHeader; +class PlaylistGame; + +template +concept raw_item = std::derived_from; + +template +concept shared_item = Qx::specializes && std::derived_from; + +template +concept raw_basic_item = std::derived_from; + +template +concept shared_basic_item = Qx::specializes && std::derived_from; + +template +concept item = raw_item || shared_item; + +template +concept basic_item = raw_basic_item || shared_basic_item; + +template +concept raw_game = std::derived_from; + +template +concept raw_addapp = std::derived_from; + +template +concept raw_playlistheader = std::derived_from; + +template +concept raw_playlistgame = std::derived_from; + //-Namespace Global Classes----------------------------------------------------------------------------------------- class Item { //-Inner Classes--------------------------------------------------------------------------------------------------- public: - template - requires std::derived_from + template class Builder; //-Instance Variables----------------------------------------------------------------------------------------------- @@ -54,8 +93,7 @@ class Item void transferOtherFields(QHash& otherFields); }; -template - requires std::derived_from +template class Item::Builder { //-Instance Variables------------------------------------------------------------------------------------------ @@ -86,8 +124,7 @@ class BasicItem : public Item { //-Inner Classes--------------------------------------------------------------------------------------------------- public: - template - requires std::derived_from + template class Builder; //-Instance Variables----------------------------------------------------------------------------------------------- @@ -106,8 +143,7 @@ class BasicItem : public Item QString name() const; }; -template - requires std::derived_from +template class BasicItem::Builder : public Item::Builder { //-Instance Functions------------------------------------------------------------------------------------------ @@ -126,8 +162,7 @@ class Game : public BasicItem { //-Inner Classes--------------------------------------------------------------------------------------------------- public: - template - requires std::derived_from + template class Builder; //-Class Variables-------------------------------------------------------------------------------------------------- @@ -149,8 +184,7 @@ class Game : public BasicItem QString platform() const; }; -template - requires std::derived_from +template class Game::Builder : public BasicItem::Builder { //-Instance Functions------------------------------------------------------------------------------------------ @@ -163,8 +197,7 @@ class AddApp : public BasicItem { //-Inner Classes--------------------------------------------------------------------------------------------------- public: - template - requires std::derived_from + template class Builder; //-Instance Variables----------------------------------------------------------------------------------------------- @@ -181,8 +214,7 @@ class AddApp : public BasicItem QUuid gameId() const; }; -template - requires std::derived_from +template class AddApp::Builder : public BasicItem::Builder { //-Instance Functions------------------------------------------------------------------------------------------ @@ -198,8 +230,7 @@ class PlaylistHeader : public BasicItem { //-Inner Classes--------------------------------------------------------------------------------------------------- public: - template - requires std::derived_from + template class Builder; //-Constructor------------------------------------------------------------------------------------------------- @@ -208,8 +239,7 @@ class PlaylistHeader : public BasicItem PlaylistHeader(QUuid id, QString name); }; -template - requires std::derived_from +template class PlaylistHeader::Builder : public BasicItem::Builder {}; @@ -217,8 +247,7 @@ class PlaylistGame : public BasicItem { //-Inner Classes--------------------------------------------------------------------------------------------------- public: - template - requires std::derived_from + template class Builder; //-Instance Variables----------------------------------------------------------------------------------------------- @@ -234,8 +263,7 @@ class PlaylistGame : public BasicItem QUuid gameId() const; }; -template - requires std::derived_from +template class PlaylistGame::Builder : public BasicItem::Builder { //-Instance Functions------------------------------------------------------------------------------------------ @@ -250,4 +278,4 @@ class PlaylistGame::Builder : public BasicItem::Builder } -#endif // LR_ITEMS_H +#endif // LR_ITEMS_INTERFACE_H diff --git a/app/src/launcher/lr-data.cpp b/app/src/launcher/lr-data.cpp deleted file mode 100644 index 0f7e047..0000000 --- a/app/src/launcher/lr-data.cpp +++ /dev/null @@ -1,576 +0,0 @@ -// Unit Include -#include "lr-data.h" - -// Qx Includes -#include -#include - -// Project Includes -#include "launcher/lr-install.h" - -namespace Lr -{ -//=============================================================================================================== -// DocHandlingError -//=============================================================================================================== - -//-Constructor------------------------------------------------------------- -//Public: -DocHandlingError::DocHandlingError() : - mType(NoError) -{} - - -DocHandlingError::DocHandlingError(const DataDoc& doc, Type t, const QString& s) : - mType(t), - mErrorStr(generatePrimaryString(doc, t)), - mSpecific(s) -{} - -//-Class Functions------------------------------------------------------------- -//Private: -QString DocHandlingError::generatePrimaryString(const DataDoc& doc, Type t) -{ - // TODO: Use Qx for this - QString formattedError = ERR_STRINGS[t]; - formattedError.replace(M_DOC_TYPE, doc.identifier().docTypeString()); - formattedError.replace(M_DOC_NAME, doc.identifier().docName()); - formattedError.replace(M_DOC_PARENT, doc.parent()->name()); - - return formattedError; -} - -//-Instance Functions------------------------------------------------------------- -//Public: -bool DocHandlingError::isValid() const { return mType != NoError; } -QString DocHandlingError::errorString() const { return mErrorStr; } -QString DocHandlingError::specific() const { return mSpecific; } -DocHandlingError::Type DocHandlingError::type() const { return mType; } - -//Private: -Qx::Severity DocHandlingError::deriveSeverity() const { return Qx::Critical; } -quint32 DocHandlingError::deriveValue() const { return mType; } -QString DocHandlingError::derivePrimary() const { return mErrorStr; } -QString DocHandlingError::deriveSecondary() const { return mSpecific; } - -//=============================================================================================================== -// ImageSources -//=============================================================================================================== - -//-Constructor-------------------------------------------------------------------------------------------------------- -//Public: -ImageSources::ImageSources() {} -ImageSources::ImageSources(const QString& logoPath, const QString& screenshotPath) : - mLogoPath(logoPath), - mScreenshotPath(screenshotPath) -{} - -//-Instance Functions-------------------------------------------------------------------------------------------------- -//Public: -bool ImageSources::isNull() const { return mLogoPath.isEmpty() && mScreenshotPath.isEmpty(); } -QString ImageSources::logoPath() const { return mLogoPath; } -QString ImageSources::screenshotPath() const { return mScreenshotPath; } -void ImageSources::setLogoPath(const QString& path) { mLogoPath = path; } -void ImageSources::setScreenshotPath(const QString& path) { mScreenshotPath = path; } - -//=============================================================================================================== -// DataDoc::Identifier -//=============================================================================================================== - -//-Operators---------------------------------------------------------------------------------------------------- -//Public: -bool operator== (const DataDoc::Identifier& lhs, const DataDoc::Identifier& rhs) noexcept -{ - return lhs.mDocType == rhs.mDocType && lhs.mDocName == rhs.mDocName; -} - -//-Hashing------------------------------------------------------------------------------------------------------ -uint qHash(const DataDoc::Identifier& key, uint seed) noexcept -{ - seed = qHash(key.mDocType, seed); - seed = qHash(key.mDocName, seed); - - return seed; -} - -//-Constructor-------------------------------------------------------------------------------------------------------- -//Public: -DataDoc::Identifier::Identifier(Type docType, QString docName) : - mDocType(docType), - mDocName(docName) -{} - -//-Instance Functions-------------------------------------------------------------------------------------------------- -//Public: -DataDoc::Type DataDoc::Identifier::docType() const { return mDocType; } -QString DataDoc::Identifier::docTypeString() const { return TYPE_STRINGS.value(mDocType); } -QString DataDoc::Identifier::docName() const { return mDocName; } - -//=============================================================================================================== -// DataDoc -//=============================================================================================================== - -//-Constructor-------------------------------------------------------------------------------------------------------- -//Public: -DataDoc::DataDoc(Install* const parent, const QString& docPath, QString docName) : - mParent(parent), - mDocumentPath(docPath), - mName(docName) -{} - -//-Destructor------------------------------------------------------------------------------------------------ -//Public: -DataDoc::~DataDoc() {} - -//-Instance Functions-------------------------------------------------------------------------------------------------- -//Public: -Install* DataDoc::parent() const { return mParent; } -QString DataDoc::path() const { return mDocumentPath; } -DataDoc::Identifier DataDoc::identifier() const { return Identifier(type(), mName); } - -//=============================================================================================================== -// DataDoc::Reader -//=============================================================================================================== - -//-Constructor----------------------------------------------------------------------------------------------------- -//Protected: -DataDoc::Reader::Reader(DataDoc* targetDoc) : - mTargetDocument(targetDoc) -{} - -//-Destructor------------------------------------------------------------------------------------------------ -//Public: -DataDoc::Reader::~Reader() {} - -//=============================================================================================================== -// DataDoc::Writer -//=============================================================================================================== - -//-Constructor----------------------------------------------------------------------------------------------------- -//Protected: -DataDoc::Writer::Writer(DataDoc* sourceDoc) : - mSourceDocument(sourceDoc) -{} - -//-Destructor------------------------------------------------------------------------------------------------ -//Public: -DataDoc::Writer::~Writer() {} - -//=============================================================================================================== -// Errorable -//=============================================================================================================== - -//-Constructor----------------------------------------------------------------------------------------------------- -//Protected: -Errorable::Errorable() {} - -//-Destructor------------------------------------------------------------------------------------------------ -//Public: -Errorable::~Errorable() {} - -//-Instance Functions------------------------------------------------------------------------------------------------- -//Protected: -bool Errorable::hasError() const { return mError.isValid(); } -Qx::Error Errorable::error() const { return mError; } - -//=============================================================================================================== -// UpdateableDoc -//=============================================================================================================== - -//-Constructor----------------------------------------------------------------------------------------------------- -//Protected: -UpdateableDoc::UpdateableDoc(Install* const parent, const QString& docPath, QString docName, const Import::UpdateOptions& updateOptions) : - DataDoc(parent, docPath, docName), - mUpdateOptions(updateOptions) -{} - -//-Instance Functions-------------------------------------------------------------------------------------------------- -//Public: -void UpdateableDoc::finalize() {} // Does nothing for base class - -//=============================================================================================================== -// PlatformDoc -//=============================================================================================================== - -//-Constructor-------------------------------------------------------------------------------------------------------- -//Protected: -PlatformDoc::PlatformDoc(Install* const parent, const QString& docPath, QString docName, const Import::UpdateOptions& updateOptions) : - UpdateableDoc(parent, docPath, docName, updateOptions) -{} - -//-Instance Functions-------------------------------------------------------------------------------------------------- -//Private: -DataDoc::Type PlatformDoc::type() const { return Type::Platform; } - -//=============================================================================================================== -// PlatformDoc::Reader -//=============================================================================================================== - -//-Constructor-------------------------------------------------------------------------------------------------------- -//Protected: -PlatformDoc::Reader::Reader(DataDoc* targetDoc) : - DataDoc::Reader(targetDoc) -{} - -//=============================================================================================================== -// PlatformDoc::Writer -//=============================================================================================================== - -PlatformDoc::Writer::Writer(DataDoc* sourceDoc) : - DataDoc::Writer(sourceDoc) -{} - -//=============================================================================================================== -// BasicPlatformDoc -//=============================================================================================================== - -//-Constructor-------------------------------------------------------------------------------------------------------- -//Protected: -BasicPlatformDoc::BasicPlatformDoc(Install* const parent, const QString& docPath, QString docName, const Import::UpdateOptions& updateOptions) : - PlatformDoc(parent, docPath, docName, updateOptions) -{} - -//-Instance Functions-------------------------------------------------------------------------------------------------- -//Public: -bool BasicPlatformDoc::isEmpty() const -{ - return mGamesFinal.isEmpty() && mGamesExisting.isEmpty() && mAddAppsFinal.isEmpty() && mAddAppsExisting.isEmpty(); -} - -const QHash>& BasicPlatformDoc::finalGames() const { return mGamesFinal; } -const QHash>& BasicPlatformDoc::finalAddApps() const { return mAddAppsFinal; } - -bool BasicPlatformDoc::containsGame(QUuid gameId) const { return mGamesFinal.contains(gameId) || mGamesExisting.contains(gameId); } -bool BasicPlatformDoc::containsAddApp(QUuid addAppId) const { return mAddAppsFinal.contains(addAppId) || mAddAppsExisting.contains(addAppId); } - -void BasicPlatformDoc::addSet(const Fp::Set& set, const ImageSources& images) -{ - if(!mError.isValid()) - { - // Prepare game - std::shared_ptr lrGame = prepareGame(set.game(), images); - - // Add game - addUpdateableItem(mGamesExisting, mGamesFinal, lrGame); - - // Handle additional apps - for(const Fp::AddApp& addApp : set.addApps()) - { - // Prepare - std::shared_ptr lrAddApp = prepareAddApp(addApp); - - // Add - addUpdateableItem(mAddAppsExisting, mAddAppsFinal, lrAddApp); - } - - // Allow install to handle images if needed - parent()->processDirectGameImages(lrGame.get(), images); - - } -} - -void BasicPlatformDoc::finalize() -{ - if(!mError.isValid()) - { - /* TODO: Have this (and all other implementations of finalize() do something like return - * the IDs of titles that were removed, or otherwise populate an internal variable so that afterwards - * the list can be used to purge all images or other title related files (like overviews with AM). - * Right now only the data portion of old games is removed) - */ - - // Finalize item stores - finalizeUpdateableItems(mGamesExisting, mGamesFinal); - finalizeUpdateableItems(mAddAppsExisting, mAddAppsFinal); - - // Perform base finalization - UpdateableDoc::finalize(); - } -} - -//=============================================================================================================== -// BasicPlatformDoc::Reader -//=============================================================================================================== - -//-Constructor-------------------------------------------------------------------------------------------------------- -//Protected: -BasicPlatformDoc::Reader::Reader(DataDoc* targetDoc) : - PlatformDoc::Reader(targetDoc) -{} - -//-Instance Functions-------------------------------------------------------------------------------------------------- -//Protected: -/* TODO: Consider removing the following and similar, and just making public getters for existing items. - * Right now this is considered to break encapsulation too much, but it might not be that big of a deal - * and would be cleaner from a usability standpoint that doing this - */ -QHash>& BasicPlatformDoc::Reader::targetDocExistingGames() -{ - return static_cast(mTargetDocument)->mGamesExisting; -} - -QHash>& BasicPlatformDoc::Reader::targetDocExistingAddApps() -{ - return static_cast(mTargetDocument)->mAddAppsExisting; -} - -//=============================================================================================================== -// BasicPlatformDoc::Writer -//=============================================================================================================== - -//-Constructor-------------------------------------------------------------------------------------------------------- -//Protected: -BasicPlatformDoc::Writer::Writer(DataDoc* sourceDoc) : - PlatformDoc::Writer(sourceDoc) -{} - -//=============================================================================================================== -// PlaylistDoc -//=============================================================================================================== - -//-Constructor-------------------------------------------------------------------------------------------------------- -//Public: -PlaylistDoc::PlaylistDoc(Install* const parent, const QString& docPath, QString docName, const Import::UpdateOptions& updateOptions) : - UpdateableDoc(parent, docPath, docName, updateOptions) -{} - -//-Instance Functions-------------------------------------------------------------------------------------------------- -//Private: -DataDoc::Type PlaylistDoc::type() const { return Type::Playlist; } - -//=============================================================================================================== -// PlaylistDoc::Reader -//=============================================================================================================== - -//-Constructor-------------------------------------------------------------------------------------------------------- -//Protected: -PlaylistDoc::Reader::Reader(DataDoc* targetDoc) : - DataDoc::Reader(targetDoc) -{} - -//=============================================================================================================== -// PlaylistDoc::Writer -//=============================================================================================================== - -//-Constructor-------------------------------------------------------------------------------------------------------- -//Protected: -PlaylistDoc::Writer::Writer(DataDoc* sourceDoc) : - DataDoc::Writer(sourceDoc) -{} - -//=============================================================================================================== -// BasicPlaylistDoc -//=============================================================================================================== - -//-Constructor-------------------------------------------------------------------------------------------------------- -//Public: -/* NOTE: Right now mPlaylistHeader is left uninitialized (unless done so explicitly by a derivative). This is fine, - * as currently 'void PlaylistDoc::setPlaylistHeader(Fp::Playlist playlist)' checks to see if an existing header - * is present before performing a field transfer (i.e. in case the playlist doc didn't already exist); however, - * if more parts of the process end up needing to interact with a doc that has a potentially null playlist header, - * it may be better to require a value for it in this base class' constructor so that all derivatives must provide - * a default (likely null/empty) playlist header. - */ -BasicPlaylistDoc::BasicPlaylistDoc(Install* const parent, const QString& docPath, QString docName, const Import::UpdateOptions& updateOptions) : - PlaylistDoc(parent, docPath, docName, updateOptions) -{} - -//-Instance Functions-------------------------------------------------------------------------------------------------- -//Public: -bool BasicPlaylistDoc::isEmpty() const -{ - // The playlist header doesn't matter if there are no games - return mPlaylistGamesFinal.isEmpty() && mPlaylistGamesExisting.isEmpty(); -} - -const std::shared_ptr& BasicPlaylistDoc::playlistHeader() const { return mPlaylistHeader; } -const QHash>& BasicPlaylistDoc::finalPlaylistGames() const { return mPlaylistGamesFinal; } - -bool BasicPlaylistDoc::containsPlaylistGame(QUuid gameId) const { return mPlaylistGamesFinal.contains(gameId) || mPlaylistGamesExisting.contains(gameId); } - - -void BasicPlaylistDoc::setPlaylistData(const Fp::Playlist& playlist) -{ - if(!mError.isValid()) - { - std::shared_ptr lrPlaylistHeader = preparePlaylistHeader(playlist); - - // Ensure doc already existed before transferring (null check) - if(mPlaylistHeader) - lrPlaylistHeader->transferOtherFields(mPlaylistHeader->otherFields()); - - // Set instance header to new one - mPlaylistHeader = lrPlaylistHeader; - - for(const auto& plg : playlist.playlistGames()) - { - // Prepare playlist game - std::shared_ptr lrPlaylistGame = preparePlaylistGame(plg); - - // Add playlist game - addUpdateableItem(mPlaylistGamesExisting, mPlaylistGamesFinal, lrPlaylistGame); - } - } -} - -void BasicPlaylistDoc::finalize() -{ - if(!mError.isValid()) - { - // Finalize item stores - finalizeUpdateableItems(mPlaylistGamesExisting, mPlaylistGamesFinal); - - // Perform base finalization - UpdateableDoc::finalize(); - } -} - -//=============================================================================================================== -// BasicPlaylistDoc::Reader -//=============================================================================================================== - -//-Constructor-------------------------------------------------------------------------------------------------------- -//Protected: -BasicPlaylistDoc::Reader::Reader(DataDoc* targetDoc) : - PlaylistDoc::Reader(targetDoc) -{} - -//-Instance Functions-------------------------------------------------------------------------------------------------- -//Protected: -QHash>& BasicPlaylistDoc::Reader::targetDocExistingPlaylistGames() -{ - return static_cast(mTargetDocument)->mPlaylistGamesExisting; -} - -std::shared_ptr& BasicPlaylistDoc::Reader::targetDocPlaylistHeader() -{ - return static_cast(mTargetDocument)->mPlaylistHeader; -} - -//=============================================================================================================== -// BasicPlaylistDoc::Writer -//=============================================================================================================== - -//-Constructor-------------------------------------------------------------------------------------------------------- -//Protected: -BasicPlaylistDoc::Writer::Writer(DataDoc* sourceDoc) : - PlaylistDoc::Writer(sourceDoc) -{} - -//=============================================================================================================== -// XmlDocReader -//=============================================================================================================== - -//-Constructor-------------------------------------------------------------------------------------------------------- -//Public: -XmlDocReader::XmlDocReader(DataDoc* targetDoc, const QString& root) : - DataDoc::Reader(targetDoc), - mXmlFile(targetDoc->path()), - mStreamReader(&mXmlFile), - mRootElement(root) -{} - -//-Instance Functions------------------------------------------------------------------------------------------------- -//Protected: -DocHandlingError XmlDocReader::streamStatus() const -{ - if(mStreamReader.hasError()) - { - Qx::XmlStreamReaderError xmlError(mStreamReader); - return DocHandlingError(*mTargetDocument, DocHandlingError::DocReadFailed, xmlError.text()); - } - - return DocHandlingError(); -} - -//Public: -DocHandlingError XmlDocReader::readInto() -{ - // Open File - if(!mXmlFile.open(QFile::ReadOnly)) - return DocHandlingError(*mTargetDocument, DocHandlingError::DocCantOpen, mXmlFile.errorString()); - - if(!mStreamReader.readNextStartElement()) - { - Qx::XmlStreamReaderError xmlError(mStreamReader); - return DocHandlingError(*mTargetDocument, DocHandlingError::DocReadFailed, xmlError.text()); - } - - if(mStreamReader.name() != mRootElement) - return DocHandlingError(*mTargetDocument, DocHandlingError::NotParentDoc); - - return readTargetDoc(); - - // File is automatically closed when reader is destroyed... -} - -//=============================================================================================================== -// XmlDocWriter -//=============================================================================================================== - -//-Constructor-------------------------------------------------------------------------------------------------------- -//Public: -XmlDocWriter::XmlDocWriter(DataDoc* sourceDoc, const QString& root) : - DataDoc::Writer(sourceDoc), - mXmlFile(sourceDoc->path()), - mStreamWriter(&mXmlFile), - mRootElement(root) -{} - -//-Instance Functions------------------------------------------------------------------------------------------------- -//Protected: -void XmlDocWriter::writeCleanTextElement(const QString& qualifiedName, const QString& text) -{ - if(text.isEmpty()) - mStreamWriter.writeEmptyElement(qualifiedName); - else - mStreamWriter.writeTextElement(qualifiedName, Qx::xmlSanitized(text)); -} - -void XmlDocWriter::writeOtherFields(const QHash& otherFields) -{ - for(QHash::const_iterator i = otherFields.constBegin(); i != otherFields.constEnd(); ++i) - writeCleanTextElement(i.key(), i.value()); -} - -DocHandlingError XmlDocWriter::streamStatus() const -{ - return mStreamWriter.hasError() ? DocHandlingError(*mSourceDocument, DocHandlingError::DocWriteFailed, mStreamWriter.device()->errorString()) : - DocHandlingError(); -} - -//Public: -DocHandlingError XmlDocWriter::writeOutOf() -{ - // Open File - if(!mXmlFile.open(QFile::WriteOnly | QFile::Truncate)) // Discard previous contents - return DocHandlingError(*mSourceDocument, DocHandlingError::DocCantSave, mXmlFile.errorString()); - - // Enable auto formatting - mStreamWriter.setAutoFormatting(true); - mStreamWriter.setAutoFormattingIndent(2); - - // Write standard XML header - mStreamWriter.writeStartDocument(u"1.0"_s, true); - - // Write main LaunchBox tag - mStreamWriter.writeStartElement(mRootElement); - - // Write main body - if(!writeSourceDoc()) - return streamStatus(); - - // Close main LaunchBox tag - mStreamWriter.writeEndElement(); - - // Finish document - mStreamWriter.writeEndDocument(); - - // Return null string on success - return streamStatus(); - - // File is automatically closed when writer is destroyed... -} - -} - diff --git a/app/src/launcher/lr-data.h b/app/src/launcher/lr-data.h deleted file mode 100644 index 0bf36d2..0000000 --- a/app/src/launcher/lr-data.h +++ /dev/null @@ -1,608 +0,0 @@ -#ifndef LR_DATA -#define LR_DATA - -// Standard Library Includes -#include -#include - -// Qt Includes -#include -#include - -// Qx Includes -#include -#include -#include -#include - -// libfp Includes -#include - -// Project Includes -#include "launcher/lr-items.h" -#include "import/settings.h" - -/* TODO: Right now all docs that need to be constructed by an install have that install marked as their friend, - * but they also are using the Passkey Idiom, a key class with a private constructor that they are also friends - * with, which is is redundant for the purposes of construction. First see if the docs really need to be friends - * with the Installs (I think they do for the parent() Install pointer to be used as it is). Then, if they do, - * the only reason the Passkey Idiom is also being used is because these docs are constructed using - * std::make_shared<>(); even if the doc itself has the Install marked as a friend, it doesnt have - * the function std::make_shared() marked as a friend, so it can't be constructed that way. Because - * of this the Install constructor has to be public and the idiom used. So, double check the minor differences - * between constructing an instance on the heap and then creating a smart pointer with the regular pointer vs. - * using std::make_shared<>(), and see if allowing for its use when creating the docs is really worth also - * having to do Passkey. - */ - -namespace Lr -{ -//-Concepts------------------------------------------------------------------------------------------------------ -template -concept raw_item = std::derived_from; - -template -concept shared_item = Qx::specializes && std::derived_from; - -template -concept raw_basic_item = std::derived_from; - -template -concept shared_basic_item = Qx::specializes && std::derived_from; - -template -concept item = raw_item || shared_item; - -template -concept basic_item = raw_basic_item || shared_basic_item; - -template -concept updateable_item_container = Qx::qassociative && item; - -template -concept updateable_basicitem_container = Qx::qassociative && basic_item && - std::same_as; - -//-External Reference-------------------------------------------------------------------------------------------- -class Install; -class DataDoc; - -//-Classes----------------------------------------------------------------------------------------------------------- -class QX_ERROR_TYPE(DocHandlingError, "Lr::DocHandlingError", 1310) -{ -//-Class Enums------------------------------------------------------------- -public: - enum Type - { - NoError = 0, - DocAlreadyOpen = 1, - DocCantOpen = 2, - DocCantSave = 3, - NotParentDoc = 4, - CantRemoveBackup = 5, - CantCreateBackup = 6, - DocInvalidType = 7, - DocReadFailed = 8, - DocWriteFailed = 9 - }; - -//-Class Variables------------------------------------------------------------- -private: - // Message Macros - static inline const QString M_DOC_TYPE = u""_s; - static inline const QString M_DOC_NAME = u""_s; - static inline const QString M_DOC_PARENT = u""_s; - - static inline const QHash ERR_STRINGS{ - {NoError, u""_s}, - {DocAlreadyOpen, u"The target document ("_s + M_DOC_TYPE + u" | "_s + M_DOC_NAME + u") is already open."_s}, - {DocCantOpen, u"The target document ("_s + M_DOC_TYPE + u" | "_s + M_DOC_NAME + u") cannot be opened."_s}, - {DocCantSave, u"The target document ("_s + M_DOC_TYPE + u" | "_s + M_DOC_NAME + u") cannot be saved."_s}, - {NotParentDoc, u"The target document ("_s + M_DOC_TYPE + u" | "_s + M_DOC_NAME + u") is not a"_s + M_DOC_PARENT + u"document."_s}, - {CantRemoveBackup, u"The existing backup of the target document ("_s + M_DOC_TYPE + u" | "_s + M_DOC_NAME + u") could not be removed."_s}, - {CantCreateBackup, u"Could not create a backup of the target document ("_s + M_DOC_TYPE + u" | "_s + M_DOC_NAME + u")."_s}, - {DocInvalidType, u"The document ("_s + M_DOC_TYPE + u" | "_s + M_DOC_NAME + u") is invalid or of the wrong type."_s}, - {DocReadFailed, u"Reading the target document ("_s + M_DOC_TYPE + u" | "_s + M_DOC_NAME + u") failed."_s}, - {DocWriteFailed, u"Writing to the target document ("_s + M_DOC_TYPE + u" | "_s + M_DOC_NAME + u") failed."_s} - }; - -//-Instance Variables------------------------------------------------------------- -private: - Type mType; - QString mErrorStr; - QString mSpecific; - -//-Constructor------------------------------------------------------------- -public: - DocHandlingError(); - DocHandlingError(const DataDoc& doc, Type t, const QString& s = {}); - -//-Class Functions------------------------------------------------------------- -private: - static QString generatePrimaryString(const DataDoc& doc, Type t); - -//-Instance Functions------------------------------------------------------------- -public: - bool isValid() const; - Type type() const; - - QString errorString() const; - QString specific() const; - -private: - Qx::Severity deriveSeverity() const override; - quint32 deriveValue() const override; - QString derivePrimary() const override; - QString deriveSecondary() const override; -}; - -class ImageSources -{ -//-Instance Members-------------------------------------------------------------------------------------------------- -private: - QString mLogoPath; - QString mScreenshotPath; - -//-Constructor-------------------------------------------------------------------------------------------------------- -public: - ImageSources(); - ImageSources(const QString& logoPath, const QString& screenshotPath); - -//-Instance Functions-------------------------------------------------------------------------------------------------- -public: - bool isNull() const; - QString logoPath() const; - QString screenshotPath() const; - void setLogoPath(const QString& path); - void setScreenshotPath(const QString& path); -}; - -class DataDoc -{ - /* TODO: Consider making this a template class where T is the type argument for the doc's parent, so that the - * parent() method can return the type directly, without a derived document needing to cast to it's parent's type - */ - -//-Class Enums--------------------------------------------------------------------------------------------------------- -public: - enum class Type {Platform, Playlist, Config}; - -//-Inner Classes---------------------------------------------------------------------------------------------------- -public: - class Reader; - class Writer; - - class Identifier - { - friend bool operator== (const Identifier& lhs, const Identifier& rhs) noexcept; - friend uint qHash(const Identifier& key, uint seed) noexcept; - - private: - Type mDocType; - QString mDocName; - - public: - Identifier(Type docType, QString docName); - - Type docType() const; - QString docTypeString() const; - QString docName() const; - }; - -//-Class Variables----------------------------------------------------------------------------------------------------- -private: - static inline const QHash TYPE_STRINGS = { - {Type::Platform, u"Platform"_s}, - {Type::Playlist, u"Playlist"_s}, - {Type::Config, u"Config"_s} - }; - -//-Instance Variables-------------------------------------------------------------------------------------------------- -protected: - Install* const mParent; - const QString mDocumentPath; - const QString mName; - -//-Constructor-------------------------------------------------------------------------------------------------------- -protected: - DataDoc(Install* const parent, const QString& docPath, QString docName); - -//-Destructor------------------------------------------------------------------------------------------------- -public: - virtual ~DataDoc(); - -//-Instance Functions-------------------------------------------------------------------------------------------------- -protected: - virtual Type type() const = 0; - -public: - Install* parent() const; - QString path() const; - Identifier identifier() const; - virtual bool isEmpty() const = 0; -}; -QX_SCOPED_ENUM_HASH_FUNC(DataDoc::Type); - -class DataDoc::Reader -{ -//-Instance Variables-------------------------------------------------------------------------------------------------- -protected: - DataDoc* mTargetDocument; - -//-Constructor-------------------------------------------------------------------------------------------------------- -protected: - Reader(DataDoc* targetDoc); - -//-Destructor------------------------------------------------------------------------------------------------- -public: - virtual ~Reader(); - -//-Instance Functions------------------------------------------------------------------------------------------------- -public: - virtual DocHandlingError readInto() = 0; -}; - -class DataDoc::Writer -{ -//-Instance Variables-------------------------------------------------------------------------------------------------- -protected: - DataDoc* mSourceDocument; - -//-Constructor-------------------------------------------------------------------------------------------------------- -protected: - Writer(DataDoc* sourceDoc); - -//-Destructor------------------------------------------------------------------------------------------------- -public: - virtual ~Writer(); - -//-Instance Functions------------------------------------------------------------------------------------------------- -public: - virtual DocHandlingError writeOutOf() = 0; -}; - -class Errorable -{ -//-Instance Variables-------------------------------------------------------------------------------------------------- -protected: - Qx::Error mError; - -//-Constructor-------------------------------------------------------------------------------------------------------- -protected: - Errorable(); - -//-Destructor------------------------------------------------------------------------------------------------- -public: - virtual ~Errorable(); - -//-Instance Functions-------------------------------------------------------------------------------------------------- -public: - bool hasError() const; - Qx::Error error() const; -}; - -class UpdateableDoc : public DataDoc -{ -//-Instance Variables-------------------------------------------------------------------------------------------------- -protected: - Import::UpdateOptions mUpdateOptions; - -//-Constructor-------------------------------------------------------------------------------------------------------- -protected: - explicit UpdateableDoc(Install* const parent, const QString& docPath, QString docName, const Import::UpdateOptions& updateOptions); - -//-Class Functions----------------------------------------------------------------------------------------------------- -template -T* itemPtr(T& item) { return &item; } - -template -T* itemPtr(std::shared_ptr item) { return item.get(); } - -//-Instance Functions-------------------------------------------------------------------------------------------------- -protected: - template - requires updateable_item_container - void finalizeUpdateableItems(C& existingItems, - C& finalItems) - { - // Copy items to final list if obsolete entries are to be kept - if(!mUpdateOptions.removeObsolete) - finalItems.insert(existingItems); - - // Clear existing lists - existingItems.clear(); - } - - template - requires updateable_item_container - void addUpdateableItem(C& existingItems, - C& finalItems, - typename C::key_type key, - typename C::mapped_type newItem) - { - // Check if item exists - if(existingItems.contains(key)) - { - // Replace if existing update is on, move existing otherwise - if(mUpdateOptions.importMode == Import::UpdateMode::NewAndExisting) - { - itemPtr(newItem)->transferOtherFields(itemPtr(existingItems[key])->otherFields()); - finalItems[key] = newItem; - existingItems.remove(key); - } - else - { - finalItems[key] = std::move(existingItems[key]); - existingItems.remove(key); - } - - } - else - finalItems[key] = newItem; - } - - template - requires updateable_basicitem_container - void addUpdateableItem(C& existingItems, - C& finalItems, - typename C::mapped_type newItem) - { - addUpdateableItem(existingItems, - finalItems, - std::static_pointer_cast(newItem)->id(), - newItem); - } - -public: - virtual void finalize(); -}; - -class PlatformDoc : public UpdateableDoc, public Errorable -{ -//-Inner Classes---------------------------------------------------------------------------------------------------- -public: - class Reader; - class Writer; - -//-Constructor-------------------------------------------------------------------------------------------------------- -protected: - explicit PlatformDoc(Install* const parent, const QString& docPath, QString docName, const Import::UpdateOptions& updateOptions); - -//-Instance Functions-------------------------------------------------------------------------------------------------- -private: - Type type() const override; - -public: - virtual bool containsGame(QUuid gameId) const = 0; - virtual bool containsAddApp(QUuid addAppId) const = 0; - - /* NOTE: The image paths provided here can be null (i.e. images unavailable). Handle accordingly in derived. - * Also in most cases, addSet should call parent()->processDirectGameImages(). - * - * TODO: The back and forth here between this and derived documents is a little silly. It mostly exists - * so that BasicPlatformDoc can call processDirectGameImages() directly; since installs need to always - * implement custom image processing logic anyway, this maybe should be changed so that BasicPlatformDoc - * has a pure virtual function similar to processDirectGameImages() with derived installs then implementing - * that if they need to use BasicPlatform doc. This would free Installs that do not use said class from having - * to re-implement that function at all (even if they have a close equivalent anyway). - */ - virtual void addSet(const Fp::Set& set, const ImageSources& images) = 0; -}; - -class PlatformDoc::Reader : public virtual DataDoc::Reader -{ -//-Constructor------------------------------------------------------------------------------------------------------- -protected: - Reader(DataDoc* targetDoc); -}; - -class PlatformDoc::Writer : public virtual DataDoc::Writer -{ -//-Constructor------------------------------------------------------------------------------------------------------- -protected: - Writer(DataDoc* sourceDoc); -}; - -class BasicPlatformDoc : public PlatformDoc -{ -//-Inner Classes---------------------------------------------------------------------------------------------------- -public: - class Reader; - class Writer; - -//-Instance Variables-------------------------------------------------------------------------------------------------- -protected: - QHash> mGamesFinal; - QHash> mGamesExisting; - QHash> mAddAppsFinal; - QHash> mAddAppsExisting; - -//-Constructor-------------------------------------------------------------------------------------------------------- -protected: - explicit BasicPlatformDoc(Install* const parent, const QString& docPath, QString docName, const Import::UpdateOptions& updateOptions); - -//-Instance Functions-------------------------------------------------------------------------------------------------- -protected: - virtual std::shared_ptr prepareGame(const Fp::Game& game, const ImageSources& images) = 0; - virtual std::shared_ptr prepareAddApp(const Fp::AddApp& game) = 0; - -public: - virtual bool isEmpty() const override; - - const QHash>& finalGames() const; - const QHash>& finalAddApps() const; - - bool containsGame(QUuid gameId) const override; // NOTE: UNUSED - bool containsAddApp(QUuid addAppId) const override; // NOTE: UNUSED - - void addSet(const Fp::Set& set, const ImageSources& images) override; - - void finalize() override; -}; - -class BasicPlatformDoc::Reader : public PlatformDoc::Reader -{ -//-Constructor------------------------------------------------------------------------------------------------------- -protected: - Reader(DataDoc* targetDoc); - -//-Instance Functions------------------------------------------------------------------------------------------------- -protected: - QHash>& targetDocExistingGames(); - QHash>& targetDocExistingAddApps(); -}; - -class BasicPlatformDoc::Writer : public PlatformDoc::Writer -{ -//-Constructor------------------------------------------------------------------------------------------------------- -protected: - Writer(DataDoc* sourceDoc); -}; - -class PlaylistDoc : public UpdateableDoc, public Errorable -{ -//-Inner Classes---------------------------------------------------------------------------------------------------- -public: - class Reader; - class Writer; - -//-Constructor-------------------------------------------------------------------------------------------------------- -protected: - explicit PlaylistDoc(Install* const parent, const QString& docPath, QString docName, const Import::UpdateOptions& updateOptions); - -//-Instance Functions-------------------------------------------------------------------------------------------------- -private: - Type type() const override; - -public: - virtual bool containsPlaylistGame(QUuid gameId) const = 0; // NOTE: UNUSED - - virtual void setPlaylistData(const Fp::Playlist& playlist) = 0; -}; - -class PlaylistDoc::Reader : public virtual DataDoc::Reader -{ -//-Constructor------------------------------------------------------------------------------------------------------- -protected: - Reader(DataDoc* targetDoc); -}; - -class PlaylistDoc::Writer : public virtual DataDoc::Writer -{ -//-Constructor------------------------------------------------------------------------------------------------------- -protected: - Writer(DataDoc* sourceDoc); -}; - -class BasicPlaylistDoc : public PlaylistDoc -{ -//-Inner Classes---------------------------------------------------------------------------------------------------- -public: - class Reader; - class Writer; - -//-Instance Variables-------------------------------------------------------------------------------------------------- -protected: - std::shared_ptr mPlaylistHeader; - QHash> mPlaylistGamesFinal; - QHash> mPlaylistGamesExisting; - -//-Constructor-------------------------------------------------------------------------------------------------------- -protected: - explicit BasicPlaylistDoc(Install* const parent, const QString& docPath, QString docName, const Import::UpdateOptions& updateOptions); - -//-Instance Functions-------------------------------------------------------------------------------------------------- -protected: - virtual std::shared_ptr preparePlaylistHeader(const Fp::Playlist& playlist) = 0; - virtual std::shared_ptr preparePlaylistGame(const Fp::PlaylistGame& game) = 0; - -public: - virtual bool isEmpty() const override; - - const std::shared_ptr& playlistHeader() const; - const QHash>& finalPlaylistGames() const; - - bool containsPlaylistGame(QUuid gameId) const override; - - void setPlaylistData(const Fp::Playlist& playlist) override; - - void finalize() override; -}; - -/* TODO: Consider making the existing items accessible through a public getter, or at least a function to add - * them through a public function (similar todo already exists). If this is done then these base readers and writers - * for specific docs can be removed since they only exist to define the "workaround" getters for existing items. - * - * This would mean that virtual inheritance wouldn't be required for the other readers/writers and greatly simplify - * things - */ -class BasicPlaylistDoc::Reader : public PlaylistDoc::Reader -{ -//-Constructor------------------------------------------------------------------------------------------------------- -protected: - Reader(DataDoc* targetDoc); - -//-Instance Functions------------------------------------------------------------------------------------------------- -protected: - QHash>& targetDocExistingPlaylistGames(); - std::shared_ptr& targetDocPlaylistHeader(); -}; - -class BasicPlaylistDoc::Writer : public PlaylistDoc::Writer -{ -//-Constructor------------------------------------------------------------------------------------------------------- -protected: - Writer(DataDoc* sourceDoc); -}; - -/* - * Not used by base implementation, but useful for multiple launchers - */ -class XmlDocReader : public virtual DataDoc::Reader -{ -//-Instance Variables-------------------------------------------------------------------------------------------------- -protected: - QFile mXmlFile; - QXmlStreamReader mStreamReader; - QString mRootElement; - -//-Constructor-------------------------------------------------------------------------------------------------------- -public: - XmlDocReader(DataDoc* targetDoc, const QString& root); - -//-Instance Functions------------------------------------------------------------------------------------------------- -private: - virtual DocHandlingError readTargetDoc() = 0; - -protected: - DocHandlingError streamStatus() const; - -public: - DocHandlingError readInto() override; -}; - -class XmlDocWriter : public virtual DataDoc::Writer -{ -//-Instance Variables-------------------------------------------------------------------------------------------------- -protected: - QFile mXmlFile; - QXmlStreamWriter mStreamWriter; - QString mRootElement; - -//-Constructor-------------------------------------------------------------------------------------------------------- -public: - XmlDocWriter(DataDoc* sourceDoc, const QString& root); - -//-Instance Functions------------------------------------------------------------------------------------------------- -protected: - virtual bool writeSourceDoc() = 0; - void writeCleanTextElement(const QString& qualifiedName, const QString& text); - void writeOtherFields(const QHash& otherFields); - DocHandlingError streamStatus() const; - -public: - DocHandlingError writeOutOf() override; -}; - -} -#endif // LR_DATA diff --git a/app/src/launcher/lr-install.cpp b/app/src/launcher/lr-install.cpp deleted file mode 100644 index 82c12d8..0000000 --- a/app/src/launcher/lr-install.cpp +++ /dev/null @@ -1,172 +0,0 @@ -// Unit Include -#include "lr-install.h" - -// Qt Includes -#include - -namespace Lr -{ - -//=============================================================================================================== -// Install -//=============================================================================================================== - -//-Constructor--------------------------------------------------------------------------------------------------- -Install::Install(const QString& installPath) : - InstallFoundation(installPath) -{} - -//-Class Functions-------------------------------------------------------------------------------------------- -//Public: -QMap& Install::registry() { static QMap registry; return registry; } - -void Install::registerInstall(const QString& name, const Entry& entry) { registry()[name] = entry; } - -std::unique_ptr Install::acquireMatch(const QString& installPath) -{ - // Check all installs against path and return match if found - QMap::const_iterator i; - - for(i = registry().constBegin(); i != registry().constEnd(); ++i) - { - Entry entry = i.value(); - std::unique_ptr possibleMatch = entry.factory->produce(installPath); - - if(possibleMatch->isValid()) - return possibleMatch; - } - - // Return nullptr on failure to find match - return nullptr; -} - -//-Instance Functions-------------------------------------------------------------------------------------------- -//Protected: -void Install::nullify() -{ - // Redundant with base version, but here to make it clear its part of the main Install interface - InstallFoundation::nullify(); -} - - -//Public: -void Install::softReset() -{ - // Redundant with base version, but here to make it clear its part of the main Install interface - InstallFoundation::softReset(); -} - -bool Install::supportsImageMode(Import::ImageMode imageMode) const { return preferredImageModeOrder().contains(imageMode); } - -QString Install::versionString() const { return u"Unknown Version"_s; } - -/* These functions can be overridden by children as needed. - * Work within them should be kept as minimal as possible since they are not accounted - * for by the import progress indicator. - */ -Qx::Error Install::preImport(const ImportDetails& details) -{ - mImportDetails = std::make_unique(details); - return Qx::Error(); -} - -Qx::Error Install::postImport() { return {}; } -Qx::Error Install::prePlatformsImport() { return {}; } -Qx::Error Install::postPlatformsImport() { return {}; } - -Qx::Error Install::preImageProcessing(QList& workerTransfers, const ImageSources& bulkSources) -{ - Q_UNUSED(workerTransfers); - Q_UNUSED(bulkSources); - return {}; -} - -Qx::Error Install::postImageProcessing() { return {}; } -Qx::Error Install::prePlaylistsImport() { return {}; } -Qx::Error Install::postPlaylistsImport() { return {}; } - -QString Install::translateDocName(const QString& originalName, DataDoc::Type type) const -{ - // Redundant with base version, but here to make it clear its part of the main Install interface - return InstallFoundation::translateDocName(originalName, type); -} - -DocHandlingError Install::checkoutPlatformDoc(std::unique_ptr& returnBuffer, const QString& name) -{ - // Translate to launcher doc name - QString translatedName = translateDocName(name, DataDoc::Type::Platform); - - // Get initialized blank doc and reader - std::shared_ptr docReader = preparePlatformDocCheckout(returnBuffer, translatedName); - - // Open document - DocHandlingError readErrorStatus = checkoutDataDocument(returnBuffer.get(), docReader); - - // Set return null on failure - if(readErrorStatus.isValid()) - returnBuffer.reset(); - - // Return status - return readErrorStatus; -} - -DocHandlingError Install::checkoutPlaylistDoc(std::unique_ptr& returnBuffer, const QString& name) -{ - // Translate to launcher doc name - QString translatedName = translateDocName(name, DataDoc::Type::Playlist); - - // Get initialized blank doc and reader - std::shared_ptr docReader = preparePlaylistDocCheckout(returnBuffer, translatedName); - - // Open document - DocHandlingError readErrorStatus = checkoutDataDocument(returnBuffer.get(), docReader); - - // Set return null on failure - if(readErrorStatus.isValid()) - returnBuffer.reset(); - - // Return status - return readErrorStatus; -} - -DocHandlingError Install::commitPlatformDoc(std::unique_ptr document) -{ - // Doc should belong to this install - assert(document->parent() == this); - - // Prepare writer - std::shared_ptr docWriter = preparePlatformDocCommit(document); - - // Write - DocHandlingError writeErrorStatus = commitDataDocument(document.get(), docWriter); - - // Ensure document is cleared - document.reset(); - - // Return write status and let document ptr auto delete - return writeErrorStatus; -} - -DocHandlingError Install::commitPlaylistDoc(std::unique_ptr document) -{ - // Doc should belong to this install - assert(document->parent() == this); - - // Prepare writer - std::shared_ptr docWriter = preparePlaylistDocCommit(document); - - // Write - DocHandlingError writeErrorStatus = commitDataDocument(document.get(), docWriter); - - // Ensure document is cleared - document.reset(); - - // Return write status and let document ptr auto delete - return writeErrorStatus; -} - -QString Install::platformCategoryIconPath() const { return QString(); } // Unsupported in default implementation -std::optional Install::platformIconsDirectory() const { return std::nullopt; } // Unsupported in default implementation -std::optional Install::playlistIconsDirectory() const { return std::nullopt; } // Unsupported in default implementation - -} diff --git a/app/src/launcher/lr-install.h b/app/src/launcher/lr-install.h deleted file mode 100644 index 8e5226f..0000000 --- a/app/src/launcher/lr-install.h +++ /dev/null @@ -1,112 +0,0 @@ -#ifndef LR_INSTALL_H -#define LR_INSTALL_H - -// Qt Includes -#include - -// Project Includes -#include "launcher/lr-installfoundation.h" - -//-Macros------------------------------------------------------------------------------------------------------------------- -#define REGISTER_LAUNCHER(lr_name, lr_install, lr_icon_path, lr_helpUrl) \ - class lr_install##Factory : public Lr::InstallFactory \ - { \ - public: \ - lr_install##Factory() \ - { \ - Install::Entry entry { \ - .factory = this, \ - .iconPath = lr_icon_path, \ - .helpUrl = lr_helpUrl \ - }; \ - Lr::Install::registerInstall(lr_name, entry); \ - } \ - virtual std::unique_ptr produce(const QString& installPath) const { return std::make_unique(installPath); } \ - }; \ - static lr_install##Factory _##install##Factory; - -namespace Lr -{ - -class InstallFactory -{ -//-Instance Functions------------------------------------------------------------------------------------------------------ -public: - virtual std::unique_ptr produce(const QString& installPath) const = 0; -}; - -class Install : public InstallFoundation -{ -//-Structs------------------------------------------------------------------------------------------------------ -public: - struct Entry - { - const InstallFactory* factory; - const QString* iconPath; - const QUrl* helpUrl; - }; - -//-Constructor--------------------------------------------------------------------------------------------------- -public: - Install(const QString& installPath); - -//-Class Functions------------------------------------------------------------------------------------------------------ -public: - // NOTE: Registry put behind function call to avoid SIOF since otherwise initialization of static registry before calls to registerLauncher would not be guaranteed - static QMap& registry(); - static void registerInstall(const QString& name, const Entry& entry); - [[nodiscard]] static std::unique_ptr acquireMatch(const QString& installPath); - -//-Instance Functions--------------------------------------------------------------------------------------------------------- -protected: - // Install management - virtual void nullify() override; - virtual Qx::Error populateExistingDocs() override = 0; - - // Doc Handling - virtual std::shared_ptr preparePlatformDocCheckout(std::unique_ptr& platformDoc, const QString& translatedName) = 0; - virtual std::shared_ptr preparePlaylistDocCheckout(std::unique_ptr& playlistDoc, const QString& translatedName) = 0; - virtual std::shared_ptr preparePlatformDocCommit(const std::unique_ptr& document) = 0; - virtual std::shared_ptr preparePlaylistDocCommit(const std::unique_ptr& document) = 0; - -public: - // Install management - virtual void softReset() override; - - // Info - virtual QString name() const = 0; - virtual QList preferredImageModeOrder() const = 0; - bool supportsImageMode(Import::ImageMode imageMode) const; - virtual QString versionString() const; - virtual bool isRunning() const = 0; - - // Import stage notifier hooks - virtual Qx::Error preImport(const ImportDetails& details); - virtual Qx::Error postImport(); - virtual Qx::Error prePlatformsImport(); - virtual Qx::Error postPlatformsImport(); - virtual Qx::Error preImageProcessing(QList& workerTransfers, const ImageSources& bulkSources); - virtual Qx::Error postImageProcessing(); - virtual Qx::Error prePlaylistsImport(); - virtual Qx::Error postPlaylistsImport(); - - // Doc handling - virtual QString translateDocName(const QString& originalName, DataDoc::Type type) const override; - DocHandlingError checkoutPlatformDoc(std::unique_ptr& returnBuffer, const QString& name); - DocHandlingError checkoutPlaylistDoc(std::unique_ptr& returnBuffer, const QString& name); - DocHandlingError commitPlatformDoc(std::unique_ptr platformDoc); - DocHandlingError commitPlaylistDoc(std::unique_ptr playlistDoc); - - // Image handling - // NOTE: The image paths provided here can be null (i.e. images unavailable). Handle accordingly in derived. - virtual void processDirectGameImages(const Game* game, const ImageSources& imageSources) = 0; - - // TODO: These might need to be changed to support launchers where the platform images are tied closely to the platform documents, - // but currently none do this so this works. - virtual QString platformCategoryIconPath() const; // Unsupported in default implementation, needs to return path with .png extension - virtual std::optional platformIconsDirectory() const; // Unsupported in default implementation - virtual std::optional playlistIconsDirectory() const; // Unsupported in default implementation -}; - -} -#endif // LR_INSTALL_H diff --git a/app/src/ui/mainwindow.cpp b/app/src/ui/mainwindow.cpp index 8e405a8..e89c045 100644 --- a/app/src/ui/mainwindow.cpp +++ b/app/src/ui/mainwindow.cpp @@ -24,7 +24,7 @@ // Project Includes #include "import/properties.h" -#include "launcher/lr-install.h" +#include "launcher/abstract/lr-registration.h" #include "kernel/clifp.h" //=============================================================================================================== @@ -307,15 +307,13 @@ void MainWindow::initializeBindings() void MainWindow::initializeLauncherHelpActions() { // Add install help link for each registered install - auto i = Lr::Install::registry().cbegin(); - auto end = Lr::Install::registry().cend(); - - for(; i != end; i++) + for(auto eItr = Lr::Registry::entries(); eItr.hasNext();) { + auto e = eItr.next(); QAction* lrHelpAction = new QAction(ui->menu_launcherHelp); - lrHelpAction->setObjectName(MENU_LR_HELP_OBJ_NAME_TEMPLATE.arg(i.key())); - lrHelpAction->setText(i.key()); - lrHelpAction->setIcon(QIcon(*(i->iconPath))); + lrHelpAction->setObjectName(MENU_LR_HELP_OBJ_NAME_TEMPLATE.arg(e.key())); + lrHelpAction->setText(e.key().toString()); + lrHelpAction->setIcon(QIcon(e.value().iconPath.toString())); ui->menu_launcherHelp->addAction(lrHelpAction); } } @@ -581,10 +579,10 @@ void MainWindow::all_on_menu_triggered(QAction *action) if(launcherMatch.hasMatch()) { QString launcherName = launcherMatch.captured(u"launcher"_s); - if(!launcherName.isNull() && Lr::Install::registry().contains(launcherName)) + QUrl helpUrl = Lr::Registry::helpUrl(launcherName); + if(helpUrl.isValid()) { - const QUrl* helpUrl = Lr::Install::registry()[launcherName].helpUrl; - QDesktopServices::openUrl(*helpUrl); + QDesktopServices::openUrl(helpUrl); return; } } From da00a74b502be67154d4c41b51493562af7017fd Mon Sep 17 00:00:00 2001 From: Christian Heimlich Date: Fri, 7 Feb 2025 10:30:37 -0500 Subject: [PATCH 04/20] Move import details to static accessor Unpolutes IInstall from stuff that's not directly related to it. --- app/CMakeLists.txt | 2 ++ app/src/import/details.cpp | 21 ++++++++++++ app/src/import/details.h | 34 +++++++++++++++++++ app/src/import/worker.cpp | 12 +++++-- app/src/import/worker.h | 4 +++ app/src/launcher/abstract/lr-data.tpp | 3 +- app/src/launcher/abstract/lr-install.tpp | 3 +- .../implementation/attractmode/am-install.cpp | 10 +++--- .../implementation/attractmode/am-install.h | 2 +- .../implementation/launchbox/lb-data.cpp | 5 +-- .../implementation/launchbox/lb-install.cpp | 9 +++-- .../interface/lr-install-interface.cpp | 15 +------- .../launcher/interface/lr-install-interface.h | 17 +--------- 13 files changed, 91 insertions(+), 46 deletions(-) create mode 100644 app/src/import/details.cpp create mode 100644 app/src/import/details.h diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index c10bbe8..d401263 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -23,6 +23,8 @@ add_custom_target(fil_copy_clifp # ------------------ Setup FIL -------------------------- set(FIL_SOURCE + import/details.h + import/details.cpp import/properties.h import/properties.cpp import/settings.h diff --git a/app/src/import/details.cpp b/app/src/import/details.cpp new file mode 100644 index 0000000..e654302 --- /dev/null +++ b/app/src/import/details.cpp @@ -0,0 +1,21 @@ +#include "details.h" + +namespace Import +{ + +//=============================================================================================================== +// Details +//=============================================================================================================== + +//-Class Variables-------------------------------------------------------------------------------------------- +//Private: +constinit std::optional
Details::mCurrent = std::nullopt; + +//-Class Functions-------------------------------------------------------------------------------------------- +//Public: +Details Details::current() { Q_ASSERT(mCurrent); return mCurrent.value(); } + +//Private: +void Details::setCurrent(const Details& details) { Q_ASSERT(!mCurrent); mCurrent = details; } +void Details::clearCurrent() { mCurrent = std::nullopt; } +} diff --git a/app/src/import/details.h b/app/src/import/details.h new file mode 100644 index 0000000..39e2231 --- /dev/null +++ b/app/src/import/details.h @@ -0,0 +1,34 @@ +#ifndef IMPORT_DETAILS_H +#define IMPORT_DETAILS_H + +/* Although somewhat redundant with settings.h, this is a collection of + * Import related info that is specifically collected to be shared with + * Installs + */ + +// Project Includes +#include "import/settings.h" + +namespace Import +{ + +struct Details +{ + friend class Worker; + Import::UpdateOptions updateOptions; + Import::ImageMode imageMode; + QString clifpPath; + QList involvedPlatforms; + QList involvedPlaylists; + + static Details current(); + +private: + static constinit std::optional
mCurrent; + static void setCurrent(const Details& details); + static void clearCurrent(); +}; + +} + +#endif // IMPORT_DETAILS_H diff --git a/app/src/import/worker.cpp b/app/src/import/worker.cpp index 5043ae8..22d518c 100644 --- a/app/src/import/worker.cpp +++ b/app/src/import/worker.cpp @@ -13,10 +13,10 @@ // Project Includes #include "kernel/clifp.h" +#include "import/details.h" namespace Import { - //=============================================================================================================== // ImageTransferError //=============================================================================================================== @@ -62,6 +62,7 @@ QString ImageTransferError::deriveCaption() const { return CAPTION_IMAGE_ERR; } //=============================================================================================================== //-Constructor--------------------------------------------------------------------------------------------------- +//Public: Worker::Worker(Fp::Install* flashpoint, Lr::IInstall* launcher, Selections importSelections, OptionSet optionSet) : mFlashpointInstall(flashpoint), mLauncherInstall(launcher), @@ -71,6 +72,10 @@ Worker::Worker(Fp::Install* flashpoint, Lr::IInstall* launcher, Selections impor mCanceled(false) {} +//-Destructor--------------------------------------------------------------------------------------------------- +//Public: +Worker::~Worker() { Details::clearCurrent(); } + //-Instance Functions-------------------------------------------------------------------------------------------- //Private: Qx::ProgressGroup* Worker::initializeProgressGroup(const QString& groupName, quint64 weight) @@ -842,15 +847,16 @@ Worker::Result Worker::doImport(Qx::Error& errorReport) connect(&mProgressManager, &Qx::GroupedProgressManager::progressUpdated, this, &Worker::pmProgressUpdated); //-Handle Launcher Specific Import Setup------------------------------ - Lr::IInstall::ImportDetails details{ + Details details{ .updateOptions = mOptionSet.updateOptions, .imageMode = mOptionSet.imageMode, .clifpPath = CLIFp::standardCLIFpPath(*mFlashpointInstall), .involvedPlatforms = involvedPlatforms, .involvedPlaylists = mImportSelections.playlists }; + Details::setCurrent(details); - errorReport = mLauncherInstall->preImport(details); + errorReport = mLauncherInstall->preImport(); if(errorReport.isValid()) return Failed; diff --git a/app/src/import/worker.h b/app/src/import/worker.h index 96369d2..b46143a 100644 --- a/app/src/import/worker.h +++ b/app/src/import/worker.h @@ -136,6 +136,10 @@ class Worker : public QObject public: Worker(Fp::Install* flashpoint, Lr::IInstall* launcher, Selections importSelections, OptionSet optionSet); +//-Destructor--------------------------------------------------------------------------------------------------- +public: + ~Worker(); + //-Instance Functions--------------------------------------------------------------------------------------------------------- private: Qx::ProgressGroup* initializeProgressGroup(const QString& groupName, quint64 weight); diff --git a/app/src/launcher/abstract/lr-data.tpp b/app/src/launcher/abstract/lr-data.tpp index d3ae6db..53b3ccb 100644 --- a/app/src/launcher/abstract/lr-data.tpp +++ b/app/src/launcher/abstract/lr-data.tpp @@ -8,6 +8,7 @@ #include // Project Includes +#include "import/details.h" #include "launcher/abstract/lr-install.h" namespace Lr @@ -107,7 +108,7 @@ void PlatformDoc::addSet(const Fp::Set& set, ImagePaths& images) * the abstract base type. */ auto install = static_cast*>(IPlatformDoc::install()); - if(game && install->importDetails().imageMode != Import::ImageMode::Reference) + if(game && Import::Details::current().imageMode != Import::ImageMode::Reference) install->convertToDestinationImages(*game, images); } } diff --git a/app/src/launcher/abstract/lr-install.tpp b/app/src/launcher/abstract/lr-install.tpp index d01ff22..266b4a6 100644 --- a/app/src/launcher/abstract/lr-install.tpp +++ b/app/src/launcher/abstract/lr-install.tpp @@ -1,8 +1,7 @@ #ifndef LR_INSTALL_TPP #define LR_INSTALL_TPP -// Helpful for previewing code in IDE, but needs to be commented out when compiling -//#include "lr-install.h" +#include "lr-install.h" // Can ignore recursion warning // Qx Includes #include diff --git a/app/src/launcher/implementation/attractmode/am-install.cpp b/app/src/launcher/implementation/attractmode/am-install.cpp index 3faf0a5..eebd324 100644 --- a/app/src/launcher/implementation/attractmode/am-install.cpp +++ b/app/src/launcher/implementation/attractmode/am-install.cpp @@ -10,6 +10,7 @@ // Project Includes #include "kernel/clifp.h" +#include "import/details.h" namespace Am { @@ -169,7 +170,7 @@ Lr::DocHandlingError Install::checkoutMainConfig(std::unique_ptr& Lr::DocHandlingError Install::checkoutFlashpointRomlist(std::unique_ptr& returnBuffer) { // Construct unopened document - returnBuffer = std::make_unique(this, mFpRomlist.fileName(), Fp::NAME, importDetails().updateOptions); + returnBuffer = std::make_unique(this, mFpRomlist.fileName(), Fp::NAME, Import::Details::current().updateOptions); // Construct doc reader std::shared_ptr docReader = std::make_shared(returnBuffer.get()); @@ -315,7 +316,7 @@ QString Install::translateDocName(const QString& originalName, Lr::IDataDoc::Typ return translatedName; } -Qx::Error Install::preImport(const ImportDetails& details) +Qx::Error Install::preImport() { //-Ensure that required directories exist---------------------------------------------------------------- @@ -331,6 +332,7 @@ Qx::Error Install::preImport(const ImportDetails& details) return Qx::IoOpReport(Qx::IO_OP_WRITE, Qx::IO_ERR_CANT_CREATE, overviewDir); // Logo and screenshot dir + auto details = Import::Details::current(); if(details.imageMode == Import::ImageMode::Copy || details.imageMode == Import::ImageMode::Link) { QDir logoDir(mFpScraperDirectory.absoluteFilePath(LOGO_FOLDER_NAME)); @@ -345,7 +347,7 @@ Qx::Error Install::preImport(const ImportDetails& details) } // Perform base tasks - return Lr::IInstall::preImport(details); + return Lr::IInstall::preImport(); } Qx::Error Install::prePlatformsImport() @@ -382,7 +384,7 @@ Qx::Error Install::postImport() return emulatorConfigReadError; // General emulator setup - QString workingDir = QDir::toNativeSeparators(QFileInfo(importDetails().clifpPath).absolutePath()); + QString workingDir = QDir::toNativeSeparators(QFileInfo(Import::Details::current().clifpPath).absolutePath()); emulatorConfig->setExecutable(CLIFp::EXE_NAME); emulatorConfig->setArgs(uR"(play -i u"[romfilename]"_s)"_s); emulatorConfig->setWorkDir(workingDir); diff --git a/app/src/launcher/implementation/attractmode/am-install.h b/app/src/launcher/implementation/attractmode/am-install.h index 88a0996..2a13a88 100644 --- a/app/src/launcher/implementation/attractmode/am-install.h +++ b/app/src/launcher/implementation/attractmode/am-install.h @@ -113,7 +113,7 @@ class Install : public Lr::Install QString translateDocName(const QString& originalName, Lr::IDataDoc::Type type) const override; // Import stage notifier hooks - Qx::Error preImport(const ImportDetails& details) override; + Qx::Error preImport() override; Qx::Error prePlatformsImport() override; Qx::Error postPlatformsImport() override; Qx::Error preImageProcessing(const Lr::ImagePaths& bulkSources) override; diff --git a/app/src/launcher/implementation/launchbox/lb-data.cpp b/app/src/launcher/implementation/launchbox/lb-data.cpp index 735f42f..6b50488 100644 --- a/app/src/launcher/implementation/launchbox/lb-data.cpp +++ b/app/src/launcher/implementation/launchbox/lb-data.cpp @@ -5,6 +5,7 @@ #include // Project Includes +#include "import/details.h" #include "launcher/implementation/launchbox/lb-install.h" namespace Xml @@ -135,7 +136,7 @@ PlatformDoc::PlatformDoc(Install* install, const QString& xmlPath, QString docNa std::shared_ptr PlatformDoc::prepareGame(const Fp::Game& game) { // Convert to LaunchBox game - const QString& clifpPath = install()->importDetails().clifpPath; + const QString& clifpPath = Import::Details::current().clifpPath; std::shared_ptr lbGame = std::make_shared(game, clifpPath); // Add details to cache @@ -155,7 +156,7 @@ std::shared_ptr PlatformDoc::prepareGame(const Fp::Game& game) std::shared_ptr PlatformDoc::prepareAddApp(const Fp::AddApp& addApp) { // Convert to LaunchBox add app - const QString& clifpPath = install()->importDetails().clifpPath; + const QString& clifpPath = Import::Details::current().clifpPath; std::shared_ptr lbAddApp = std::make_shared(addApp, clifpPath); // Return converted game diff --git a/app/src/launcher/implementation/launchbox/lb-install.cpp b/app/src/launcher/implementation/launchbox/lb-install.cpp index 86618aa..6cba5f4 100644 --- a/app/src/launcher/implementation/launchbox/lb-install.cpp +++ b/app/src/launcher/implementation/launchbox/lb-install.cpp @@ -15,6 +15,9 @@ #include #include +// Project Includes +#include "import/details.h" + namespace Lb { //=============================================================================================================== @@ -158,7 +161,7 @@ std::unique_ptr Install::preparePlatformDocCheckout(const QString& Lr::IDataDoc::Identifier docId(Lr::IDataDoc::Type::Platform, translatedName); // Construct unopened document and return - return std::make_unique(this, dataDocPath(docId), translatedName, importDetails().updateOptions); + return std::make_unique(this, dataDocPath(docId), translatedName, Import::Details::current().updateOptions); } std::unique_ptr Install::preparePlaylistDocCheckout(const QString& translatedName) @@ -167,7 +170,7 @@ std::unique_ptr Install::preparePlaylistDocCheckout(const QString& Lr::IDataDoc::Identifier docId(Lr::IDataDoc::Type::Playlist, translatedName); // Construct unopened document - return std::make_unique(this, dataDocPath(docId), translatedName, importDetails().updateOptions); + return std::make_unique(this, dataDocPath(docId), translatedName, Import::Details::current().updateOptions); } Lr::DocHandlingError Install::checkoutPlatformsConfigDoc(std::unique_ptr& returnBuffer) @@ -389,7 +392,7 @@ Qx::Error Install::preImageProcessing(const Lr::ImagePaths& bulkSources) //TODO Deal with the fact that when we drop bulk sources we need to still have the part of editBulkImageRefernces happen that purges old references - switch(importDetails().imageMode) + switch(Import::Details::current().imageMode) { case Import::ImageMode::Link: case Import::ImageMode::Copy: diff --git a/app/src/launcher/interface/lr-install-interface.cpp b/app/src/launcher/interface/lr-install-interface.cpp index 588a9d5..72a1678 100644 --- a/app/src/launcher/interface/lr-install-interface.cpp +++ b/app/src/launcher/interface/lr-install-interface.cpp @@ -190,14 +190,6 @@ void IInstall::softReset() mModifiedDocuments.clear(); mDeletedDocuments.clear(); mLeasedDocuments.clear(); - mImportDetails.reset(); -} - -IInstall::ImportDetails IInstall::importDetails() const -{ - // We just assert here because no install should ever use this before it's available - Q_ASSERT(mImportDetails); - return mImportDetails.value(); } QString IInstall::translateDocName(const QString& originalName, IDataDoc::Type type) const @@ -277,12 +269,7 @@ int IInstall::revertNextChange(RevertError& error, bool skipOnFail) * Work within them should be kept as minimal as possible since they are not accounted * for by the import progress indicator. */ -Qx::Error IInstall::preImport(const ImportDetails& details) -{ - mImportDetails = details; - return Qx::Error(); -} - +Qx::Error IInstall::preImport() { return {}; } Qx::Error IInstall::postImport() { return {}; } Qx::Error IInstall::prePlatformsImport() { return {}; } Qx::Error IInstall::postPlatformsImport() { return {}; } diff --git a/app/src/launcher/interface/lr-install-interface.h b/app/src/launcher/interface/lr-install-interface.h index da9d41d..9516df3 100644 --- a/app/src/launcher/interface/lr-install-interface.h +++ b/app/src/launcher/interface/lr-install-interface.h @@ -63,15 +63,6 @@ class IInstall { //-Class Structs------------------------------------------------------------------------------------------------------ public: - struct ImportDetails - { - Import::UpdateOptions updateOptions; - Import::ImageMode imageMode; - QString clifpPath; - QList involvedPlatforms; - QList involvedPlaylists; - }; - struct ImageMap { QString sourcePath; @@ -112,9 +103,6 @@ class IInstall // Backup/Deletion tracking QStringList mRevertableFilePaths; - // Import details - std::optional mImportDetails; - //-Constructor--------------------------------------------------------------------------------------------------- public: IInstall(const QString& installPath); @@ -162,9 +150,6 @@ class IInstall QString path() const; virtual void softReset(); - // Import - ImportDetails importDetails() const; - // Docs virtual QString translateDocName(const QString& originalName, IDataDoc::Type type) const; Qx::Error refreshExistingDocs(bool* changed = nullptr); @@ -184,7 +169,7 @@ class IInstall int revertNextChange(RevertError& error, bool skipOnFail); // Import stage notifier hooks - virtual Qx::Error preImport(const ImportDetails& details); + virtual Qx::Error preImport(); virtual Qx::Error postImport(); virtual Qx::Error prePlatformsImport(); virtual Qx::Error postPlatformsImport(); From a50639060bc0ec916207e3b104a0ac8bf14aacdb Mon Sep 17 00:00:00 2001 From: Christian Heimlich Date: Fri, 7 Feb 2025 15:44:19 -0500 Subject: [PATCH 05/20] Change bulk source handoff to dedicated virtual function --- app/src/import/worker.cpp | 21 +++++++------- app/src/launcher/abstract/lr-install.h | 1 + .../implementation/attractmode/am-install.cpp | 12 ++++---- .../implementation/attractmode/am-install.h | 2 +- .../implementation/launchbox/lb-install.cpp | 28 ++++++------------- .../implementation/launchbox/lb-install.h | 3 +- .../launcher/interface/lr-data-interface.h | 2 +- .../interface/lr-install-interface.cpp | 8 +----- .../launcher/interface/lr-install-interface.h | 3 +- 9 files changed, 34 insertions(+), 46 deletions(-) diff --git a/app/src/import/worker.cpp b/app/src/import/worker.cpp index 22d518c..a57a4f1 100644 --- a/app/src/import/worker.cpp +++ b/app/src/import/worker.cpp @@ -573,16 +573,8 @@ Worker::Result Worker::processImages(Qx::Error& errorReport) // Update progress dialog label emit progressStepChanged(STEP_IMPORTING_IMAGES); - // Provide launcher with bulk reference locations and acquire any transfer tasks - Lr::ImagePaths bulkSources; - if(mOptionSet.imageMode == ImageMode::Reference) - { - bulkSources.setLogoPath(QDir::toNativeSeparators(mFlashpointInstall->entryLogosDirectory().absolutePath())); - bulkSources.setScreenshotPath(QDir::toNativeSeparators(mFlashpointInstall->entryScreenshotsDirectory().absolutePath())); - } - - Qx::Error imageExchangeError = mLauncherInstall->preImageProcessing(bulkSources); - + // Notify of step + Qx::Error imageExchangeError = mLauncherInstall->preImageProcessing(); if(imageExchangeError.isValid()) { // Emit import failure @@ -590,6 +582,15 @@ Worker::Result Worker::processImages(Qx::Error& errorReport) return Failed; } + // Provide launcher with bulk reference locations + if(mOptionSet.imageMode == ImageMode::Reference) + { + Lr::ImagePaths bulkSources(QDir::toNativeSeparators(mFlashpointInstall->entryLogosDirectory().absolutePath()), + QDir::toNativeSeparators(mFlashpointInstall->entryScreenshotsDirectory().absolutePath())); + + mLauncherInstall->processBulkImageSources(bulkSources); + } + // Perform transfers if required if(mOptionSet.imageMode == ImageMode::Copy || mOptionSet.imageMode == ImageMode::Link) { diff --git a/app/src/launcher/abstract/lr-install.h b/app/src/launcher/abstract/lr-install.h index 177c118..5b9426c 100644 --- a/app/src/launcher/abstract/lr-install.h +++ b/app/src/launcher/abstract/lr-install.h @@ -56,6 +56,7 @@ class Install : public IInstall // IMPLEMENT using IInstall::preferredImageModeOrder; using IInstall::isRunning; + using IInstall::processBulkImageSources; // Just do nothing if Reference mode isn't supported virtual void convertToDestinationImages(const GameT& game, ImagePaths& images) = 0; // NOTE: The image paths provided here can be null (i.e. images unavailable). // OPTIONALLY RE-IMPLEMENT diff --git a/app/src/launcher/implementation/attractmode/am-install.cpp b/app/src/launcher/implementation/attractmode/am-install.cpp index eebd324..ebbec15 100644 --- a/app/src/launcher/implementation/attractmode/am-install.cpp +++ b/app/src/launcher/implementation/attractmode/am-install.cpp @@ -365,12 +365,6 @@ Qx::Error Install::postPlatformsImport() return commitFlashpointRomlist(std::move(mRomlist)); } -Qx::Error Install::preImageProcessing(const Lr::ImagePaths& bulkSources) -{ - Q_UNUSED(bulkSources); - return {}; -} - Qx::Error Install::postImport() { //-Create/update emulator settings----------------------------------- @@ -504,6 +498,12 @@ Qx::Error Install::postImport() return Qx::Error(); } +void Install::processBulkImageSources(const Lr::ImagePaths& bulkSources) +{ + Q_UNUSED(bulkSources); + qFatal("Attract Mode does not support Reference image mode, and that option should not be available."); +} + void Install::convertToDestinationImages(const RomEntry& game, Lr::ImagePaths& images) { if(!images.logoPath().isEmpty()) diff --git a/app/src/launcher/implementation/attractmode/am-install.h b/app/src/launcher/implementation/attractmode/am-install.h index 2a13a88..3e2afd2 100644 --- a/app/src/launcher/implementation/attractmode/am-install.h +++ b/app/src/launcher/implementation/attractmode/am-install.h @@ -116,10 +116,10 @@ class Install : public Lr::Install Qx::Error preImport() override; Qx::Error prePlatformsImport() override; Qx::Error postPlatformsImport() override; - Qx::Error preImageProcessing(const Lr::ImagePaths& bulkSources) override; Qx::Error postImport() override; // Image handling + void processBulkImageSources(const Lr::ImagePaths& bulkSources) override; void convertToDestinationImages(const RomEntry& game, Lr::ImagePaths& images) override; }; diff --git a/app/src/launcher/implementation/launchbox/lb-install.cpp b/app/src/launcher/implementation/launchbox/lb-install.cpp index 6cba5f4..accca0f 100644 --- a/app/src/launcher/implementation/launchbox/lb-install.cpp +++ b/app/src/launcher/implementation/launchbox/lb-install.cpp @@ -385,27 +385,12 @@ Qx::Error Install::postPlatformsImport() return Qx::Error(); } -Qx::Error Install::preImageProcessing(const Lr::ImagePaths& bulkSources) +Qx::Error Install::preImageProcessing() { - if(Qx::Error superErr = Lr::IInstall::preImageProcessing(bulkSources); superErr.isValid()) - return superErr; - - //TODO Deal with the fact that when we drop bulk sources we need to still have the part of editBulkImageRefernces happen that purges old references - - switch(Import::Details::current().imageMode) - { - case Import::ImageMode::Link: - case Import::ImageMode::Copy: - editBulkImageReferences(bulkSources); - break; - case Import::ImageMode::Reference: - editBulkImageReferences(bulkSources); - break; - default: - qWarning("unhandled image mode"); - } + if(Import::Details::current().imageMode != Import::ImageMode::Reference) + editBulkImageReferences(Lr::ImagePaths());// Null arg will remove old references - return Qx::Error(); + return Lr::IInstall::preImageProcessing(); } Qx::Error Install::postImageProcessing() @@ -441,6 +426,11 @@ Qx::Error Install::postPlaylistsImport() return commitParentsDoc(std::move(mParents)); } +void Install::processBulkImageSources(const Lr::ImagePaths& bulkSources) +{ + editBulkImageReferences(bulkSources); +} + void Install::convertToDestinationImages(const Game& game, Lr::ImagePaths& images) { if(!images.logoPath().isEmpty()) diff --git a/app/src/launcher/implementation/launchbox/lb-install.h b/app/src/launcher/implementation/launchbox/lb-install.h index 215a9ff..e1ea9f7 100644 --- a/app/src/launcher/implementation/launchbox/lb-install.h +++ b/app/src/launcher/implementation/launchbox/lb-install.h @@ -114,11 +114,12 @@ class Install : public Lr::Install // Import stage notifier hooks Qx::Error prePlatformsImport() override; Qx::Error postPlatformsImport() override; - Qx::Error preImageProcessing(const Lr::ImagePaths& bulkSources) override; + Qx::Error preImageProcessing() override; Qx::Error postImageProcessing() override; Qx::Error postPlaylistsImport() override; // Image handling + void processBulkImageSources(const Lr::ImagePaths& bulkSources) override; void convertToDestinationImages(const Game& game, Lr::ImagePaths& images) override; QString platformCategoryIconPath() const override; std::optional platformIconsDirectory() const override; diff --git a/app/src/launcher/interface/lr-data-interface.h b/app/src/launcher/interface/lr-data-interface.h index 85dddc0..ec2a1fb 100644 --- a/app/src/launcher/interface/lr-data-interface.h +++ b/app/src/launcher/interface/lr-data-interface.h @@ -104,7 +104,7 @@ class QX_ERROR_TYPE(DocHandlingError, "Lr::DocHandlingError", 1310) QString deriveSecondary() const override; }; -class ImagePaths +class ImagePaths // TODO: Move me somewhere else { //-Instance Members-------------------------------------------------------------------------------------------------- private: diff --git a/app/src/launcher/interface/lr-install-interface.cpp b/app/src/launcher/interface/lr-install-interface.cpp index 72a1678..5ce4814 100644 --- a/app/src/launcher/interface/lr-install-interface.cpp +++ b/app/src/launcher/interface/lr-install-interface.cpp @@ -273,13 +273,7 @@ Qx::Error IInstall::preImport() { return {}; } Qx::Error IInstall::postImport() { return {}; } Qx::Error IInstall::prePlatformsImport() { return {}; } Qx::Error IInstall::postPlatformsImport() { return {}; } - -Qx::Error IInstall::preImageProcessing(const ImagePaths& bulkSources) -{ - Q_UNUSED(bulkSources); - return {}; -} - +Qx::Error IInstall::preImageProcessing() { return {}; } Qx::Error IInstall::postImageProcessing() { return {}; } Qx::Error IInstall::prePlaylistsImport() { return {}; } Qx::Error IInstall::postPlaylistsImport() { return {}; } diff --git a/app/src/launcher/interface/lr-install-interface.h b/app/src/launcher/interface/lr-install-interface.h index 9516df3..83a98cd 100644 --- a/app/src/launcher/interface/lr-install-interface.h +++ b/app/src/launcher/interface/lr-install-interface.h @@ -173,12 +173,13 @@ class IInstall virtual Qx::Error postImport(); virtual Qx::Error prePlatformsImport(); virtual Qx::Error postPlatformsImport(); - virtual Qx::Error preImageProcessing(const ImagePaths& bulkSources); + virtual Qx::Error preImageProcessing(); virtual Qx::Error postImageProcessing(); virtual Qx::Error prePlaylistsImport(); virtual Qx::Error postPlaylistsImport(); // Images + virtual void processBulkImageSources(const ImagePaths& bulkSources) = 0; virtual QString platformCategoryIconPath() const; // Unsupported in default implementation, needs to return path with .png extension virtual std::optional platformIconsDirectory() const; // Unsupported in default implementation virtual std::optional playlistIconsDirectory() const; // Unsupported in default implementation From 2d4bcca2dace67a723fde80034d17ed32e6c8a90 Mon Sep 17 00:00:00 2001 From: Christian Heimlich Date: Fri, 7 Feb 2025 20:38:05 -0500 Subject: [PATCH 06/20] Move ImageMap type to Worker --- app/src/import/worker.cpp | 18 +++++++++--------- app/src/import/worker.h | 10 ++++++++-- .../launcher/interface/lr-install-interface.h | 10 +--------- 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/app/src/import/worker.cpp b/app/src/import/worker.cpp index a57a4f1..779146e 100644 --- a/app/src/import/worker.cpp +++ b/app/src/import/worker.cpp @@ -202,14 +202,14 @@ ImageTransferError Worker::transferImage(bool symlink, QString sourcePath, QStri return ImageTransferError(); } -bool Worker::performImageJobs(const QList& jobs, bool symlink, Qx::ProgressGroup* pg) +bool Worker::performImageJobs(const QList& jobs, bool symlink, Qx::ProgressGroup* pg) { // Setup for image transfers ImageTransferError imageTransferError; // Error return reference *mBlockingErrorResponse = QMessageBox::NoToAll; // Default to choice "NoToAll" in case the signal is not correctly connected using Qt::BlockingQueuedConnection bool ignoreAllTransferErrors = false; // NoToAll response tracker - for(const Lr::IInstall::ImageMap& imageJob : jobs) + for(const ImageMap& imageJob : jobs) { while((imageTransferError = transferImage(symlink, imageJob.sourcePath, imageJob.destPath)).isValid() && !ignoreAllTransferErrors) { @@ -284,8 +284,8 @@ Worker::Result Worker::processPlatformGames(Qx::Error& errorReport, std::unique_ QString checkedLogoPath = (logoLocalInfo.exists() || mOptionSet.downloadImages) ? logoLocalInfo.absoluteFilePath() : QString(); QString checkedScreenshotPath = (ssLocalInfo.exists() || mOptionSet.downloadImages) ? ssLocalInfo.absoluteFilePath() : QString(); Lr::ImagePaths imagePaths(checkedLogoPath, checkedScreenshotPath); - Lr::IInstall::ImageMap logoMap{.sourcePath = imagePaths.logoPath(), .destPath = ""}; - Lr::IInstall::ImageMap screenshotMap{.sourcePath = imagePaths.screenshotPath(), .destPath = ""}; + ImageMap logoMap{.sourcePath = imagePaths.logoPath(), .destPath = ""}; + ImageMap screenshotMap{.sourcePath = imagePaths.screenshotPath(), .destPath = ""}; // Add set to doc platformDoc->addSet(builtSet, imagePaths); @@ -620,7 +620,7 @@ Worker::Result Worker::processImages(Qx::Error& errorReport) Worker::Result Worker::processIcons(Qx::Error& errorReport, const QStringList& platforms, const QList& playlists) { - QList jobs; + QList jobs; QString mainDest = mLauncherInstall->platformCategoryIconPath(); std::optional platformDestDir = mLauncherInstall->platformIconsDirectory(); std::optional playlistDestDir = mLauncherInstall->playlistIconsDirectory(); @@ -629,7 +629,7 @@ Worker::Result Worker::processIcons(Qx::Error& errorReport, const QStringList& p // Main Job if(!mainDest.isEmpty()) - jobs.emplace_back(Lr::IInstall::ImageMap{.sourcePath = u":/flashpoint/icon.png"_s, .destPath = mainDest}); + jobs.emplace_back(ImageMap{.sourcePath = u":/flashpoint/icon.png"_s, .destPath = mainDest}); // Platform jobs if(platformDestDir) @@ -639,8 +639,8 @@ Worker::Result Worker::processIcons(Qx::Error& errorReport, const QStringList& p { QString src = tk->platformLogoPath(p); if(QFile::exists(src)) - jobs.emplace_back(Lr::IInstall::ImageMap{.sourcePath = src, - .destPath = pdd.absoluteFilePath(p + ".png")}); + jobs.emplace_back(ImageMap{.sourcePath = src, + .destPath = pdd.absoluteFilePath(p + ".png")}); } } @@ -690,7 +690,7 @@ Worker::Result Worker::processIcons(Qx::Error& errorReport, const QStringList& p return Failed; } - jobs.emplace_back(Lr::IInstall::ImageMap{.sourcePath = source, .destPath = dest}); + jobs.emplace_back(ImageMap{.sourcePath = source, .destPath = dest}); } } diff --git a/app/src/import/worker.h b/app/src/import/worker.h index b46143a..e319b51 100644 --- a/app/src/import/worker.h +++ b/app/src/import/worker.h @@ -93,6 +93,12 @@ class Worker : public QObject static inline const QString PlaylistImport = u"PlaylistImport"_s; }; + struct ImageMap + { + QString sourcePath; + QString destPath; + }; + //-Class Variables----------------------------------------------------------------------------------------------- public: // Import Steps @@ -112,7 +118,7 @@ class Worker : public QObject // Image processing Qx::SyncDownloadManager mImageDownloadManager; - QList mImageTransferJobs; + QList mImageTransferJobs; // Job details Selections mImportSelections; @@ -146,7 +152,7 @@ class Worker : public QObject Qx::Error preloadPlaylists(QList& targetPlaylists); QList getPlaylistSpecificGameIds(const QList& playlists); ImageTransferError transferImage(bool symlink, QString sourcePath, QString destPath); - bool performImageJobs(const QList& jobs, bool symlink, Qx::ProgressGroup* pg = nullptr); + bool performImageJobs(const QList& jobs, bool symlink, Qx::ProgressGroup* pg = nullptr); Result processPlatformGames(Qx::Error& errorReport, std::unique_ptr& platformDoc, Fp::Db::QueryBuffer& gameQueryResult); void cullUnimportedPlaylistGames(QList& playlists); diff --git a/app/src/launcher/interface/lr-install-interface.h b/app/src/launcher/interface/lr-install-interface.h index 83a98cd..b59347f 100644 --- a/app/src/launcher/interface/lr-install-interface.h +++ b/app/src/launcher/interface/lr-install-interface.h @@ -61,14 +61,6 @@ class QX_ERROR_TYPE(RevertError, "Lr::RevertError", 1301) class IInstall { -//-Class Structs------------------------------------------------------------------------------------------------------ -public: - struct ImageMap - { - QString sourcePath; - QString destPath; - }; - //-Class Variables----------------------------------------------------------------------------------------------- private: // Files @@ -132,12 +124,12 @@ class IInstall void declareValid(bool valid); // Docs - virtual Qx::Error populateExistingDocs() = 0; void catalogueExistingDoc(IDataDoc::Identifier existingDoc); DocHandlingError checkoutDataDocument(IDataDoc* docToOpen, std::shared_ptr docReader); DocHandlingError commitDataDocument(IDataDoc* docToSave, std::shared_ptr docWriter); QList modifiedPlatforms() const; QList modifiedPlaylists() const; + virtual Qx::Error populateExistingDocs() = 0; public: // Details From 11e06c24fc79b976dc480cc4b8d97b823dc1c6c0 Mon Sep 17 00:00:00 2001 From: Christian Heimlich Date: Fri, 7 Feb 2025 20:52:17 -0500 Subject: [PATCH 07/20] Move ImagePaths to settings.h --- app/CMakeLists.txt | 1 + app/src/import/details.cpp | 2 ++ app/src/import/settings.cpp | 27 +++++++++++++++++++ app/src/import/settings.h | 21 +++++++++++++++ app/src/import/worker.cpp | 6 ++--- app/src/launcher/abstract/lr-data.h | 2 +- app/src/launcher/abstract/lr-data.tpp | 2 +- app/src/launcher/abstract/lr-install.h | 2 +- .../implementation/attractmode/am-install.cpp | 4 +-- .../implementation/attractmode/am-install.h | 4 +-- .../implementation/launchbox/lb-install.cpp | 8 +++--- .../implementation/launchbox/lb-install.h | 6 ++--- .../launcher/interface/lr-data-interface.cpp | 20 -------------- .../launcher/interface/lr-data-interface.h | 23 +--------------- .../launcher/interface/lr-install-interface.h | 2 +- 15 files changed, 70 insertions(+), 60 deletions(-) create mode 100644 app/src/import/settings.cpp diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index d401263..e63ef08 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -28,6 +28,7 @@ set(FIL_SOURCE import/properties.h import/properties.cpp import/settings.h + import/settings.cpp import/worker.h import/worker.cpp launcher/abstract/lr-data.h diff --git a/app/src/import/details.cpp b/app/src/import/details.cpp index e654302..4df090f 100644 --- a/app/src/import/details.cpp +++ b/app/src/import/details.cpp @@ -1,3 +1,4 @@ +// Unit Includes #include "details.h" namespace Import @@ -18,4 +19,5 @@ Details Details::current() { Q_ASSERT(mCurrent); return mCurrent.value(); } //Private: void Details::setCurrent(const Details& details) { Q_ASSERT(!mCurrent); mCurrent = details; } void Details::clearCurrent() { mCurrent = std::nullopt; } + } diff --git a/app/src/import/settings.cpp b/app/src/import/settings.cpp new file mode 100644 index 0000000..e2269e6 --- /dev/null +++ b/app/src/import/settings.cpp @@ -0,0 +1,27 @@ +// Unit Include +#include "settings.h" + +namespace Import +{ + +//=============================================================================================================== +// ImageSources +//=============================================================================================================== + +//-Constructor-------------------------------------------------------------------------------------------------------- +//Public: +ImagePaths::ImagePaths() {} +ImagePaths::ImagePaths(const QString& logoPath, const QString& screenshotPath) : + mLogoPath(logoPath), + mScreenshotPath(screenshotPath) +{} + +//-Instance Functions-------------------------------------------------------------------------------------------------- +//Public: +bool ImagePaths::isNull() const { return mLogoPath.isEmpty() && mScreenshotPath.isEmpty(); } +QString ImagePaths::logoPath() const { return mLogoPath; } +QString ImagePaths::screenshotPath() const { return mScreenshotPath; } +void ImagePaths::setLogoPath(const QString& path) { mLogoPath = path; } +void ImagePaths::setScreenshotPath(const QString& path) { mScreenshotPath = path; } + +} diff --git a/app/src/import/settings.h b/app/src/import/settings.h index 40c990c..f8c16d7 100644 --- a/app/src/import/settings.h +++ b/app/src/import/settings.h @@ -45,6 +45,27 @@ struct OptionSet Fp::Db::InclusionOptions inclusionOptions; }; +class ImagePaths +{ +//-Instance Members-------------------------------------------------------------------------------------------------- +private: + QString mLogoPath; + QString mScreenshotPath; + +//-Constructor-------------------------------------------------------------------------------------------------------- +public: + ImagePaths(); + ImagePaths(const QString& logoPath, const QString& screenshotPath); + +//-Instance Functions-------------------------------------------------------------------------------------------------- +public: + bool isNull() const; + QString logoPath() const; + QString screenshotPath() const; + void setLogoPath(const QString& path); + void setScreenshotPath(const QString& path); +}; + } #endif // IMPORT_SETTINGS_H diff --git a/app/src/import/worker.cpp b/app/src/import/worker.cpp index 779146e..35e0f11 100644 --- a/app/src/import/worker.cpp +++ b/app/src/import/worker.cpp @@ -283,7 +283,7 @@ Worker::Result Worker::processPlatformGames(Qx::Error& errorReport, std::unique_ QFileInfo ssLocalInfo(tk->entryImageLocalPath(Fp::ImageType::Screenshot, builtGame.id())); QString checkedLogoPath = (logoLocalInfo.exists() || mOptionSet.downloadImages) ? logoLocalInfo.absoluteFilePath() : QString(); QString checkedScreenshotPath = (ssLocalInfo.exists() || mOptionSet.downloadImages) ? ssLocalInfo.absoluteFilePath() : QString(); - Lr::ImagePaths imagePaths(checkedLogoPath, checkedScreenshotPath); + Import::ImagePaths imagePaths(checkedLogoPath, checkedScreenshotPath); ImageMap logoMap{.sourcePath = imagePaths.logoPath(), .destPath = ""}; ImageMap screenshotMap{.sourcePath = imagePaths.screenshotPath(), .destPath = ""}; @@ -585,8 +585,8 @@ Worker::Result Worker::processImages(Qx::Error& errorReport) // Provide launcher with bulk reference locations if(mOptionSet.imageMode == ImageMode::Reference) { - Lr::ImagePaths bulkSources(QDir::toNativeSeparators(mFlashpointInstall->entryLogosDirectory().absolutePath()), - QDir::toNativeSeparators(mFlashpointInstall->entryScreenshotsDirectory().absolutePath())); + Import::ImagePaths bulkSources(QDir::toNativeSeparators(mFlashpointInstall->entryLogosDirectory().absolutePath()), + QDir::toNativeSeparators(mFlashpointInstall->entryScreenshotsDirectory().absolutePath())); mLauncherInstall->processBulkImageSources(bulkSources); } diff --git a/app/src/launcher/abstract/lr-data.h b/app/src/launcher/abstract/lr-data.h index befde5a..7cd5c9d 100644 --- a/app/src/launcher/abstract/lr-data.h +++ b/app/src/launcher/abstract/lr-data.h @@ -126,7 +126,7 @@ class PlatformDoc : public IPlatformDoc public: InstallT* install() const; - void addSet(const Fp::Set& set, ImagePaths& images) override; + void addSet(const Fp::Set& set, Import::ImagePaths& images) override; // IMPLEMENT using IPlatformDoc::isEmpty; diff --git a/app/src/launcher/abstract/lr-data.tpp b/app/src/launcher/abstract/lr-data.tpp index 53b3ccb..94bca18 100644 --- a/app/src/launcher/abstract/lr-data.tpp +++ b/app/src/launcher/abstract/lr-data.tpp @@ -95,7 +95,7 @@ template Id::InstallT* PlatformDoc::install() const { return static_cast(IDataDoc::install()); } template -void PlatformDoc::addSet(const Fp::Set& set, ImagePaths& images) +void PlatformDoc::addSet(const Fp::Set& set, Import::ImagePaths& images) { if(!mError.isValid()) { diff --git a/app/src/launcher/abstract/lr-install.h b/app/src/launcher/abstract/lr-install.h index 5b9426c..07e367c 100644 --- a/app/src/launcher/abstract/lr-install.h +++ b/app/src/launcher/abstract/lr-install.h @@ -57,7 +57,7 @@ class Install : public IInstall using IInstall::preferredImageModeOrder; using IInstall::isRunning; using IInstall::processBulkImageSources; // Just do nothing if Reference mode isn't supported - virtual void convertToDestinationImages(const GameT& game, ImagePaths& images) = 0; // NOTE: The image paths provided here can be null (i.e. images unavailable). + virtual void convertToDestinationImages(const GameT& game, Import::ImagePaths& images) = 0; // NOTE: The image paths provided here can be null (i.e. images unavailable). // OPTIONALLY RE-IMPLEMENT using IInstall::preImport; diff --git a/app/src/launcher/implementation/attractmode/am-install.cpp b/app/src/launcher/implementation/attractmode/am-install.cpp index ebbec15..c588c64 100644 --- a/app/src/launcher/implementation/attractmode/am-install.cpp +++ b/app/src/launcher/implementation/attractmode/am-install.cpp @@ -498,13 +498,13 @@ Qx::Error Install::postImport() return Qx::Error(); } -void Install::processBulkImageSources(const Lr::ImagePaths& bulkSources) +void Install::processBulkImageSources(const Import::ImagePaths& bulkSources) { Q_UNUSED(bulkSources); qFatal("Attract Mode does not support Reference image mode, and that option should not be available."); } -void Install::convertToDestinationImages(const RomEntry& game, Lr::ImagePaths& images) +void Install::convertToDestinationImages(const RomEntry& game, Import::ImagePaths& images) { if(!images.logoPath().isEmpty()) images.setLogoPath(imageDestinationPath(Fp::ImageType::Logo, game)); diff --git a/app/src/launcher/implementation/attractmode/am-install.h b/app/src/launcher/implementation/attractmode/am-install.h index 3e2afd2..8a5a57a 100644 --- a/app/src/launcher/implementation/attractmode/am-install.h +++ b/app/src/launcher/implementation/attractmode/am-install.h @@ -119,8 +119,8 @@ class Install : public Lr::Install Qx::Error postImport() override; // Image handling - void processBulkImageSources(const Lr::ImagePaths& bulkSources) override; - void convertToDestinationImages(const RomEntry& game, Lr::ImagePaths& images) override; + void processBulkImageSources(const Import::ImagePaths& bulkSources) override; + void convertToDestinationImages(const RomEntry& game, Import::ImagePaths& images) override; }; } diff --git a/app/src/launcher/implementation/launchbox/lb-install.cpp b/app/src/launcher/implementation/launchbox/lb-install.cpp index accca0f..936ce66 100644 --- a/app/src/launcher/implementation/launchbox/lb-install.cpp +++ b/app/src/launcher/implementation/launchbox/lb-install.cpp @@ -107,7 +107,7 @@ QString Install::imageDestinationPath(Fp::ImageType imageType, const Lr::Game& g '.' + IMAGE_EXT; } -void Install::editBulkImageReferences(const Lr::ImagePaths& imageSources) +void Install::editBulkImageReferences(const Import::ImagePaths& imageSources) { // Set media folder paths const QList affectedPlatforms = modifiedPlatforms(); @@ -388,7 +388,7 @@ Qx::Error Install::postPlatformsImport() Qx::Error Install::preImageProcessing() { if(Import::Details::current().imageMode != Import::ImageMode::Reference) - editBulkImageReferences(Lr::ImagePaths());// Null arg will remove old references + editBulkImageReferences(Import::ImagePaths());// Null arg will remove old references return Lr::IInstall::preImageProcessing(); } @@ -426,12 +426,12 @@ Qx::Error Install::postPlaylistsImport() return commitParentsDoc(std::move(mParents)); } -void Install::processBulkImageSources(const Lr::ImagePaths& bulkSources) +void Install::processBulkImageSources(const Import::ImagePaths& bulkSources) { editBulkImageReferences(bulkSources); } -void Install::convertToDestinationImages(const Game& game, Lr::ImagePaths& images) +void Install::convertToDestinationImages(const Game& game, Import::ImagePaths& images) { if(!images.logoPath().isEmpty()) images.setLogoPath(imageDestinationPath(Fp::ImageType::Logo, game)); diff --git a/app/src/launcher/implementation/launchbox/lb-install.h b/app/src/launcher/implementation/launchbox/lb-install.h index e1ea9f7..872d667 100644 --- a/app/src/launcher/implementation/launchbox/lb-install.h +++ b/app/src/launcher/implementation/launchbox/lb-install.h @@ -89,7 +89,7 @@ class Install : public Lr::Install // Image Processing QString imageDestinationPath(Fp::ImageType imageType, const Lr::Game& game) const; - void editBulkImageReferences(const Lr::ImagePaths& imageSources); + void editBulkImageReferences(const Import::ImagePaths& imageSources); // Doc handling QString dataDocPath(Lr::IDataDoc::Identifier identifier) const; @@ -119,8 +119,8 @@ class Install : public Lr::Install Qx::Error postPlaylistsImport() override; // Image handling - void processBulkImageSources(const Lr::ImagePaths& bulkSources) override; - void convertToDestinationImages(const Game& game, Lr::ImagePaths& images) override; + void processBulkImageSources(const Import::ImagePaths& bulkSources) override; + void convertToDestinationImages(const Game& game, Import::ImagePaths& images) override; QString platformCategoryIconPath() const override; std::optional platformIconsDirectory() const override; std::optional playlistIconsDirectory() const override; diff --git a/app/src/launcher/interface/lr-data-interface.cpp b/app/src/launcher/interface/lr-data-interface.cpp index 9cf7e91..cdcfa6e 100644 --- a/app/src/launcher/interface/lr-data-interface.cpp +++ b/app/src/launcher/interface/lr-data-interface.cpp @@ -48,26 +48,6 @@ quint32 DocHandlingError::deriveValue() const { return mType; } QString DocHandlingError::derivePrimary() const { return mErrorStr; } QString DocHandlingError::deriveSecondary() const { return mSpecific; } -//=============================================================================================================== -// ImageSources -//=============================================================================================================== - -//-Constructor-------------------------------------------------------------------------------------------------------- -//Public: -ImagePaths::ImagePaths() {} -ImagePaths::ImagePaths(const QString& logoPath, const QString& screenshotPath) : - mLogoPath(logoPath), - mScreenshotPath(screenshotPath) -{} - -//-Instance Functions-------------------------------------------------------------------------------------------------- -//Public: -bool ImagePaths::isNull() const { return mLogoPath.isEmpty() && mScreenshotPath.isEmpty(); } -QString ImagePaths::logoPath() const { return mLogoPath; } -QString ImagePaths::screenshotPath() const { return mScreenshotPath; } -void ImagePaths::setLogoPath(const QString& path) { mLogoPath = path; } -void ImagePaths::setScreenshotPath(const QString& path) { mScreenshotPath = path; } - //=============================================================================================================== // IDataDoc::Identifier //=============================================================================================================== diff --git a/app/src/launcher/interface/lr-data-interface.h b/app/src/launcher/interface/lr-data-interface.h index ec2a1fb..5c9ccdd 100644 --- a/app/src/launcher/interface/lr-data-interface.h +++ b/app/src/launcher/interface/lr-data-interface.h @@ -104,27 +104,6 @@ class QX_ERROR_TYPE(DocHandlingError, "Lr::DocHandlingError", 1310) QString deriveSecondary() const override; }; -class ImagePaths // TODO: Move me somewhere else -{ -//-Instance Members-------------------------------------------------------------------------------------------------- -private: - QString mLogoPath; - QString mScreenshotPath; - -//-Constructor-------------------------------------------------------------------------------------------------------- -public: - ImagePaths(); - ImagePaths(const QString& logoPath, const QString& screenshotPath); - -//-Instance Functions-------------------------------------------------------------------------------------------------- -public: - bool isNull() const; - QString logoPath() const; - QString screenshotPath() const; - void setLogoPath(const QString& path); - void setScreenshotPath(const QString& path); -}; - class IDataDoc { //-Class Enums--------------------------------------------------------------------------------------------------------- @@ -340,7 +319,7 @@ class IPlatformDoc : public IUpdateableDoc, public IErrorable public: virtual bool containsGame(QUuid gameId) const = 0; // NOTE: UNUSED virtual bool containsAddApp(QUuid addAppId) const = 0; // NOTE: UNUSED - virtual void addSet(const Fp::Set& set, ImagePaths& images) = 0; + virtual void addSet(const Fp::Set& set, Import::ImagePaths& images) = 0; }; class IPlaylistDoc : public IUpdateableDoc, public IErrorable diff --git a/app/src/launcher/interface/lr-install-interface.h b/app/src/launcher/interface/lr-install-interface.h index b59347f..0b83162 100644 --- a/app/src/launcher/interface/lr-install-interface.h +++ b/app/src/launcher/interface/lr-install-interface.h @@ -171,7 +171,7 @@ class IInstall virtual Qx::Error postPlaylistsImport(); // Images - virtual void processBulkImageSources(const ImagePaths& bulkSources) = 0; + virtual void processBulkImageSources(const Import::ImagePaths& bulkSources) = 0; virtual QString platformCategoryIconPath() const; // Unsupported in default implementation, needs to return path with .png extension virtual std::optional platformIconsDirectory() const; // Unsupported in default implementation virtual std::optional playlistIconsDirectory() const; // Unsupported in default implementation From b20f2a8a8e48e851013f4fe4c7a1f5be0c118c03 Mon Sep 17 00:00:00 2001 From: Christian Heimlich Date: Fri, 7 Feb 2025 21:34:25 -0500 Subject: [PATCH 08/20] Remove need to pass doc for base checkout/commit methods --- app/src/launcher/abstract/lr-data.h | 6 ++---- app/src/launcher/abstract/lr-data.tpp | 4 ++-- app/src/launcher/abstract/lr-install.tpp | 8 ++++---- .../implementation/attractmode/am-install.cpp | 12 ++++++------ .../launcher/implementation/launchbox/lb-install.cpp | 8 ++++---- app/src/launcher/interface/lr-data-interface.cpp | 4 ++-- app/src/launcher/interface/lr-data-interface.h | 10 +++------- app/src/launcher/interface/lr-install-interface.cpp | 7 +++++-- app/src/launcher/interface/lr-install-interface.h | 8 ++++---- 9 files changed, 32 insertions(+), 35 deletions(-) diff --git a/app/src/launcher/abstract/lr-data.h b/app/src/launcher/abstract/lr-data.h index 7cd5c9d..b91f353 100644 --- a/app/src/launcher/abstract/lr-data.h +++ b/app/src/launcher/abstract/lr-data.h @@ -60,10 +60,9 @@ class DataDocReader : public IDataDoc::Reader virtual ~DataDocReader() = default; //-Instance Functions------------------------------------------------------------------------------------------------- -protected: +public: DocT* target() const; -public: // IMPLEMENT using IDataDoc::Reader::readInto; }; @@ -80,10 +79,9 @@ class DataDocWriter : public IDataDoc::Writer virtual ~DataDocWriter() = default; //-Instance Functions------------------------------------------------------------------------------------------------- -protected: +public: DocT* source() const; -public: // IMPLEMENT using IDataDoc::Writer::writeOutOf; }; diff --git a/app/src/launcher/abstract/lr-data.tpp b/app/src/launcher/abstract/lr-data.tpp index 94bca18..0acedbb 100644 --- a/app/src/launcher/abstract/lr-data.tpp +++ b/app/src/launcher/abstract/lr-data.tpp @@ -42,7 +42,7 @@ DataDocReader::DataDocReader(DocT* targetDoc) : {} //-Instance Functions-------------------------------------------------------------------------------------------------- -//Protected: +//Public: template DocT* DataDocReader::target() const { return static_cast(IDataDoc::Reader::target()); } @@ -58,7 +58,7 @@ DataDocWriter::DataDocWriter(DocT* sourceDoc) : {} //-Instance Functions-------------------------------------------------------------------------------------------------- -//Protected: +//Public: template DocT* DataDocWriter::source() const { return static_cast(IDataDoc::Writer::source()); } diff --git a/app/src/launcher/abstract/lr-install.tpp b/app/src/launcher/abstract/lr-install.tpp index 266b4a6..dabe430 100644 --- a/app/src/launcher/abstract/lr-install.tpp +++ b/app/src/launcher/abstract/lr-install.tpp @@ -55,7 +55,7 @@ DocHandlingError Install::checkoutPlatformDoc(std::unique_ptr& docReader = std::make_shared(platformDoc.get()); // Open document - DocHandlingError readErrorStatus = checkoutDataDocument(platformDoc.get(), docReader); + DocHandlingError readErrorStatus = checkoutDataDocument(docReader); // Fill return buffer on success if(!readErrorStatus.isValid()) @@ -78,7 +78,7 @@ DocHandlingError Install::checkoutPlaylistDoc(std::unique_ptr& docReader = std::make_shared(playlistDoc.get()); // Open document - DocHandlingError readErrorStatus = checkoutDataDocument(playlistDoc.get(), docReader); + DocHandlingError readErrorStatus = checkoutDataDocument(docReader); // Fill return buffer on success if(!readErrorStatus.isValid()) @@ -102,7 +102,7 @@ DocHandlingError Install::commitPlatformDoc(std::unique_ptr do // Write std::shared_ptr docWriter = std::make_shared(nativeDoc); - DocHandlingError writeErrorStatus = commitDataDocument(nativeDoc, docWriter); + DocHandlingError writeErrorStatus = commitDataDocument(docWriter); // Return write status and let document ptr auto delete return writeErrorStatus; @@ -122,7 +122,7 @@ DocHandlingError Install::commitPlaylistDoc(std::unique_ptr do // Write std::shared_ptr docWriter = std::make_shared(nativeDoc); - DocHandlingError writeErrorStatus = commitDataDocument(nativeDoc, docWriter); + DocHandlingError writeErrorStatus = commitDataDocument(docWriter); // Return write status and let document ptr auto delete return writeErrorStatus; diff --git a/app/src/launcher/implementation/attractmode/am-install.cpp b/app/src/launcher/implementation/attractmode/am-install.cpp index c588c64..3832bdd 100644 --- a/app/src/launcher/implementation/attractmode/am-install.cpp +++ b/app/src/launcher/implementation/attractmode/am-install.cpp @@ -157,7 +157,7 @@ Lr::DocHandlingError Install::checkoutMainConfig(std::unique_ptr& std::shared_ptr docReader = std::make_shared(returnBuffer.get()); // Open document - Lr::DocHandlingError readErrorStatus = checkoutDataDocument(returnBuffer.get(), docReader); + Lr::DocHandlingError readErrorStatus = checkoutDataDocument(docReader); // Set return null on failure if(readErrorStatus.isValid()) @@ -176,7 +176,7 @@ Lr::DocHandlingError Install::checkoutFlashpointRomlist(std::unique_ptr std::shared_ptr docReader = std::make_shared(returnBuffer.get()); // Open document - Lr::DocHandlingError readErrorStatus = checkoutDataDocument(returnBuffer.get(), docReader); + Lr::DocHandlingError readErrorStatus = checkoutDataDocument(docReader); // Set return null on failure if(readErrorStatus.isValid()) @@ -195,7 +195,7 @@ Lr::DocHandlingError Install::checkoutClifpEmulatorConfig(std::unique_ptr docReader = std::make_shared(returnBuffer.get()); // Open document - Lr::DocHandlingError readErrorStatus = checkoutDataDocument(returnBuffer.get(), docReader); + Lr::DocHandlingError readErrorStatus = checkoutDataDocument(docReader); // Set return null on failure if(readErrorStatus.isValid()) @@ -213,7 +213,7 @@ Lr::DocHandlingError Install::commitMainConfig(std::unique_ptr do std::shared_ptr docWriter = std::make_shared(document.get()); // Write - Lr::DocHandlingError writeErrorStatus = commitDataDocument(document.get(), docWriter); + Lr::DocHandlingError writeErrorStatus = commitDataDocument(docWriter); // Ensure document is cleared document.reset(); @@ -230,7 +230,7 @@ Lr::DocHandlingError Install::commitFlashpointRomlist(std::unique_ptr d std::shared_ptr docWriter = std::make_shared(document.get()); // Write - Lr::DocHandlingError writeErrorStatus = commitDataDocument(document.get(), docWriter); + Lr::DocHandlingError writeErrorStatus = commitDataDocument(docWriter); // Ensure document is cleared document.reset(); @@ -248,7 +248,7 @@ Lr::DocHandlingError Install::commitClifpEmulatorConfig(std::unique_ptr docWriter = std::make_shared(document.get()); // Write - Lr::DocHandlingError writeErrorStatus = commitDataDocument(document.get(), docWriter); + Lr::DocHandlingError writeErrorStatus = commitDataDocument(docWriter); // Ensure document is cleared document.reset(); diff --git a/app/src/launcher/implementation/launchbox/lb-install.cpp b/app/src/launcher/implementation/launchbox/lb-install.cpp index 936ce66..5307858 100644 --- a/app/src/launcher/implementation/launchbox/lb-install.cpp +++ b/app/src/launcher/implementation/launchbox/lb-install.cpp @@ -186,7 +186,7 @@ Lr::DocHandlingError Install::checkoutPlatformsConfigDoc(std::unique_ptr docReader = std::make_shared(returnBuffer.get()); // Open document - Lr::DocHandlingError readErrorStatus = checkoutDataDocument(returnBuffer.get(), docReader); + Lr::DocHandlingError readErrorStatus = checkoutDataDocument(docReader); // Set return null on failure if(readErrorStatus.isValid()) @@ -204,7 +204,7 @@ Lr::DocHandlingError Install::commitPlatformsConfigDoc(std::unique_ptr docWriter = std::make_shared(document.get()); // Write - Lr::DocHandlingError writeErrorStatus = commitDataDocument(document.get(), docWriter); + Lr::DocHandlingError writeErrorStatus = commitDataDocument(docWriter); // Ensure document is cleared document.reset(); @@ -225,7 +225,7 @@ Lr::DocHandlingError Install::checkoutParentsDoc(std::unique_ptr& re std::shared_ptr docReader = std::make_shared(returnBuffer.get()); // Open document - Lr::DocHandlingError readErrorStatus = checkoutDataDocument(returnBuffer.get(), docReader); + Lr::DocHandlingError readErrorStatus = checkoutDataDocument(docReader); // Set return null on failure if(readErrorStatus.isValid()) @@ -243,7 +243,7 @@ Lr::DocHandlingError Install::commitParentsDoc(std::unique_ptr docum std::shared_ptr docWriter = std::make_shared(document.get()); // Write - Lr::DocHandlingError writeErrorStatus = commitDataDocument(document.get(), docWriter); + Lr::DocHandlingError writeErrorStatus = commitDataDocument(docWriter); // Ensure document is cleared document.reset(); diff --git a/app/src/launcher/interface/lr-data-interface.cpp b/app/src/launcher/interface/lr-data-interface.cpp index cdcfa6e..b16ac99 100644 --- a/app/src/launcher/interface/lr-data-interface.cpp +++ b/app/src/launcher/interface/lr-data-interface.cpp @@ -114,7 +114,7 @@ IDataDoc::Reader::Reader(IDataDoc* targetDoc) : IDataDoc::Reader::~Reader() {} //-Instance Functions------------------------------------------------------------------------------------------------- -//Protected: +//Public: IDataDoc* IDataDoc::Reader::target() const { return mTargetDocument; } //=============================================================================================================== @@ -132,7 +132,7 @@ IDataDoc::Writer::Writer(IDataDoc* sourceDoc) : IDataDoc::Writer::~Writer() {} //-Instance Functions------------------------------------------------------------------------------------------------- -//Protected: +//Public: IDataDoc* IDataDoc::Writer::source() const { return mSourceDocument; } //=============================================================================================================== diff --git a/app/src/launcher/interface/lr-data-interface.h b/app/src/launcher/interface/lr-data-interface.h index 5c9ccdd..02191b4 100644 --- a/app/src/launcher/interface/lr-data-interface.h +++ b/app/src/launcher/interface/lr-data-interface.h @@ -180,11 +180,9 @@ class IDataDoc::Reader public: virtual ~Reader(); -//-Instance Functions------------------------------------------------------------------------------------------------- -protected: - IDataDoc* target() const; - +//-Instance Functions------------------------------------------------------------------------------------------------ public: + IDataDoc* target() const; virtual DocHandlingError readInto() = 0; }; @@ -203,10 +201,8 @@ class IDataDoc::Writer virtual ~Writer(); //-Instance Functions------------------------------------------------------------------------------------------------- -protected: - IDataDoc* source() const; - public: + IDataDoc* source() const; virtual DocHandlingError writeOutOf() = 0; }; diff --git a/app/src/launcher/interface/lr-install-interface.cpp b/app/src/launcher/interface/lr-install-interface.cpp index 5ce4814..2a1ea03 100644 --- a/app/src/launcher/interface/lr-install-interface.cpp +++ b/app/src/launcher/interface/lr-install-interface.cpp @@ -95,8 +95,10 @@ void IInstall::declareValid(bool valid) void IInstall::catalogueExistingDoc(IDataDoc::Identifier existingDoc) { mExistingDocuments.insert(existingDoc); } -DocHandlingError IInstall::checkoutDataDocument(IDataDoc* docToOpen, std::shared_ptr docReader) +DocHandlingError IInstall::checkoutDataDocument(std::shared_ptr docReader) { + auto docToOpen = docReader->target(); + // Error report to return DocHandlingError openReadError; // Defaults to no error @@ -118,8 +120,9 @@ DocHandlingError IInstall::checkoutDataDocument(IDataDoc* docToOpen, std::shared return openReadError; } -DocHandlingError IInstall::commitDataDocument(IDataDoc* docToSave, std::shared_ptr docWriter) +DocHandlingError IInstall::commitDataDocument(std::shared_ptr docWriter) { + auto docToSave = docWriter->source(); IDataDoc::Identifier id = docToSave->identifier(); // Check if the doc was saved previously to prevent double-backups diff --git a/app/src/launcher/interface/lr-install-interface.h b/app/src/launcher/interface/lr-install-interface.h index 0b83162..093114f 100644 --- a/app/src/launcher/interface/lr-install-interface.h +++ b/app/src/launcher/interface/lr-install-interface.h @@ -125,8 +125,8 @@ class IInstall // Docs void catalogueExistingDoc(IDataDoc::Identifier existingDoc); - DocHandlingError checkoutDataDocument(IDataDoc* docToOpen, std::shared_ptr docReader); - DocHandlingError commitDataDocument(IDataDoc* docToSave, std::shared_ptr docWriter); + DocHandlingError checkoutDataDocument(std::shared_ptr docReader); + DocHandlingError commitDataDocument(std::shared_ptr docWriter); QList modifiedPlatforms() const; QList modifiedPlaylists() const; virtual Qx::Error populateExistingDocs() = 0; @@ -147,8 +147,8 @@ class IInstall Qx::Error refreshExistingDocs(bool* changed = nullptr); bool containsPlatform(const QString& name) const; bool containsPlaylist(const QString& name) const; - bool containsAnyPlatform(const QList& names) const; - bool containsAnyPlaylist(const QList& names) const; + bool containsAnyPlatform(const QList& names) const; // Unused + bool containsAnyPlaylist(const QList& names) const; // Unused virtual DocHandlingError checkoutPlatformDoc(std::unique_ptr& returnBuffer, const QString& name) = 0; virtual DocHandlingError checkoutPlaylistDoc(std::unique_ptr& returnBuffer, const QString& name) = 0; From bcd5fb0f20f94971c62fe253423665e13fc701e6 Mon Sep 17 00:00:00 2001 From: Christian Heimlich Date: Sat, 8 Feb 2025 09:40:07 -0500 Subject: [PATCH 09/20] Write AM overviews in PlatformInterfaceWriter, allows removing Errorable --- app/src/import/worker.cpp | 14 --- app/src/launcher/abstract/lr-data.h | 2 - app/src/launcher/abstract/lr-data.tpp | 118 ++++++++---------- .../implementation/attractmode/am-data.cpp | 86 ++++++------- .../implementation/attractmode/am-data.h | 16 ++- .../implementation/attractmode/am-items.cpp | 16 +++ .../implementation/attractmode/am-items.h | 17 +++ .../launcher/interface/lr-data-interface.cpp | 16 +-- .../launcher/interface/lr-data-interface.h | 42 +++---- 9 files changed, 165 insertions(+), 162 deletions(-) diff --git a/app/src/import/worker.cpp b/app/src/import/worker.cpp index 35e0f11..ac05548 100644 --- a/app/src/import/worker.cpp +++ b/app/src/import/worker.cpp @@ -431,13 +431,6 @@ Worker::Result Worker::processGames(Qx::Error& errorReport, QListfinalize(); - // Check for internal doc errors - if(currentPlatformDoc->hasError()) - { - errorReport = currentPlatformDoc->error(); - return Failed; - } - // Forfeit document lease and save it Lr::DocHandlingError saveError; if((saveError = mLauncherInstall->commitPlatformDoc(std::move(currentPlatformDoc))).isValid()) @@ -492,13 +485,6 @@ Worker::Result Worker::processPlaylists(Qx::Error& errorReport, const QListfinalize(); - // Check for internal doc errors - if(currentPlaylistDoc->hasError()) - { - errorReport = currentPlaylistDoc->error(); - return Failed; - } - // Forfeit document lease and save it Lr::DocHandlingError saveError; if((saveError = mLauncherInstall->commitPlaylistDoc(std::move(currentPlaylistDoc))).isValid()) diff --git a/app/src/launcher/abstract/lr-data.h b/app/src/launcher/abstract/lr-data.h index b91f353..5195974 100644 --- a/app/src/launcher/abstract/lr-data.h +++ b/app/src/launcher/abstract/lr-data.h @@ -165,7 +165,6 @@ class BasicPlatformDoc : public PlatformDoc using InstallT = Id::InstallT; using GameT = Id::GameT; using AddAppT = Id::AddAppT; - using IErrorable::mError; using IUpdateableDoc::finalizeUpdateableItems; using IUpdateableDoc::addUpdateableItem; @@ -208,7 +207,6 @@ class BasicPlaylistDoc : public PlaylistDoc using InstallT = Id::InstallT; using PlaylistHeaderT = Id::PlaylistHeaderT; using PlaylistGameT = Id::PlaylistGameT; - using IErrorable::mError; using IUpdateableDoc::finalizeUpdateableItems; using IUpdateableDoc::addUpdateableItem; diff --git a/app/src/launcher/abstract/lr-data.tpp b/app/src/launcher/abstract/lr-data.tpp index 0acedbb..e293aa9 100644 --- a/app/src/launcher/abstract/lr-data.tpp +++ b/app/src/launcher/abstract/lr-data.tpp @@ -97,20 +97,18 @@ Id::InstallT* PlatformDoc::install() const { return static_cast(I template void PlatformDoc::addSet(const Fp::Set& set, Import::ImagePaths& images) { - if(!mError.isValid()) - { - // Process set (technically this can fail and return nullptr) - std::shared_ptr game = processSet(set); - - /* Process single image if applicable. - * - * The derived install type will not be defined at this point so we must access install() via - * the abstract base type. - */ - auto install = static_cast*>(IPlatformDoc::install()); - if(game && Import::Details::current().imageMode != Import::ImageMode::Reference) - install->convertToDestinationImages(*game, images); - } + // Process set + std::shared_ptr game = processSet(set); + Q_ASSERT(game); + + /* Process single image if applicable. + * + * The derived install type will not be defined at this point so we must access install() via + * the abstract base type. + */ + auto install = static_cast*>(IPlatformDoc::install()); + if(Import::Details::current().imageMode != Import::ImageMode::Reference) + install->convertToDestinationImages(*game, images); } //=============================================================================================================== @@ -160,46 +158,40 @@ bool BasicPlatformDoc::containsAddApp(QUuid addAppId) const { return mAddApp template std::shared_ptr BasicPlatformDoc::processSet(const Fp::Set& set) { - std::shared_ptr game; - if(!mError.isValid()) - { - // Prepare game - game = prepareGame(set.game()); + // Prepare game + std::shared_ptr game = prepareGame(set.game()); - // Add game - addUpdateableItem(mGamesExisting, mGamesFinal, game); + // Add game + addUpdateableItem(mGamesExisting, mGamesFinal, game); - // Handle additional apps - for(const Fp::AddApp& addApp : set.addApps()) - { - // Prepare - std::shared_ptr lrAddApp = prepareAddApp(addApp); + // Handle additional apps + for(const Fp::AddApp& addApp : set.addApps()) + { + // Prepare + std::shared_ptr lrAddApp = prepareAddApp(addApp); - // Add - addUpdateableItem(mAddAppsExisting, mAddAppsFinal, lrAddApp); - } + // Add + addUpdateableItem(mAddAppsExisting, mAddAppsFinal, lrAddApp); } + return game; } template void BasicPlatformDoc::finalize() { - if(!mError.isValid()) - { - /* TODO: Have this (and all other implementations of finalize() do something like return - * the IDs of titles that were removed, or otherwise populate an internal variable so that afterwards - * the list can be used to purge all images or other title related files (like overviews with AM). - * Right now only the data portion of old games is removed) - */ - - // Finalize item stores - finalizeUpdateableItems(mGamesExisting, mGamesFinal); - finalizeUpdateableItems(mAddAppsExisting, mAddAppsFinal); - - // Perform base finalization - IUpdateableDoc::finalize(); - } + /* TODO: Have this (and all other implementations of finalize() do something like return + * the IDs of titles that were removed, or otherwise populate an internal variable so that afterwards + * the list can be used to purge all images or other title related files (like overviews with AM). + * Right now only the data portion of old games is removed) + */ + + // Finalize item stores + finalizeUpdateableItems(mGamesExisting, mGamesFinal); + finalizeUpdateableItems(mAddAppsExisting, mAddAppsFinal); + + // Perform base finalization + IUpdateableDoc::finalize(); } template @@ -243,39 +235,33 @@ bool BasicPlaylistDoc::containsPlaylistGame(QUuid gameId) const { return mPl template void BasicPlaylistDoc::setPlaylistData(const Fp::Playlist& playlist) { - if(!mError.isValid()) - { - std::shared_ptr lrPlaylistHeader = preparePlaylistHeader(playlist); + std::shared_ptr lrPlaylistHeader = preparePlaylistHeader(playlist); - // Ensure doc already existed before transferring (null check) - if(mPlaylistHeader) - lrPlaylistHeader->transferOtherFields(mPlaylistHeader->otherFields()); + // Ensure doc already existed before transferring (null check) + if(mPlaylistHeader) + lrPlaylistHeader->transferOtherFields(mPlaylistHeader->otherFields()); - // Set instance header to new one - mPlaylistHeader = lrPlaylistHeader; + // Set instance header to new one + mPlaylistHeader = lrPlaylistHeader; - for(const auto& plg : playlist.playlistGames()) - { - // Prepare playlist game - std::shared_ptr lrPlaylistGame = preparePlaylistGame(plg); + for(const auto& plg : playlist.playlistGames()) + { + // Prepare playlist game + std::shared_ptr lrPlaylistGame = preparePlaylistGame(plg); - // Add playlist game - addUpdateableItem(mPlaylistGamesExisting, mPlaylistGamesFinal, lrPlaylistGame); - } + // Add playlist game + addUpdateableItem(mPlaylistGamesExisting, mPlaylistGamesFinal, lrPlaylistGame); } } template void BasicPlaylistDoc::finalize() { - if(!mError.isValid()) - { - // Finalize item stores - finalizeUpdateableItems(mPlaylistGamesExisting, mPlaylistGamesFinal); + // Finalize item stores + finalizeUpdateableItems(mPlaylistGamesExisting, mPlaylistGamesFinal); - // Perform base finalization - IUpdateableDoc::finalize(); - } + // Perform base finalization + IUpdateableDoc::finalize(); } template diff --git a/app/src/launcher/implementation/attractmode/am-data.cpp b/app/src/launcher/implementation/attractmode/am-data.cpp index 9d85fef..d9dc073 100644 --- a/app/src/launcher/implementation/attractmode/am-data.cpp +++ b/app/src/launcher/implementation/attractmode/am-data.cpp @@ -388,17 +388,17 @@ BulkOverviewWriter::BulkOverviewWriter(const QDir& overviewDir) : QString BulkOverviewWriter::currentFilePath() { return mFile.fileName(); } QString BulkOverviewWriter::fileErrorString() { return mFile.errorString(); } -bool BulkOverviewWriter::writeOverview(const QUuid& gameId, const QString& overview) +bool BulkOverviewWriter::writeOverview(const Overview& overview) { // Set file to overview path - QString fileName = gameId.toString(QUuid::WithoutBraces) + u".txt"_s; + QString fileName = overview.gameId().toString(QUuid::WithoutBraces) + u".txt"_s; mFile.setFileName(mOverviewDir.absoluteFilePath(fileName)); // Open file, always truncate mFile.open(QSaveFile::WriteOnly); // Write only implies truncate // Write overview - mFile.write(overview.toUtf8()); + mFile.write(overview.text().toUtf8()); // Save and return status return mFile.commit(); @@ -414,55 +414,40 @@ PlatformInterface::PlatformInterface(Install* install, const QString& platformTa const QDir& overviewDir) : Lr::PlatformDoc(install, {}, platformName, {}), mPlatformTaglist(install, platformTaglistPath, platformName), - mOverviewWriter(overviewDir) + mOverviewDir(overviewDir) {} //-Instance Functions-------------------------------------------------------------------------------------------------- //Private: std::shared_ptr PlatformInterface::processSet(const Fp::Set& set) { - std::shared_ptr romEntry; + //-Handle game---------------------------------------------------------- + const Fp::Game& game = set.game(); - if(!hasError()) - { - //-Handle game---------------------------------------------------------- - const Fp::Game& game = set.game(); - - // Add game ID to platform tag list - mPlatformTaglist.appendTag(game.id().toString(QUuid::WithoutBraces)); + // Add game ID to platform tag list + mPlatformTaglist.appendTag(game.id().toString(QUuid::WithoutBraces)); - // Create game overview - QString overview = game.originalDescription(); + // Create game overview + QString overviewText = game.originalDescription(); + if(!overviewText.isEmpty()) + mOverviews.emplaceBack(game.id(), overviewText); - if(!overview.isEmpty()) - { - bool written = mOverviewWriter.writeOverview(game.id(), overview); - - if(written) - install()->addRevertableFile(mOverviewWriter.currentFilePath()); - else - mError = Lr::DocHandlingError(*this, Lr::DocHandlingError::DocWriteFailed, mOverviewWriter.fileErrorString()); - } - - //-Handle add apps------------------------------------------------------- - - // Add add app IDs to platform tag list - for(const Fp::AddApp& addApp : set.addApps()) - { - /* Ignore non-playable add apps to avoid useless clutter in AM - * TODO: Consider doing this in Import Worker to make it a standard since - * LB doesn't actually need the non-playable entries either. Importing them - * is basically a leftover from an earlier CLIFp version - */ - if(addApp.isPlayable()) - mPlatformTaglist.appendTag(addApp.id().toString(QUuid::WithoutBraces)); - } + //-Handle add apps------------------------------------------------------- - //-Forward game insertion to main Romlist-------------------------------- - romEntry = install()->mRomlist->processSet(set); + // Add add app IDs to platform tag list + for(const Fp::AddApp& addApp : set.addApps()) + { + /* Ignore non-playable add apps to avoid useless clutter in AM + * TODO: Consider doing this in Import Worker to make it a standard since + * LB doesn't actually need the non-playable entries either. Importing them + * is basically a leftover from an earlier CLIFp version + */ + if(addApp.isPlayable()) + mPlatformTaglist.appendTag(addApp.id().toString(QUuid::WithoutBraces)); } - return romEntry; + //-Forward game insertion to main Romlist-------------------------------- + return install()->mRomlist->processSet(set); } //Public: @@ -494,12 +479,29 @@ bool PlatformInterface::containsAddApp(QUuid addAppId) const //Public: PlatformInterfaceWriter::PlatformInterfaceWriter(PlatformInterface* sourceDoc) : Lr::DataDocWriter(sourceDoc), - mTaglistWriter(&sourceDoc->mPlatformTaglist) + mTaglistWriter(&sourceDoc->mPlatformTaglist), + mOverviewWriter(sourceDoc->mOverviewDir) // TODO: Maybe a better way to pass this then it just sitting in the doc? {} //-Instance Functions-------------------------------------------------------------------------------------------------- //Public: -Lr::DocHandlingError PlatformInterfaceWriter::writeOutOf() { return mTaglistWriter.writeOutOf(); } +Lr::DocHandlingError PlatformInterfaceWriter::writeOutOf() +{ + // Write tag list + if(auto err = mTaglistWriter.writeOutOf()) + return err; + + // Write overviews + for(const auto& o : std::as_const(source()->mOverviews)) + { + if(mOverviewWriter.writeOverview(o)) + source()->install()->addRevertableFile(mOverviewWriter.currentFilePath()); + else + return Lr::DocHandlingError(*source(), Lr::DocHandlingError::DocWriteFailed, mOverviewWriter.fileErrorString()); + } + + return {}; +} //=============================================================================================================== // PlaylistInterface diff --git a/app/src/launcher/implementation/attractmode/am-data.h b/app/src/launcher/implementation/attractmode/am-data.h index cd2f76d..3febb77 100644 --- a/app/src/launcher/implementation/attractmode/am-data.h +++ b/app/src/launcher/implementation/attractmode/am-data.h @@ -268,7 +268,7 @@ class BulkOverviewWriter public: QString currentFilePath(); QString fileErrorString(); - bool writeOverview(const QUuid& gameId, const QString& overview); + bool writeOverview(const Overview& overview); }; class PlatformInterface : public Lr::PlatformDoc @@ -277,10 +277,8 @@ class PlatformInterface : public Lr::PlatformDoc //-Instance Variables-------------------------------------------------------------------------------------------------- private: PlatformTaglist mPlatformTaglist; - BulkOverviewWriter mOverviewWriter; - /* NOTE: Would just use Qx::writeStringToFile() but that is slower due to lots of checks/error handling, whereas - * this needs to be as fast as possible - */ + QList mOverviews; + QDir mOverviewDir; //-Constructor-------------------------------------------------------------------------------------------------------- public: @@ -300,11 +298,13 @@ class PlatformInterface : public Lr::PlatformDoc class PlatformInterfaceWriter : public Lr::DataDocWriter { - // Shell for writing the taglist of the interface - //-Instance Variables-------------------------------------------------------------------------------------------------- private: Taglist::Writer mTaglistWriter; + BulkOverviewWriter mOverviewWriter; + /* NOTE: Would just use Qx::writeStringToFile() but that is slower due to lots of checks/error handling, whereas + * this needs to be as fast as possible + */ //-Constructor-------------------------------------------------------------------------------------------------------- public: @@ -337,8 +337,6 @@ class PlaylistInterface : public Lr::PlaylistDoc class PlaylistInterfaceWriter : public Lr::DataDocWriter { - // Shell for writing the taglist of the interface - //-Instance Variables-------------------------------------------------------------------------------------------------- private: Taglist::Writer mTaglistWriter; diff --git a/app/src/launcher/implementation/attractmode/am-items.cpp b/app/src/launcher/implementation/attractmode/am-items.cpp index 2eeeaa7..f141440 100644 --- a/app/src/launcher/implementation/attractmode/am-items.cpp +++ b/app/src/launcher/implementation/attractmode/am-items.cpp @@ -143,6 +143,22 @@ RomEntry::Builder& RomEntry::Builder::wLanguage(const QString& language) { mItem RomEntry::Builder& RomEntry::Builder::wRegion(const QString& region) { mItemBlueprint.mRegion = region; return *this; } RomEntry::Builder& RomEntry::Builder::wRating(const QString& rating) { mItemBlueprint.mRating = rating; return *this; } +//=============================================================================================================== +// Overview +//=============================================================================================================== + +//-Constructor------------------------------------------------------------------------------------------------ +//Public: +Overview::Overview(const QUuid& gameId, const QString& text) : + mGameId(gameId), + mText(text) +{} + +//-Instance Functions------------------------------------------------------------------------------------------------ +//Public: +QUuid Overview::gameId() const{ return mGameId; } +QString Overview::text() const{ return mText; } + //=============================================================================================================== // EmulatorArtworkEntry //=============================================================================================================== diff --git a/app/src/launcher/implementation/attractmode/am-items.h b/app/src/launcher/implementation/attractmode/am-items.h index 38877f9..d9e5331 100644 --- a/app/src/launcher/implementation/attractmode/am-items.h +++ b/app/src/launcher/implementation/attractmode/am-items.h @@ -111,6 +111,23 @@ class RomEntry::Builder : public Lr::Game::Builder Builder& wRating(const QString& rating); }; +class Overview +{ +//-Instance Variables----------------------------------------------------------------------------------------------- +private: + QUuid mGameId; + QString mText; + +//-Constructor------------------------------------------------------------------------------------------------- +public: + Overview(const QUuid& gameId, const QString& text); + +//-Instance Functions------------------------------------------------------------------------------------------------------ +public: + QUuid gameId() const; + QString text() const; +}; + class EmulatorArtworkEntry : public Lr::Item { //-Inner Classes--------------------------------------------------------------------------------------------------- diff --git a/app/src/launcher/interface/lr-data-interface.cpp b/app/src/launcher/interface/lr-data-interface.cpp index b16ac99..63415da 100644 --- a/app/src/launcher/interface/lr-data-interface.cpp +++ b/app/src/launcher/interface/lr-data-interface.cpp @@ -136,24 +136,24 @@ IDataDoc::Writer::~Writer() {} IDataDoc* IDataDoc::Writer::source() const { return mSourceDocument; } //=============================================================================================================== -// Errorable +// IErrorable //=============================================================================================================== //-Constructor----------------------------------------------------------------------------------------------------- //Protected: -IErrorable::IErrorable() {} +//IErrorable::IErrorable() {} //-Destructor------------------------------------------------------------------------------------------------ //Public: -IErrorable::~IErrorable() {} +//IErrorable::~IErrorable() {} //-Instance Functions------------------------------------------------------------------------------------------------- //Protected: -bool IErrorable::hasError() const { return mError.isValid(); } -Qx::Error IErrorable::error() const { return mError; } +//bool IErrorable::hasError() const { return mError.isValid(); } +//Qx::Error IErrorable::error() const { return mError; } //=============================================================================================================== -// UpdateableDoc +// IUpdateableDoc //=============================================================================================================== //-Constructor----------------------------------------------------------------------------------------------------- @@ -168,7 +168,7 @@ IUpdateableDoc::IUpdateableDoc(IInstall* install, const QString& docPath, QStrin void IUpdateableDoc::finalize() {} // Does nothing for base class //=============================================================================================================== -// PlatformDoc +// IPlatformDoc //=============================================================================================================== //-Constructor-------------------------------------------------------------------------------------------------------- @@ -182,7 +182,7 @@ IPlatformDoc::IPlatformDoc(IInstall* install, const QString& docPath, QString do IDataDoc::Type IPlatformDoc::type() const { return Type::Platform; } //=============================================================================================================== -// PlaylistDoc +// IPlaylistDoc //=============================================================================================================== //-Constructor-------------------------------------------------------------------------------------------------------- diff --git a/app/src/launcher/interface/lr-data-interface.h b/app/src/launcher/interface/lr-data-interface.h index 02191b4..2eb4869 100644 --- a/app/src/launcher/interface/lr-data-interface.h +++ b/app/src/launcher/interface/lr-data-interface.h @@ -206,25 +206,25 @@ class IDataDoc::Writer virtual DocHandlingError writeOutOf() = 0; }; -class IErrorable -{ -//-Instance Variables-------------------------------------------------------------------------------------------------- -protected: - Qx::Error mError; - -//-Constructor-------------------------------------------------------------------------------------------------------- -protected: - IErrorable(); - -//-Destructor------------------------------------------------------------------------------------------------- -public: - virtual ~IErrorable(); - -//-Instance Functions-------------------------------------------------------------------------------------------------- -public: - bool hasError() const; - Qx::Error error() const; -}; +// class IErrorable +// { +// //-Instance Variables-------------------------------------------------------------------------------------------------- +// protected: +// Qx::Error mError; + +// //-Constructor-------------------------------------------------------------------------------------------------------- +// protected: +// IErrorable(); + +// //-Destructor------------------------------------------------------------------------------------------------- +// public: +// virtual ~IErrorable(); + +// //-Instance Functions-------------------------------------------------------------------------------------------------- +// public: +// bool hasError() const; +// Qx::Error error() const; +// }; class IUpdateableDoc : public IDataDoc { @@ -302,7 +302,7 @@ static T* itemPtr(std::shared_ptr item) { return item.get(); } virtual void finalize(); }; -class IPlatformDoc : public IUpdateableDoc, public IErrorable +class IPlatformDoc : public IUpdateableDoc { //-Constructor-------------------------------------------------------------------------------------------------------- protected: @@ -318,7 +318,7 @@ class IPlatformDoc : public IUpdateableDoc, public IErrorable virtual void addSet(const Fp::Set& set, Import::ImagePaths& images) = 0; }; -class IPlaylistDoc : public IUpdateableDoc, public IErrorable +class IPlaylistDoc : public IUpdateableDoc { //-Constructor-------------------------------------------------------------------------------------------------------- protected: From a386aed3bb7b2a7c68273ba032d812bb96f06bc7 Mon Sep 17 00:00:00 2001 From: Christian Heimlich Date: Sat, 8 Feb 2025 10:20:53 -0500 Subject: [PATCH 10/20] Handle some TODOs - Remove dated ones - Add note about Register<> - Use Qx::String::mapArg() where applicable --- app/src/import/properties.h | 8 -------- app/src/import/worker.cpp | 2 +- app/src/launcher/abstract/lr-install.h | 2 +- app/src/launcher/abstract/lr-registration.h | 9 +++++++-- .../implementation/attractmode/am-install.cpp | 10 ++++++---- .../implementation/launchbox/lb-install.cpp | 14 ++++++++------ app/src/launcher/interface/lr-data-interface.cpp | 15 ++++++++------- app/src/launcher/interface/lr-install-interface.h | 4 ---- app/src/ui/mainwindow.cpp | 4 ---- 9 files changed, 31 insertions(+), 37 deletions(-) diff --git a/app/src/import/properties.h b/app/src/import/properties.h index b6b0c77..b45aae8 100644 --- a/app/src/import/properties.h +++ b/app/src/import/properties.h @@ -14,14 +14,6 @@ #include "import/settings.h" #include "project_vars.h" -/* TODO: PROBABLY OK NOW The number of properties here has gotten somewhat out of hand. - * Mainwindow should probably just be given access to something like - * const QBindable> (and same for - * launcher) so that it can setup its own bindings/properties directly - * off of that. Since it would be read only, that still lets Controller - * have control of the instances. - */ - namespace Lr { class IInstall; } namespace Fp { class Install; } diff --git a/app/src/import/worker.cpp b/app/src/import/worker.cpp index ac05548..31ae7ef 100644 --- a/app/src/import/worker.cpp +++ b/app/src/import/worker.cpp @@ -926,7 +926,7 @@ void Worker::pmProgressUpdated(quint64 currentProgress) { /* NOTE: This is required because if the value isn't actually different than the current when * the connected QProgressDialog::setValue() is triggered then processEvents() won't be called. - * This is a problem because the fixed range of QGroupedProgressManager of 0-100 means that groups + * This is a problem because the fixed range of Qx::GroupedProgressManager of 0-100 means that groups * with a high number of steps won't actually trigger an emissions of the manager valueChanged() signal * until a large enough number of those steps have been completed to increase its weighted sum by 1. * processEvents() needs to be called every time progress is updated even a little in order to diff --git a/app/src/launcher/abstract/lr-install.h b/app/src/launcher/abstract/lr-install.h index 07e367c..4ef6172 100644 --- a/app/src/launcher/abstract/lr-install.h +++ b/app/src/launcher/abstract/lr-install.h @@ -57,7 +57,7 @@ class Install : public IInstall using IInstall::preferredImageModeOrder; using IInstall::isRunning; using IInstall::processBulkImageSources; // Just do nothing if Reference mode isn't supported - virtual void convertToDestinationImages(const GameT& game, Import::ImagePaths& images) = 0; // NOTE: The image paths provided here can be null (i.e. images unavailable). + virtual void convertToDestinationImages(const GameT& game, Import::ImagePaths& images) = 0; // NOTE: One or both of the image paths provided here can be null (i.e. images unavailable). // OPTIONALLY RE-IMPLEMENT using IInstall::preImport; diff --git a/app/src/launcher/abstract/lr-registration.h b/app/src/launcher/abstract/lr-registration.h index a175dd4..3e0c0c6 100644 --- a/app/src/launcher/abstract/lr-registration.h +++ b/app/src/launcher/abstract/lr-registration.h @@ -168,15 +168,20 @@ class StaticRegistry template class Register { + /* NOTE: This is used by the REGISTER_LAUNCHER() macro to cause launcher registration + * at runtime, and to setup the StaticRegistry for the launcher of 'Id'. This has to + * be done separately from Registrar because we need the definition of the launcher's + * Install type to be available. + */ + private: static std::unique_ptr createInstall(const QString& path) { - return std::make_unique(path); + return std::make_unique(path); } public: Register() { - // TODO: MAKE NOTE HERE ABOUT USAGE AND WHY THIS HAS TO EXIST StaticRegistry::smEntry = Registry::registerInstall(Registry::Entry{ .name = Id::Name, .make = createInstall, diff --git a/app/src/launcher/implementation/attractmode/am-install.cpp b/app/src/launcher/implementation/attractmode/am-install.cpp index 3832bdd..0902dfb 100644 --- a/app/src/launcher/implementation/attractmode/am-install.cpp +++ b/app/src/launcher/implementation/attractmode/am-install.cpp @@ -7,6 +7,7 @@ // Qx Includes #include #include +#include // Project Includes #include "kernel/clifp.h" @@ -77,7 +78,7 @@ Qx::Error Install::populateExistingDocs() // Platforms and Playlists if(mFpTagDirectory.exists()) { - /* NOTE: Qt globbing syntax is slightly weird (mainly '\' cannot be used as an escape character, and instead character to + /* NOTE: Qt globbing syntax is slightly weird (mainly '\' cannot be used as an escape character, and instead characters to * be escaped must individually be placed between braces. This makes using variables as part of the expression awkward * so instead they must be mostly written out and care must be taken to modify them if the file names change. * @@ -468,9 +469,10 @@ Qx::Error Install::postImport() for(const QString& tagFile : tagFiles) { // Escape brackets in name since AM uses regex for value - // TODO: Use Qx for this - QString escaped = tagFile; - escaped.replace(u"["_s, u"\\["_s).replace(u"]"_s, u"\\]"_s); + QString escaped = Qx::String::mapArg(tagFile,{ + {u"["_s, u"\\["_s}, + {u"]"_s, u"\\]"_s} + }); DisplayFilter::Builder dfb; dfb = DisplayFilter::Builder(); diff --git a/app/src/launcher/implementation/launchbox/lb-install.cpp b/app/src/launcher/implementation/launchbox/lb-install.cpp index 5307858..e08713b 100644 --- a/app/src/launcher/implementation/launchbox/lb-install.cpp +++ b/app/src/launcher/implementation/launchbox/lb-install.cpp @@ -13,11 +13,14 @@ #include #include #include +#include #include // Project Includes #include "import/details.h" +using namespace Qt::StringLiterals; + namespace Lb { //=============================================================================================================== @@ -294,13 +297,12 @@ QString Install::translateDocName(const QString& originalName, Lr::IDataDoc::Typ * basis as they come up. */ - QString translatedName = originalName; - // LB matched changes (LB might replace all illegal characters with underscores, but these are is known for sure) - // TODO: Use Qx for this - translatedName.replace(':','_'); - translatedName.replace('#','_'); - translatedName.replace('\'','_'); + QString translatedName = Qx::String::mapArg(originalName,{ + {u":"_s, u"_"_s}, + {u"#"_s, u"_"_s}, + {u"'"_s, u"_"_s} + }); // General kosherization translatedName = Qx::kosherizeFileName(translatedName); diff --git a/app/src/launcher/interface/lr-data-interface.cpp b/app/src/launcher/interface/lr-data-interface.cpp index 63415da..84b4473 100644 --- a/app/src/launcher/interface/lr-data-interface.cpp +++ b/app/src/launcher/interface/lr-data-interface.cpp @@ -1,6 +1,9 @@ // Unit Include #include "lr-data-interface.h" +// Qx Includes +#include + // Project Includes #include "launcher/interface/lr-install-interface.h" @@ -26,13 +29,11 @@ DocHandlingError::DocHandlingError(const IDataDoc& doc, Type t, const QString& s //Private: QString DocHandlingError::generatePrimaryString(const IDataDoc& doc, Type t) { - // TODO: Use Qx for this - QString formattedError = ERR_STRINGS[t]; - formattedError.replace(M_DOC_TYPE, doc.identifier().docTypeString()); - formattedError.replace(M_DOC_NAME, doc.identifier().docName()); - formattedError.replace(M_DOC_PARENT, doc.install()->name()); - - return formattedError; + return Qx::String::mapArg(ERR_STRINGS[t],{ + {M_DOC_TYPE, doc.identifier().docTypeString()}, + {M_DOC_NAME, doc.identifier().docName()}, + {M_DOC_PARENT, doc.install()->name()} + }); } //-Instance Functions------------------------------------------------------------- diff --git a/app/src/launcher/interface/lr-install-interface.h b/app/src/launcher/interface/lr-install-interface.h index 093114f..6c01bd9 100644 --- a/app/src/launcher/interface/lr-install-interface.h +++ b/app/src/launcher/interface/lr-install-interface.h @@ -71,10 +71,6 @@ class IInstall static inline const QString IMAGE_EXT = u"png"_s; public: - // Base errors - // TODO: This is unused, should it be in-use somewhere? - static inline const QString ERR_UNSUPPORTED_FEATURE = u"A feature unsupported by the launcher was called upon!"_s; - // Image Errors static inline const QString CAPTION_IMAGE_ERR = u"Error importing game image(s)"_s; diff --git a/app/src/ui/mainwindow.cpp b/app/src/ui/mainwindow.cpp index e89c045..2076737 100644 --- a/app/src/ui/mainwindow.cpp +++ b/app/src/ui/mainwindow.cpp @@ -143,10 +143,6 @@ void MainWindow::initializeForms() // If no link permissions, inform user if(!mImportProperties.hasLinkPermissions()) ui->radioButton_link->setText(ui->radioButton_link->text().append(REQUIRE_ELEV)); - - // NOTE: THIS IS FOR DEBUG PURPOSES - //checkLaunchBoxInput("C:/Users/Player/Desktop/LBTest/LaunchBox"); - //checkFlashpointInput("D:/FP/Flashpoint 8.1 Ultimate"); } Qx::Bimap MainWindow::initializeImageModeMap() const From 0a844d8ae1d7447445317f7aefe26a25a9b8e6c1 Mon Sep 17 00:00:00 2001 From: Christian Heimlich Date: Sun, 9 Feb 2025 10:37:14 -0500 Subject: [PATCH 11/20] Update deprecated code --- CMakeLists.txt | 8 ++++---- app/CMakeLists.txt | 4 ++-- app/src/import/worker.cpp | 8 ++++---- app/src/launcher/implementation/attractmode/am-data.cpp | 4 ++-- .../launcher/implementation/attractmode/am-install.cpp | 4 ++-- .../implementation/attractmode/am-settings-data.cpp | 4 ++-- app/src/launcher/implementation/launchbox/lb-data.cpp | 2 +- app/src/launcher/implementation/launchbox/lb-install.cpp | 8 ++++---- 8 files changed, 21 insertions(+), 21 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 34475e7..47f1cac 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,7 +13,7 @@ project(FIL # Get helper scripts include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/FetchOBCMake.cmake) -fetch_ob_cmake("e88d54a044319c0cd12a9e68c5538bf1194e6285") +fetch_ob_cmake("d64498ec2654e7f3145d7efa36bfaa73ac30cc74") # Initialize project according to standard rules include(OB/Project) @@ -74,20 +74,20 @@ endif() include(OB/FetchQx) ob_fetch_qx( - REF "a7ff0ecbabc51c89b99b93812145a0fa6b2f7283" + REF "1bfe8bdb6047c1d6c3246e431652869e63cbe797" COMPONENTS ${FIL_QX_COMPONENTS} ) # Fetch libfp (build and import from source) include(OB/Fetchlibfp) -ob_fetch_libfp("183a479d00235d332aa1046a9b5ba98f62699752") +ob_fetch_libfp("d2b1960b76c10537f2beb933206d42a31576ab07") # Fetch CLIFp (build and import from source) include(OB/Utility) ob_cache_project_version(CLIFp) include(OB/FetchCLIFp) -ob_fetch_clifp("7139ae998b292eb595e751ba4cb8599230435358") +ob_fetch_clifp("f56246bad2504af08762d91ea1cc1361135c58ad") # TODO: The shared build of this is essentially useless as only the CLIFp executable # is deployed, which only works if it's statically linked. There isn't a simple way diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index e63ef08..9c241a0 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -8,9 +8,9 @@ set(CLIFP_RES_PATH "${CMAKE_CURRENT_BINARY_DIR}/res/file/clifp") add_custom_command(OUTPUT "${CLIFP_RES_PATH}" - COMMAND ${CMAKE_COMMAND} -E copy $ + COMMAND ${CMAKE_COMMAND} -E copy $ "${CLIFP_RES_PATH}" - DEPENDS CLIFp::CLIFp + DEPENDS CLIFp::FrontendGui ) # This statement creates a target that by itself does nothing, but since it depends diff --git a/app/src/import/worker.cpp b/app/src/import/worker.cpp index 31ae7ef..d45eede 100644 --- a/app/src/import/worker.cpp +++ b/app/src/import/worker.cpp @@ -740,7 +740,7 @@ Worker::Result Worker::doImport(Qx::Error& errorReport) // Make unselected platforms list QStringList availablePlatforms = fpDatabase->platformNames(); QStringList unselectedPlatforms = QStringList(availablePlatforms); - for(const QString& selPlatform : qAsConst(mImportSelections.platforms)) + for(const QString& selPlatform : std::as_const(mImportSelections.platforms)) unselectedPlatforms.removeAll(selPlatform); // Make game query @@ -768,7 +768,7 @@ Worker::Result Worker::doImport(Qx::Error& errorReport) quint64 totalGameCount = 0; QStringList playlistSpecPlatforms; - for(const Fp::Db::QueryBuffer& query : qAsConst(playlistSpecGameQueries)) + for(const Fp::Db::QueryBuffer& query : std::as_const(playlistSpecGameQueries)) playlistSpecPlatforms.append(query.source); QStringList involvedPlatforms = mImportSelections.platforms + playlistSpecPlatforms; @@ -780,14 +780,14 @@ Worker::Result Worker::doImport(Qx::Error& errorReport) Qx::ProgressGroup* pgGameImport = initializeProgressGroup(Pg::GameImport, 2); // All games - for(const Fp::Db::QueryBuffer& query : qAsConst(gameQueries)) + for(const Fp::Db::QueryBuffer& query : std::as_const(gameQueries)) { pgGameImport->increaseMaximum(query.size); totalGameCount += query.size; } // All playlist specific games - for(const Fp::Db::QueryBuffer& query : qAsConst(playlistSpecGameQueries)) + for(const Fp::Db::QueryBuffer& query : std::as_const(playlistSpecGameQueries)) { pgGameImport->increaseMaximum(query.size); totalGameCount += query.size; diff --git a/app/src/launcher/implementation/attractmode/am-data.cpp b/app/src/launcher/implementation/attractmode/am-data.cpp index d9dc073..505e487 100644 --- a/app/src/launcher/implementation/attractmode/am-data.cpp +++ b/app/src/launcher/implementation/attractmode/am-data.cpp @@ -68,7 +68,7 @@ Taglist::Writer::Writer(Taglist* sourceDoc) : bool Taglist::Writer::writeSourceDoc() { // Write tags - for(const QString& tag : qAsConst(source()->mTags)) + for(const QString& tag : std::as_const(source()->mTags)) mStreamWriter << tag << '\n'; // Return error status @@ -327,7 +327,7 @@ bool Romlist::Writer::writeSourceDoc() mStreamWriter.writeLine(Romlist::HEADER); // Write all rom entries - for(const std::shared_ptr& entry : qAsConst(source()->finalEntries())) + for(const std::shared_ptr& entry : std::as_const(source()->finalEntries())) { if(!writeRomEntry(*entry)) return false; diff --git a/app/src/launcher/implementation/attractmode/am-install.cpp b/app/src/launcher/implementation/attractmode/am-install.cpp index 0902dfb..be35376 100644 --- a/app/src/launcher/implementation/attractmode/am-install.cpp +++ b/app/src/launcher/implementation/attractmode/am-install.cpp @@ -90,7 +90,7 @@ Qx::Error Install::populateExistingDocs() if(existingCheck.isFailure()) return existingCheck; - for(const QFileInfo& platformFile : qAsConst(existingList)) + for(const QFileInfo& platformFile : std::as_const(existingList)) catalogueExistingDoc(Lr::IDataDoc::Identifier(Lr::IDataDoc::Type::Platform, platformFile.baseName())); // Check for playlists @@ -99,7 +99,7 @@ Qx::Error Install::populateExistingDocs() if(existingCheck.isFailure()) return existingCheck; - for(const QFileInfo& playlistFile : qAsConst(existingList)) + for(const QFileInfo& playlistFile : std::as_const(existingList)) catalogueExistingDoc(Lr::IDataDoc::Identifier(Lr::IDataDoc::Type::Playlist, playlistFile.baseName())); // Check for special "Flashpoint" platform (more like a config doc but OK for now) diff --git a/app/src/launcher/implementation/attractmode/am-settings-data.cpp b/app/src/launcher/implementation/attractmode/am-settings-data.cpp index 069231d..a87461f 100644 --- a/app/src/launcher/implementation/attractmode/am-settings-data.cpp +++ b/app/src/launcher/implementation/attractmode/am-settings-data.cpp @@ -303,14 +303,14 @@ void CrudeSettingsWriter::writeKeyValue(const QString& key, const QString& value bool CrudeSettingsWriter::writeConfigDoc() { // Write all display entries - for(const Display& display : qAsConst(source()->mDisplays)) + for(const Display& display : std::as_const(source()->mDisplays)) { if(!writeDisplay(display)) return false; } // Write all other settings - for(const OtherSetting& setting : qAsConst(source()->mOtherSettings)) + for(const OtherSetting& setting : std::as_const(source()->mOtherSettings)) { if(!writeOtherSetting(setting)) return false; diff --git a/app/src/launcher/implementation/launchbox/lb-data.cpp b/app/src/launcher/implementation/launchbox/lb-data.cpp index 6b50488..f6c9528 100644 --- a/app/src/launcher/implementation/launchbox/lb-data.cpp +++ b/app/src/launcher/implementation/launchbox/lb-data.cpp @@ -364,7 +364,7 @@ bool PlatformDocWriter::writeSourceDoc() } // Write all custom fields - for(const std::shared_ptr& customField : qAsConst(source()->mCustomFieldsFinal)) + for(const std::shared_ptr& customField : std::as_const(source()->mCustomFieldsFinal)) { if(!writeCustomField(*customField)) return false; diff --git a/app/src/launcher/implementation/launchbox/lb-install.cpp b/app/src/launcher/implementation/launchbox/lb-install.cpp index e08713b..237bd9b 100644 --- a/app/src/launcher/implementation/launchbox/lb-install.cpp +++ b/app/src/launcher/implementation/launchbox/lb-install.cpp @@ -78,7 +78,7 @@ Qx::Error Install::populateExistingDocs() if(existingCheck.isFailure()) return existingCheck; - for(const QFileInfo& platformFile : qAsConst(existingList)) + for(const QFileInfo& platformFile : std::as_const(existingList)) catalogueExistingDoc(Lr::IDataDoc::Identifier(Lr::IDataDoc::Type::Platform, platformFile.baseName())); // Check for playlists @@ -86,7 +86,7 @@ Qx::Error Install::populateExistingDocs() if(existingCheck.isFailure()) return existingCheck; - for(const QFileInfo& playlistFile : qAsConst(existingList)) + for(const QFileInfo& playlistFile : std::as_const(existingList)) catalogueExistingDoc(Lr::IDataDoc::Identifier(Lr::IDataDoc::Type::Playlist, playlistFile.baseName())); // Check for config docs @@ -94,7 +94,7 @@ Qx::Error Install::populateExistingDocs() if(existingCheck.isFailure()) return existingCheck; - for(const QFileInfo& configDocFile : qAsConst(existingList)) + for(const QFileInfo& configDocFile : std::as_const(existingList)) catalogueExistingDoc(Lr::IDataDoc::Identifier(Lr::IDataDoc::Type::Config, configDocFile.baseName())); // Return success @@ -410,7 +410,7 @@ Qx::Error Install::postImageProcessing() Qx::Error Install::postPlaylistsImport() { // Add playlists to Parents.xml - for(const QUuid& pId : qAsConst(mModifiedPlaylistIds)) + for(const QUuid& pId : std::as_const(mModifiedPlaylistIds)) { if(!mParents->containsPlaylist(pId, PLAYLISTS_PLATFORM_CATEGORY)) { From 06129d199e6eeaf7fc6a6d54c188f9c3ab39d0a7 Mon Sep 17 00:00:00 2001 From: Christian Heimlich Date: Mon, 10 Feb 2025 12:51:33 -0500 Subject: [PATCH 12/20] Minor conformancy changes --- CMakeLists.txt | 8 ++++---- app/src/kernel/clifp.cpp | 3 ++- .../implementation/attractmode/am-settings-data.cpp | 4 ++-- app/src/launcher/implementation/launchbox/lb-install.cpp | 6 +++--- app/src/launcher/interface/lr-data-interface.cpp | 7 ++----- app/src/launcher/interface/lr-data-interface.h | 2 +- app/src/ui/mainwindow.cpp | 2 -- 7 files changed, 14 insertions(+), 18 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 47f1cac..5a12bac 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,7 +13,7 @@ project(FIL # Get helper scripts include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/FetchOBCMake.cmake) -fetch_ob_cmake("d64498ec2654e7f3145d7efa36bfaa73ac30cc74") +fetch_ob_cmake("3010a962688a63689cdc365722423f5de40c3c59") # Initialize project according to standard rules include(OB/Project) @@ -74,20 +74,20 @@ endif() include(OB/FetchQx) ob_fetch_qx( - REF "1bfe8bdb6047c1d6c3246e431652869e63cbe797" + REF "d12b1a3dd8445ba3bae1271e4a6fc6fcb0420dfd" COMPONENTS ${FIL_QX_COMPONENTS} ) # Fetch libfp (build and import from source) include(OB/Fetchlibfp) -ob_fetch_libfp("d2b1960b76c10537f2beb933206d42a31576ab07") +ob_fetch_libfp("34ff2c06224a4b97b1f3d1bcdc514a537c8116a8") # Fetch CLIFp (build and import from source) include(OB/Utility) ob_cache_project_version(CLIFp) include(OB/FetchCLIFp) -ob_fetch_clifp("f56246bad2504af08762d91ea1cc1361135c58ad") +ob_fetch_clifp("1fa81edb32119d0f0cdf7f9aa2180078361ca62b") # TODO: The shared build of this is essentially useless as only the CLIFp executable # is deployed, which only works if it's statically linked. There isn't a simple way diff --git a/app/src/kernel/clifp.cpp b/app/src/kernel/clifp.cpp index 056f989..bc94550 100644 --- a/app/src/kernel/clifp.cpp +++ b/app/src/kernel/clifp.cpp @@ -32,7 +32,7 @@ Qx::VersionNumber CLIFp::installedVersion(const Fp::Install& fpInstall) { #ifdef _WIN32 return Qx::FileDetails::readFileDetails(standardCLIFpPath(fpInstall)).fileVersion().normalized(); -#endif +#else /* TODO: For now on Linux we just return a null version so that deployment always * occurs. Eventually, find a good way to grab version info from the installed ELF. * @@ -40,6 +40,7 @@ Qx::VersionNumber CLIFp::installedVersion(const Fp::Install& fpInstall) * standardized way to embed the info as part of the ELF structure. */ return Qx::VersionNumber(); +#endif } } diff --git a/app/src/launcher/implementation/attractmode/am-settings-data.cpp b/app/src/launcher/implementation/attractmode/am-settings-data.cpp index a87461f..c66acf7 100644 --- a/app/src/launcher/implementation/attractmode/am-settings-data.cpp +++ b/app/src/launcher/implementation/attractmode/am-settings-data.cpp @@ -247,8 +247,8 @@ Lr::DocHandlingError CrudeSettingsReader::readTargetDoc() { if(!mCurrentSubSettingParser->parse(key, value, depth)) { - QString setting = mCurrentSubSettingParser->settingName(); - errorStatus = Lr::DocHandlingError(*target(), Lr::DocHandlingError::DocReadFailed, UNKNOWN_KEY_ERROR.arg(key, setting)); + QString subSetting = mCurrentSubSettingParser->settingName(); + errorStatus = Lr::DocHandlingError(*target(), Lr::DocHandlingError::DocReadFailed, UNKNOWN_KEY_ERROR.arg(key, subSetting)); break; } } diff --git a/app/src/launcher/implementation/launchbox/lb-install.cpp b/app/src/launcher/implementation/launchbox/lb-install.cpp index 237bd9b..da7e961 100644 --- a/app/src/launcher/implementation/launchbox/lb-install.cpp +++ b/app/src/launcher/implementation/launchbox/lb-install.cpp @@ -368,9 +368,9 @@ Qx::Error Install::postPlatformsImport() const QList affectedPlatforms = modifiedPlatforms(); for(const QString& pn :affectedPlatforms) { - Lb::Platform::Builder pb; - pb.wName(pn); - mPlatformsConfig->addPlatform(pb.build()); + Lb::Platform::Builder plb; + plb.wName(pn); + mPlatformsConfig->addPlatform(plb.build()); if(!mParents->containsPlatform(pn, PLATFORMS_PLATFORM_CATEGORY)) { diff --git a/app/src/launcher/interface/lr-data-interface.cpp b/app/src/launcher/interface/lr-data-interface.cpp index 84b4473..b870b3a 100644 --- a/app/src/launcher/interface/lr-data-interface.cpp +++ b/app/src/launcher/interface/lr-data-interface.cpp @@ -61,12 +61,9 @@ bool operator== (const IDataDoc::Identifier& lhs, const IDataDoc::Identifier& rh } //-Hashing------------------------------------------------------------------------------------------------------ -uint qHash(const IDataDoc::Identifier& key, uint seed) noexcept +size_t qHash(const IDataDoc::Identifier& key, size_t seed) noexcept { - seed = qHash(key.mDocType, seed); - seed = qHash(key.mDocName, seed); - - return seed; + return qHashMulti(seed, key.mDocType, key.mDocName); } //-Constructor-------------------------------------------------------------------------------------------------------- diff --git a/app/src/launcher/interface/lr-data-interface.h b/app/src/launcher/interface/lr-data-interface.h index 2eb4869..c310572 100644 --- a/app/src/launcher/interface/lr-data-interface.h +++ b/app/src/launcher/interface/lr-data-interface.h @@ -118,7 +118,7 @@ class IDataDoc class Identifier { friend bool operator== (const Identifier& lhs, const Identifier& rhs) noexcept; - friend uint qHash(const Identifier& key, uint seed) noexcept; + friend size_t qHash(const Identifier& key, size_t seed) noexcept; private: Type mDocType; diff --git a/app/src/ui/mainwindow.cpp b/app/src/ui/mainwindow.cpp index 2076737..b8c35da 100644 --- a/app/src/ui/mainwindow.cpp +++ b/app/src/ui/mainwindow.cpp @@ -367,8 +367,6 @@ Fp::Db::InclusionOptions MainWindow::getSelectedInclusionOptions() const Import::UpdateOptions MainWindow::getSelectedUpdateOptions() const { - return {ui->radioButton_onlyAdd->isChecked() ? Import::UpdateMode::OnlyNew : Import::UpdateMode::NewAndExisting, ui->checkBox_removeMissing->isChecked() }; - QRadioButton* sel = static_cast(ui->buttonGroup_updateMode->checkedButton()); Q_ASSERT(sel); return mUpdateModeMap[sel]; From 6c1bb091b13b94a1efd745059c25b5698a6fe44d Mon Sep 17 00:00:00 2001 From: Christian Heimlich Date: Tue, 11 Feb 2025 19:51:44 -0500 Subject: [PATCH 13/20] Move backup facilities out of IInstall to dedicated class --- CMakeLists.txt | 2 +- app/CMakeLists.txt | 2 + app/src/import/backup.cpp | 181 ++++++++++++++++++ app/src/import/backup.h | 111 +++++++++++ app/src/import/worker.cpp | 44 +---- app/src/kernel/controller.cpp | 8 +- .../implementation/attractmode/am-data.cpp | 5 +- .../interface/lr-install-interface.cpp | 100 ++-------- .../launcher/interface/lr-install-interface.h | 64 ------- 9 files changed, 323 insertions(+), 194 deletions(-) create mode 100644 app/src/import/backup.cpp create mode 100644 app/src/import/backup.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 5a12bac..fe4f892 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -74,7 +74,7 @@ endif() include(OB/FetchQx) ob_fetch_qx( - REF "d12b1a3dd8445ba3bae1271e4a6fc6fcb0420dfd" + REF "66ccfeff2eddd912fff7e0116539bbfe84e9503c" COMPONENTS ${FIL_QX_COMPONENTS} ) diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 9c241a0..c5dfd75 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -23,6 +23,8 @@ add_custom_target(fil_copy_clifp # ------------------ Setup FIL -------------------------- set(FIL_SOURCE + import/backup.h + import/backup.cpp import/details.h import/details.cpp import/properties.h diff --git a/app/src/import/backup.cpp b/app/src/import/backup.cpp new file mode 100644 index 0000000..5dc6871 --- /dev/null +++ b/app/src/import/backup.cpp @@ -0,0 +1,181 @@ +// Unit Includes +#include "backup.h" + +// Qt Includes +#include +#include + +namespace Import +{ + +//=============================================================================================================== +// BackupError +//=============================================================================================================== + +//-Constructor------------------------------------------------------------- +//Private: +BackupError::BackupError(Type t, const QString& s) : + mType(t), + mSpecific(s) +{} + +//Public: +BackupError::BackupError() : + mType(NoError) +{} + +//-Instance Functions------------------------------------------------------------- +//Public: +bool BackupError::isValid() const { return mType != NoError; } +QString BackupError::specific() const { return mSpecific; } +BackupError::Type BackupError::type() const { return mType; } + +//Private: +Qx::Severity BackupError::deriveSeverity() const { return Qx::Err; } +quint32 BackupError::deriveValue() const { return mType; } +QString BackupError::derivePrimary() const { return ERR_STRINGS.value(mType); } +QString BackupError::deriveSecondary() const { return mSpecific; } +QString BackupError::deriveCaption() const { return CAPTION_REVERT_ERR; } + +//=============================================================================================================== +// BackupManager +//=============================================================================================================== + +//-Constructor------------------------------------------------------------- +//Private: +BackupManager::BackupManager() {} + +//-Class Functions------------------------------------------------------------- +//Private: +QString BackupManager::filePathToBackupPath(const QString& filePath) +{ + return filePath + '.' + BACKUP_FILE_EXT; +} + +//Public: +BackupManager* BackupManager::instance() { static BackupManager inst; return &inst; } + +//-Instance Functions------------------------------------------------------------- +//Private: +BackupError BackupManager::backup(const QString& path, bool (*fn)(const QString& a, const QString& b)) +{ + // Prevent double+ backups + if(mRevertablePaths.contains(path)) + return BackupError(); + + // Note revertable + mRevertablePaths.insert(path); + + // Backup if exists + if(QFile::exists(path)) + { + QString backupPath = filePathToBackupPath(path); + + if(QFile::exists(backupPath) && QFileInfo(backupPath).isFile()) + { + if(!QFile::remove(backupPath)) + return BackupError(BackupError::FileWontDelete, backupPath); + } + + if(!fn(path, backupPath)) + return BackupError(BackupError::FileWontBackup, path); + } + + return BackupError(); +} + +BackupError BackupManager::restore(QSet::const_iterator pathItr) +{ + Q_ASSERT(pathItr != mRevertablePaths.cend()); + + const QString path = *pathItr; + mRevertablePaths.erase(pathItr); + QString backupPath = filePathToBackupPath(path); + + if(QFile::exists(path) && !QFile::remove(path)) + return BackupError(BackupError::FileWontDelete, path); + + if(!QFile::exists(path) && QFile::exists(backupPath) && !QFile::rename(backupPath, path)) + return BackupError(BackupError::FileWontRestore, backupPath); + + return BackupError(); +} + +//Public: +BackupError BackupManager::backupCopy(const QString& path) +{ + return backup(path, [](const QString& a, const QString& b){ return QFile::copy(a, b); }); +} + +BackupError BackupManager::backupRename(const QString& path) +{ + return backup(path, [](const QString& a, const QString& b){ return QFile::rename(a, b); }); +} + +BackupError BackupManager::restore(const QString& path) +{ + auto store = mRevertablePaths.constFind(path); + if(store == mRevertablePaths.cend()) + return BackupError(); + + return restore(store); +} + +BackupError BackupManager::safeReplace(const QString& src, const QString& dst, bool symlink) +{ + // Maybe make sure destination folder exists here? + + // Backup + QString backupPath = filePathToBackupPath(dst); + bool dstOccupied = QFile::exists(dst); + if(dstOccupied) + if(!QFile::rename(dst, backupPath)) // Temp backup + return BackupError(BackupError::FileWontBackup, dst); + + // Replace + std::error_code replaceError; + if(symlink) + std::filesystem::create_symlink(src.toStdString(), dst.toStdString(), replaceError); + else + replaceError = QFile::copy(src, dst) ? std::error_code() : std::make_error_code(std::io_errc::stream); + + // Restore on fail + if(replaceError) + { + if(dstOccupied) + QFile::rename(backupPath, dst); + return BackupError(BackupError::FileWontReplace, src); + } + + // Remove backup immediately + if(dstOccupied) + QFile::remove(backupPath); + else // Mark new files (only) as revertible so that existing ones will remain in the event of a revert + mRevertablePaths.insert(dst); + + return BackupError(); +} + +int BackupManager::revertQueueCount() const { return mRevertablePaths.size(); } + +int BackupManager::revertNextChange(BackupError& error, bool skipOnFail) +{ + // Ensure error message is null + error = BackupError(); + + // Delete new files and restore backups if present + if(!mRevertablePaths.isEmpty()) + { + BackupError rErr = restore(mRevertablePaths.cbegin()); + if(rErr && !skipOnFail) + error = rErr; + + return mRevertablePaths.size(); + } + + // Return 0 if all empty (shouldn't be reached if function is used correctly) + qWarning("Reversion function called with no reverts left!"); + return 0; +} + +} diff --git a/app/src/import/backup.h b/app/src/import/backup.h new file mode 100644 index 0000000..5d274b2 --- /dev/null +++ b/app/src/import/backup.h @@ -0,0 +1,111 @@ +#ifndef BACKUP_H +#define BACKUP_H + +// Qt Includes +#include +#include + +// Qx Includes +#include + +using namespace Qt::StringLiterals; + +/* TODO: The approach, or at least the language around doing a full revert (i.e. emptying the revert + * queue) could use some touch-up. + */ + +namespace Import +{ + +class QX_ERROR_TYPE(BackupError, "Lr::BackupError", 1301) +{ + friend class BackupManager; + //-Class Enums------------------------------------------------------------- +public: + enum Type + { + NoError, + FileWontDelete, + FileWontRestore, + FileWontBackup, + FileWontReplace + }; + +//-Class Variables------------------------------------------------------------- +private: + static inline const QHash ERR_STRINGS{ + {NoError, u""_s}, + {FileWontDelete, u"Cannot remove a file. It may need to be deleted manually."_s}, + {FileWontRestore, u"Cannot restore a file backup. It may need to be renamed manually.."_s}, + {FileWontBackup, u"Cannot backup file."_s}, + {FileWontReplace, u"A file that was part of a safe replace operation could not be transfered."_s} + }; + + static inline const QString CAPTION_REVERT_ERR = u"Error managing backups"_s; + +//-Instance Variables------------------------------------------------------------- +private: + Type mType; + QString mSpecific; + +//-Constructor------------------------------------------------------------- +private: + BackupError(Type t, const QString& s); + +public: + BackupError(); + +//-Instance Functions------------------------------------------------------------- +public: + bool isValid() const; + Type type() const; + QString specific() const; + +private: + Qx::Severity deriveSeverity() const override; + quint32 deriveValue() const override; + QString derivePrimary() const override; + QString deriveSecondary() const override; + QString deriveCaption() const override; +}; + +class BackupManager +{ +//-Class Variables----------------------------------------------------------------------------------------------- +private: + // Files + static inline const QString BACKUP_FILE_EXT = u"fbk"_s; + +//-Instance Variables------------------------------------------------------------- +private: + QSet mRevertablePaths; + +//-Constructor------------------------------------------------------------- +private: + BackupManager(); + +//-Class Functions------------------------------------------------------------- +private: + static QString filePathToBackupPath(const QString& filePath); + +public: + static BackupManager* instance(); + +//-Instance Functions------------------------------------------------------------- +private: + BackupError backup(const QString& path, bool (*fn)(const QString& a, const QString& b)); + BackupError restore(QSet::const_iterator pathItr); + +public: + BackupError backupCopy(const QString& path); + BackupError backupRename(const QString& path); + BackupError restore(const QString& path); + BackupError safeReplace(const QString& src, const QString& dst, bool symlink); + + int revertQueueCount() const; + int revertNextChange(BackupError& error, bool skipOnFail); +}; + +} + +#endif // BACKUP_H diff --git a/app/src/import/worker.cpp b/app/src/import/worker.cpp index d45eede..fedf4a1 100644 --- a/app/src/import/worker.cpp +++ b/app/src/import/worker.cpp @@ -14,6 +14,7 @@ // Project Includes #include "kernel/clifp.h" #include "import/details.h" +#include "import/backup.h" namespace Import { @@ -148,6 +149,7 @@ ImageTransferError Worker::transferImage(bool symlink, QString sourcePath, QStri QString sourceChecksum; QString destinationChecksum; + // TODO: Probably better to just byte-wise compare if(!Qx::calculateFileChecksum(sourceChecksum, source, QCryptographicHash::Md5).isFailure() && !Qx::calculateFileChecksum(destinationChecksum, destination, QCryptographicHash::Md5).isFailure() && sourceChecksum.compare(destinationChecksum, Qt::CaseInsensitive) == 0) @@ -160,42 +162,16 @@ ImageTransferError Worker::transferImage(bool symlink, QString sourcePath, QStri if(!destinationDir.mkpath(u"."_s)) return ImageTransferError(ImageTransferError::CantCreateDirectory, QString(), destinationDir.absolutePath()); - // Determine backup path - QString backupPath = Lr::IInstall::filePathToBackupPath(destinationInfo.absoluteFilePath()); - - // Temporarily backup image if it already exists (also acts as deletion marking in case images for the title were removed in an update) - if(destinationOccupied) - if(!QFile::rename(destinationPath, backupPath)) // Temp backup - return ImageTransferError(ImageTransferError::ImageWontBackup, QString(), destinationPath); - - // Linking error tracker - std::error_code linkError; - - // Handle transfer - if(symlink) - { - std::filesystem::create_symlink(sourcePath.toStdString(), destinationPath.toStdString(), linkError); - if(linkError) - { - QFile::rename(backupPath, destinationPath); // Restore Backup - return ImageTransferError(ImageTransferError::ImageWontLink, sourcePath, destinationPath); - } - else if(QFile::exists(backupPath)) - QFile::remove(backupPath); - else - mLauncherInstall->addRevertableFile(destinationPath); // Only queue image to be removed on failure if its new, so existing images aren't deleted on revert - } - else + // Transfer image + BackupError bErr = BackupManager::instance()->safeReplace(sourcePath, destinationPath, symlink); + if(bErr) { - if(!QFile::copy(sourcePath, destinationPath)) - { - QFile::rename(backupPath, destinationPath); // Restore Backup - return ImageTransferError(ImageTransferError::ImageWontCopy, sourcePath, destinationPath); - } - else if(QFile::exists(backupPath)) - QFile::remove(backupPath); + if(bErr.type() == BackupError::FileWontBackup) + return ImageTransferError(ImageTransferError::ImageWontBackup, QString(), destinationPath); + else if(bErr.type() == BackupError::FileWontReplace) + return ImageTransferError(symlink ? ImageTransferError::ImageWontLink : ImageTransferError::ImageWontCopy, QString(), destinationPath); else - mLauncherInstall->addRevertableFile(destinationPath); // Only queue image to be removed on failure if its new, so existing images aren't deleted on revert + qFatal("Unhandled image transfer error type."); } // Return null error on success diff --git a/app/src/kernel/controller.cpp b/app/src/kernel/controller.cpp index 47cdc06..c75dd19 100644 --- a/app/src/kernel/controller.cpp +++ b/app/src/kernel/controller.cpp @@ -12,6 +12,7 @@ // Project Includes #include "launcher/abstract/lr-registration.h" +#include "import/backup.h" /* TODO: Consider having this tool deploy a .ini file (or the like) into the target launcher install * (with the exact location probably being guided by the specific Install child) that saves the settings @@ -111,14 +112,15 @@ void Controller::revertAllLauncherChanges() // Trackers bool tempSkip = false; bool alwaysSkip = false; - Lr::RevertError currentError; + Import::BackupError currentError; int retryChoice; // Progress + auto bm = Import::BackupManager::instance(); mProgressPresenter.setMinimum(0); - mProgressPresenter.setMaximum(launcher->revertQueueCount()); + mProgressPresenter.setMaximum(bm->revertQueueCount()); mProgressPresenter.setCaption(CAPTION_REVERT); - while(launcher->revertNextChange(currentError, alwaysSkip || tempSkip) != 0) + while(bm->revertNextChange(currentError, alwaysSkip || tempSkip) != 0) { // Check for error if(!currentError.isValid()) diff --git a/app/src/launcher/implementation/attractmode/am-data.cpp b/app/src/launcher/implementation/attractmode/am-data.cpp index 505e487..6f222aa 100644 --- a/app/src/launcher/implementation/attractmode/am-data.cpp +++ b/app/src/launcher/implementation/attractmode/am-data.cpp @@ -494,9 +494,8 @@ Lr::DocHandlingError PlatformInterfaceWriter::writeOutOf() // Write overviews for(const auto& o : std::as_const(source()->mOverviews)) { - if(mOverviewWriter.writeOverview(o)) - source()->install()->addRevertableFile(mOverviewWriter.currentFilePath()); - else + // This uses QSaveFile as a form of "safe replace" write, so we don't need to manually back-up + if(!mOverviewWriter.writeOverview(o)) return Lr::DocHandlingError(*source(), Lr::DocHandlingError::DocWriteFailed, mOverviewWriter.fileErrorString()); } diff --git a/app/src/launcher/interface/lr-install-interface.cpp b/app/src/launcher/interface/lr-install-interface.cpp index 2a1ea03..7b508b0 100644 --- a/app/src/launcher/interface/lr-install-interface.cpp +++ b/app/src/launcher/interface/lr-install-interface.cpp @@ -1,38 +1,12 @@ // Unit Include #include "lr-install-interface.h" +// Project Includes +#include "import/backup.h" + namespace Lr { -//=============================================================================================================== -// RevertError -//=============================================================================================================== - -//-Constructor------------------------------------------------------------- -//Private: -RevertError::RevertError(Type t, const QString& s) : - mType(t), - mSpecific(s) -{} - -//Public: -RevertError::RevertError() : - mType(NoError) -{} - -//-Instance Functions------------------------------------------------------------- -//Public: -bool RevertError::isValid() const { return mType != NoError; } -QString RevertError::specific() const { return mSpecific; } -RevertError::Type RevertError::type() const { return mType; } - -//Private: -Qx::Severity RevertError::deriveSeverity() const { return Qx::Err; } -quint32 RevertError::deriveValue() const { return mType; } -QString RevertError::derivePrimary() const { return ERR_STRINGS.value(mType); } -QString RevertError::deriveSecondary() const { return mSpecific; } -QString RevertError::deriveCaption() const { return CAPTION_REVERT_ERR; } - //=============================================================================================================== // IInstall //=============================================================================================================== @@ -47,12 +21,6 @@ IInstall::IInstall(const QString& installPath) : //Public: IInstall::~IInstall() {} -//Public: -QString IInstall::filePathToBackupPath(const QString& filePath) -{ - return filePath + '.' + BACKUP_FILE_EXT; -} - //-Instance Functions-------------------------------------------------------------------------------------------- //Private: bool IInstall::containsAnyDataDoc(IDataDoc::Type type, const QList& names) const @@ -126,6 +94,7 @@ DocHandlingError IInstall::commitDataDocument(std::shared_ptr IDataDoc::Identifier id = docToSave->identifier(); // Check if the doc was saved previously to prevent double-backups + // TODO: SEE IF THIS LEVEL OF DISTINCTION IS NEEDED WITH NEW BACKUP STRAT bool wasDeleted = mDeletedDocuments.contains(id); bool wasModified = mModifiedDocuments.contains(id); bool wasUntouched = !wasDeleted && !wasModified; @@ -133,23 +102,14 @@ DocHandlingError IInstall::commitDataDocument(std::shared_ptr // Handle backup/revert prep if(wasUntouched) { - QString docPath = docToSave->path(); - mRevertableFilePaths.append(docPath); // Correctly handles if doc ends up deleted - // Backup - if(QFile::exists(docPath)) - { - QString backupPath = filePathToBackupPath(docPath); - - if(QFile::exists(backupPath) && QFileInfo(backupPath).isFile()) - { - if(!QFile::remove(backupPath)) - return DocHandlingError(*docToSave, DocHandlingError::CantRemoveBackup); - } - - if(!QFile::copy(docPath, backupPath)) - return DocHandlingError(*docToSave, DocHandlingError::CantCreateBackup); - } + QString docPath = docToSave->path(); + Import::BackupError bErr = Import::BackupManager::instance()->backupCopy(docPath); + if(bErr.type() == Import::BackupError::FileWontDelete) + return DocHandlingError(*docToSave, DocHandlingError::CantRemoveBackup); + else if(bErr.type() == Import::BackupError::FileWontBackup) + return DocHandlingError(*docToSave, DocHandlingError::CantCreateBackup); + Q_ASSERT(!bErr.isValid()); // All relevant types should be handled here } // Error State @@ -189,7 +149,6 @@ QString IInstall::path() const { return mRootDirectory.absolutePath(); } void IInstall::softReset() { - mRevertableFilePaths.clear(); mModifiedDocuments.clear(); mDeletedDocuments.clear(); mLeasedDocuments.clear(); @@ -231,43 +190,6 @@ bool IInstall::containsAnyPlaylist(const QList& names) const return containsAnyDataDoc(IDataDoc::Type::Playlist, names); } -void IInstall::addRevertableFile(const QString& filePath) { mRevertableFilePaths.append(filePath); } -int IInstall::revertQueueCount() const { return mRevertableFilePaths.size(); } - -int IInstall::revertNextChange(RevertError& error, bool skipOnFail) -{ - // Ensure error message is null - error = RevertError(); - - // Get operation count for return - int operationsLeft = mRevertableFilePaths.size(); - - // Delete new files and restore backups if present - if(!mRevertableFilePaths.isEmpty()) - { - QString filePath = mRevertableFilePaths.takeFirst(); - QString backupPath = filePathToBackupPath(filePath); - - if(QFile::exists(filePath) && !QFile::remove(filePath) && !skipOnFail) - { - error = RevertError(RevertError::FileWontDelete, filePath); - return operationsLeft; - } - - if(!QFile::exists(filePath) && QFile::exists(backupPath) && !QFile::rename(backupPath, filePath) && !skipOnFail) - { - error = RevertError(RevertError::FileWontRestore, backupPath); - return operationsLeft; - } - - // Decrement op count - return operationsLeft - 1; - } - - // Return 0 if all empty (shouldn't be reached if function is used correctly) - return 0; -} - /* These functions can be overridden by children as needed. * Work within them should be kept as minimal as possible since they are not accounted * for by the import progress indicator. diff --git a/app/src/launcher/interface/lr-install-interface.h b/app/src/launcher/interface/lr-install-interface.h index 6c01bd9..daf6b5e 100644 --- a/app/src/launcher/interface/lr-install-interface.h +++ b/app/src/launcher/interface/lr-install-interface.h @@ -11,61 +11,9 @@ namespace Lr { -class QX_ERROR_TYPE(RevertError, "Lr::RevertError", 1301) -{ - friend class IInstall; -//-Class Enums------------------------------------------------------------- -public: - enum Type - { - NoError = 0, - FileWontDelete = 1, - FileWontRestore = 2 - }; - -//-Class Variables------------------------------------------------------------- -private: - static inline const QHash ERR_STRINGS{ - {NoError, u""_s}, - {FileWontDelete, u"Cannot remove a file. It may need to be deleted manually."_s}, - {FileWontRestore, u"Cannot restore a file backup. It may need to be renamed manually.."_s} - }; - - static inline const QString CAPTION_REVERT_ERR = u"Error reverting changes"_s; - -//-Instance Variables------------------------------------------------------------- -private: - Type mType; - QString mSpecific; - -//-Constructor------------------------------------------------------------- -private: - RevertError(Type t, const QString& s); - -public: - RevertError(); - -//-Instance Functions------------------------------------------------------------- -public: - bool isValid() const; - Type type() const; - QString specific() const; - -private: - Qx::Severity deriveSeverity() const override; - quint32 deriveValue() const override; - QString derivePrimary() const override; - QString deriveSecondary() const override; - QString deriveCaption() const override; -}; - class IInstall { //-Class Variables----------------------------------------------------------------------------------------------- -private: - // Files - static inline const QString BACKUP_FILE_EXT = u"fbk"_s; - protected: // Files static inline const QString IMAGE_EXT = u"png"_s; @@ -88,9 +36,6 @@ class IInstall QSet mDeletedDocuments; QSet mLeasedDocuments; - // Backup/Deletion tracking - QStringList mRevertableFilePaths; - //-Constructor--------------------------------------------------------------------------------------------------- public: IInstall(const QString& installPath); @@ -103,10 +48,6 @@ class IInstall private: static void ensureModifiable(const QString& filePath); -public: - // TODO: Improve the backup system so that its more encapsulated and this doesn't need to be public - static QString filePathToBackupPath(const QString& filePath); - //-Instance Functions--------------------------------------------------------------------------------------------------------- private: // Support @@ -151,11 +92,6 @@ class IInstall virtual DocHandlingError commitPlatformDoc(std::unique_ptr platformDoc) = 0; virtual DocHandlingError commitPlaylistDoc(std::unique_ptr playlistDoc) = 0; - // Reversion - void addRevertableFile(const QString& filePath); - int revertQueueCount() const; - int revertNextChange(RevertError& error, bool skipOnFail); - // Import stage notifier hooks virtual Qx::Error preImport(); virtual Qx::Error postImport(); From 4e7b71671a01c931c56d612de19f5b3125bbf402 Mon Sep 17 00:00:00 2001 From: Christian Heimlich Date: Tue, 11 Feb 2025 21:36:51 -0500 Subject: [PATCH 14/20] Update README.md --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 637a15b..e1ed136 100644 --- a/README.md +++ b/README.md @@ -170,9 +170,11 @@ This tool automatically handles installing/updating the command-line interface F ### Summary - - C++20 + - C++23 - CMake >= 3.24.0 - - Targets Windows 10 and above + - Targets + - Windows 10+ + - Ubuntu 20.04+ ### Dependencies - Qt6 From faf04c9652f51f77d881f5c69379b4ab7f407dde Mon Sep 17 00:00:00 2001 From: Christian Heimlich Date: Wed, 12 Feb 2025 20:30:40 -0500 Subject: [PATCH 15/20] Use Qx::execute() for AM exe version --- CMakeLists.txt | 2 +- .../attractmode/am-install_linux.cpp | 21 +++---------------- 2 files changed, 4 insertions(+), 19 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index fe4f892..2786fb7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -74,7 +74,7 @@ endif() include(OB/FetchQx) ob_fetch_qx( - REF "66ccfeff2eddd912fff7e0116539bbfe84e9503c" + REF "f98cb3e3af74af09d277b4a45c414a5f22aef4f3" COMPONENTS ${FIL_QX_COMPONENTS} ) diff --git a/app/src/launcher/implementation/attractmode/am-install_linux.cpp b/app/src/launcher/implementation/attractmode/am-install_linux.cpp index 13df019..9dfad63 100644 --- a/app/src/launcher/implementation/attractmode/am-install_linux.cpp +++ b/app/src/launcher/implementation/attractmode/am-install_linux.cpp @@ -3,6 +3,7 @@ // Qx Includes #include +#include namespace Am { @@ -12,24 +13,8 @@ namespace Am QString Install::versionFromExecutable() const { - QProcess attract; - attract.setProgram(MAIN_EXE_PATH); - attract.setArguments({"--version"}); - - attract.start(); - if(!attract.waitForStarted(1000)) - return QString(); - - if(!attract.waitForFinished(1000)) - { - attract.kill(); // Force close - attract.waitForFinished(); - - return QString(); - } - - QString versionInfo = QString::fromLatin1(attract.readAllStandardOutput()); - QRegularExpressionMatch sv = Qx::RegularExpression::SEMANTIC_VERSION.match(versionInfo); + Qx::ExecuteResult res = Qx::execute(MAIN_EXE_PATH, {"--version"}, 1000); + QRegularExpressionMatch sv = Qx::RegularExpression::SEMANTIC_VERSION.match(res.output); return sv.hasMatch() ? sv.captured() : QString(); } From 9edb02df2227f9731a8b4c3e551492c0627fb89f Mon Sep 17 00:00:00 2001 From: Christian Heimlich Date: Fri, 14 Feb 2025 10:20:23 -0500 Subject: [PATCH 16/20] Remove no longer needeed deleted doc tracking --- app/src/import/backup.cpp | 2 +- .../interface/lr-install-interface.cpp | 36 +++++-------------- .../launcher/interface/lr-install-interface.h | 1 - 3 files changed, 9 insertions(+), 30 deletions(-) diff --git a/app/src/import/backup.cpp b/app/src/import/backup.cpp index 5dc6871..05e8ee1 100644 --- a/app/src/import/backup.cpp +++ b/app/src/import/backup.cpp @@ -59,7 +59,7 @@ BackupManager* BackupManager::instance() { static BackupManager inst; return &in //Private: BackupError BackupManager::backup(const QString& path, bool (*fn)(const QString& a, const QString& b)) { - // Prevent double+ backups + // Prevent double+ backups (THIS IS CRITICAL, HENCE WHY A SET IS USED) if(mRevertablePaths.contains(path)) return BackupError(); diff --git a/app/src/launcher/interface/lr-install-interface.cpp b/app/src/launcher/interface/lr-install-interface.cpp index 7b508b0..b989a91 100644 --- a/app/src/launcher/interface/lr-install-interface.cpp +++ b/app/src/launcher/interface/lr-install-interface.cpp @@ -93,24 +93,14 @@ DocHandlingError IInstall::commitDataDocument(std::shared_ptr auto docToSave = docWriter->source(); IDataDoc::Identifier id = docToSave->identifier(); - // Check if the doc was saved previously to prevent double-backups - // TODO: SEE IF THIS LEVEL OF DISTINCTION IS NEEDED WITH NEW BACKUP STRAT - bool wasDeleted = mDeletedDocuments.contains(id); - bool wasModified = mModifiedDocuments.contains(id); - bool wasUntouched = !wasDeleted && !wasModified; - - // Handle backup/revert prep - if(wasUntouched) - { - // Backup - QString docPath = docToSave->path(); - Import::BackupError bErr = Import::BackupManager::instance()->backupCopy(docPath); - if(bErr.type() == Import::BackupError::FileWontDelete) - return DocHandlingError(*docToSave, DocHandlingError::CantRemoveBackup); - else if(bErr.type() == Import::BackupError::FileWontBackup) - return DocHandlingError(*docToSave, DocHandlingError::CantCreateBackup); - Q_ASSERT(!bErr.isValid()); // All relevant types should be handled here - } + // Backup (redundant backups are prevented). Acts as deletion for empty docs + QString docPath = docToSave->path(); + Import::BackupError bErr = Import::BackupManager::instance()->backupCopy(docPath); + if(bErr.type() == Import::BackupError::FileWontDelete) + return DocHandlingError(*docToSave, DocHandlingError::CantRemoveBackup); + else if(bErr.type() == Import::BackupError::FileWontBackup) + return DocHandlingError(*docToSave, DocHandlingError::CantCreateBackup); + Q_ASSERT(!bErr.isValid()); // All relevant types should be handled here // Error State DocHandlingError commitError; @@ -119,18 +109,9 @@ DocHandlingError IInstall::commitDataDocument(std::shared_ptr if(!docToSave->isEmpty()) { mModifiedDocuments.insert(id); - if(wasDeleted) - mDeletedDocuments.remove(id); - commitError = docWriter->writeOutOf(); ensureModifiable(docToSave->path()); } - else // Handle deletion - { - mDeletedDocuments.insert(id); - if(wasModified) - mModifiedDocuments.remove(id); - } // Remove handle reservation mLeasedDocuments.remove(docToSave->identifier()); @@ -150,7 +131,6 @@ QString IInstall::path() const { return mRootDirectory.absolutePath(); } void IInstall::softReset() { mModifiedDocuments.clear(); - mDeletedDocuments.clear(); mLeasedDocuments.clear(); } diff --git a/app/src/launcher/interface/lr-install-interface.h b/app/src/launcher/interface/lr-install-interface.h index daf6b5e..8bfb10a 100644 --- a/app/src/launcher/interface/lr-install-interface.h +++ b/app/src/launcher/interface/lr-install-interface.h @@ -33,7 +33,6 @@ class IInstall // Document tracking QSet mExistingDocuments; QSet mModifiedDocuments; - QSet mDeletedDocuments; QSet mLeasedDocuments; //-Constructor--------------------------------------------------------------------------------------------------- From e55286c76a0bd3d35069013d0474d005825d92af Mon Sep 17 00:00:00 2001 From: Christian Heimlich Date: Fri, 14 Feb 2025 15:10:54 -0500 Subject: [PATCH 17/20] Cleanup --- app/src/import/backup.h | 2 +- app/src/launcher/abstract/lr-data.tpp | 2 +- app/src/launcher/implementation/attractmode/am-data.cpp | 3 ++- app/src/launcher/interface/lr-install-interface.h | 4 ---- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/app/src/import/backup.h b/app/src/import/backup.h index 5d274b2..8620966 100644 --- a/app/src/import/backup.h +++ b/app/src/import/backup.h @@ -20,7 +20,7 @@ namespace Import class QX_ERROR_TYPE(BackupError, "Lr::BackupError", 1301) { friend class BackupManager; - //-Class Enums------------------------------------------------------------- +//-Class Enums------------------------------------------------------------- public: enum Type { diff --git a/app/src/launcher/abstract/lr-data.tpp b/app/src/launcher/abstract/lr-data.tpp index e293aa9..3f053a7 100644 --- a/app/src/launcher/abstract/lr-data.tpp +++ b/app/src/launcher/abstract/lr-data.tpp @@ -183,7 +183,7 @@ void BasicPlatformDoc::finalize() /* TODO: Have this (and all other implementations of finalize() do something like return * the IDs of titles that were removed, or otherwise populate an internal variable so that afterwards * the list can be used to purge all images or other title related files (like overviews with AM). - * Right now only the data portion of old games is removed) + * Right now only the data portion of old games is removed). */ // Finalize item stores diff --git a/app/src/launcher/implementation/attractmode/am-data.cpp b/app/src/launcher/implementation/attractmode/am-data.cpp index 6f222aa..0ac1a12 100644 --- a/app/src/launcher/implementation/attractmode/am-data.cpp +++ b/app/src/launcher/implementation/attractmode/am-data.cpp @@ -440,7 +440,8 @@ std::shared_ptr PlatformInterface::processSet(const Fp::Set& set) /* Ignore non-playable add apps to avoid useless clutter in AM * TODO: Consider doing this in Import Worker to make it a standard since * LB doesn't actually need the non-playable entries either. Importing them - * is basically a leftover from an earlier CLIFp version + * is basically a leftover from an earlier CLIFp version that required them + * for games to work (i.e. before auto mode). */ if(addApp.isPlayable()) mPlatformTaglist.appendTag(addApp.id().toString(QUuid::WithoutBraces)); diff --git a/app/src/launcher/interface/lr-install-interface.h b/app/src/launcher/interface/lr-install-interface.h index 8bfb10a..f2d864a 100644 --- a/app/src/launcher/interface/lr-install-interface.h +++ b/app/src/launcher/interface/lr-install-interface.h @@ -18,10 +18,6 @@ class IInstall // Files static inline const QString IMAGE_EXT = u"png"_s; -public: - // Image Errors - static inline const QString CAPTION_IMAGE_ERR = u"Error importing game image(s)"_s; - //-Instance Variables-------------------------------------------------------------------------------------------- private: // Validity From bca75863b33ba83681d19cbe854f0db70eccd44c Mon Sep 17 00:00:00 2001 From: Christian Heimlich Date: Sat, 15 Feb 2025 15:10:03 -0500 Subject: [PATCH 18/20] Break up doc markdown files and move images to repo --- README.md | 142 +++---------------------------------- doc/COMPILING.md | 23 ++++++ doc/LAUNCHERS.md | 58 +++++++++++++++ doc/USAGE.md | 51 +++++++++++++ doc/images/logo.png | Bin 0 -> 280195 bytes doc/images/main_window.png | Bin 0 -> 43667 bytes doc/images/tag_filter.png | Bin 0 -> 7133 bytes 7 files changed, 140 insertions(+), 134 deletions(-) create mode 100644 doc/COMPILING.md create mode 100644 doc/LAUNCHERS.md create mode 100644 doc/USAGE.md create mode 100644 doc/images/logo.png create mode 100644 doc/images/main_window.png create mode 100644 doc/images/tag_filter.png diff --git a/README.md b/README.md index e1ed136..8038edb 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # FIL (Flashpoint Importer for Launchers) - + FIL is an importer tool for several launchers/frontends that allows one to add platforms and playlists from [Flashpoint Archive](https://flashpointarchive.org/) to their collection. It is fully automated and only requires the user to provide the paths to their launcher and Flashpoint installs, choose which Platforms/Playlists they wish to import, and select between a few import mode options. Once the import is started the current progress is displayed and any errors that occur are shown to the user, with resolvable errors including a prompt for what the user would like to do. After the process has completed, the specified launcher can be started and the games from Flashpoint can be played like those from any other Platform. For Platforms, the importer is capable of importing each game/animation along with any additional apps, images, and most of the metadata fields (i.e. Title, Description, etc, see below). -Checkout **[Usage (Primary)](#usage-primary)** to get started. +Checkout **[Usage](#usage)** to get started. [![Dev Builds](https://github.com/oblivioncth/FIL/actions/workflows/build-project.yml/badge.svg?branch=dev)](https://github.com/oblivioncth/FIL/actions/workflows/build-project.yml) @@ -45,118 +45,14 @@ Using a version of FIL that does not target the version of Flashpoint you wish t The title of each [release](github.com/oblivioncth/FIL/releases) will indicate which version of Flashpoint it targets. ## Launcher Specific Details -*If enough frontends are added this section will likely be converted into a wiki.* +See [LAUNCHER](doc/LAUNCHER.md) --------------------------------------------------------------------------------------------------- -**LaunchBox** +## Usage +Essentially, you just need to download and run the program, provide paths to both your Flashpoint and Launcher installs, select your options, and go. -The import strategy for LaunchBox results in a setup that is straightforward and very similar to when Flashpoint Archive used LaunchBox as its frontend. Platforms to platforms, playlists to playlists, games to games, additional apps to additional apps, and so forth. +For more details, see [USAGE](doc/USAGE.md) -Each platform is grouped within the platform category "Flashpoint". - -All entry metadata is converted to its nearest LaunchBox equivalent, with nearly all fields being covered. One minor exception is the Flashpoint "Language" field, as it is added as a LaunchBox Custom Field, which requires a premium license to see. - -Everything should work out-of-the-box after an import. - --------------------------------------------------------------------------------------------------- -**AttractMode** - -Summary: - - Everything is considered to be tied to the platform/system "Flashpoint", as well as an emulator by the same name - - All selections are imported to a master "Flashpoint" romlist - - A tag list is created for each Platform and Playlist with the prefixes "[Platform]" and "[Playlist]" respectively - - Game descriptions are added as overviews - - After each import, if a Display titled "Flashpoint" is not present in your config, one will be created with sensible defaults - - A Flashpoint system marquee is provided - - Additional applications are added as romlist entries using the following naming scheme for their title `[parent_game_name] |> [add_app_name]` - - Title images are added as 'flyers' and screenshots are added as 'snaps' - - Everything should work out-of-the-box after an import - -Details: - -The default Display entry will only be created if it's missing, allowing you to customize it as you see fit afterwards; however, the Platform/Playlist specific filters will always be updated to match your selections from the most recent import. Alternatively you can simply make your own Display entry under a different name and leave the default alone (as well as potentially. - -The default sort of all Display filters uses the 'AltTitle' field, which is based on Flashpoint's 'sortTitle' field. This guarantees the that all games appear in the same order as they do within Flashpoint and that additional applications appear directly under their parent games. - -The romlist fields are mapped as follows (AttractMode `->` Flashpoint): - - - Name `->` Title ID - - Title `->` Title - - Platform `->` Platform - - Emulator `->` "Flashpoint" - - CloneOf `->` Parent Title ID (if an additional app) - - Year `->` Release date-time (date portion only) - - Manufacturer `->` Developer - - Players `->` Play Mode - - Status `->` Status - - AltTitle `->` Sort Title (use for correct sorting) - - Series `->` Series - - Language `->` Language - -Any fields not listed are unused or set to a general default. - -All of the default AttractMode layouts don't work particularly well with Flashpoint. The main issues are: - - - Not enough space for many titles, especially additional apps - - Some layouts showing the "AltTitle" of each entry beside them, wasting further space since that field is used for sorting purposes in this use case and isn't really intended to be displayed. - - Most layouts stretch the images instead of preserving their aspect ratio. This can be changed for some layouts, though since layout settings are global this will affect all of your displays so I did not configure the importer to make this change automatically. - - No layouts actually display overviews - -For this reason it is recommended to use a third-party layout that avoids these issues as best as possible. I cannot recommend one as at this time I do not use AttractMode personally. In the future I may try creating a simple one that is ideal for Flashpoint, thought I cannot promise this. If someone wants to share one they end up creating or recommend an existing one that works well that would be appreciated. - -Given that AttractMode is highly customizable and designed to encourage each user to have a unique-to-them setup, ultimately you can do whatever you want with the resultant romlist, tag lists, and overviews. The default Display/Filters are just for getting started. - -## Usage (Primary) - - **Before using FIL, be sure to have ran Flashpoint through its regular launcher at least once** - - 1. Download and run the latest [release](https://github.com/oblivioncth/FIL/releases) (the static variant is recommended) - 2. Ensure Flashpoint and the launcher are both not running - 3. Manually specify or browse for the path to your launcher install, the utility will let you know if there are any problems. If everything is OK the icon next to the install path will change to a green check - 4. Manually specify or browse for the path to your Flashpoint install, the utility will let you know if there are any problems. If everything is OK the icon next to the install path will change to a green check - 5. The lists of available Platforms and Playlists will quickly load - 6. Select which Platforms and Playlists you want to import. Existing entries that are considered an update will be highlighted in green - 7. If importing Playlists, select a Playlist Game Mode. These are described with the nearby Help button in the program, but here is a basic overview of their differences: - - **Selected Platforms Only** - Only games that are present within the selected platforms will be included - - **Force All** - All games in the playlist will be included, importing portions of unselected platforms as required - 8. If any entries you have selected are for updates you may select update mode settings. These are described with the nearby Help button in the program, but here is a basic overview of their differences: - - (Exclusive) **New Only** - Only adds new games - - (Exclusive) **New & Existing** - Adds new games and updates the non-user specific metadata for games already in your collection - - (Applies to either of the above) **Remove Missing** - Removes any games from your collection for the selected Platforms that are no longer in Flashpoint - 9. Select a method to handle game images. These are described with the nearby Help button in the program, but here is a basic overview of their differences: - - **Copy** - Copies all relevant images from Flashpoint into your launcher install (slow import) - - **Reference** - Changes your launcher install configuration to directly use the Flashpoint images in-place (slow image refresh) - - **Symlink** - Creates a symbolic link to all relevant images from Flashpoint into your launcher install. Overall the best option - - 10. Press the "Start Import" button - -The symbolic link related options for handling images require the importer to be run as an administrator or for you to enable [Developer mode](https://www.howtogeek.com/292914/what-is-developer-mode-in-windows-10/#:~:text=How%20to%20Enable%20Developer%20Mode,be%20put%20into%20Developer%20Mode.) within Windows 10 - -**Example:** - -![FIL Example Usage](https://i.imgur.com/YrlecCK.png) - -## Usage (Tools) - -### Tag Filter -The tag filter editor allows you to customize which titles will be imported based on their tags. - -![Tag Filter](https://i.imgur.com/EzEd0H1.png) - -Tags are listed alphabetically, nested under their categories names so that you can select or unselect an entire category easily. Exclusions take precedence, so if a title features a single tag that you have unselected it will not be included in the import. - -All tags are included by default. - -### Image Downloading -Only available when using Flashpoint Infinity, the "Force Download Images" option will download the cover art and screenshot for each imported title if they have not yet been retrieved through normal use of Infinity. - -**WARNING:** The Flashpoint Infinity client was only designed to download images gradually while scrolling through titles within its interface, and so the Flashpoint image server has bandwidth restrictions that severely limit the practicality of downloading a large number of images in bulk. Therefore, it is recommended to only use this feature when using Infinity to access a small subset of the Flashpoint collection, such as a specific playlist, or curated list of favorites. Otherwise, if having all game images available in your launcher is important to you, you should be using Ultimate, or be prepared to wait an **extremely** long time. - -### Animations -Since most launchers are game oriented, animations are ignored by default. If you wish to include them you can do so by selecting the "Include Animations" option. - -### CLIFp Distribution -This tool automatically handles installing/updating the command-line interface Flashpoint client as needed; however, if for whatever reason you deem it necessary/useful to manually insert a copy of FIL's bundled CLIFp version, you can do so using the "Deploy CLIFp" option. +![Main Window](doc/images/main_window.png) ## Other Features - The playlist import feature is "smart" in the sense that it won't include games that you aren't importing. So if you only want to import the Flash platform for example and a couple playlists, you wont have to worry about useless entries in the playlist that point to games from other platforms you didn't import. This of course does not apply if you are using the "Force All" playlist game mode. @@ -188,26 +84,4 @@ This tool automatically handles installing/updating the command-line interface F The source for this project is managed by a sensible CMake configuration that allows for straightforward compilation and consumption of its target(s), either as a sub-project or as an imported package. All required dependencies except for Qt6 are automatically acquired via CMake's FetchContent mechanism. ### Building -Ensure Qt6 is installed and locatable by CMake (or alternatively use the `qt-cmake` script that comes with Qt in-place of the`cmake` command). - -Right now, a static build is required in order for CLIFp to work correctly. - -Should work with MSVC, MINGW64, clang, and gcc. - -``` -# Acquire source -git clone https://github.com/oblivioncth/FIL - -# Configure (ninja optional, but recommended) -cmake -S FIL -B build-FIL -G "Ninja Multi-config" - -# Build -cmake --build build-FIL - -# Install -cmake --install build-FIL - -# Run -cd "build-FIL/out/install/bin" -fil -``` +See [COMPILING](doc/COMPILING.md) diff --git a/doc/COMPILING.md b/doc/COMPILING.md new file mode 100644 index 0000000..6dd6e11 --- /dev/null +++ b/doc/COMPILING.md @@ -0,0 +1,23 @@ +Ensure Qt6 is installed and locatable by CMake (or alternatively use the `qt-cmake` script that comes with Qt in-place of the`cmake` command). + +Right now, a static build is required in order for CLIFp to work correctly. + +Should work with MSVC, MINGW64, clang, and gcc. + +``` +# Acquire source +git clone https://github.com/oblivioncth/FIL + +# Configure (ninja optional, but recommended) +cmake -S FIL -B build-FIL -G "Ninja Multi-config" + +# Build +cmake --build build-FIL + +# Install +cmake --install build-FIL + +# Run +cd "build-FIL/out/install/bin" +fil +``` \ No newline at end of file diff --git a/doc/LAUNCHERS.md b/doc/LAUNCHERS.md new file mode 100644 index 0000000..7f6d2c0 --- /dev/null +++ b/doc/LAUNCHERS.md @@ -0,0 +1,58 @@ +# Launcher Specific Details + +## LaunchBox + +The import strategy for LaunchBox results in a setup that is straightforward and very similar to when Flashpoint Archive used LaunchBox as its frontend. Platforms to platforms, playlists to playlists, games to games, additional apps to additional apps, and so forth. + +Each platform is grouped within the platform category "Flashpoint". + +All entry metadata is converted to its nearest LaunchBox equivalent, with nearly all fields being covered. One minor exception is the Flashpoint "Language" field, as it is added as a LaunchBox Custom Field, which requires a premium license to see. + +Everything should work out-of-the-box after an import. + +## AttractMode + +Summary: + - Everything is considered to be tied to the platform/system "Flashpoint", as well as an emulator by the same name + - All selections are imported to a master "Flashpoint" romlist + - A tag list is created for each Platform and Playlist with the prefixes "[Platform]" and "[Playlist]" respectively + - Game descriptions are added as overviews + - After each import, if a Display titled "Flashpoint" is not present in your config, one will be created with sensible defaults + - A Flashpoint system marquee is provided + - Additional applications are added as romlist entries using the following naming scheme for their title `[parent_game_name] |> [add_app_name]` + - Title images are added as 'flyers' and screenshots are added as 'snaps' + - Everything should work out-of-the-box after an import + +Details: + +The default Display entry will only be created if it's missing, allowing you to customize it as you see fit afterwards; however, the Platform/Playlist specific filters will always be updated to match your selections from the most recent import. Alternatively you can simply make your own Display entry under a different name and leave the default alone (as well as potentially. + +The default sort of all Display filters uses the 'AltTitle' field, which is based on Flashpoint's 'sortTitle' field. This guarantees the that all games appear in the same order as they do within Flashpoint and that additional applications appear directly under their parent games. + +The romlist fields are mapped as follows (AttractMode `->` Flashpoint): + + - Name `->` Title ID + - Title `->` Title + - Platform `->` Platform + - Emulator `->` "Flashpoint" + - CloneOf `->` Parent Title ID (if an additional app) + - Year `->` Release date-time (date portion only) + - Manufacturer `->` Developer + - Players `->` Play Mode + - Status `->` Status + - AltTitle `->` Sort Title (use for correct sorting) + - Series `->` Series + - Language `->` Language + +Any fields not listed are unused or set to a general default. + +All of the default AttractMode layouts don't work particularly well with Flashpoint. The main issues are: + + - Not enough space for many titles, especially additional apps + - Some layouts showing the "AltTitle" of each entry beside them, wasting further space since that field is used for sorting purposes in this use case and isn't really intended to be displayed. + - Most layouts stretch the images instead of preserving their aspect ratio. This can be changed for some layouts, though since layout settings are global this will affect all of your displays so I did not configure the importer to make this change automatically. + - No layouts actually display overviews + +For this reason it is recommended to use a third-party layout that avoids these issues as best as possible. I cannot recommend one as at this time I do not use AttractMode personally. In the future I may try creating a simple one that is ideal for Flashpoint, thought I cannot promise this. If someone wants to share one they end up creating or recommend an existing one that works well that would be appreciated. + +Given that AttractMode is highly customizable and designed to encourage each user to have a unique-to-them setup, ultimately you can do whatever you want with the resultant romlist, tag lists, and overviews. The default Display/Filters are just for getting started. \ No newline at end of file diff --git a/doc/USAGE.md b/doc/USAGE.md new file mode 100644 index 0000000..bfff0b7 --- /dev/null +++ b/doc/USAGE.md @@ -0,0 +1,51 @@ +# Usage (Primary) + + **Before using FIL, be sure to have ran Flashpoint through its regular launcher at least once** + + 1. Download and run the latest [release](https://github.com/oblivioncth/FIL/releases) (the static variant is recommended) + 2. Ensure Flashpoint and the launcher are both not running + 3. Manually specify or browse for the path to your launcher install, the utility will let you know if there are any problems. If everything is OK the icon next to the install path will change to a green check + 4. Manually specify or browse for the path to your Flashpoint install, the utility will let you know if there are any problems. If everything is OK the icon next to the install path will change to a green check + 5. The lists of available Platforms and Playlists will quickly load + 6. Select which Platforms and Playlists you want to import. Existing entries that are considered an update will be highlighted in green + 7. If importing Playlists, select a Playlist Game Mode. These are described with the nearby Help button in the program, but here is a basic overview of their differences: + - **Selected Platforms Only** - Only games that are present within the selected platforms will be included + - **Force All** - All games in the playlist will be included, importing portions of unselected platforms as required + 8. If any entries you have selected are for updates you may select update mode settings. These are described with the nearby Help button in the program, but here is a basic overview of their differences: + - (Exclusive) **New Only** - Only adds new games + - (Exclusive) **New & Existing** - Adds new games and updates the non-user specific metadata for games already in your collection + - (Applies to either of the above) **Remove Missing** - Removes any games from your collection for the selected Platforms that are no longer in Flashpoint + 9. Select a method to handle game images. These are described with the nearby Help button in the program, but here is a basic overview of their differences: + - **Copy** - Copies all relevant images from Flashpoint into your launcher install (slow import) + - **Reference** - Changes your launcher install configuration to directly use the Flashpoint images in-place (slow image refresh) + - **Symlink** - Creates a symbolic link to all relevant images from Flashpoint into your launcher install. Overall the best option + + 10. Press the "Start Import" button + +The symbolic link related options for handling images require the importer to be run as an administrator or for you to enable [Developer mode](https://www.howtogeek.com/292914/what-is-developer-mode-in-windows-10/#:~:text=How%20to%20Enable%20Developer%20Mode,be%20put%20into%20Developer%20Mode.) within Windows 10 + +**Example:** + +![FIL Example Usage](docs/images/main_window.png) + +# Usage (Tools) + +## Tag Filter +The tag filter editor allows you to customize which titles will be imported based on their tags. + +![Tag Filter](docs/images/tag_filter.png) + +Tags are listed alphabetically, nested under their categories names so that you can select or unselect an entire category easily. Exclusions take precedence, so if a title features a single tag that you have unselected it will not be included in the import. + +All tags are included by default. + +## Image Downloading +Only available when using Flashpoint Infinity, the "Force Download Images" option will download the cover art and screenshot for each imported title if they have not yet been retrieved through normal use of Infinity. + +**WARNING:** The Flashpoint Infinity client was only designed to download images gradually while scrolling through titles within its interface, and so the Flashpoint image server has bandwidth restrictions that severely limit the practicality of downloading a large number of images in bulk. Therefore, it is recommended to only use this feature when using Infinity to access a small subset of the Flashpoint collection, such as a specific playlist, or curated list of favorites. Otherwise, if having all game images available in your launcher is important to you, you should be using Ultimate, or be prepared to wait an **extremely** long time. + +## Animations +Since most launchers are game oriented, animations are ignored by default. If you wish to include them you can do so by selecting the "Include Animations" option. + +## CLIFp Distribution +This tool automatically handles installing/updating the command-line interface Flashpoint client as needed; however, if for whatever reason you deem it necessary/useful to manually insert a copy of FIL's bundled CLIFp version, you can do so using the "Deploy CLIFp" option. diff --git a/doc/images/logo.png b/doc/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..402db10f1779357e2e90282bd43548fcf6024b34 GIT binary patch literal 280195 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

7LrLPcP}2F$!B_p(dF78--B{ZFdFp|Dkc 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 literal 0 HcmV?d00001 diff --git a/doc/images/tag_filter.png b/doc/images/tag_filter.png new file mode 100644 index 0000000000000000000000000000000000000000..0a6aa7c8107e0721b59214294463f2b0be05c038 GIT binary patch literal 7133 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 mZ~R2!<4` Date: Mon, 17 Feb 2025 09:41:45 -0500 Subject: [PATCH 19/20] Initial FP14 support --- CMakeLists.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2786fb7..362ec96 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ cmake_minimum_required(VERSION 3.24.0...3.30.0) # Project # NOTE: DON'T USE TRAILING ZEROS IN VERSIONS project(FIL - VERSION 0.7.5.5 + VERSION 0.7.6 LANGUAGES CXX DESCRIPTION "Flashpoint Importer for Launchers" ) @@ -20,7 +20,7 @@ include(OB/Project) ob_standard_project_setup() # Additional Project Variables -set(TARGET_FP_VERSION_PREFIX 13.0) +set(TARGET_FP_VERSION_PREFIX 14.0) # Configuration options # Handled by fetched libs, but set this here formally since they aren't part of the main project @@ -81,13 +81,13 @@ ob_fetch_qx( # Fetch libfp (build and import from source) include(OB/Fetchlibfp) -ob_fetch_libfp("34ff2c06224a4b97b1f3d1bcdc514a537c8116a8") +ob_fetch_libfp("9995681fe222f240512e9bd83b211ecc585716db") # Fetch CLIFp (build and import from source) include(OB/Utility) ob_cache_project_version(CLIFp) include(OB/FetchCLIFp) -ob_fetch_clifp("1fa81edb32119d0f0cdf7f9aa2180078361ca62b") +ob_fetch_clifp("2853b88c193b1884f75bc85e262e02652a27aa26") # TODO: The shared build of this is essentially useless as only the CLIFp executable # is deployed, which only works if it's statically linked. There isn't a simple way From ee235e560fa6978f19f11f3ac99bdd662dc214db Mon Sep 17 00:00:00 2001 From: Christian Heimlich Date: Mon, 17 Feb 2025 10:58:31 -0500 Subject: [PATCH 20/20] Bump --- CMakeLists.txt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 362ec96..3eadcf8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,14 +6,14 @@ cmake_minimum_required(VERSION 3.24.0...3.30.0) # Project # NOTE: DON'T USE TRAILING ZEROS IN VERSIONS project(FIL - VERSION 0.7.6 + VERSION 0.8 LANGUAGES CXX DESCRIPTION "Flashpoint Importer for Launchers" ) # Get helper scripts include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/FetchOBCMake.cmake) -fetch_ob_cmake("3010a962688a63689cdc365722423f5de40c3c59") +fetch_ob_cmake("v0.3.9") # Initialize project according to standard rules include(OB/Project) @@ -74,20 +74,20 @@ endif() include(OB/FetchQx) ob_fetch_qx( - REF "f98cb3e3af74af09d277b4a45c414a5f22aef4f3" + REF "v0.6.1" COMPONENTS ${FIL_QX_COMPONENTS} ) # Fetch libfp (build and import from source) include(OB/Fetchlibfp) -ob_fetch_libfp("9995681fe222f240512e9bd83b211ecc585716db") +ob_fetch_libfp("v0.5.5") # Fetch CLIFp (build and import from source) include(OB/Utility) ob_cache_project_version(CLIFp) include(OB/FetchCLIFp) -ob_fetch_clifp("2853b88c193b1884f75bc85e262e02652a27aa26") +ob_fetch_clifp("v0.9.13") # TODO: The shared build of this is essentially useless as only the CLIFp executable # is deployed, which only works if it's statically linked. There isn't a simple way

$y(jmxzWJmR)9iC8zl=tBs2!dY+&rso`(;f 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