From a366fcc56a5ca07dfeaa713f533f2037d138ca8c Mon Sep 17 00:00:00 2001 From: Elias Steurer Date: Fri, 6 May 2022 14:51:56 +0200 Subject: [PATCH] Add .screenplay project export import via QArchive --- CMake/FetchContentThirdParty.cmake | 11 ++ CMakeLists.txt | 8 +- ScreenPlay/CMakeLists.txt | 1 + ScreenPlay/inc/public/ScreenPlay/util.h | 9 +- ScreenPlay/qml/Installed/Installed.qml | 127 +++++++++++++- ScreenPlay/src/util.cpp | 110 ++++++++++++ ScreenPlayUtil/CMakeLists.txt | 10 +- ScreenPlayUtil/assets/icons/attach_file.svg | 1 + ScreenPlayUtil/assets/icons/description.svg | 1 + ScreenPlayUtil/assets/icons/folder.svg | 1 + ScreenPlayUtil/qml/FileDropAnimation.qml | 176 ++++++++++++++++++++ Tools/setup.py | 3 +- 12 files changed, 450 insertions(+), 8 deletions(-) create mode 100644 ScreenPlayUtil/assets/icons/attach_file.svg create mode 100644 ScreenPlayUtil/assets/icons/description.svg create mode 100644 ScreenPlayUtil/assets/icons/folder.svg create mode 100644 ScreenPlayUtil/qml/FileDropAnimation.qml diff --git a/CMake/FetchContentThirdParty.cmake b/CMake/FetchContentThirdParty.cmake index 363799ab..0a309751 100644 --- a/CMake/FetchContentThirdParty.cmake +++ b/CMake/FetchContentThirdParty.cmake @@ -4,6 +4,17 @@ include(FetchContent) set(THIRD_PARTY_PATH "${CMAKE_SOURCE_DIR}/ThirdParty/") +FetchContent_Populate( + QArchive + GIT_REPOSITORY https://github.com/antony-jr/QArchive.git + GIT_TAG 2d05e652ad9a2bff8c87962d5525e2b3c4d7351b + # Workaround because: + # 1. QtCreator cannot handle QML_ELEMENT stuff when it is in bin folder + # https://bugreports.qt.io/browse/QTCREATORBUG-27083 + SOURCE_DIR ${THIRD_PARTY_PATH}/QArchive +) + + FetchContent_Populate( qml-plausible GIT_REPOSITORY https://gitlab.com/kelteseth/qml-plausible.git diff --git a/CMakeLists.txt b/CMakeLists.txt index e9b9680b..b7701dca 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -72,6 +72,10 @@ execute_process( OUTPUT_VARIABLE GIT_COMMIT_HASH OUTPUT_STRIP_TRAILING_WHITESPACE) + +include(${CMAKE_CURRENT_SOURCE_DIR}/CMake/FetchContentThirdParty.cmake) + +add_subdirectory(CMake) add_subdirectory(ScreenPlay) add_subdirectory(ScreenPlaySDK) add_subdirectory(ScreenPlayShader) @@ -79,14 +83,14 @@ add_subdirectory(ScreenPlayWallpaper) add_subdirectory(ScreenPlayWidget) add_subdirectory(ScreenPlayUtil) add_subdirectory(ScreenPlayWeather) -add_subdirectory(CMake) +add_subdirectory(ThirdParty/QArchive) add_subdirectory(Tools) + if(${SCREENPLAY_TESTS}) enable_testing() endif() -include(${CMAKE_CURRENT_SOURCE_DIR}/CMake/FetchContentThirdParty.cmake) # Only add target SteamSDKQtEnums add_subdirectory(ScreenPlayWorkshop/SteamSDK) diff --git a/ScreenPlay/CMakeLists.txt b/ScreenPlay/CMakeLists.txt index 43940426..004a00fe 100644 --- a/ScreenPlay/CMakeLists.txt +++ b/ScreenPlay/CMakeLists.txt @@ -299,6 +299,7 @@ target_link_libraries( Qt6::Svg ScreenPlayUtil ScreenPlayUtilplugin + QArchive SteamSDKQtEnums) if(${SCREENPLAY_STEAM}) diff --git a/ScreenPlay/inc/public/ScreenPlay/util.h b/ScreenPlay/inc/public/ScreenPlay/util.h index 10ae3381..5a5c1f70 100644 --- a/ScreenPlay/inc/public/ScreenPlay/util.h +++ b/ScreenPlay/inc/public/ScreenPlay/util.h @@ -56,6 +56,10 @@ #include "ScreenPlay/globalvariables.h" #include "ScreenPlayUtil/util.h" +#include "qarchive_enums.hpp" +#include "qarchivediskcompressor.hpp" +#include "qarchivediskextractor.hpp" + #include #include #include @@ -106,7 +110,8 @@ public slots: void copyToClipboard(const QString& text) const; void openFolderInExplorer(const QString& url) const; QString toLocal(const QString& url); - + bool exportProject(QString& contentPath, QString& exportPath); + bool importProject(QString& archivePath, QString& extractionPath); void requestAllLicenses(); void requestDataProtection(); @@ -148,6 +153,8 @@ private: QString m_debugMessages {}; QFuture m_requestAllLicensesFuture; + std::unique_ptr m_compressor; + std::unique_ptr m_extractor; }; // Used for redirect content from static logToGui to setDebugMessages diff --git a/ScreenPlay/qml/Installed/Installed.qml b/ScreenPlay/qml/Installed/Installed.qml index 9bf11e17..c47607dc 100644 --- a/ScreenPlay/qml/Installed/Installed.qml +++ b/ScreenPlay/qml/Installed/Installed.qml @@ -1,13 +1,16 @@ import QtQuick import QtQuick.Controls +import QtQuick.Dialogs +import QtQuick.Layouts import QtQuick.Controls.Material import Qt5Compat.GraphicalEffects import QtQuick.Controls.Material.impl +import Qt.labs.platform 1.1 as Labs import ScreenPlayApp import ScreenPlay import ScreenPlay.Enums.InstalledType import ScreenPlay.Enums.SearchType -import ScreenPlayUtil +import ScreenPlayUtil as Util Item { id: root @@ -245,6 +248,7 @@ Item { // Set the menu to the current item informations contextMenu.publishedFileID = delegate.publishedFileID contextMenu.absoluteStoragePath = delegate.absoluteStoragePath + contextMenu.fileName = delegate.customTitle const pos = delegate.mapToItem(root, position.x, position.y) // Disable duplicate opening. The can happen if we // call popup when we are in the closing animtion. @@ -266,14 +270,29 @@ Item { property var publishedFileID: 0 property url absoluteStoragePath + property string fileName MenuItem { text: qsTr("Open containing folder") objectName: "openFolder" icon.source: "qrc:/qml/ScreenPlayApp/assets/icons/icon_folder_open.svg" onClicked: { - App.util.openFolderInExplorer( - contextMenu.absoluteStoragePath) + App.util.openFolderInExplorer(contextMenu.absoluteStoragePath) + } + } + + MenuItem { + text: qsTr("Export") + objectName: enabled ? "removeItem" : "removeWorkshopItem" + icon.source: "qrc:/qml/ScreenPlayApp/assets/icons/icon_download.svg" + onClicked: { + exportFileDialog.absoluteStoragePath = contextMenu.absoluteStoragePath + let urlFileName = Labs.StandardPaths.writableLocation( + Labs.StandardPaths.DesktopLocation) + "/" + + contextMenu.fileName + ".screenplay" + + exportFileDialog.currentFile = urlFileName + exportFileDialog.open() } } @@ -297,6 +316,29 @@ Item { } } } + Labs.FileDialog { + id: exportFileDialog + fileMode: FileDialog.SaveFile + property string absoluteStoragePath + onAccepted: { + const success = App.util.exportProject( + exportFileDialog.absoluteStoragePath, + exportFileDialog.currentFile) + } + + Dialog { + id: exportFileProgressDialog + modal: true + anchors.centerIn: Overlay.overlay + standardButtons: Dialog.Ok + onAccepted: errorDialog.close() + + ProgressBar { + id: exportFileProgressBar + anchors.centerIn: parent + } + } + } Dialog { id: deleteDialog @@ -321,4 +363,83 @@ Item { left: parent.left } } + + DropArea { + id: dropArea + anchors.fill: parent + property string filePath + onEntered: function (drag) { + dropPopup.open() + } + onDropped: function (drop) { + dropPopup.close() + dropArea.enabled = false + + if (drop.urls.length > 1) { + errorDialog.title = qsTr( + "We only support adding one item at once.") + errorDialog.open() + return + } + var file = "" // Convert url to string + file = "" + drop.urls[0] + if (!file.endsWith('.screenplay')) { + errorDialog.title = qsTr( + "File type not supported. We only support '.screenplay' files.") + errorDialog.open() + return + } + importDialog.open() + dropArea.filePath = file + } + onExited: { + dropPopup.close() + } + + Dialog { + id: errorDialog + modal: true + anchors.centerIn: Overlay.overlay + standardButtons: Dialog.Ok + onAccepted: errorDialog.close() + } + Dialog { + id: importDialog + modal: true + anchors.centerIn: Overlay.overlay + standardButtons: Dialog.Ok + RowLayout { + Text { + text: qsTr("Import Content...") + } + } + onOpened: { + App.util.importProject(dropArea.filePath, + App.globalVariables.localStoragePath) + dropArea.filePath = "" + } + onAccepted: { + importDialog.close() + } + } + } + + Popup { + id: dropPopup + anchors.centerIn: Overlay.overlay + width: root.width * .95 + height: root.height * .95 + dim: true + modal: true + onOpened: fileDropAnimation.state = "fileDrop" + onClosed: { + fileDropAnimation.state = "" + dropArea.enabled = true + } + + Util.FileDropAnimation { + id: fileDropAnimation + anchors.centerIn: parent + } + } } diff --git a/ScreenPlay/src/util.cpp b/ScreenPlay/src/util.cpp index ff689f16..633ed3cf 100644 --- a/ScreenPlay/src/util.cpp +++ b/ScreenPlay/src/util.cpp @@ -97,11 +97,121 @@ void Util::openFolderInExplorer(const QString& url) const explorer.startDetached(); } +/*! + \brief Removes file///: or file:// from the url/string +*/ QString Util::toLocal(const QString& url) { return ScreenPlayUtil::toLocal(url); } +/*! + \brief Exports a given project into a .screenplay 7Zip file. +*/ +bool Util::exportProject(QString& contentPath, QString& exportPath) +{ + contentPath = ScreenPlayUtil::toLocal(contentPath); + exportPath = ScreenPlayUtil::toLocal(exportPath); + + QDir dir(contentPath); + bool success = true; + if (!dir.exists()) { + qWarning() << "Directory does not exist!" << dir; + return false; + } + QStringList files; + for (auto& item : dir.entryInfoList(QDir::Files)) { + files.append(item.absoluteFilePath()); + } + m_compressor = std::make_unique(exportPath); + m_compressor->setArchiveFormat(QArchive::SevenZipFormat); + m_compressor->addFiles(files); + /* Connect Signals with Slots (in this case lambda functions). */ + QObject::connect(m_compressor.get(), &QArchive::DiskCompressor::started, [&]() { + qInfo() << "[+] Starting Compressor... "; + }); + QObject::connect(m_compressor.get(), &QArchive::DiskCompressor::finished, [&]() { + qInfo() << "[+] Compressed File(s) Successfully!"; + + return; + }); + QObject::connect(m_compressor.get(), &QArchive::DiskCompressor::error, [&](short code, QString file) { + qInfo() << "[-] An error has occured :: " << QArchive::errorCodeToString(code) << " :: " << file; + + return; + }); + + QObject::connect(m_compressor.get(), &QArchive::DiskCompressor::progress, [&](QString file, int proc, int total, qint64 br, qint64 bt) { + qInfo() << "Progress::" << file << ":: Done ( " << proc << " / " << total << ") " << (br * 100 / bt) << "%."; + return; + }); + + m_compressor->start(); + return true; +} + +/*! + \brief Imports a given project from a .screenplay zip file. +*/ +bool Util::importProject(QString& archivePath, QString& extractionPath) +{ + archivePath = ScreenPlayUtil::toLocal(archivePath); + extractionPath = ScreenPlayUtil::toLocal(extractionPath); + + QFileInfo fileInfo(archivePath); + if (!fileInfo.fileName().endsWith(".screenplay")) { + qWarning() << "Unsupported file type: " << fileInfo.fileName() << ". We only support '.screenplay' files."; + return false; + } + const QString name = fileInfo.fileName().remove(".screenplay"); + + const auto timestamp = QDateTime::currentDateTime().toString("ddMMyyyyhhmmss-"); + extractionPath = extractionPath + "/" + timestamp + name + "/"; + QDir dir(extractionPath); + + if (dir.exists()) { + qWarning() << "Directory does already exist!" << dir; + return false; + } + + if (!dir.mkdir(extractionPath)) { + qWarning() << "Unable to create directory:" << dir; + return false; + } + + m_extractor = std::make_unique(archivePath, extractionPath); + + QObject::connect(m_extractor.get(), &QArchive::DiskExtractor::started, [&]() { + qInfo() << "[+] Starting Extractor... "; + }); + QObject::connect(m_extractor.get(), &QArchive::DiskExtractor::finished, [&]() { + qInfo() << "[+] Extracted File(s) Successfully!"; + return; + }); + QObject::connect(m_extractor.get(), &QArchive::DiskExtractor::error, [&](short code) { + if (code == QArchive::ArchivePasswordNeeded || code == QArchive::ArchivePasswordIncorrect) { + return; + } + qInfo() << "[-] An error has occured :: " << QArchive::errorCodeToString(code); + return; + }); + QObject::connect(m_extractor.get(), &QArchive::DiskExtractor::info, [&](QJsonObject info) { + qInfo() << "ARCHIVE CONTENTS:: " << info; + return; + }); + + QObject::connect(m_extractor.get(), &QArchive::DiskExtractor::progress, + [&](QString file, int proc, int total, qint64 br, qint64 bt) { + qInfo() << "Progress(" << proc << "/" << total << "): " + << file << " : " << (br * 100 / bt) << "% done."; + }); + + m_extractor->setCalculateProgress(true); + m_extractor->getInfo(); + m_extractor->start(); + return true; +} + /*! \brief Loads all content of the legal folder in the qrc into a property string of this class. allLicenseLoaded is emited when loading is finished. diff --git a/ScreenPlayUtil/CMakeLists.txt b/ScreenPlayUtil/CMakeLists.txt index 90a437ee..7ff832cb 100644 --- a/ScreenPlayUtil/CMakeLists.txt +++ b/ScreenPlayUtil/CMakeLists.txt @@ -29,6 +29,7 @@ set(QML qml/RippleEffect.qml qml/Search.qml qml/Shake.qml + qml/FileDropAnimation.qml qml/Slider.qml qml/Tag.qml qml/TagSelector.qml @@ -52,6 +53,11 @@ set(HEADER inc/public/ScreenPlayUtil/SingletonHelper.h inc/public/ScreenPlayUtil/util.h) +set(RESOURCES + assets/icons/folder.svg + assets/icons/description.svg + assets/icons/attach_file.svg) + qt_add_library( ${PROJECT_NAME} STATIC @@ -68,7 +74,9 @@ qt_add_qml_module( VERSION 1.0 QML_FILES - ${QML}) + ${QML} + RESOURCES + ${RESOURCES}) find_path(CPP_HTTPLIB_INCLUDE_DIRS "httplib.h") target_include_directories(${PROJECT_NAME} PUBLIC ${CPP_HTTPLIB_INCLUDE_DIRS}) diff --git a/ScreenPlayUtil/assets/icons/attach_file.svg b/ScreenPlayUtil/assets/icons/attach_file.svg new file mode 100644 index 00000000..8b61e69b --- /dev/null +++ b/ScreenPlayUtil/assets/icons/attach_file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ScreenPlayUtil/assets/icons/description.svg b/ScreenPlayUtil/assets/icons/description.svg new file mode 100644 index 00000000..a8b66e86 --- /dev/null +++ b/ScreenPlayUtil/assets/icons/description.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ScreenPlayUtil/assets/icons/folder.svg b/ScreenPlayUtil/assets/icons/folder.svg new file mode 100644 index 00000000..9e82e175 --- /dev/null +++ b/ScreenPlayUtil/assets/icons/folder.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ScreenPlayUtil/qml/FileDropAnimation.qml b/ScreenPlayUtil/qml/FileDropAnimation.qml new file mode 100644 index 00000000..98c3a8a0 --- /dev/null +++ b/ScreenPlayUtil/qml/FileDropAnimation.qml @@ -0,0 +1,176 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls.Material +import Qt5Compat.GraphicalEffects + +Item { + id: root + height: 400 + width: 400 + + property int secondaryIconSize: 180 + property int centerIconSize: 210 + property int growSize: 40 + + Image { + id: fileRight + opacity: 0 + width: root.secondaryIconSize + height: root.secondaryIconSize + rotation: -5 + smooth: true + antialiasing: true + mipmap: true + source: "qrc:/qml/ScreenPlayUtil/assets/icons/attach_file.svg" + sourceSize: Qt.size(width, width) + layer { + enabled: true + smooth: true + effect: ColorOverlay { + color: Material.iconColor + } + } + anchors { + top: parent.top + topMargin: 150 + left: fileCenter.horizontalCenter + leftMargin: -root.secondaryIconSize + } + } + Image { + id: fileLeft + opacity: 0 + width: root.secondaryIconSize + height: root.secondaryIconSize + source: "qrc:/qml/ScreenPlayUtil/assets/icons/description.svg" + sourceSize: Qt.size(width, width) + layer { + enabled: true + smooth: true + effect: ColorOverlay { + color: Material.iconColor + } + } + rotation: 5 + smooth: true + antialiasing: true + mipmap: true + anchors { + top: parent.top + topMargin: 150 + right: fileCenter.horizontalCenter + rightMargin: -root.secondaryIconSize + } + } + Image { + id: fileCenter + opacity: 0 + width: root.centerIconSize + height: root.centerIconSize + source: "qrc:/qml/ScreenPlayUtil/assets/icons/folder.svg" + sourceSize: Qt.size(width, width) + layer { + enabled: true + smooth: true + effect: ColorOverlay { + color: Material.color(Material.Orange) + } + } + smooth: true + antialiasing: true + mipmap: true + anchors { + top: parent.top + topMargin: 150 + horizontalCenter: parent.horizontalCenter + } + Material.elevation: 6 + } + + Text { + id: txt + color: Material.primaryTextColor + opacity: 0 + anchors.top: fileCenter.bottom + anchors.topMargin: 100 + anchors.horizontalCenter: parent.horizontalCenter + font.pointSize: 16 + text: qsTr("Drop your '.screenplay' file here to add it to your Installed content.") + horizontalAlignment: Text.AlignHCenter + } + + states: [ + State { + name: "fileDrop" + PropertyChanges { + target: fileCenter + opacity: 1 + anchors.topMargin: 100 + width: root.centerIconSize + root.growSize + height: root.centerIconSize + root.growSize + } + PropertyChanges { + target: txt + anchors.topMargin: 20 + opacity: 1 + } + PropertyChanges { + target: fileRight + opacity: 1 + anchors.topMargin: 100 + anchors.leftMargin: -170 - root.growSize + width: root.secondaryIconSize + root.growSize + height: root.secondaryIconSize + root.growSize + rotation: -10 + } + PropertyChanges { + target: fileLeft + opacity: 1 + anchors.topMargin: 100 + anchors.rightMargin: -170 - root.growSize + width: root.secondaryIconSize + root.growSize + height: root.secondaryIconSize + root.growSize + rotation: 10 + } + } + ] + + transitions: Transition { + AnchorAnimation { + duration: 400 + } + PropertyAnimation { + target: fileCenter + properties: "anchors.topMargin, width, height, opacity" + duration: 400 + easing.type: Easing.OutCirc + } + PropertyAnimation { + target: txt + properties: "anchors.topMargin, opacity" + duration: 400 + easing.type: Easing.OutCirc + } + PropertyAnimation { + target: fileRight + properties: "anchors.topMargin,anchors.leftMargin, width, height, rotation, opacity" + duration: 400 + easing.type: Easing.OutCirc + } + PropertyAnimation { + target: fileLeft + properties: "anchors.topMargin, anchors.rightMargin, width, height, rotation, opacity" + duration: 400 + easing.type: Easing.OutCirc + } + + RotationAnimation { + target: fileLeft + duration: 800 + } + RotationAnimation { + target: fileRight + duration: 800 + } + } +} diff --git a/Tools/setup.py b/Tools/setup.py index 7d429b44..0a38287c 100755 --- a/Tools/setup.py +++ b/Tools/setup.py @@ -12,7 +12,8 @@ vcpkg_version = "2ac61f8" # Master 23.04.2022 vcpkg_packages_list = [ "openssl", "curl", - "cpp-httplib" + "cpp-httplib", + "libarchive" ] class commands_list():