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 43dfe32..3eadcf8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,28 +6,28 @@ 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.8 LANGUAGES CXX DESCRIPTION "Flashpoint Importer for Launchers" ) # Get helper scripts include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/FetchOBCMake.cmake) -fetch_ob_cmake("9f45dec8dc1cd09d99862b7b7ea335b361cf7ff7") +fetch_ob_cmake("v0.3.9") # Initialize project according to standard rules 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 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 @@ -74,20 +74,20 @@ endif() include(OB/FetchQx) ob_fetch_qx( - REF "0572d288936afd63ff6f5b6ce4b1fbfc0f03b0eb" + REF "v0.6.1" COMPONENTS ${FIL_QX_COMPONENTS} ) # Fetch libfp (build and import from source) include(OB/Fetchlibfp) -ob_fetch_libfp("183a479d00235d332aa1046a9b5ba98f62699752") +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("7139ae998b292eb595e751ba4cb8599230435358") +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 @@ -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/README.md b/README.md index 637a15b..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. @@ -170,9 +66,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 @@ -186,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/app/CMakeLists.txt b/app/CMakeLists.txt index d5e7e9b..c5dfd75 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 @@ -23,48 +23,67 @@ 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/backup.h + import/backup.cpp + import/details.h + import/details.cpp + import/properties.h + import/properties.cpp + import/settings.h + import/settings.cpp + import/worker.h + import/worker.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 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/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() @@ -104,6 +123,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/frontend/fe-data.cpp b/app/src/frontend/fe-data.cpp deleted file mode 100644 index 7d8377e..0000000 --- a/app/src/frontend/fe-data.cpp +++ /dev/null @@ -1,575 +0,0 @@ -// Unit Include -#include "fe-data.h" - -// Qx Includes -#include -#include - -// Project Includes -#include "fe-install.h" - -namespace Fe -{ -//=============================================================================================================== -// 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) -{ - 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 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 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 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 feGame = prepareGame(set.game(), images); - - // Add game - addUpdateableItem(mGamesExisting, mGamesFinal, feGame); - - // Handle additional apps - for(const Fp::AddApp& addApp : set.addApps()) - { - // Prepare - std::shared_ptr feAddApp = prepareAddApp(addApp); - - // Add - addUpdateableItem(mAddAppsExisting, mAddAppsFinal, feAddApp); - } - - // Allow install to handle images if needed - parent()->processDirectGameImages(feGame.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 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 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 fePlaylistHeader = preparePlaylistHeader(playlist); - - // Ensure doc already existed before transferring (null check) - if(mPlaylistHeader) - fePlaylistHeader->transferOtherFields(mPlaylistHeader->otherFields()); - - // Set instance header to new one - mPlaylistHeader = fePlaylistHeader; - - for(const auto& plg : playlist.playlistGames()) - { - // Prepare playlist game - std::shared_ptr fePlaylistGame = preparePlaylistGame(plg); - - // Add playlist game - addUpdateableItem(mPlaylistGamesExisting, mPlaylistGamesFinal, fePlaylistGame); - } - } -} - -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(Fe::DataDoc* targetDoc, const QString& root) : - Fe::DataDoc::Reader(targetDoc), - mXmlFile(targetDoc->path()), - mStreamReader(&mXmlFile), - mRootElement(root) -{} - -//-Instance Functions------------------------------------------------------------------------------------------------- -//Protected: -Fe::DocHandlingError XmlDocReader::streamStatus() const -{ - if(mStreamReader.hasError()) - { - Qx::XmlStreamReaderError xmlError(mStreamReader); - return Fe::DocHandlingError(*mTargetDocument, Fe::DocHandlingError::DocReadFailed, xmlError.text()); - } - - return Fe::DocHandlingError(); -} - -//Public: -Fe::DocHandlingError XmlDocReader::readInto() -{ - // Open File - if(!mXmlFile.open(QFile::ReadOnly)) - return Fe::DocHandlingError(*mTargetDocument, Fe::DocHandlingError::DocCantOpen, mXmlFile.errorString()); - - if(!mStreamReader.readNextStartElement()) - { - Qx::XmlStreamReaderError xmlError(mStreamReader); - return Fe::DocHandlingError(*mTargetDocument, Fe::DocHandlingError::DocReadFailed, xmlError.text()); - } - - if(mStreamReader.name() != mRootElement) - return Fe::DocHandlingError(*mTargetDocument, Fe::DocHandlingError::NotParentDoc); - - return readTargetDoc(); - - // File is automatically closed when reader is destroyed... -} - -//=============================================================================================================== -// XmlDocWriter -//=============================================================================================================== - -//-Constructor-------------------------------------------------------------------------------------------------------- -//Public: -XmlDocWriter::XmlDocWriter(Fe::DataDoc* sourceDoc, const QString& root) : - Fe::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()); -} - -Fe::DocHandlingError XmlDocWriter::streamStatus() const -{ - return mStreamWriter.hasError() ? Fe::DocHandlingError(*mSourceDocument, Fe::DocHandlingError::DocWriteFailed, mStreamWriter.device()->errorString()) : - Fe::DocHandlingError(); -} - -//Public: -Fe::DocHandlingError XmlDocWriter::writeOutOf() -{ - // Open File - if(!mXmlFile.open(QFile::WriteOnly | QFile::Truncate)) // Discard previous contents - return Fe::DocHandlingError(*mSourceDocument, Fe::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/frontend/fe-data.h b/app/src/frontend/fe-data.h deleted file mode 100644 index c6486cf..0000000 --- a/app/src/frontend/fe-data.h +++ /dev/null @@ -1,617 +0,0 @@ -#ifndef FE_DATA -#define FE_DATA - -// Standard Library Includes -#include -#include - -// Qt Includes -#include -#include - -// Qx Includes -#include -#include -#include -#include - -// libfp Includes -#include - -// Project Includes -#include "fe-items.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 Fe -{ -//-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; - -//-Enums---------------------------------------------------------------------------------------------------------- -enum class ImportMode {OnlyNew, NewAndExisting}; - -//-Structs--------------------------------------------------------------------------------------------------------- -struct UpdateOptions -{ - ImportMode importMode; - bool removeObsolete; -}; - -//-Classes----------------------------------------------------------------------------------------------------------- -class QX_ERROR_TYPE(DocHandlingError, "Fe::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 Fe::DocHandlingError readInto() = 0; -}; - -class DataDoc::Writer -{ -//-Instance Variables-------------------------------------------------------------------------------------------------- -protected: - DataDoc* mSourceDocument; - -//-Constructor-------------------------------------------------------------------------------------------------------- -protected: - Writer(DataDoc* sourceDoc); - -//-Destructor------------------------------------------------------------------------------------------------- -public: - virtual ~Writer(); - -//-Instance Functions------------------------------------------------------------------------------------------------- -public: - virtual Fe::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: - UpdateOptions mUpdateOptions; - -//-Constructor-------------------------------------------------------------------------------------------------------- -protected: - explicit UpdateableDoc(Install* const parent, const QString& docPath, QString docName, const 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 == ImportMode::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 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 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 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 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 frontends - */ -class XmlDocReader : public virtual Fe::DataDoc::Reader -{ -//-Instance Variables-------------------------------------------------------------------------------------------------- -protected: - QFile mXmlFile; - QXmlStreamReader mStreamReader; - QString mRootElement; - -//-Constructor-------------------------------------------------------------------------------------------------------- -public: - XmlDocReader(Fe::DataDoc* targetDoc, const QString& root); - -//-Instance Functions------------------------------------------------------------------------------------------------- -private: - virtual Fe::DocHandlingError readTargetDoc() = 0; - -protected: - Fe::DocHandlingError streamStatus() const; - -public: - Fe::DocHandlingError readInto() override; -}; - -class XmlDocWriter : public virtual Fe::DataDoc::Writer -{ -//-Instance Variables-------------------------------------------------------------------------------------------------- -protected: - QFile mXmlFile; - QXmlStreamWriter mStreamWriter; - QString mRootElement; - -//-Constructor-------------------------------------------------------------------------------------------------------- -public: - XmlDocWriter(Fe::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; - -public: - Fe::DocHandlingError writeOutOf() override; -}; - -} -#endif // FE_DATA diff --git a/app/src/frontend/fe-install.cpp b/app/src/frontend/fe-install.cpp deleted file mode 100644 index 9a2a5dd..0000000 --- a/app/src/frontend/fe-install.cpp +++ /dev/null @@ -1,172 +0,0 @@ -// Unit Include -#include "fe-install.h" - -// Qt Includes -#include - -namespace Fe -{ - -//=============================================================================================================== -// 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::shared_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::shared_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(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); -} - -Fe::DocHandlingError Install::checkoutPlatformDoc(std::unique_ptr& returnBuffer, const QString& name) -{ - // Translate to frontend 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); - - // Set return null on failure - if(readErrorStatus.isValid()) - returnBuffer.reset(); - - // Return status - return readErrorStatus; -} - -Fe::DocHandlingError Install::checkoutPlaylistDoc(std::unique_ptr& returnBuffer, const QString& name) -{ - // Translate to frontend 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); - - // Set return null on failure - if(readErrorStatus.isValid()) - returnBuffer.reset(); - - // Return status - return readErrorStatus; -} - -Fe::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 - Fe::DocHandlingError writeErrorStatus = commitDataDocument(document.get(), docWriter); - - // Ensure document is cleared - document.reset(); - - // Return write status and let document ptr auto delete - return writeErrorStatus; -} - -Fe::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 - Fe::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/frontend/fe-install.h b/app/src/frontend/fe-install.h deleted file mode 100644 index 471ea9f..0000000 --- a/app/src/frontend/fe-install.h +++ /dev/null @@ -1,112 +0,0 @@ -#ifndef FE_INSTALL_H -#define FE_INSTALL_H - -// Qt Includes -#include - -// Project Includes -#include "fe-installfoundation.h" - -//-Macros------------------------------------------------------------------------------------------------------------------- -#define REGISTER_FRONTEND(fe_name, fe_install, fe_icon_path, fe_helpUrl) \ - class fe_install##Factory : public Fe::InstallFactory \ - { \ - public: \ - fe_install##Factory() \ - { \ - Install::Entry entry { \ - .factory = this, \ - .iconPath = fe_icon_path, \ - .helpUrl = fe_helpUrl \ - }; \ - Fe::Install::registerInstall(fe_name, entry); \ - } \ - virtual std::shared_ptr produce(const QString& installPath) const { return std::make_shared(installPath); } \ - }; \ - static fe_install##Factory _##install##Factory; - -namespace Fe -{ - -class InstallFactory -{ -//-Instance Functions------------------------------------------------------------------------------------------------------ -public: - virtual std::shared_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 registerFrontend would not be guaranteed - static QMap& registry(); - static void registerInstall(const QString& name, const Entry& entry); - static std::shared_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(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; - 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); - - // 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; - - // 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 // FE_INSTALL_H diff --git a/app/src/frontend/fe-installfoundation.cpp b/app/src/frontend/fe-installfoundation.cpp deleted file mode 100644 index 1e6148e..0000000 --- a/app/src/frontend/fe-installfoundation.cpp +++ /dev/null @@ -1,264 +0,0 @@ -// Unit Include -#include "fe-installfoundation.h" - -namespace Fe -{ - -//=============================================================================================================== -// 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; } - -//=============================================================================================================== -// InstallFoundation -//=============================================================================================================== - -//-Constructor--------------------------------------------------------------------------------------------------- -InstallFoundation::InstallFoundation(const QString& installPath) : - mValid(false), // Path is invalid until proven otherwise - mRootDirectory(installPath) -{} - -//-Destructor------------------------------------------------------------------------------------------------ -//Public: -InstallFoundation::~InstallFoundation() {} - -//Public: -QString InstallFoundation::filePathToBackupPath(const QString& filePath) -{ - return filePath + '.' + BACKUP_FILE_EXT; -} - -//-Instance Functions-------------------------------------------------------------------------------------------- -//Private: -bool InstallFoundation::containsAnyDataDoc(DataDoc::Type type, const QList& names) const -{ - // Create identifier set of names - QSet searchSet; - for(const QString& docName : names) - searchSet << DataDoc::Identifier(type, translateDocName(docName, type)); - - // Cross reference with existing documents - return mExistingDocuments.intersects(searchSet); -} - -QList InstallFoundation::modifiedDataDocs(DataDoc::Type type) const -{ - QList modList; - - for(const DataDoc::Identifier& dataDocId : mModifiedDocuments) - if(dataDocId.docType() == type) - modList.append(dataDocId.docName()); - - return modList; -} - -//Protected: -void InstallFoundation::nullify() -{ - mValid = false; - mRootDirectory = QDir(); -} - -void InstallFoundation::softReset() -{ - mRevertableFilePaths.clear(); - mModifiedDocuments.clear(); - mDeletedDocuments.clear(); - mLeasedDocuments.clear(); - mImportDetails.reset(); -} - -void InstallFoundation::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); } - -Fe::DocHandlingError InstallFoundation::checkoutDataDocument(DataDoc* docToOpen, std::shared_ptr docReader) -{ - // Error report to return - Fe::DocHandlingError openReadError; // Defaults to no error - - // Check if lease is already out - if(mLeasedDocuments.contains(docToOpen->identifier())) - openReadError = Fe::DocHandlingError(*docToOpen, Fe::DocHandlingError::DocAlreadyOpen); - else - { - // Read existing file if present and a reader was provided - if(docReader && mExistingDocuments.contains(docToOpen->identifier())) - openReadError = docReader->readInto(); - - // Add lease to ledger if no error occurred while reading - if(!openReadError.isValid()) - mLeasedDocuments.insert(docToOpen->identifier()); - } - - // Return opened document and status - return openReadError; -} - -Fe::DocHandlingError InstallFoundation::commitDataDocument(DataDoc* docToSave, std::shared_ptr docWriter) -{ - DataDoc::Identifier id = docToSave->identifier(); - bool wasDeleted = mDeletedDocuments.contains(id); - bool wasModified = mDeletedDocuments.contains(id); - bool wasUntouched = !wasDeleted && !wasModified; - - // 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 Fe::DocHandlingError(*docToSave, Fe::DocHandlingError::CantRemoveBackup); - } - - if(!QFile::copy(docPath, backupPath)) - return Fe::DocHandlingError(*docToSave, Fe::DocHandlingError::CantCreateBackup); - } - } - - // Error State - Fe::DocHandlingError commitError; - - // Handle modification - 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()); - - // Return write status and let document ptr auto delete - return commitError; -} - -QList InstallFoundation::modifiedPlatforms() const { return modifiedDataDocs(DataDoc::Type::Platform); } -QList InstallFoundation::modifiedPlaylists() const { return modifiedDataDocs(DataDoc::Type::Playlist);} - -//Public: -bool InstallFoundation::isValid() const { return mValid; } -QString InstallFoundation::path() const { return mRootDirectory.absolutePath(); } - -Qx::Error InstallFoundation::refreshExistingDocs(bool* changed) -{ - QSet oldDocSet; - oldDocSet.swap(mExistingDocuments); - Qx::Error error = populateExistingDocs(); - if(changed) - *changed = mExistingDocuments != oldDocSet; - return error; -} - -bool InstallFoundation::containsPlatform(const QString& name) const -{ - return containsAnyDataDoc(DataDoc::Type::Platform, {name}); -} - -bool InstallFoundation::containsPlaylist(const QString& name) const -{ - return containsAnyDataDoc(DataDoc::Type::Playlist, {name}); -} - -bool InstallFoundation::containsAnyPlatform(const QList& names) const -{ - return containsAnyDataDoc(DataDoc::Type::Platform, names); -} - -bool InstallFoundation::containsAnyPlaylist(const QList& names) const -{ - return containsAnyDataDoc(DataDoc::Type::Playlist, names); -} - -void InstallFoundation::addRevertableFile(const QString& filePath) { mRevertableFilePaths.append(filePath); } -int InstallFoundation::revertQueueCount() const { return mRevertableFilePaths.size(); } - -int InstallFoundation::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; -} - -} diff --git a/app/src/frontend/fe-installfoundation.h b/app/src/frontend/fe-installfoundation.h deleted file mode 100644 index cc9e307..0000000 --- a/app/src/frontend/fe-installfoundation.h +++ /dev/null @@ -1,173 +0,0 @@ -#ifndef FE_INSTALLFOUNDATION_H -#define FE_INSTALLFOUNDATION_H - -// Qt Includes -#include - -// Project Includes -#include "fe-data.h" - -namespace Fe -{ - -//-Enums---------------------------------------------------------------------------------------------------------- -enum class ImageMode {Copy, Reference, Link}; - -class QX_ERROR_TYPE(RevertError, "Fe::RevertError", 1301) -{ - friend class InstallFoundation; -//-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 InstallFoundation -{ -//-Class Structs------------------------------------------------------------------------------------------------------ -public: - struct ImportDetails - { - UpdateOptions updateOptions; - ImageMode imageMode; - QString clifpPath; - QList involvedPlatforms; - QList involvedPlaylists; - }; - - struct ImageMap - { - QString sourcePath; - QString destPath; - }; - -//-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; - -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; - - // Image Errors - static inline const QString CAPTION_IMAGE_ERR = u"Error importing game image(s)"_s; - -//-Instance Variables-------------------------------------------------------------------------------------------- -private: - // Validity - bool mValid; - - // Files and directories - QDir mRootDirectory; - - // Document tracking - QSet mExistingDocuments; - QSet mModifiedDocuments; - QSet mDeletedDocuments; - QSet mLeasedDocuments; - - // Backup/Deletion tracking - QStringList mRevertableFilePaths; - -protected: - // Import details - std::unique_ptr mImportDetails; - -//-Constructor--------------------------------------------------------------------------------------------------- -public: - InstallFoundation(const QString& installPath); - -//-Destructor------------------------------------------------------------------------------------------------- -public: - virtual ~InstallFoundation(); - -//-Class Functions------------------------------------------------------------------------------------------------------ -private: - static void ensureModifiable(const QString& filePath); - -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; - -protected: - 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); - - Fe::DocHandlingError checkoutDataDocument(DataDoc* docToOpen, std::shared_ptr docReader); - Fe::DocHandlingError commitDataDocument(DataDoc* docToSave, std::shared_ptr docWriter); - - QList modifiedPlatforms() const; - QList modifiedPlaylists() const; - -public: - bool isValid() const; - QString path() 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; - - void addRevertableFile(const QString& filePath); - int revertQueueCount() const; - int revertNextChange(RevertError& error, bool skipOnFail); -}; - -} - -#endif // FE_INSTALLFOUNDATION_H diff --git a/app/src/import/backup.cpp b/app/src/import/backup.cpp new file mode 100644 index 0000000..05e8ee1 --- /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 (THIS IS CRITICAL, HENCE WHY A SET IS USED) + 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..8620966 --- /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/details.cpp b/app/src/import/details.cpp new file mode 100644 index 0000000..4df090f --- /dev/null +++ b/app/src/import/details.cpp @@ -0,0 +1,23 @@ +// Unit Includes +#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/properties.cpp b/app/src/import/properties.cpp new file mode 100644 index 0000000..4c9b3d9 --- /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/interface/lr-install-interface.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::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 new file mode 100644 index 0000000..b45aae8 --- /dev/null +++ b/app/src/import/properties.h @@ -0,0 +1,90 @@ +#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" + +namespace Lr { class IInstall; } +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::IInstall* launcher(); + Fp::Install* flashpoint(); +}; + +} + +#endif // IMPORT_PROPERTIES_H 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 new file mode 100644 index 0000000..f8c16d7 --- /dev/null +++ b/app/src/import/settings.h @@ -0,0 +1,71 @@ +#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; +}; + +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 similarity index 77% rename from app/src/import-worker.cpp rename to app/src/import/worker.cpp index 8207e55..fedf4a1 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,8 +12,12 @@ #include // Project Includes -#include "clifp.h" +#include "kernel/clifp.h" +#include "import/details.h" +#include "import/backup.h" +namespace Import +{ //=============================================================================================================== // ImageTransferError //=============================================================================================================== @@ -55,25 +59,27 @@ 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), +//Public: +Worker::Worker(Fp::Install* flashpoint, Lr::IInstall* launcher, Selections importSelections, OptionSet optionSet) : + mFlashpointInstall(flashpoint), + mLauncherInstall(launcher), mImportSelections(importSelections), mOptionSet(optionSet), mCurrentProgress(0), mCanceled(false) {} +//-Destructor--------------------------------------------------------------------------------------------------- +//Public: +Worker::~Worker() { Details::clearCurrent(); } + //-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 +88,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 +109,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 +120,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 @@ -143,6 +149,7 @@ ImageTransferError ImportWorker::transferImage(bool symlink, QString sourcePath, 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) @@ -155,56 +162,30 @@ ImageTransferError ImportWorker::transferImage(bool symlink, QString sourcePath, if(!destinationDir.mkpath(u"."_s)) return ImageTransferError(ImageTransferError::CantCreateDirectory, QString(), destinationDir.absolutePath()); - // Determine backup path - QString backupPath = Fe::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) - if(!QFile::rename(destinationPath, backupPath)) // Temp backup - return ImageTransferError(ImageTransferError::ImageWontBackup, QString(), destinationPath); - - // Linking error tracker - std::error_code linkError; - - // Handle transfer - if(symlink) + // Transfer image + BackupError bErr = BackupManager::instance()->safeReplace(sourcePath, destinationPath, symlink); + if(bErr) { - 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 - mFrontendInstall->addRevertableFile(destinationPath); // Only queue image to be removed on failure if its new, so existing images aren't deleted on revert - } - else - { - 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 - mFrontendInstall->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 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 ImageMap& imageJob : jobs) { while((imageTransferError = transferImage(symlink, imageJob.sourcePath, imageJob.destPath)).isValid() && !ignoreAllTransferErrors) { @@ -229,7 +210,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(); @@ -276,11 +257,14 @@ ImportWorker::ImportResult ImportWorker::processPlatformGames(Qx::Error& errorRe // 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, Fe::ImageSources(checkedLogoPath, checkedScreenshotPath)); + Import::ImagePaths imagePaths(checkedLogoPath, checkedScreenshotPath); + ImageMap logoMap{.sourcePath = imagePaths.logoPath(), .destPath = ""}; + 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 +289,20 @@ ImportWorker::ImportResult ImportWorker::processPlatformGames(Qx::Error& errorRe mProgressManager.group(Pg::ImageDownload)->decrementMaximum(); // Already exists, remove download step from progress bar } - // Handle image transfer progress - if(mOptionSet.imageMode == Fe::ImageMode::Copy || mOptionSet.imageMode == Fe::ImageMode::Link) + // 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 } @@ -330,7 +321,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 +332,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 +371,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()) @@ -416,16 +407,9 @@ ImportWorker::ImportResult ImportWorker::processGames(Qx::Error& errorReport, QL //---Finalize document---------------------------------- currentPlatformDoc->finalize(); - // Check for internal doc errors - if(currentPlatformDoc->hasError()) - { - errorReport = currentPlatformDoc->error(); - return Failed; - } - // 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 +437,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()) @@ -477,16 +461,9 @@ ImportWorker::ImportResult ImportWorker::processPlaylists(Qx::Error& errorReport // Finalize document currentPlaylistDoc->finalize(); - // Check for internal doc errors - if(currentPlaylistDoc->hasError()) - { - errorReport = currentPlaylistDoc->error(); - return Failed; - } - // 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 +484,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 +505,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,17 +535,8 @@ 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) - { - bulkSources.setLogoPath(QDir::toNativeSeparators(mFlashpointInstall->entryLogosDirectory().absolutePath())); - bulkSources.setScreenshotPath(QDir::toNativeSeparators(mFlashpointInstall->entryScreenshotsDirectory().absolutePath())); - } - - Qx::Error imageExchangeError = mFrontendInstall->preImageProcessing(imageTransferJobs, bulkSources); - + // Notify of step + Qx::Error imageExchangeError = mLauncherInstall->preImageProcessing(); if(imageExchangeError.isValid()) { // Emit import failure @@ -576,43 +544,54 @@ ImportWorker::ImportResult ImportWorker::processImages(Qx::Error& errorReport) return Failed; } + // Provide launcher with bulk reference locations + if(mOptionSet.imageMode == ImageMode::Reference) + { + Import::ImagePaths bulkSources(QDir::toNativeSeparators(mFlashpointInstall->entryLogosDirectory().absolutePath()), + QDir::toNativeSeparators(mFlashpointInstall->entryScreenshotsDirectory().absolutePath())); + + mLauncherInstall->processBulkImageSources(bulkSources); + } + // 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. * 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 == Fe::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 frontend 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 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(ImageMap{.sourcePath = u":/flashpoint/icon.png"_s, .destPath = mainDest}); // Platform jobs if(platformDestDir) @@ -622,8 +601,8 @@ 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, - .destPath = pdd.absoluteFilePath(p + ".png")}); + jobs.emplace_back(ImageMap{.sourcePath = src, + .destPath = pdd.absoluteFilePath(p + ".png")}); } } @@ -650,19 +629,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::IDataDoc::Type::Playlist) + ".png";; QString source = iconInflateDir.filePath(sFilename); QString dest = pdd.absoluteFilePath(dFilename); @@ -673,7 +652,7 @@ ImportWorker::ImportResult ImportWorker::processIcons(Qx::Error& errorReport, co return Failed; } - jobs.emplace_back(Fe::Install::ImageMap{.sourcePath = source, .destPath = dest}); + jobs.emplace_back(ImageMap{.sourcePath = source, .destPath = dest}); } } @@ -691,12 +670,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; @@ -737,7 +716,7 @@ ImportWorker::ImportResult ImportWorker::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 @@ -765,7 +744,7 @@ ImportWorker::ImportResult ImportWorker::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; @@ -777,14 +756,14 @@ ImportWorker::ImportResult ImportWorker::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; @@ -798,7 +777,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 +786,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,18 +807,19 @@ 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------------------------------ + Details details{ .updateOptions = mOptionSet.updateOptions, .imageMode = mOptionSet.imageMode, .clifpPath = CLIFp::standardCLIFpPath(*mFlashpointInstall), .involvedPlatforms = involvedPlatforms, .involvedPlaylists = mImportSelections.playlists }; + Details::setCurrent(details); - errorReport = mFrontendInstall->preImport(details); + errorReport = mLauncherInstall->preImport(); if(errorReport.isValid()) return Failed; @@ -853,8 +833,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 +842,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 +861,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 +889,7 @@ ImportWorker::ImportResult ImportWorker::doImport(Qx::Error& errorReport) } // Reset install - mFrontendInstall->softReset(); + mLauncherInstall->softReset(); // Report successful import completion errorReport = Qx::Error(); @@ -918,11 +898,11 @@ 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. - * 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 @@ -940,4 +920,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 74% rename from app/src/import-worker.h rename to app/src/import/worker.h index 7f0a9a9..e319b51 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/interface/lr-install-interface.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,21 +93,10 @@ class ImportWorker : public QObject static inline const QString PlaylistImport = u"PlaylistImport"_s; }; -//-Class Structs------------------------------------------------------------------------------------------------- -public: - struct ImportSelections + struct ImageMap { - QStringList platforms; - QStringList playlists; - }; - - struct OptionSet - { - Fe::UpdateOptions updateOptions; - Fe::ImageMode imageMode; - bool downloadImages; - PlaylistGameMode playlistMode; - Fp::Db::InclusionOptions inclusionOptions; + QString sourcePath; + QString destPath; }; //-Class Variables----------------------------------------------------------------------------------------------- @@ -122,14 +113,15 @@ class ImportWorker : public QObject //-Instance Variables-------------------------------------------------------------------------------------------- private: // Install links - std::shared_ptr mFlashpointInstall; - std::shared_ptr mFrontendInstall; + Fp::Install* mFlashpointInstall; + Lr::IInstall* mLauncherInstall; // Image processing Qx::SyncDownloadManager mImageDownloadManager; + QList mImageTransferJobs; // Job details - ImportSelections mImportSelections; + Selections mImportSelections; OptionSet mOptionSet; // Job Caches @@ -148,10 +140,11 @@ class ImportWorker : public QObject //-Constructor--------------------------------------------------------------------------------------------------- public: - ImportWorker(std::shared_ptr fpInstallForWork, - std::shared_ptr feInstallForWork, - ImportSelections importSelections, - OptionSet optionSet); + Worker(Fp::Install* flashpoint, Lr::IInstall* launcher, Selections importSelections, OptionSet optionSet); + +//-Destructor--------------------------------------------------------------------------------------------------- +public: + ~Worker(); //-Instance Functions--------------------------------------------------------------------------------------------------------- private: @@ -159,18 +152,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 +184,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 99% rename from app/src/clifp.cpp rename to app/src/kernel/clifp.cpp index 056f989..bc94550 100644 --- a/app/src/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/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..c75dd19 --- /dev/null +++ b/app/src/kernel/controller.cpp @@ -0,0 +1,346 @@ +// Unit Include +#include "controller.h" + +// Qt Includes +#include +#include + +// Qx Includes +#include +#include +#include + +// 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 + * 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; + Import::BackupError currentError; + int retryChoice; + + // Progress + auto bm = Import::BackupManager::instance(); + mProgressPresenter.setMinimum(0); + mProgressPresenter.setMaximum(bm->revertQueueCount()); + mProgressPresenter.setCaption(CAPTION_REVERT); + while(bm->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::Registry::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/launcher/abstract/lr-data.h b/app/src/launcher/abstract/lr-data.h new file mode 100644 index 0000000..5195974 --- /dev/null +++ b/app/src/launcher/abstract/lr-data.h @@ -0,0 +1,300 @@ +#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------------------------------------------------------------------------------------------------- +public: + DocT* target() const; + + // IMPLEMENT + using IDataDoc::Reader::readInto; +}; + +template +class DataDocWriter : public IDataDoc::Writer +{ +//-Constructor-------------------------------------------------------------------------------------------------------- +protected: + DataDocWriter(DocT* sourceDoc); + +//-Destructor------------------------------------------------------------------------------------------------- +public: + virtual ~DataDocWriter() = default; + +//-Instance Functions------------------------------------------------------------------------------------------------- +public: + DocT* source() const; + + // 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, Import::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 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 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..3f053a7 --- /dev/null +++ b/app/src/launcher/abstract/lr-data.tpp @@ -0,0 +1,399 @@ +#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 "import/details.h" +#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-------------------------------------------------------------------------------------------------- +//Public: +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-------------------------------------------------------------------------------------------------- +//Public: +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, Import::ImagePaths& 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); +} + +//=============================================================================================================== +// 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) +{ + // Prepare game + std::shared_ptr 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() +{ + /* 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) +{ + 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() +{ + // 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..4ef6172 --- /dev/null +++ b/app/src/launcher/abstract/lr-install.h @@ -0,0 +1,80 @@ +#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; + using IInstall::processBulkImageSources; // Just do nothing if Reference mode isn't supported + 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; + 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..dabe430 --- /dev/null +++ b/app/src/launcher/abstract/lr-install.tpp @@ -0,0 +1,133 @@ +#ifndef LR_INSTALL_TPP +#define LR_INSTALL_TPP + +#include "lr-install.h" // Can ignore recursion warning + +// 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(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(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(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(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..3e0c0c6 --- /dev/null +++ b/app/src/launcher/abstract/lr-registration.h @@ -0,0 +1,196 @@ +#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 +{ + /* 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); + } + +public: + Register() + { + 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/frontend/attractmode/am-data.cpp b/app/src/launcher/implementation/attractmode/am-data.cpp similarity index 61% rename from app/src/frontend/attractmode/am-data.cpp rename to app/src/launcher/implementation/attractmode/am-data.cpp index f0f4e68..0ac1a12 100644 --- a/app/src/frontend/attractmode/am-data.cpp +++ b/app/src/launcher/implementation/attractmode/am-data.cpp @@ -8,96 +8,11 @@ #include // Project Includes -#include "am-install.h" +#include "launcher/implementation/attractmode/am-install.h" namespace Am { -//=============================================================================================================== -// CommonDocReader -//=============================================================================================================== - -//-Constructor-------------------------------------------------------------------------------------------------------- -//Protected: -CommonDocReader::CommonDocReader(Fe::DataDoc* targetDoc) : - Fe::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: -Fe::DocHandlingError CommonDocReader::readInto() -{ - // Open file - Qx::IoOpReport openError = mStreamReader.openFile(); - if(openError.isFailure()) - return Fe::DocHandlingError(*mTargetDocument, Fe::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()); - else if(!isValid) - return Fe::DocHandlingError(*mTargetDocument, Fe::DocHandlingError::DocInvalidType); - - // Read doc - Fe::DocHandlingError parseError = readTargetDoc(); - - // Close file - mStreamReader.closeFile(); - - // Return outcome - if(parseError.isValid()) - return parseError; - else if(mStreamReader.hasError()) - return Fe::DocHandlingError(*mTargetDocument, Fe::DocHandlingError::DocWriteFailed, mStreamReader.status().outcomeInfo()); - else - return Fe::DocHandlingError(); -} - -//=============================================================================================================== -// CommonDocWriter -//=============================================================================================================== - -//-Constructor-------------------------------------------------------------------------------------------------------- -//Protected: -CommonDocWriter::CommonDocWriter(Fe::DataDoc* sourceDoc) : - Fe::DataDoc::Writer(sourceDoc), - mStreamWriter(sourceDoc->path(), Qx::WriteMode::Truncate) -{} - -//-Instance Functions------------------------------------------------------------------------------------------------- -//Public: -Fe::DocHandlingError CommonDocWriter::writeOutOf() -{ - // Open file - Qx::IoOpReport openError = mStreamWriter.openFile(); - if(openError.isFailure()) - return Fe::DocHandlingError(*mSourceDocument, Fe::DocHandlingError::DocCantOpen, openError.outcomeInfo()); - - // Write doc - bool writeSuccess = writeSourceDoc(); - // Close file - mStreamWriter.closeFile(); - - // Return outcome - return writeSuccess ? Fe::DocHandlingError() : - Fe::DocHandlingError(*mSourceDocument, Fe::DocHandlingError::DocWriteFailed, mStreamWriter.status().outcomeInfo()); -} //=============================================================================================================== // ConfigDoc @@ -105,97 +20,18 @@ Fe::DocHandlingError CommonDocWriter::writeOutOf() //-Constructor-------------------------------------------------------------------------------------------------------- //Public: -ConfigDoc::ConfigDoc(Install* const parent, const QString& filePath, QString docName) : - Fe::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; -} - -//=============================================================================================================== -// 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; - } + return TAGLINE + install()->versionString(); } -//-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) : - Fe::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 : std::as_const(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: -Fe::DataDoc::Type PlatformTaglist::type() const { return Fe::DataDoc::Type::Platform; } +Lr::IDataDoc::Type PlatformTaglist::type() const { return Lr::IDataDoc::Type::Platform; } //=============================================================================================================== // PlaylistTaglist @@ -262,13 +95,13 @@ Fe::DataDoc::Type PlatformTaglist::type() const { return Fe::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: -Fe::DataDoc::Type PlaylistTaglist::type() const { return Fe::DataDoc::Type::Playlist; } +Lr::IDataDoc::Type PlaylistTaglist::type() const { return Lr::IDataDoc::Type::Playlist; } //=============================================================================================================== // Romlist @@ -276,14 +109,13 @@ 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, - const DocKey&) : - Fe::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: -Fe::DataDoc::Type Romlist::type() const { return Fe::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 Fe::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 Fe::ImageSources& images) } } - // Allow install to process images as necessary - parent()->processDirectGameImages(mainRomEntry.get(), images); + return mainRomEntry; } void Romlist::finalize() { finalizeUpdateableItems(mEntriesExisting, mEntriesFinal); - Fe::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 @@ -355,7 +181,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 +191,7 @@ Fe::DocHandlingError Romlist::Reader::readTargetDoc() } // Only can have stream errors - return Fe::DocHandlingError(); + return Lr::DocHandlingError(); } void Romlist::Reader::parseRomEntry(const QString& rawEntry) @@ -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 : std::as_const(source()->finalEntries())) { if(!writeRomEntry(*entry)) return false; @@ -562,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(); @@ -584,14 +410,47 @@ 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&) : - Fe::PlatformDoc(parent, {}, platformName, {}), - mPlatformTaglist(parent, platformTaglistPath, platformName), - mOverviewWriter(overviewDir) +PlatformInterface::PlatformInterface(Install* install, const QString& platformTaglistPath, QString platformName, + const QDir& overviewDir) : + Lr::PlatformDoc(install, {}, platformName, {}), + mPlatformTaglist(install, platformTaglistPath, platformName), + mOverviewDir(overviewDir) {} //-Instance Functions-------------------------------------------------------------------------------------------------- +//Private: +std::shared_ptr PlatformInterface::processSet(const Fp::Set& set) +{ + //-Handle game---------------------------------------------------------- + const Fp::Game& game = set.game(); + + // Add game ID to platform tag list + mPlatformTaglist.appendTag(game.id().toString(QUuid::WithoutBraces)); + + // Create game overview + QString overviewText = game.originalDescription(); + if(!overviewText.isEmpty()) + mOverviews.emplaceBack(game.id(), overviewText); + + //-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 that required them + * for games to work (i.e. before auto mode). + */ + if(addApp.isPlayable()) + mPlatformTaglist.appendTag(addApp.id().toString(QUuid::WithoutBraces)); + } + + //-Forward game insertion to main Romlist-------------------------------- + return install()->mRomlist->processSet(set); +} + //Public: bool PlatformInterface::isEmpty() const { return mPlatformTaglist.isEmpty(); } @@ -601,7 +460,7 @@ bool PlatformInterface::containsGame(QUuid gameId) const * 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); + return install()->mRomlist->containsGame(gameId); } bool PlatformInterface::containsAddApp(QUuid addAppId) const @@ -610,66 +469,39 @@ bool PlatformInterface::containsAddApp(QUuid addAppId) const * 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); + return install()->mRomlist->containsAddApp(addAppId); }; -void PlatformInterface::addSet(const Fp::Set& set, const Fe::ImageSources& images) -{ - if(!hasError()) - { - //-Handle game---------------------------------------------------------- - const Fp::Game& game = set.game(); - - // Add game ID to platform tag list - mPlatformTaglist.appendTag(game.id().toString(QUuid::WithoutBraces)); - - // Create game overview - QString overview = game.originalDescription(); - - if(!overview.isEmpty()) - { - bool written = mOverviewWriter.writeOverview(game.id(), overview); - - if(written) - parent()->addRevertableFile(mOverviewWriter.currentFilePath()); - else - mError = Fe::DocHandlingError(*this, Fe::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)); - } - - //-Forward game insertion to main Romlist-------------------------------- - static_cast(parent())->mRomlist->addSet(set, images); - } -} - //=============================================================================================================== -// PlatformInterface::Writer +// PlatformInterfaceWriter //=============================================================================================================== //-Constructor-------------------------------------------------------------------------------------------------------- //Public: -PlatformInterface::Writer::Writer(PlatformInterface* sourceDoc) : - Fe::DataDoc::Writer(sourceDoc), - Fe::PlatformDoc::Writer(sourceDoc), - mTaglistWriter(&sourceDoc->mPlatformTaglist) +PlatformInterfaceWriter::PlatformInterfaceWriter(PlatformInterface* sourceDoc) : + Lr::DataDocWriter(sourceDoc), + mTaglistWriter(&sourceDoc->mPlatformTaglist), + mOverviewWriter(sourceDoc->mOverviewDir) // TODO: Maybe a better way to pass this then it just sitting in the doc? {} //-Instance Functions-------------------------------------------------------------------------------------------------- //Public: -Fe::DocHandlingError PlatformInterface::Writer::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)) + { + // 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()); + } + + return {}; +} //=============================================================================================================== // PlaylistInterface @@ -677,10 +509,9 @@ Fe::DocHandlingError PlatformInterface::Writer::writeOutOf() { return mTaglistWr //-Constructor-------------------------------------------------------------------------------------------------------- //Public: -PlaylistInterface::PlaylistInterface(Install* const parent, const QString& playlistTaglistPath, QString playlistName, - const DocKey&) : - Fe::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 +530,19 @@ void PlaylistInterface::setPlaylistData(const Fp::Playlist& playlist) } //=============================================================================================================== -// PlaylistInterface::Writer +// PlaylistInterfaceWriter //=============================================================================================================== //-Constructor-------------------------------------------------------------------------------------------------------- //Public: -PlaylistInterface::Writer::Writer(PlaylistInterface* sourceDoc) : - Fe::DataDoc::Writer(sourceDoc), - Fe::PlaylistDoc::Writer(sourceDoc), +PlaylistInterfaceWriter::PlaylistInterfaceWriter(PlaylistInterface* sourceDoc) : + Lr::DataDocWriter(sourceDoc), mTaglistWriter(&sourceDoc->mPlaylistTaglist) {} //-Instance Functions-------------------------------------------------------------------------------------------------- //Public: -Fe::DocHandlingError PlaylistInterface::Writer::writeOutOf() { return mTaglistWriter.writeOutOf(); } +Lr::DocHandlingError PlaylistInterfaceWriter::writeOutOf() { return mTaglistWriter.writeOutOf(); } //=============================================================================================================== // Emulator @@ -720,14 +550,14 @@ Fe::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 -Fe::DataDoc::Type Emulator::type() const { return Fe::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,12 +587,12 @@ void Emulator::setArtworkEntry(const EmulatorArtworkEntry& entry) { mArtworkEntr //-Constructor-------------------------------------------------------------------------------------------------------- //Public: EmulatorReader::EmulatorReader(Emulator* targetDoc) : - ConfigDoc::Reader(targetDoc) + ConfigDoc::Reader(targetDoc) {} //-Instance Functions-------------------------------------------------------------------------------------------------- //Private: -Fe::DocHandlingError EmulatorReader::readTargetDoc() +Lr::DocHandlingError EmulatorReader::readTargetDoc() { while(!mStreamReader.atEnd()) { @@ -774,7 +604,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) @@ -799,20 +629,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 +653,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 +663,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 +674,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 +711,4 @@ void Emulator::Writer::writeArtworkEntry(const EmulatorArtworkEntry& entry) mStreamWriter << '\n'; } -Emulator* Emulator::Writer::sourceEmulator() { return static_cast(mSourceDocument); } - } diff --git a/app/src/frontend/attractmode/am-data.h b/app/src/launcher/implementation/attractmode/am-data.h similarity index 81% rename from app/src/frontend/attractmode/am-data.h rename to app/src/launcher/implementation/attractmode/am-data.h index be477a9..3febb77 100644 --- a/app/src/frontend/attractmode/am-data.h +++ b/app/src/launcher/implementation/attractmode/am-data.h @@ -9,66 +9,67 @@ #include // Project Includes -#include "am-items.h" -#include "frontend/fe-data.h" + +#include "launcher/abstract/lr-data.h" +#include "launcher/implementation/attractmode/am-registration.h" +#include "launcher/implementation/attractmode/am-items.h" namespace Am { -class Install; - -class DocKey -{ - friend class Install; -private: - DocKey() {} - DocKey(const DocKey&) = default; -}; - -class CommonDocReader : public Fe::DataDoc::Reader +template +class CommonDocReader : public Lr::DataDocReader { +protected: + using Lr::DataDocReader::target; //-Instance Variables-------------------------------------------------------------------------------------------------- protected: Qx::TextStreamReader mStreamReader; //-Constructor-------------------------------------------------------------------------------------------------------- protected: - CommonDocReader(Fe::DataDoc* targetDoc); + CommonDocReader(DocT* 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 +template +class CommonDocWriter : public Lr::DataDocWriter { +protected: + using Lr::DataDocWriter::source; //-Instance Variables-------------------------------------------------------------------------------------------------- protected: Qx::TextStreamWriter mStreamWriter; //-Constructor-------------------------------------------------------------------------------------------------------- protected: - CommonDocWriter(Fe::DataDoc* sourceDoc); + CommonDocWriter(DocT* 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: + template class Reader; + + template class Writer; //-Class Variables-------------------------------------------------------------------------------------------------- @@ -77,7 +78,7 @@ class ConfigDoc : public Fe::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 Fe::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 Fe::DataDoc +class Taglist : public Lr::DataDoc { //-Inner Classes---------------------------------------------------------------------------------------------------- public: @@ -129,7 +139,7 @@ class Taglist : public Fe::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 Fe::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 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,12 +209,11 @@ class Romlist : public Fe::UpdateableDoc //-Constructor-------------------------------------------------------------------------------------------------------- public: - explicit Romlist(Install* const parent, const QString& listPath, QString docName, const Fe::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 Fe::UpdateableDoc bool containsGame(QUuid gameId) const; bool containsAddApp(QUuid addAppId) const; - void addSet(const Fp::Set& set, const Fe::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; - Fe::DocHandlingError readTargetDoc() 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: @@ -260,69 +268,63 @@ class BulkOverviewWriter public: QString currentFilePath(); QString fileErrorString(); - bool writeOverview(const QUuid& gameId, const QString& overview); + bool writeOverview(const Overview& overview); }; -class PlatformInterface : public Fe::PlatformDoc +class PlatformInterface : public Lr::PlatformDoc { -//-Inner Classes---------------------------------------------------------------------------------------------------- -public: - class Writer; - + friend class PlatformInterfaceWriter; //-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: - 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 Fe::ImageSources& images) override; }; -class PlatformInterface::Writer : public Fe::PlatformDoc::Writer +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: - Writer(PlatformInterface* sourceDoc); + PlatformInterfaceWriter(PlatformInterface* sourceDoc); //-Instance Functions------------------------------------------------------------------------------------------------- public: - Fe::DocHandlingError writeOutOf() override; + Lr::DocHandlingError writeOutOf() override; }; -class PlaylistInterface : public Fe::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,21 +335,19 @@ class PlaylistInterface : public Fe::PlaylistDoc void setPlaylistData(const Fp::Playlist& playlist) override; }; -class PlaylistInterface::Writer : public Fe::PlaylistDoc::Writer +class PlaylistInterfaceWriter : public Lr::DataDocWriter { - // Shell for writing the taglist of the interface - //-Instance Variables-------------------------------------------------------------------------------------------------- private: Taglist::Writer mTaglistWriter; //-Constructor-------------------------------------------------------------------------------------------------------- public: - Writer(PlaylistInterface* sourceDoc); + PlaylistInterfaceWriter(PlaylistInterface* sourceDoc); //-Instance Functions------------------------------------------------------------------------------------------------- private: - Fe::DocHandlingError writeOutOf() override; + Lr::DocHandlingError writeOutOf() override; }; class Emulator : public ConfigDoc @@ -400,7 +400,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 +429,7 @@ class Emulator : public ConfigDoc void setArtworkEntry(const EmulatorArtworkEntry& entry); }; -class EmulatorReader : public ConfigDoc::Reader +class EmulatorReader : public ConfigDoc::Reader { //-Constructor-------------------------------------------------------------------------------------------------------- public: @@ -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); @@ -448,11 +448,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 +467,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/frontend/attractmode/am-install.cpp b/app/src/launcher/implementation/attractmode/am-install.cpp similarity index 69% rename from app/src/frontend/attractmode/am-install.cpp rename to app/src/launcher/implementation/attractmode/am-install.cpp index adc9a82..be35376 100644 --- a/app/src/frontend/attractmode/am-install.cpp +++ b/app/src/launcher/implementation/attractmode/am-install.cpp @@ -7,9 +7,11 @@ // Qx Includes #include #include +#include // Project Includes -#include "clifp.h" +#include "kernel/clifp.h" +#include "import/details.h" namespace Am { @@ -20,7 +22,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 +57,7 @@ Install::Install(const QString& installPath) : //Private: void Install::nullify() { - Fe::Install::nullify(); + Lr::IInstall::nullify(); mEmulatorsDirectory = QDir(); mRomlistsDirectory = QDir(); @@ -76,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. * @@ -88,8 +90,8 @@ Qx::Error Install::populateExistingDocs() if(existingCheck.isFailure()) return existingCheck; - for(const QFileInfo& platformFile : qAsConst(existingList)) - catalogueExistingDoc(Fe::DataDoc::Identifier(Fe::DataDoc::Type::Platform, platformFile.baseName())); + for(const QFileInfo& platformFile : std::as_const(existingList)) + catalogueExistingDoc(Lr::IDataDoc::Identifier(Lr::IDataDoc::Type::Platform, platformFile.baseName())); // Check for playlists existingCheck = Qx::dirContentInfoList(existingList, mFpTagDirectory, {u"[[]Playlist[]] *."_s + TAG_EXT}, @@ -97,36 +99,36 @@ Qx::Error Install::populateExistingDocs() if(existingCheck.isFailure()) return existingCheck; - for(const QFileInfo& playlistFile : qAsConst(existingList)) - catalogueExistingDoc(Fe::DataDoc::Identifier(Fe::DataDoc::Type::Playlist, playlistFile.baseName())); + 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) QFileInfo mainRomlistInfo(mFpRomlist); if(mainRomlistInfo.exists()) - catalogueExistingDoc(Fe::DataDoc::Identifier(Fe::DataDoc::Type::Platform, mainRomlistInfo.baseName())); + catalogueExistingDoc(Lr::IDataDoc::Identifier(Lr::IDataDoc::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::IDataDoc::Identifier(Lr::IDataDoc::Type::Config, mainCfgInfo.baseName())); // Must exist QFileInfo emulatorCfgInfo(mEmulatorConfigFile); if(emulatorCfgInfo.exists()) - catalogueExistingDoc(Fe::DataDoc::Identifier(Fe::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 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) + '/' + - 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,52 +137,28 @@ 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); } -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{}); + returnBuffer = std::make_unique(this, mMainConfigFile.fileName()); // 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(docReader); // Set return null on failure if(readErrorStatus.isValid()) @@ -190,16 +168,16 @@ 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{}); + 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()); // Open document - Fe::DocHandlingError readErrorStatus = checkoutDataDocument(returnBuffer.get(), docReader); + Lr::DocHandlingError readErrorStatus = checkoutDataDocument(docReader); // Set return null on failure if(readErrorStatus.isValid()) @@ -209,16 +187,16 @@ 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{}); + returnBuffer = std::make_unique(this, mEmulatorConfigFile.fileName()); // 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(docReader); // Set return null on failure if(readErrorStatus.isValid()) @@ -228,15 +206,15 @@ Fe::DocHandlingError Install::checkoutClifpEmulatorConfig(std::unique_ptr document) +Lr::DocHandlingError Install::commitMainConfig(std::unique_ptr document) { - assert(document->parent() == this); + Q_ASSERT(document->install() == this); // Prepare writer std::shared_ptr docWriter = std::make_shared(document.get()); // Write - Fe::DocHandlingError writeErrorStatus = commitDataDocument(document.get(), docWriter); + Lr::DocHandlingError writeErrorStatus = commitDataDocument(docWriter); // Ensure document is cleared document.reset(); @@ -245,15 +223,15 @@ 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); + Q_ASSERT(document->install() == this); // Prepare writer std::shared_ptr docWriter = std::make_shared(document.get()); // Write - Fe::DocHandlingError writeErrorStatus = commitDataDocument(document.get(), docWriter); + Lr::DocHandlingError writeErrorStatus = commitDataDocument(docWriter); // Ensure document is cleared document.reset(); @@ -263,15 +241,15 @@ 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); + Q_ASSERT(document->install() == this); // Prepare writer std::shared_ptr docWriter = std::make_shared(document.get()); // Write - Fe::DocHandlingError writeErrorStatus = commitDataDocument(document.get(), docWriter); + Lr::DocHandlingError writeErrorStatus = commitDataDocument(docWriter); // Ensure document is cleared document.reset(); @@ -283,15 +261,13 @@ 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,24 +300,24 @@ QString Install::versionString() const } // Can't determine version - return Fe::Install::versionString(); + return Lr::IInstall::versionString(); } -QString Install::translateDocName(const QString& originalName, Fe::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 == Fe::DataDoc::Type::Platform) + if(type == Lr::IDataDoc::Type::Platform) translatedName.prepend(PLATFORM_TAG_PREFIX); - else if(type == Fe::DataDoc::Type::Playlist) + else if(type == Lr::IDataDoc::Type::Playlist) translatedName.prepend(PLAYLIST_TAG_PREFIX); return translatedName; } -Qx::Error Install::preImport(const ImportDetails& details) +Qx::Error Install::preImport() { //-Ensure that required directories exist---------------------------------------------------------------- @@ -357,7 +333,8 @@ 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) + auto details = Import::Details::current(); + if(details.imageMode == Import::ImageMode::Copy || details.imageMode == Import::ImageMode::Link) { QDir logoDir(mFpScraperDirectory.absoluteFilePath(LOGO_FOLDER_NAME)); if(!logoDir.exists()) @@ -371,7 +348,7 @@ Qx::Error Install::preImport(const ImportDetails& details) } // Perform base tasks - return Fe::Install::preImport(details); + return Lr::IInstall::preImport(); } Qx::Error Install::prePlatformsImport() @@ -389,39 +366,20 @@ Qx::Error Install::postPlatformsImport() return commitFlashpointRomlist(std::move(mRomlist)); } -Qx::Error Install::preImageProcessing(QList& workerTransfers, const Fe::ImageSources& bulkSources) -{ - Q_UNUSED(bulkSources); - - switch(mImportDetails->imageMode) - { - case Fe::ImageMode::Link: - case Fe::ImageMode::Copy: - workerTransfers.swap(mWorkerImageJobs); - return Qx::Error(); - case Fe::ImageMode::Reference: - qWarning("unsupported image mode"); - return Qx::Error(); - default: - qWarning("unhandled image mode"); - return Qx::Error(); - } -} - Qx::Error Install::postImport() { //-Create/update emulator settings----------------------------------- // 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()) return emulatorConfigReadError; // General emulator setup - QString workingDir = QDir::toNativeSeparators(QFileInfo(mImportDetails->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); @@ -445,7 +403,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 +413,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,8 +469,10 @@ Qx::Error Install::postImport() for(const QString& tagFile : tagFiles) { // Escape brackets in name since AM uses regex for value - 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(); @@ -524,7 +484,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,25 +500,19 @@ Qx::Error Install::postImport() return Qx::Error(); } -void Install::processDirectGameImages(const Fe::Game* game, const Fe::ImageSources& imageSources) +void Install::processBulkImageSources(const Import::ImagePaths& bulkSources) { - Fe::ImageMode mode = mImportDetails->imageMode; - if(mode == Fe::ImageMode::Link || mode == Fe::ImageMode::Copy) - { - if(!imageSources.logoPath().isEmpty()) - { - ImageMap logoMap{.sourcePath = imageSources.logoPath(), - .destPath = imageDestinationPath(Fp::ImageType::Logo, game)}; - mWorkerImageJobs.append(logoMap); - } + Q_UNUSED(bulkSources); + qFatal("Attract Mode does not support Reference image mode, and that option should not be available."); +} - if(!imageSources.screenshotPath().isEmpty()) - { - ImageMap ssMap{.sourcePath = imageSources.screenshotPath(), - .destPath = imageDestinationPath(Fp::ImageType::Screenshot, game)}; - mWorkerImageJobs.append(ssMap); - } - } +void Install::convertToDestinationImages(const RomEntry& game, Import::ImagePaths& images) +{ + if(!images.logoPath().isEmpty()) + images.setLogoPath(imageDestinationPath(Fp::ImageType::Logo, game)); + + if(!images.screenshotPath().isEmpty()) + images.setScreenshotPath(imageDestinationPath(Fp::ImageType::Screenshot, game)); } } diff --git a/app/src/frontend/attractmode/am-install.h b/app/src/launcher/implementation/attractmode/am-install.h similarity index 60% rename from app/src/frontend/attractmode/am-install.h rename to app/src/launcher/implementation/attractmode/am-install.h index 81b7540..8a5a57a 100644 --- a/app/src/frontend/attractmode/am-install.h +++ b/app/src/launcher/implementation/attractmode/am-install.h @@ -5,25 +5,20 @@ #include // Project Includes -#include "frontend/fe-install.h" -#include "am-data.h" -#include "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 Fe::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":/frontend/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; @@ -52,9 +47,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 +57,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: @@ -79,9 +74,6 @@ class Install : public Fe::Install QFile mFpRomlist; QFile mEmulatorConfigFile; - // Image transfers for import worker - QList mWorkerImageJobs; - // Main romlist std::unique_ptr mRomlist; @@ -97,43 +89,39 @@ 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::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); + 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 void softReset() override; // 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::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(QList& workerTransfers, const Fe::ImageSources& bulkSources) override; Qx::Error postImport() override; // Image handling - void processDirectGameImages(const Fe::Game* game, const Fe::ImageSources& imageSources) override; + void processBulkImageSources(const Import::ImagePaths& bulkSources) override; + void convertToDestinationImages(const RomEntry& game, Import::ImagePaths& images) override; }; -REGISTER_FRONTEND(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/implementation/attractmode/am-install_linux.cpp similarity index 53% rename from app/src/frontend/attractmode/am-install_linux.cpp rename to app/src/launcher/implementation/attractmode/am-install_linux.cpp index 13df019..9dfad63 100644 --- a/app/src/frontend/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(); } diff --git a/app/src/frontend/attractmode/am-install_win.cpp b/app/src/launcher/implementation/attractmode/am-install_win.cpp similarity index 100% rename from app/src/frontend/attractmode/am-install_win.cpp rename to app/src/launcher/implementation/attractmode/am-install_win.cpp diff --git a/app/src/frontend/attractmode/am-items.cpp b/app/src/launcher/implementation/attractmode/am-items.cpp similarity index 90% rename from app/src/frontend/attractmode/am-items.cpp rename to app/src/launcher/implementation/attractmode/am-items.cpp index ca0e391..f141440 100644 --- a/app/src/frontend/attractmode/am-items.cpp +++ b/app/src/launcher/implementation/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; } @@ -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/frontend/attractmode/am-items.h b/app/src/launcher/implementation/attractmode/am-items.h similarity index 79% rename from app/src/frontend/attractmode/am-items.h rename to app/src/launcher/implementation/attractmode/am-items.h index b2accaf..d9e5331 100644 --- a/app/src/frontend/attractmode/am-items.h +++ b/app/src/launcher/implementation/attractmode/am-items.h @@ -9,12 +9,12 @@ #include // Project Includes -#include "frontend/fe-items.h" +#include "launcher/interface/lr-items-interface.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,24 @@ class RomEntry::Builder : public Fe::Game::Builder Builder& wRating(const QString& rating); }; -class EmulatorArtworkEntry : public Fe::Item +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--------------------------------------------------------------------------------------------------- public: @@ -132,7 +149,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/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/frontend/attractmode/am-settings-data.cpp b/app/src/launcher/implementation/attractmode/am-settings-data.cpp similarity index 91% rename from app/src/frontend/attractmode/am-settings-data.cpp rename to app/src/launcher/implementation/attractmode/am-settings-data.cpp index 7d1a73e..c66acf7 100644 --- a/app/src/frontend/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(); } -Fe::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,14 +212,9 @@ int CrudeSettingsReader::checkTabDepth(const QString& line) //-Instance Functions-------------------------------------------------------------------------------------------------- //Private: -CrudeSettings* CrudeSettingsReader::targetCrudeSettings() const +Lr::DocHandlingError CrudeSettingsReader::readTargetDoc() { - return static_cast(mTargetDocument); -} - -Fe::DocHandlingError CrudeSettingsReader::readTargetDoc() -{ - Fe::DocHandlingError errorStatus; + Lr::DocHandlingError errorStatus; // Got through all entries while(!mStreamReader.atEnd()) @@ -242,8 +237,8 @@ Fe::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); @@ -252,8 +247,8 @@ 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)); + QString subSetting = mCurrentSubSettingParser->settingName(); + errorStatus = Lr::DocHandlingError(*target(), Lr::DocHandlingError::DocReadFailed, UNKNOWN_KEY_ERROR.arg(key, subSetting)); 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 : std::as_const(source()->mDisplays)) { if(!writeDisplay(display)) return false; } // Write all other settings - for(const OtherSetting& setting : qAsConst(sourceCrudeSettings()->mOtherSettings)) + for(const OtherSetting& setting : std::as_const(source()->mOtherSettings)) { if(!writeOtherSetting(setting)) return false; diff --git a/app/src/frontend/attractmode/am-settings-data.h b/app/src/launcher/implementation/attractmode/am-settings-data.h similarity index 93% rename from app/src/frontend/attractmode/am-settings-data.h rename to app/src/launcher/implementation/attractmode/am-settings-data.h index 67c580f..a01836b 100644 --- a/app/src/frontend/attractmode/am-settings-data.h +++ b/app/src/launcher/implementation/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/implementation/attractmode/am-data.h" +#include "launcher/implementation/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 @@ -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; - Fe::DocHandlingError readTargetDoc() override; + 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/frontend/attractmode/am-settings-items.cpp b/app/src/launcher/implementation/attractmode/am-settings-items.cpp similarity index 100% rename from app/src/frontend/attractmode/am-settings-items.cpp rename to app/src/launcher/implementation/attractmode/am-settings-items.cpp diff --git a/app/src/frontend/attractmode/am-settings-items.h b/app/src/launcher/implementation/attractmode/am-settings-items.h similarity index 94% rename from app/src/frontend/attractmode/am-settings-items.h rename to app/src/launcher/implementation/attractmode/am-settings-items.h index ea1029e..8abcbb5 100644 --- a/app/src/frontend/attractmode/am-settings-items.h +++ b/app/src/launcher/implementation/attractmode/am-settings-items.h @@ -2,14 +2,14 @@ #define AM_SETTINGS_ITEMS_H // Project Includes -#include "frontend/fe-items.h" +#include "launcher/interface/lr-items-interface.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/implementation/launchbox/lb-data.cpp similarity index 85% rename from app/src/frontend/launchbox/lb-data.cpp rename to app/src/launcher/implementation/launchbox/lb-data.cpp index e065744..f6c9528 100644 --- a/app/src/frontend/launchbox/lb-data.cpp +++ b/app/src/launcher/implementation/launchbox/lb-data.cpp @@ -5,7 +5,8 @@ #include // Project Includes -#include "lb-install.h" +#include "import/details.h" +#include "launcher/implementation/launchbox/lb-install.h" namespace Xml { @@ -126,23 +127,20 @@ namespace Lb //-Constructor-------------------------------------------------------------------------------------------------------- //Public: -PlatformDoc::PlatformDoc(Install* const parent, const QString& xmlPath, QString docName, const Fe::UpdateOptions& updateOptions, - const DocKey&) : - Fe::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 Fe::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 = Import::Details::current().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 +153,10 @@ 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; + const QString& clifpPath = Import::Details::current().clifpPath; std::shared_ptr lbAddApp = std::make_shared(addApp, clifpPath); // Return converted game @@ -174,7 +172,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,24 +190,22 @@ void PlatformDoc::finalize() ++i; } - Fe::BasicPlatformDoc::finalize(); + Lr::BasicPlatformDoc::finalize(); } //=============================================================================================================== -// PlatformDoc::Reader +// PlatformDocReader //=============================================================================================================== //-Constructor-------------------------------------------------------------------------------------------------------- //Public: -PlatformDoc::Reader::Reader(PlatformDoc* targetDoc) : - Fe::DataDoc::Reader(targetDoc), - Fe::BasicPlatformDoc::Reader(targetDoc), - Fe::XmlDocReader(targetDoc, Xml::ROOT_ELEMENT) +PlatformDocReader::PlatformDocReader(PlatformDoc* targetDoc) : + Lr::XmlDocReader(targetDoc, Xml::ROOT_ELEMENT) {} //-Instance Functions------------------------------------------------------------------------------------------------- //Private: -Fe::DocHandlingError PlatformDoc::Reader::readTargetDoc() +Lr::DocHandlingError PlatformDocReader::readTargetDoc() { while(mStreamReader.readNextStartElement()) { @@ -227,7 +223,7 @@ Fe::DocHandlingError PlatformDoc::Reader::readTargetDoc() return streamStatus(); } -void PlatformDoc::Reader::parseGame() +void PlatformDocReader::parseGame() { // Game to build Game::Builder gb; @@ -281,10 +277,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 +308,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 +332,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) : - Fe::DataDoc::Writer(sourceDoc), - Fe::BasicPlatformDoc::Writer(sourceDoc), - Fe::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 : std::as_const(source()->mCustomFieldsFinal)) { if(!writeCustomField(*customField)) return false; @@ -380,7 +374,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 +419,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 +443,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 +469,14 @@ bool PlatformDoc::Writer::writeCustomField(const CustomField& customField) //-Constructor-------------------------------------------------------------------------------------------------------- //Public: -PlaylistDoc::PlaylistDoc(Install* const parent, const QString& xmlPath, QString docName, const Fe::UpdateOptions& updateOptions, - const DocKey&) : - Fe::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,17 +485,17 @@ 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(); 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 @@ -516,20 +509,18 @@ std::shared_ptr PlaylistDoc::preparePlaylistGame(const Fp::Pla } //=============================================================================================================== -// PlaylistDoc::Reader +// PlaylistDocReader //=============================================================================================================== //-Constructor-------------------------------------------------------------------------------------------------------- //Public: -PlaylistDoc::Reader::Reader(PlaylistDoc* targetDoc) : - Fe::DataDoc::Reader(targetDoc), - Fe::BasicPlaylistDoc::Reader(targetDoc), - Fe::XmlDocReader(targetDoc, Xml::ROOT_ELEMENT) +PlaylistDocReader::PlaylistDocReader(PlaylistDoc* targetDoc) : + Lr::XmlDocReader(targetDoc, Xml::ROOT_ELEMENT) {} //-Instance Functions------------------------------------------------------------------------------------------------- //Private: -Fe::DocHandlingError PlaylistDoc::Reader::readTargetDoc() +Lr::DocHandlingError PlaylistDocReader::readTargetDoc() { while(mStreamReader.readNextStartElement()) { @@ -545,7 +536,7 @@ Fe::DocHandlingError PlaylistDoc::Reader::readTargetDoc() return streamStatus(); } -void PlaylistDoc::Reader::parsePlaylistHeader() +void PlaylistDocReader::parsePlaylistHeader() { // Playlist Header to Build PlaylistHeader::Builder phb; @@ -566,10 +557,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 +590,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) : - Fe::DataDoc::Writer(sourceDoc), - Fe::BasicPlaylistDoc::Writer(sourceDoc), - Fe::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 +630,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 +651,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 +679,13 @@ bool PlaylistDoc::Writer::writePlaylistGame(const PlaylistGame& playlistGame) //-Constructor-------------------------------------------------------------------------------------------------------- //Public: -PlatformsConfigDoc::PlatformsConfigDoc(Install* const parent, const QString& xmlPath, const Fe::UpdateOptions& updateOptions, - const DocKey&) : - Fe::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: -Fe::DataDoc::Type PlatformsConfigDoc::type() const { return Fe::DataDoc::Type::Config; } +Lr::IDataDoc::Type PlatformsConfigDoc::type() const { return Lr::IDataDoc::Type::Config; } //Public: bool PlatformsConfigDoc::isEmpty() const @@ -758,7 +746,7 @@ void PlatformsConfigDoc::finalize() finalizeUpdateableItems(mPlatformCategoriesExisting, mPlatformCategoriesFinal); // Finalize base - Fe::UpdateableDoc::finalize(); + Lr::IUpdateableDoc::finalize(); } //=============================================================================================================== @@ -768,13 +756,12 @@ void PlatformsConfigDoc::finalize() //-Constructor-------------------------------------------------------------------------------------------------------- //Public: PlatformsConfigDoc::Reader::Reader(PlatformsConfigDoc* targetDoc) : - Fe::DataDoc::Reader(targetDoc), - Fe::XmlDocReader(targetDoc, Xml::ROOT_ELEMENT) + Lr::XmlDocReader(targetDoc, Xml::ROOT_ELEMENT) {} //-Instance Functions------------------------------------------------------------------------------------------------- //Private: -Fe::DocHandlingError PlatformsConfigDoc::Reader::readTargetDoc() +Lr::DocHandlingError PlatformsConfigDoc::Reader::readTargetDoc() { while(mStreamReader.readNextStartElement()) { @@ -782,7 +769,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) @@ -813,10 +800,10 @@ 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; } -Fe::DocHandlingError PlatformsConfigDoc::Reader::parsePlatformFolder() +Lr::DocHandlingError PlatformsConfigDoc::Reader::parsePlatformFolder() { // Platform Folder to Build PlatformFolder::Builder pfb; @@ -831,14 +818,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(*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 Fe::DocHandlingError(); + return Lr::DocHandlingError(); } void PlatformsConfigDoc::Reader::parsePlatformCategory() @@ -861,7 +848,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 +858,7 @@ void PlatformsConfigDoc::Reader::parsePlatformCategory() //-Constructor-------------------------------------------------------------------------------------------------------- //Public: PlatformsConfigDoc::Writer::Writer(PlatformsConfigDoc* sourceDoc) : - Fe::DataDoc::Writer(sourceDoc), - Fe::XmlDocWriter(sourceDoc, Xml::ROOT_ELEMENT) + Lr::XmlDocWriter(sourceDoc, Xml::ROOT_ELEMENT) {} //-Instance Functions------------------------------------------------------------------------------------------------- @@ -880,21 +866,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 +951,13 @@ 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) +ParentsDoc::ParentsDoc(Install* install, const QString& xmlPath) : + Lr::DataDoc(install, xmlPath, STD_NAME) {} //-Instance Functions-------------------------------------------------------------------------------------------------- //Private: -Fe::DataDoc::Type ParentsDoc::type() const { return Fe::DataDoc::Type::Config; } +Lr::IDataDoc::Type ParentsDoc::type() const { return Lr::IDataDoc::Type::Config; } bool ParentsDoc::removeIfPresent(qsizetype idx) { @@ -1042,13 +1028,12 @@ 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::XmlDocReader(targetDoc, Xml::ROOT_ELEMENT) {} //-Instance Functions------------------------------------------------------------------------------------------------- //Private: -Fe::DocHandlingError ParentsDoc::Reader::readTargetDoc() +Lr::DocHandlingError ParentsDoc::Reader::readTargetDoc() { while(mStreamReader.readNextStartElement()) { @@ -1084,7 +1069,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 +1079,7 @@ void ParentsDoc::Reader::parseParent() //-Constructor-------------------------------------------------------------------------------------------------------- //Public: ParentsDoc::Writer::Writer(ParentsDoc* sourceDoc) : - Fe::DataDoc::Writer(sourceDoc), - Fe::XmlDocWriter(sourceDoc, Xml::ROOT_ELEMENT) + Lr::XmlDocWriter(sourceDoc, Xml::ROOT_ELEMENT) {} //-Instance Functions------------------------------------------------------------------------------------------------- @@ -1103,7 +1087,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/frontend/launchbox/lb-data.h b/app/src/launcher/implementation/launchbox/lb-data.h similarity index 65% rename from app/src/frontend/launchbox/lb-data.h rename to app/src/launcher/implementation/launchbox/lb-data.h index 835073b..26b3d13 100644 --- a/app/src/frontend/launchbox/lb-data.h +++ b/app/src/launcher/implementation/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,33 +12,17 @@ #include // Project Includes -#include "lb-items.h" -#include "frontend/fe-data.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 Fe::BasicPlatformDoc -{ -//-Inner Classes---------------------------------------------------------------------------------------------------- -public: - class Reader; - class Writer; - + friend PlatformDocReader; + friend PlatformDocWriter; //-Instance Variables-------------------------------------------------------------------------------------------------- private: QHash> mCustomFieldsFinal; @@ -46,13 +30,12 @@ class PlatformDoc : public Fe::BasicPlatformDoc //-Constructor-------------------------------------------------------------------------------------------------------- public: - explicit PlatformDoc(Install* const parent, const QString& xmlPath, QString docName, const Fe::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 Fe::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,25 +45,25 @@ class PlatformDoc : public Fe::BasicPlatformDoc void finalize() override; }; -class PlatformDoc::Reader : public Fe::BasicPlatformDoc::Reader, public Fe::XmlDocReader +class PlatformDocReader : public Lr::XmlDocReader { //-Constructor-------------------------------------------------------------------------------------------------------- public: - Reader(PlatformDoc* targetDoc); + PlatformDocReader(PlatformDoc* targetDoc); //-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 PlatformDocWriter : public Lr::XmlDocWriter { //-Constructor-------------------------------------------------------------------------------------------------------- public: - Writer(PlatformDoc* sourceDoc); + PlatformDocWriter(PlatformDoc* sourceDoc); //-Instance Functions-------------------------------------------------------------------------------------------------- private: @@ -90,46 +73,42 @@ 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: - 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 Fe::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 Fe::BasicPlaylistDoc::Reader, public Fe::XmlDocReader +class PlaylistDocReader : public Lr::XmlDocReader { //-Constructor-------------------------------------------------------------------------------------------------------- public: - Reader(PlaylistDoc* targetDoc); + PlaylistDocReader(PlaylistDoc* targetDoc); //-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 PlaylistDocWriter : public Lr::XmlDocWriter { //-Constructor-------------------------------------------------------------------------------------------------------- public: - Writer(PlaylistDoc* sourceDoc); + PlaylistDocWriter(PlaylistDoc* sourceDoc); //-Instance Functions------------------------------------------------------------------------------------------------- private: @@ -138,7 +117,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,8 +139,7 @@ class PlatformsConfigDoc : public Fe::UpdateableDoc //-Constructor-------------------------------------------------------------------------------------------------------- public: - explicit PlatformsConfigDoc(Install* const parent, const QString& xmlPath, const Fe::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 Fe::UpdateableDoc void finalize() override; }; -class PlatformsConfigDoc::Reader : public Fe::XmlDocReader +class PlatformsConfigDoc::Reader : public Lr::XmlDocReader { //-Constructor-------------------------------------------------------------------------------------------------------- public: @@ -194,13 +172,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,26 +192,26 @@ class PlatformsConfigDoc::Writer : public Fe::XmlDocWriter bool writePlatformCategory(const PlatformCategory& platformCategory); }; -class ParentsDoc : public Fe::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,29 +241,29 @@ class ParentsDoc : public Fe::DataDoc void addParent(const Parent& parent); }; -class ParentsDoc::Reader : public Fe::XmlDocReader +class ParentsDoc::Reader : public Lr::XmlDocReader { - //-Constructor-------------------------------------------------------------------------------------------------------- +//-Constructor-------------------------------------------------------------------------------------------------------- public: Reader(ParentsDoc* targetDoc); - //-Instance Functions------------------------------------------------------------------------------------------------- +//-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-------------------------------------------------------------------------------------------------------- +//-Constructor-------------------------------------------------------------------------------------------------------- public: Writer(ParentsDoc* sourceDoc); - //-Instance Functions------------------------------------------------------------------------------------------------- +//-Instance Functions------------------------------------------------------------------------------------------------- private: bool writeSourceDoc() override; bool writeParent(const Parent& parent); }; } -#endif // LAUNCHBOX_XML_H +#endif // LAUNCHBOX_DATA_H diff --git a/app/src/frontend/launchbox/lb-install.cpp b/app/src/launcher/implementation/launchbox/lb-install.cpp similarity index 62% rename from app/src/frontend/launchbox/lb-install.cpp rename to app/src/launcher/implementation/launchbox/lb-install.cpp index a4769d7..da7e961 100644 --- a/app/src/frontend/launchbox/lb-install.cpp +++ b/app/src/launcher/implementation/launchbox/lb-install.cpp @@ -13,8 +13,14 @@ #include #include #include +#include #include +// Project Includes +#include "import/details.h" + +using namespace Qt::StringLiterals; + namespace Lb { //=============================================================================================================== @@ -24,7 +30,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 +57,7 @@ Install::Install(const QString& installPath) : //Private: void Install::nullify() { - Fe::Install::nullify(); + Lr::IInstall::nullify(); mDataDirectory = QDir(); mPlatformsDirectory = QDir(); @@ -72,39 +78,39 @@ Qx::Error Install::populateExistingDocs() if(existingCheck.isFailure()) return existingCheck; - for(const QFileInfo& platformFile : qAsConst(existingList)) - catalogueExistingDoc(Fe::DataDoc::Identifier(Fe::DataDoc::Type::Platform, platformFile.baseName())); + for(const QFileInfo& platformFile : std::as_const(existingList)) + 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); if(existingCheck.isFailure()) return existingCheck; - for(const QFileInfo& playlistFile : qAsConst(existingList)) - catalogueExistingDoc(Fe::DataDoc::Identifier(Fe::DataDoc::Type::Playlist, playlistFile.baseName())); + for(const QFileInfo& playlistFile : std::as_const(existingList)) + catalogueExistingDoc(Lr::IDataDoc::Identifier(Lr::IDataDoc::Type::Playlist, playlistFile.baseName())); // Check for config docs existingCheck = Qx::dirContentInfoList(existingList, mDataDirectory, {u"*."_s + XML_EXT}); if(existingCheck.isFailure()) return existingCheck; - for(const QFileInfo& configDocFile : qAsConst(existingList)) - catalogueExistingDoc(Fe::DataDoc::Identifier(Fe::DataDoc::Type::Config, configDocFile.baseName())); + for(const QFileInfo& configDocFile : std::as_const(existingList)) + catalogueExistingDoc(Lr::IDataDoc::Identifier(Lr::IDataDoc::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() + '/' + + 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 Fe::ImageSources& imageSources) +void Install::editBulkImageReferences(const Import::ImagePaths& imageSources) { // Set media folder paths const QList affectedPlatforms = modifiedPlatforms(); @@ -130,97 +136,60 @@ void Install::editBulkImageReferences(const Fe::ImageSources& imageSources) } } -QString Install::dataDocPath(Fe::DataDoc::Identifier identifier) const +QString Install::dataDocPath(Lr::IDataDoc::Identifier identifier) const { QString fileName = identifier.docName() + u"."_s + XML_EXT; switch(identifier.docType()) { - case Fe::DataDoc::Type::Platform: + case Lr::IDataDoc::Type::Platform: return mPlatformsDirectory.absoluteFilePath(fileName); break; - case Fe::DataDoc::Type::Playlist: + case Lr::IDataDoc::Type::Playlist: return mPlaylistsDirectory.absoluteFilePath(fileName); break; - case Fe::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 Fe::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 - Fe::DataDoc::Identifier docId(Fe::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())); + Lr::IDataDoc::Identifier docId(Lr::IDataDoc::Type::Platform, translatedName); - // Return reader and doc - return docReader; + // Construct unopened document and return + return std::make_unique(this, dataDocPath(docId), translatedName, Import::Details::current().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 - Fe::DataDoc::Identifier docId(Fe::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; + return std::make_unique(this, dataDocPath(docId), translatedName, Import::Details::current().updateOptions); } -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; -} - -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::IDataDoc::Identifier docId(Lr::IDataDoc::Type::Config, PlatformsConfigDoc::STD_NAME); // Construct unopened document - Fe::UpdateOptions uo{.importMode = Fe::ImportMode::NewAndExisting, .removeObsolete = false}; - returnBuffer = std::make_unique(this, dataDocPath(docId), uo, DocKey{}); + Import::UpdateOptions uo{.importMode = Import::UpdateMode::NewAndExisting, .removeObsolete = false}; + returnBuffer = std::make_unique(this, dataDocPath(docId), uo); // 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(docReader); // Set return null on failure if(readErrorStatus.isValid()) @@ -230,15 +199,15 @@ Fe::DocHandlingError Install::checkoutPlatformsConfigDoc(std::unique_ptr document) +Lr::DocHandlingError Install::commitPlatformsConfigDoc(std::unique_ptr document) { - assert(document->parent() == this); + Q_ASSERT(document->install() == this); // Prepare writer std::shared_ptr docWriter = std::make_shared(document.get()); // Write - Fe::DocHandlingError writeErrorStatus = commitDataDocument(document.get(), docWriter); + Lr::DocHandlingError writeErrorStatus = commitDataDocument(docWriter); // Ensure document is cleared document.reset(); @@ -247,19 +216,19 @@ 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::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()); // Open document - Fe::DocHandlingError readErrorStatus = checkoutDataDocument(returnBuffer.get(), docReader); + Lr::DocHandlingError readErrorStatus = checkoutDataDocument(docReader); // Set return null on failure if(readErrorStatus.isValid()) @@ -269,15 +238,15 @@ 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); + Q_ASSERT(document->install() == this); // Prepare writer std::shared_ptr docWriter = std::make_shared(document.get()); // Write - Fe::DocHandlingError writeErrorStatus = commitDataDocument(document.get(), docWriter); + Lr::DocHandlingError writeErrorStatus = commitDataDocument(docWriter); // Ensure document is cleared document.reset(); @@ -289,20 +258,18 @@ Fe::DocHandlingError Install::commitParentsDoc(std::unique_ptr docum //Public: void Install::softReset() { - Fe::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; } +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 +279,10 @@ QString Install::versionString() const else if(!productVersionStr.isEmpty()) return productVersionStr; else - return Fe::Install::versionString(); + return Lr::IInstall::versionString(); } -QString Install::translateDocName(const QString& originalName, Fe::DataDoc::Type type) const +QString Install::translateDocName(const QString& originalName, Lr::IDataDoc::Type type) const { Q_UNUSED(type); @@ -330,12 +297,12 @@ QString Install::translateDocName(const QString& originalName, Fe::DataDoc::Type * 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) - 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); @@ -345,7 +312,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::IInstall::prePlatformsImport(); superErr.isValid()) return superErr; // Open platforms document @@ -354,11 +321,11 @@ Qx::Error Install::prePlatformsImport() Qx::Error Install::postPlatformsImport() { - if(Qx::Error superErr = Fe::Install::postPlatformsImport(); superErr.isValid()) + if(Qx::Error superErr = Lr::IInstall::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 @@ -401,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)) { @@ -420,36 +387,22 @@ Qx::Error Install::postPlatformsImport() return Qx::Error(); } -Qx::Error Install::preImageProcessing(QList& workerTransfers, const Fe::ImageSources& bulkSources) +Qx::Error Install::preImageProcessing() { - if(Qx::Error superErr = Fe::Install::preImageProcessing(workerTransfers, bulkSources); superErr.isValid()) - return superErr; + if(Import::Details::current().imageMode != Import::ImageMode::Reference) + editBulkImageReferences(Import::ImagePaths());// Null arg will remove old references - switch(mImportDetails->imageMode) - { - case Fe::ImageMode::Link: - case Fe::ImageMode::Copy: - workerTransfers.swap(mWorkerImageJobs); - editBulkImageReferences(bulkSources); - break; - case Fe::ImageMode::Reference: - editBulkImageReferences(bulkSources); - break; - default: - qWarning("unhandled image mode"); - } - - return Qx::Error(); + return Lr::IInstall::preImageProcessing(); } Qx::Error Install::postImageProcessing() { - if(Qx::Error superErr = Fe::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 mPlatformsConfig->finalize(); - Fe::DocHandlingError saveError = commitPlatformsConfigDoc(std::move(mPlatformsConfig)); + Lr::DocHandlingError saveError = commitPlatformsConfigDoc(std::move(mPlatformsConfig)); return saveError; } @@ -457,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)) { @@ -475,25 +428,18 @@ Qx::Error Install::postPlaylistsImport() return commitParentsDoc(std::move(mParents)); } -void Install::processDirectGameImages(const Fe::Game* game, const Fe::ImageSources& imageSources) +void Install::processBulkImageSources(const Import::ImagePaths& bulkSources) { - Fe::ImageMode mode = mImportDetails->imageMode; - if(mode == Fe::ImageMode::Link || mode == Fe::ImageMode::Copy) - { - if(!imageSources.logoPath().isEmpty()) - { - ImageMap logoMap{.sourcePath = imageSources.logoPath(), - .destPath = imageDestinationPath(Fp::ImageType::Logo, game)}; - mWorkerImageJobs.append(logoMap); - } + editBulkImageReferences(bulkSources); +} - if(!imageSources.screenshotPath().isEmpty()) - { - ImageMap ssMap{.sourcePath = imageSources.screenshotPath(), - .destPath = imageDestinationPath(Fp::ImageType::Screenshot, game)}; - mWorkerImageJobs.append(ssMap); - } - } +void Install::convertToDestinationImages(const Game& game, Import::ImagePaths& images) +{ + if(!images.logoPath().isEmpty()) + images.setLogoPath(imageDestinationPath(Fp::ImageType::Logo, game)); + + 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/frontend/launchbox/lb-install.h b/app/src/launcher/implementation/launchbox/lb-install.h similarity index 65% rename from app/src/frontend/launchbox/lb-install.h rename to app/src/launcher/implementation/launchbox/lb-install.h index 48d7667..872d667 100644 --- a/app/src/frontend/launchbox/lb-install.h +++ b/app/src/launcher/implementation/launchbox/lb-install.h @@ -9,23 +9,17 @@ #include // Project Includes -#include "frontend/fe-install.h" -#include "lb-items.h" -#include "lb-data.h" +#include "launcher/abstract/lr-install.h" +#include "launcher/implementation/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; //-Class Variables-------------------------------------------------------------------------------------------------- -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 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; @@ -53,10 +47,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----------------------------------------------------------------------------------------------- @@ -72,9 +66,6 @@ class Install : public Fe::Install QDir mCoreDirectory; QFileInfo mExeFile; - // Image transfers for import worker - QList mWorkerImageJobs; - // Persistent config handles std::unique_ptr mPlatformsConfig; std::unique_ptr mParents; @@ -97,47 +88,44 @@ 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 Import::ImagePaths& 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::IDataDoc::Identifier identifier) const; + std::unique_ptr preparePlatformDocCheckout(const QString& translatedName) override; + std::unique_ptr preparePlaylistDocCheckout(const QString& translatedName) 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 void softReset() override; // 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::IDataDoc::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() override; Qx::Error postImageProcessing() override; Qx::Error postPlaylistsImport() override; // Image handling - void processDirectGameImages(const Fe::Game* game, const Fe::ImageSources& imageSources) 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; - }; -REGISTER_FRONTEND(Install::NAME, Install, &Install::ICON_PATH, &Install::HELP_URL); + } diff --git a/app/src/frontend/launchbox/lb-items.cpp b/app/src/launcher/implementation/launchbox/lb-items.cpp similarity index 98% rename from app/src/frontend/launchbox/lb-items.cpp rename to app/src/launcher/implementation/launchbox/lb-items.cpp index 205137c..2042a64 100644 --- a/app/src/frontend/launchbox/lb-items.cpp +++ b/app/src/launcher/implementation/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/implementation/launchbox/lb-items.h similarity index 91% rename from app/src/frontend/launchbox/lb-items.h rename to app/src/launcher/implementation/launchbox/lb-items.h index 9c7ad1d..1ccb5c2 100644 --- a/app/src/frontend/launchbox/lb-items.h +++ b/app/src/launcher/implementation/launchbox/lb-items.h @@ -10,12 +10,12 @@ #include // Project Includes -#include "frontend/fe-items.h" +#include "launcher/interface/lr-items-interface.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 +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 +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 +class PlatformCategory::Builder : public Lr::Item::Builder { //-Constructor------------------------------------------------------------------------------------------------- public: @@ -378,7 +378,7 @@ class PlatformCategory::Builder : public Fe::Item::Builder +class Parent::Builder : public Lr::Item::Builder { //-Constructor------------------------------------------------------------------------------------------------- public: 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..b870b3a --- /dev/null +++ b/app/src/launcher/interface/lr-data-interface.cpp @@ -0,0 +1,197 @@ +// Unit Include +#include "lr-data-interface.h" + +// Qx Includes +#include + +// 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) +{ + 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------------------------------------------------------------- +//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; } + +//=============================================================================================================== +// 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------------------------------------------------------------------------------------------------------ +size_t qHash(const IDataDoc::Identifier& key, size_t seed) noexcept +{ + return qHashMulti(seed, key.mDocType, key.mDocName); +} + +//-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------------------------------------------------------------------------------------------------- +//Public: +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------------------------------------------------------------------------------------------------- +//Public: +IDataDoc* IDataDoc::Writer::source() const { return mSourceDocument; } + +//=============================================================================================================== +// IErrorable +//=============================================================================================================== + +//-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; } + +//=============================================================================================================== +// IUpdateableDoc +//=============================================================================================================== + +//-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 + +//=============================================================================================================== +// IPlatformDoc +//=============================================================================================================== + +//-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; } + +//=============================================================================================================== +// IPlaylistDoc +//=============================================================================================================== + +//-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..c310572 --- /dev/null +++ b/app/src/launcher/interface/lr-data-interface.h @@ -0,0 +1,337 @@ +#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 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 size_t qHash(const Identifier& key, size_t 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------------------------------------------------------------------------------------------------ +public: + IDataDoc* target() const; + virtual DocHandlingError readInto() = 0; +}; + +class IDataDoc::Writer +{ +//-Instance Variables-------------------------------------------------------------------------------------------------- +private: + IDataDoc* mSourceDocument; + +//-Constructor-------------------------------------------------------------------------------------------------------- +protected: + Writer(IDataDoc* sourceDoc); + +//-Destructor------------------------------------------------------------------------------------------------- +public: + virtual ~Writer(); + +//-Instance Functions------------------------------------------------------------------------------------------------- +public: + IDataDoc* source() const; + 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 +{ +//-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, Import::ImagePaths& images) = 0; +}; + +class IPlaylistDoc : public IUpdateableDoc +{ +//-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/interface/lr-install-interface.cpp b/app/src/launcher/interface/lr-install-interface.cpp new file mode 100644 index 0000000..b989a91 --- /dev/null +++ b/app/src/launcher/interface/lr-install-interface.cpp @@ -0,0 +1,190 @@ +// Unit Include +#include "lr-install-interface.h" + +// Project Includes +#include "import/backup.h" + +namespace Lr +{ + +//=============================================================================================================== +// IInstall +//=============================================================================================================== + +//-Constructor--------------------------------------------------------------------------------------------------- +IInstall::IInstall(const QString& installPath) : + mValid(false), // Path is invalid until proven otherwise + mRootDirectory(installPath) +{} + +//-Destructor------------------------------------------------------------------------------------------------ +//Public: +IInstall::~IInstall() {} + +//-Instance Functions-------------------------------------------------------------------------------------------- +//Private: +bool IInstall::containsAnyDataDoc(IDataDoc::Type type, const QList& names) const +{ + // Create identifier set of names + QSet searchSet; + for(const QString& docName : names) + searchSet << IDataDoc::Identifier(type, translateDocName(docName, type)); + + // Cross reference with existing documents + return mExistingDocuments.intersects(searchSet); +} + +bool IInstall::supportsImageMode(Import::ImageMode imageMode) const { return preferredImageModeOrder().contains(imageMode); } + +QList IInstall::modifiedDataDocs(IDataDoc::Type type) const +{ + QList modList; + + for(const IDataDoc::Identifier& dataDocId : mModifiedDocuments) + if(dataDocId.docType() == type) + modList.append(dataDocId.docName()); + + return modList; +} + +//Protected: +void IInstall::nullify() +{ + mValid = false; + mRootDirectory = QDir(); +} + +void IInstall::declareValid(bool valid) +{ + mValid = valid; + if(!valid) + nullify(); +} + +void IInstall::catalogueExistingDoc(IDataDoc::Identifier existingDoc) { mExistingDocuments.insert(existingDoc); } + +DocHandlingError IInstall::checkoutDataDocument(std::shared_ptr docReader) +{ + auto docToOpen = docReader->target(); + + // Error report to return + DocHandlingError openReadError; // Defaults to no error + + // Check if lease is already out + if(mLeasedDocuments.contains(docToOpen->identifier())) + openReadError = DocHandlingError(*docToOpen, DocHandlingError::DocAlreadyOpen); + else + { + // Read existing file if present and a reader was provided + if(docReader && mExistingDocuments.contains(docToOpen->identifier())) + openReadError = docReader->readInto(); + + // Add lease to ledger if no error occurred while reading + if(!openReadError.isValid()) + mLeasedDocuments.insert(docToOpen->identifier()); + } + + // Return opened document and status + return openReadError; +} + +DocHandlingError IInstall::commitDataDocument(std::shared_ptr docWriter) +{ + auto docToSave = docWriter->source(); + IDataDoc::Identifier id = docToSave->identifier(); + + // 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; + + // Handle modification + if(!docToSave->isEmpty()) + { + mModifiedDocuments.insert(id); + commitError = docWriter->writeOutOf(); + ensureModifiable(docToSave->path()); + } + + // Remove handle reservation + mLeasedDocuments.remove(docToSave->identifier()); + + // Return write status and let document ptr auto delete + return commitError; +} + +QList IInstall::modifiedPlatforms() const { return modifiedDataDocs(IDataDoc::Type::Platform); } +QList IInstall::modifiedPlaylists() const { return modifiedDataDocs(IDataDoc::Type::Playlist); } + +//Public: +QString IInstall::versionString() const { return u"Unknown Version"_s; } +bool IInstall::isValid() const { return mValid; } +QString IInstall::path() const { return mRootDirectory.absolutePath(); } + +void IInstall::softReset() +{ + mModifiedDocuments.clear(); + mLeasedDocuments.clear(); +} + +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) + *changed = mExistingDocuments != oldDocSet; + return error; +} + +bool IInstall::containsPlatform(const QString& name) const +{ + return containsAnyDataDoc(IDataDoc::Type::Platform, {name}); +} + +bool IInstall::containsPlaylist(const QString& name) const +{ + return containsAnyDataDoc(IDataDoc::Type::Playlist, {name}); +} + +bool IInstall::containsAnyPlatform(const QList& names) const +{ + return containsAnyDataDoc(IDataDoc::Type::Platform, names); +} + +bool IInstall::containsAnyPlaylist(const QList& names) const +{ + return containsAnyDataDoc(IDataDoc::Type::Playlist, names); +} + +/* 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() { return {}; } +Qx::Error IInstall::postImport() { return {}; } +Qx::Error IInstall::prePlatformsImport() { return {}; } +Qx::Error IInstall::postPlatformsImport() { return {}; } +Qx::Error IInstall::preImageProcessing() { 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/interface/lr-install-interface.h b/app/src/launcher/interface/lr-install-interface.h new file mode 100644 index 0000000..f2d864a --- /dev/null +++ b/app/src/launcher/interface/lr-install-interface.h @@ -0,0 +1,111 @@ +#ifndef LR_INSTALL_INTERFACE_H +#define LR_INSTALL_INTERFACE_H + +// Qt Includes +#include + +// Project Includes +#include "launcher/interface/lr-data-interface.h" +#include "import/settings.h" + +namespace Lr +{ + +class IInstall +{ +//-Class Variables----------------------------------------------------------------------------------------------- +protected: + // Files + static inline const QString IMAGE_EXT = u"png"_s; + +//-Instance Variables-------------------------------------------------------------------------------------------- +private: + // Validity + bool mValid; + + // Files and directories + QDir mRootDirectory; + + // Document tracking + QSet mExistingDocuments; + QSet mModifiedDocuments; + QSet mLeasedDocuments; + +//-Constructor--------------------------------------------------------------------------------------------------- +public: + IInstall(const QString& installPath); + +//-Destructor------------------------------------------------------------------------------------------------- +public: + virtual ~IInstall(); + +//-Class Functions------------------------------------------------------------------------------------------------------ +private: + static void ensureModifiable(const QString& filePath); + +//-Instance Functions--------------------------------------------------------------------------------------------------------- +private: + // 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(); + void declareValid(bool valid); + + // Docs + void catalogueExistingDoc(IDataDoc::Identifier existingDoc); + DocHandlingError checkoutDataDocument(std::shared_ptr docReader); + DocHandlingError commitDataDocument(std::shared_ptr docWriter); + QList modifiedPlatforms() const; + QList modifiedPlaylists() const; + virtual Qx::Error populateExistingDocs() = 0; + +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(); + + // 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; // 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; + virtual DocHandlingError commitPlatformDoc(std::unique_ptr platformDoc) = 0; + virtual DocHandlingError commitPlaylistDoc(std::unique_ptr playlistDoc) = 0; + + // Import stage notifier hooks + virtual Qx::Error preImport(); + virtual Qx::Error postImport(); + virtual Qx::Error prePlatformsImport(); + virtual Qx::Error postPlatformsImport(); + virtual Qx::Error preImageProcessing(); + virtual Qx::Error postImageProcessing(); + virtual Qx::Error prePlaylistsImport(); + virtual Qx::Error postPlaylistsImport(); + + // Images + 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 + // 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_INSTALL_INTERFACE_H diff --git a/app/src/frontend/fe-installfoundation_linux.cpp b/app/src/launcher/interface/lr-install-interface_linux.cpp similarity index 79% rename from app/src/frontend/fe-installfoundation_linux.cpp rename to app/src/launcher/interface/lr-install-interface_linux.cpp index b802867..aefd230 100644 --- a/app/src/frontend/fe-installfoundation_linux.cpp +++ b/app/src/launcher/interface/lr-install-interface_linux.cpp @@ -1,18 +1,18 @@ // Unit Include -#include "fe-installfoundation.h" +#include "lr-install-interface.h" // Qt Includes #include -namespace Fe +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/frontend/fe-installfoundation_win.cpp b/app/src/launcher/interface/lr-install-interface_win.cpp similarity index 94% rename from app/src/frontend/fe-installfoundation_win.cpp rename to app/src/launcher/interface/lr-install-interface_win.cpp index cc79457..d528d3a 100644 --- a/app/src/frontend/fe-installfoundation_win.cpp +++ b/app/src/launcher/interface/lr-install-interface_win.cpp @@ -1,19 +1,20 @@ // Unit Include -#include "fe-installfoundation.h" +#include "lr-install-interface.h" // Windows Includes (Specifically for changing file permissions) #include "Aclapi.h" -namespace Fe +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; PSECURITY_DESCRIPTOR pSecurityDescriptor = nullptr; PSID pOwnerId = nullptr; diff --git a/app/src/frontend/fe-items.cpp b/app/src/launcher/interface/lr-items-interface.cpp similarity index 98% rename from app/src/frontend/fe-items.cpp rename to app/src/launcher/interface/lr-items-interface.cpp index 646c3c3..5c62e73 100644 --- a/app/src/frontend/fe-items.cpp +++ b/app/src/launcher/interface/lr-items-interface.cpp @@ -1,7 +1,7 @@ // Unit Include -#include "fe-items.h" +#include "lr-items-interface.h" -namespace Fe +namespace Lr { //=============================================================================================================== diff --git a/app/src/frontend/fe-items.h b/app/src/launcher/interface/lr-items-interface.h similarity index 67% rename from app/src/frontend/fe-items.h rename to app/src/launcher/interface/lr-items-interface.h index 1c2cf1b..10961d3 100644 --- a/app/src/frontend/fe-items.h +++ b/app/src/launcher/interface/lr-items-interface.h @@ -1,5 +1,5 @@ -#ifndef FE_ITEMS_H -#define FE_ITEMS_H +#ifndef LR_ITEMS_INTERFACE_H +#define LR_ITEMS_INTERFACE_H // Standard Library Includes #include @@ -8,31 +8,70 @@ #include #include +// Qx Includes +#include + 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 */ +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------------------------------------------------------------------------------------------ @@ -72,10 +110,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,8 +124,7 @@ class BasicItem : public Item { //-Inner Classes--------------------------------------------------------------------------------------------------- public: - template - requires std::derived_from + template class Builder; //-Instance Variables----------------------------------------------------------------------------------------------- @@ -105,23 +143,26 @@ class BasicItem : public Item QString name() const; }; -template - requires std::derived_from -class BasicItem::Builder : public Item::Builder +template +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 - requires std::derived_from + template class Builder; //-Class Variables-------------------------------------------------------------------------------------------------- @@ -143,21 +184,20 @@ class Game : public BasicItem QString platform() const; }; -template - requires std::derived_from -class Game::Builder : public BasicItem::Builder +template +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 - requires std::derived_from + template class Builder; //-Instance Variables----------------------------------------------------------------------------------------------- @@ -174,22 +214,23 @@ class AddApp : public BasicItem QUuid gameId() const; }; -template - requires std::derived_from -class AddApp::Builder : public BasicItem::Builder +template +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 - requires std::derived_from + template class Builder; //-Constructor------------------------------------------------------------------------------------------------- @@ -198,17 +239,15 @@ class PlaylistHeader : public BasicItem PlaylistHeader(QUuid id, QString name); }; -template - requires std::derived_from -class PlaylistHeader::Builder : public BasicItem::Builder +template +class PlaylistHeader::Builder : public BasicItem::Builder {}; class PlaylistGame : public BasicItem { //-Inner Classes--------------------------------------------------------------------------------------------------- public: - template - requires std::derived_from + template class Builder; //-Instance Variables----------------------------------------------------------------------------------------------- @@ -224,17 +263,19 @@ class PlaylistGame : public BasicItem QUuid gameId() const; }; -template - requires std::derived_from -class PlaylistGame::Builder : public BasicItem::Builder +template +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; } }; } -#endif // FE_ITEMS_H +#endif // LR_ITEMS_INTERFACE_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..b8c35da 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/abstract/lr-registration.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,188 @@ 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); - }; - mWidgetEnableConditionMap[ui->radioButton_copy] = [&](){ - return mFrontendInstall && mFlashpointInstall && mFrontendInstall->supportsImageMode(Fe::ImageMode::Copy); + return{ + {Import::ImageMode::Link, ui->radioButton_link}, + {Import::ImageMode::Copy, ui->radioButton_copy}, + {Import::ImageMode::Reference, ui->radioButton_reference}, }; - - 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); -} - -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 - - // Check frontend existing items - bool changed = false; - mFrontendInstall->refreshExistingDocs(&changed); - return changed; -} + QStandardItem* modelRoot = mTagModel.invisibleRootItem(); -void MainWindow::redoInputChecks() -{ - // Check existing locations again - validateInstall(mFrontendInstall->path(), InstallType::Frontend); - validateInstall(mFlashpointInstall->dir().absolutePath(), InstallType::Flashpoint); -} + // 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); -void MainWindow::invalidateInstall(InstallType install, bool informUser) -{ - clearListWidgets(); - mTagSelectionModel.reset(); // Void tag selection model + rootItem->appendRow(childItem); + } - 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; - } + modelRoot->appendRow(rootItem); + } - refreshEnableStates(); -} + // Sort + mTagModel.sort(0); + }); -void MainWindow::clearListWidgets() -{ - ui->listWidget_platformChoices->clear(); - ui->listWidget_playlistChoices->clear(); - mPlatformItemCheckStates.clear(); - mPlaylistItemCheckStates.clear(); -} + // List widget items + mImportProperties.bindablePlatforms().subscribeLifetime([&]{ + // Always clear any existing + mPlatformSelections.clear(); -bool MainWindow::isExistingPlatformSelected() -{ - // 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; - } + auto platforms = mImportProperties.platforms(); + if(!platforms.isEmpty()) + mPlatformSelections.fill(platforms); + }); + mImportProperties.bindablePlaylists().subscribeLifetime([&]{ + // Always clear any existing + mPlaylistSelections.clear(); - // Return false if no match - return false; + auto playlists = mImportProperties.playlists(); + if(!playlists.isEmpty()) + mPlaylistSelections.fill(playlists); + }); } -bool MainWindow::isExistingPlaylistSelected() +void MainWindow::initializeLauncherHelpActions() { - // Check platform choices - for(int i = 0; i < ui->listWidget_playlistChoices->count(); i++) + // Add install help link for each registered install + for(auto eItr = Lr::Registry::entries(); eItr.hasNext();) { - if(ui->listWidget_playlistChoices->item(i)->checkState() == Qt::Checked && - mFrontendInstall->containsPlaylist(ui->listWidget_playlistChoices->item(i)->text())) - return true; + auto e = eItr.next(); + QAction* lrHelpAction = new QAction(ui->menu_launcherHelp); + 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); } - - // 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 +346,37 @@ 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; + 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 +386,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 +437,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 +452,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 +469,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 +499,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 +535,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); + QUrl helpUrl = Lr::Registry::helpUrl(launcherName); + if(helpUrl.isValid()) { - const QUrl* helpUrl = Fe::Install::registry()[frontendName].helpUrl; - QDesktopServices::openUrl(*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: 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 0000000..402db10 Binary files /dev/null and b/doc/images/logo.png differ diff --git a/doc/images/main_window.png b/doc/images/main_window.png new file mode 100644 index 0000000..07b6fbc Binary files /dev/null and b/doc/images/main_window.png differ diff --git a/doc/images/tag_filter.png b/doc/images/tag_filter.png new file mode 100644 index 0000000..0a6aa7c Binary files /dev/null and b/doc/images/tag_filter.png differ