diff --git a/exporter/src/ui/exportPanel/ProgressListModel.cpp b/exporter/src/ui/exportPanel/ProgressListModel.cpp index a72284bc41..7b0a9700de 100644 --- a/exporter/src/ui/exportPanel/ProgressListModel.cpp +++ b/exporter/src/ui/exportPanel/ProgressListModel.cpp @@ -30,7 +30,7 @@ void ProgressListModel::addSession(std::shared_ptr session) { if (iter != sessionList.end()) { return; } - int index = static_cast(sessionList.size() - 1); + int index = static_cast(sessionList.size()); connect(&session->progressModel, &ProgressModel::exportStatusChanged, this, [this, index]() { QModelIndex modelIndex = createIndex(index, 0); Q_EMIT dataChanged(modelIndex, modelIndex, diff --git a/viewer/CMakeLists.txt b/viewer/CMakeLists.txt index a8eb3a6e14..60491286f0 100644 --- a/viewer/CMakeLists.txt +++ b/viewer/CMakeLists.txt @@ -89,8 +89,8 @@ else () set(UpdateChannel "beta") endif () configure_file(${CMAKE_SOURCE_DIR}/package/templates/version.h.in - ${CMAKE_SOURCE_DIR}/src/version.h - @ONLY + ${CMAKE_SOURCE_DIR}/src/version.h + @ONLY ) message(STATUS "Build Version: ${MajorVersion}.${MinorVersion}.${BuildNumber}") @@ -120,6 +120,8 @@ elseif (WIN32) list(APPEND PAG_VIEWER_PLATFORM_LIBS ${Bcrypt_LIB}) find_library(ws2_32_LIB ws2_32) list(APPEND PAG_VIEWER_PLATFORM_LIBS ${ws2_32_LIB}) + find_library(version_LIB version) + list(APPEND PAG_VIEWER_PLATFORM_LIBS ${version_LIB}) endif () set(PAG_USE_QT ON) @@ -188,8 +190,8 @@ if (APPLE) set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15" CACHE STRING "Minimum OS X deployment version" FORCE) elseif (WIN32) set(RESOURCES "${CMAKE_CURRENT_SOURCE_DIR}/package/templates/PAGViewer.rc" - "${CMAKE_CURRENT_SOURCE_DIR}/package/templates/appIcon.ico" - "${CMAKE_CURRENT_SOURCE_DIR}/package/templates/pagIcon.ico") + "${CMAKE_CURRENT_SOURCE_DIR}/package/templates/appIcon.ico" + "${CMAKE_CURRENT_SOURCE_DIR}/package/templates/pagIcon.ico") add_executable(PAGViewer WIN32 ${RC_FILES} ${PAG_VIEWER_SOURCE_FILES} ${QT_RESOURCES} ${RESOURCES}) list(APPEND PAG_VIEWER_SYSTEM_INCLUDES ../third_party/out/rttr/win/include) diff --git a/viewer/assets/qml/Main.qml b/viewer/assets/qml/Main.qml index ce65e48d4d..8179e100fd 100644 --- a/viewer/assets/qml/Main.qml +++ b/viewer/assets/qml/Main.qml @@ -197,6 +197,11 @@ PAGWindow { objectName: "taskFactory" } + PluginInstallerModel { + id: pluginInstaller + objectName: "pluginInstaller" + } + FileDialog { id: openFileDialog @@ -526,6 +531,12 @@ PAGWindow { case "open-commerce-page": Qt.openUrlExternally("https://pag.io/product.html#pag-enterprise-edition"); break; + case "install-plugin": + pluginInstaller.installPlugin(); + break; + case "uninstall-plugin": + pluginInstaller.uninstallPlugin(); + break; case "minimize-window": viewWindow.showMinimized(); break; diff --git a/viewer/assets/qml/Menu.qml b/viewer/assets/qml/Menu.qml index 4dac24f32b..e617cd1cff 100644 --- a/viewer/assets/qml/Menu.qml +++ b/viewer/assets/qml/Menu.qml @@ -189,6 +189,18 @@ Item { root.command("open-commerce-page"); } } + Action { + text: qsTr("Install Plugin") + onTriggered: { + root.command("install-plugin"); + } + } + Action { + text: qsTr("Uninstall Plugin") + onTriggered: { + root.command("uninstall-plugin"); + } + } } } } @@ -216,6 +228,22 @@ Item { root.command("check-for-updates"); } } + Platform.MenuItem { + visible: windowActive + text: qsTr("Install Plugin") + role: "ApplicationSpecificRole" + onTriggered: { + root.command("install-plugin"); + } + } + Platform.MenuItem { + visible: windowActive + text: qsTr("Uninstall Plugin") + role: "ApplicationSpecificRole" + onTriggered: { + root.command("uninstall-plugin"); + } + } Platform.MenuItem { visible: windowActive text: qsTr("Preference Settings") diff --git a/viewer/qttools/copy_qt_resource.sh b/viewer/qttools/copy_qt_resource.sh new file mode 100644 index 0000000000..8579e381ee --- /dev/null +++ b/viewer/qttools/copy_qt_resource.sh @@ -0,0 +1,200 @@ +#!/bin/bash +AE_PLUGIN_PATH='/Library/Application Support/Adobe/Common/Plug-ins/7.0/MediaCore' +AE_EXPORTER_PATH="$AE_PLUGIN_PATH/PAGExporter.plugin" + +# Locate PAGViewer.app +PAGVIEWER_APP="" +if [ -d "/Applications/PAGViewer.app" ]; then + PAGVIEWER_APP="/Applications/PAGViewer.app" +elif [ -d "$HOME/Applications/PAGViewer.app" ]; then + PAGVIEWER_APP="$HOME/Applications/PAGViewer.app" +else + PAGVIEWER_APP=$(mdfind "kMDItemDisplayName == 'PAGViewer.app' && kMDItemContentType == 'com.apple.application-bundle'" | head -n 1) +fi + +if [ -z "$PAGVIEWER_APP" ] || [ ! -d "$PAGVIEWER_APP" ]; then + exit 1 +fi + +echo "✓ Found PAGViewer: $PAGVIEWER_APP" + +# Copy Qt resources from PAGViewer to PAGExporter plugin +function copyQtFromViewerToPlugin() { + VIEWER_FRAMEWORKS="$PAGVIEWER_APP/Contents/Frameworks" + VIEWER_PLUGINS="$PAGVIEWER_APP/Contents/PlugIns" + VIEWER_RESOURCES="$PAGVIEWER_APP/Contents/Resources" + + PLUGIN_FRAMEWORKS="$AE_EXPORTER_PATH/Contents/Frameworks" + PLUGIN_PLUGINS="$AE_EXPORTER_PATH/Contents/PlugIns" + PLUGIN_RESOURCES="$AE_EXPORTER_PATH/Contents/Resources" + + mkdir -p "$PLUGIN_FRAMEWORKS" + mkdir -p "$PLUGIN_PLUGINS" + mkdir -p "$PLUGIN_RESOURCES" + + # Copy Qt frameworks + if [ -d "$VIEWER_FRAMEWORKS" ]; then + for framework in "$VIEWER_FRAMEWORKS"/Qt*.framework; do + if [ -d "$framework" ]; then + frameworkName=$(basename "$framework") + echo " - $frameworkName" + cp -R "$framework" "$PLUGIN_FRAMEWORKS/" + fi + done + fi + + # Copy Qt plugins + if [ -d "$VIEWER_PLUGINS" ]; then + for plugin in "$VIEWER_PLUGINS"/*; do + if [ -d "$plugin" ]; then + pluginName=$(basename "$plugin") + echo " - $pluginName" + cp -R "$plugin" "$PLUGIN_PLUGINS/" + fi + done + fi + + # Copy Qt translation files + if [ -d "$VIEWER_RESOURCES" ]; then + for resource in "$VIEWER_RESOURCES"/*.qm; do + if [ -f "$resource" ]; then + resourceName=$(basename "$resource") + echo " - $resourceName" + cp "$resource" "$PLUGIN_RESOURCES/" + fi + done + fi + + # Generate qt.conf for plugin + # When plugin loads as dylib, Qt searches for qt.conf in library directory + # Relative paths calculated from MacOS directory, need ../ to access sibling directories + PLUGIN_MACOS="$AE_EXPORTER_PATH/Contents/MacOS" + PLUGIN_QT_CONF="$PLUGIN_MACOS/qt.conf" + echo " - qt.conf (generated in MacOS directory)" + cat > "$PLUGIN_QT_CONF" << 'EOF' +[Paths] +Plugins = ../PlugIns +Imports = ../Resources/qml +QmlImports = ../Resources/qml +EOF + + # Copy QML resources + VIEWER_QML="$VIEWER_RESOURCES/qml" + PLUGIN_QML="$PLUGIN_RESOURCES/qml" + if [ -d "$VIEWER_QML" ]; then + mkdir -p "$PLUGIN_QML" + cp -R "$VIEWER_QML/"* "$PLUGIN_QML/" + QML_COUNT=$(find "$PLUGIN_QML" -type d | wc -l | tr -d ' ') + fi + + QT_FW_COUNT=$(find "$PLUGIN_FRAMEWORKS" -maxdepth 1 -name "Qt*.framework" -type d | wc -l | tr -d ' ') +} + +function doCopyFileToAEApp() { + aeAppPath="$1" + if [ -d "$aeAppPath" ]; then + targetPath="$aeAppPath/Contents/$dirName/" + if [ ! -d "$targetPath" ]; then + mkdir "$targetPath" + fi + + for file in * + do + cp -r -f "$file" "$targetPath" + done + fi +} + +function copyResourceToAEApp() { + dirName="$2" + dirPath="$1/Contents/$2" + if [ -d "$dirPath" ]; then + cd "$dirPath" + for((i=2017;i<=2030;i++)); + do + aeAppPath="/Applications/Adobe After Effects $i/Adobe After Effects $i.app" + doCopyFileToAEApp "$aeAppPath" + + aeAppPath2="/Applications/Adobe After Effects CC $i/Adobe After Effects CC $i.app" + doCopyFileToAEApp "$aeAppPath2" + done + fi +} + +function copyQtResouceToAEApp() { + exporterPath="$1" + copyResouceToAEApp "$1" "Frameworks" + copyResouceToAEApp "$1" "PlugIns" + copyResouceToAEApp "$1" "Resources" +} + +# Code sign all copied files to prevent macOS Gatekeeper crashes in AE +function signCopiedFiles() { + echo "Signing copied Qt components..." + + PLUGIN_FRAMEWORKS="$AE_EXPORTER_PATH/Contents/Frameworks" + PLUGIN_PLUGINS="$AE_EXPORTER_PATH/Contents/PlugIns" + PLUGIN_QML="$AE_EXPORTER_PATH/Contents/Resources/qml" + + # Remove extended attributes to avoid signing issues + xattr -cr "$AE_EXPORTER_PATH" 2>/dev/null || true + + # Change ownership to current user (ensure signing permissions) + # Note: When executed via osascript with administrator privileges, runs as root + # But signing requires write permissions to files + chown -R "$(logname 2>/dev/null || echo $USER):admin" "$AE_EXPORTER_PATH" 2>/dev/null || true + + # Sign all Qt frameworks + if [ -d "$PLUGIN_FRAMEWORKS" ]; then + for fw in "$PLUGIN_FRAMEWORKS"/*.framework; do + if [ -d "$fw" ]; then + fwName=$(basename "$fw") + if codesign --force --deep --sign - "$fw" 2>&1; then + echo " ✓ Signed $fwName" + else + echo " ✗ Failed to sign $fwName (exit code: $?)" + fi + fi + done + + # Sign libffaudio.dylib + if [ -f "$PLUGIN_FRAMEWORKS/libffaudio.dylib" ]; then + if codesign --force --sign - "$PLUGIN_FRAMEWORKS/libffaudio.dylib" 2>&1; then + echo " ✓ Signed libffaudio.dylib" + else + echo " ✗ Failed to sign libffaudio.dylib (exit code: $?)" + fi + fi + fi + + # Sign dylibs in PlugIns directory + if [ -d "$PLUGIN_PLUGINS" ]; then + find "$PLUGIN_PLUGINS" -name "*.dylib" -exec codesign --force --sign - {} \; 2>&1 + echo " ✓ Signed PlugIns directory" + fi + + # Sign dylibs in QML directory + if [ -d "$PLUGIN_QML" ]; then + find "$PLUGIN_QML" -name "*.dylib" -exec codesign --force --sign - {} \; 2>&1 + echo " ✓ Signed Resources/qml directory" + fi + + # Finally sign the entire plugin + if codesign --force --deep --sign - "$AE_EXPORTER_PATH" 2>&1; then + echo " ✓ Signed PAGExporter.plugin" + else + echo " ✗ Failed to sign PAGExporter.plugin (exit code: $?)" + fi + + echo "Signing completed" +} + +copyQtFromViewerToPlugin +copyQtResouceToAEApp "$AE_EXPORTER_PATH" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if [ -f "$SCRIPT_DIR/replace_qt_path.sh" ]; then + sh "$SCRIPT_DIR/replace_qt_path.sh" "$AE_EXPORTER_PATH" +fi + +signCopiedFiles diff --git a/viewer/qttools/delete_old_qt_resource.sh b/viewer/qttools/delete_old_qt_resource.sh new file mode 100755 index 0000000000..85fe1d2d4a --- /dev/null +++ b/viewer/qttools/delete_old_qt_resource.sh @@ -0,0 +1,64 @@ +AE_PLUGIN_PATH='/Library/Application Support/Adobe/Common/Plug-ins/7.0/MediaCore' +AE_EXPORTER_PATH="$AE_PLUGIN_PATH/PAGExporter.plugin" +QT_FRAMEWORK_DIR="$AE_EXPORTER_PATH/Contents/Frameworks" +QT_PLUGINS_DIR="$AE_EXPORTER_PATH/Contents/PlugIns" +QT_RESOURCE_DIR="$AE_EXPORTER_PATH/Contents/Resources" + +function doDeleteFileInApps() { + aeAppPath="$1" + if [ -d "$aeAppPath" ]; then + deletePath="$aeAppPath/Contents/$parentDir/$deleteFile" + if [ -d "$deletePath" ]; then + rm -fr "$deletePath" + elif [ -f "$deletePath" ]; then + rm -f "$deletePath" + fi + fi +} + +function deleteFileInAEApps() { + deleteFile="$1" + parentDir="$2" + for((i=2017;i<=2030;i++)); + do + aeAppPath="/Applications/Adobe After Effects $i/Adobe After Effects $i.app" + doDeleteFileInApps "$aeAppPath" + + aeAppPath2="/Applications/Adobe After Effects CC $i/Adobe After Effects CC $i.app" + doDeleteFileInApps "$aeAppPath2" + done +} + +function deleteFileInMEApps() { + deleteFile="$1" + parentDir="$2" + for((i=2017;i<=2030;i++)); + do + aeAppPath="/Applications/Adobe Media Encoder $i/Adobe Media Encoder $i.app" + doDeleteFileInApps "$aeAppPath" + + aeAppPath2="/Applications/Adobe Media Encoder CC $i/Adobe Media Encoder CC $i.app" + doDeleteFileInApps "$aeAppPath2" + done +} + +# Traverse directory and delete corresponding files in AE apps +function traversalAndDeleteFiles() { + cd "$1" + for file in * + do + + deleteFileInAEApps "$file" "$2" + done +} + +function deleteOldQtResources() { + if [ ! -d "$AE_EXPORTER_PATH" ]; then + return + fi + traversalAndDeleteFiles "$QT_FRAMEWORK_DRI" "Frameworks" + traversalAndDeleteFiles "$QT_PLUGINS_DRI" "PlugIns" + traversalAndDeleteFiles "$QT_RESOURCE_DRI" "Resources" +} + +deleteOldQtResources diff --git a/viewer/qttools/replace_qt_path.sh b/viewer/qttools/replace_qt_path.sh new file mode 100755 index 0000000000..2dbb572b42 --- /dev/null +++ b/viewer/qttools/replace_qt_path.sh @@ -0,0 +1,102 @@ +AE_PLUGIN_PATH='/Library/Application Support/Adobe/Common/Plug-ins/7.0/MediaCore' +AE_EXPORTER_PATH="$AE_PLUGIN_PATH/PAGExporter.plugin" +QT_FRAMEWORK_DIR="$AE_EXPORTER_PATH/Contents/Frameworks" +TARGET_QML_DIR="$AE_EXPORTER_PATH/Contents/Resources/qml" + +# Replace paths in executable/library files +function startReplacePathInExecuteFile(){ + executeName=$1 + isQML="$2" + currentPath=`pwd` + + otool -L $executeName + for linkPath in `otool -L $executeName | tr " " "\?"` + do + # Replace executable paths + if [[ "$linkPath" =~ ^@executable_path/\.\./Frameworks ]]; then + oldPath=`echo "$linkPath" | grep -e "^@executable_path/\.\./Frameworks/[A-Z|a-z|0-9|/|\.]*" -o` + newPath=`echo "$oldPath" | sed -e "s/^@executable_path\/\.\.\/Frameworks//"` + newPath="@rpath$newPath" + install_name_tool -change "$oldPath" "$newPath" $executeName + fi + done + + # Replace library ID + idName=`otool -l $executeName | grep -A 10 LC_ID_DYLIB | grep name` + if [ -n "$idName" ]; then + idName=`echo ${idName#*name }` + idName=`echo ${idName% (*}` + + if [ "$isQML" == "true" ]; then + oldIdName=`echo ${idName#*qml}` + newIdName="@rpath$oldIdName" + else + oldIdName=`echo "$idName" | grep -e "^@executable_path/\.\./Frameworks/[A-Z|a-z|0-9|/|\ + .]*" -o` + newIdName=`echo "$oldIdName" | sed -e "s/^@executable_path\/\.\.\/Frameworks//"` + newIdName="@rpath$newIdName" + fi + + install_name_tool -id "$newIdName" $executeName + fi + + # [CODE REVIEW - 健壮性] delete_rpath 可能失败,应该捕获错误或先检查是否存在 + # 使用 2>/dev/null 忽略错误输出是一种方法 + install_name_tool -delete_rpath "@executable_path/../Frameworks" $executeName 2>/dev/null || true + + # Add new rpath + if [ "$isQML" == "true" ]; then + install_name_tool -add_rpath "$TARGET_QML_DIR" $executeName + install_name_tool -add_rpath "$QT_FRAMEWORK_DRI" $executeName + else + install_name_tool -add_rpath "$QT_FRAMEWORK_DRI" $executeName + fi +} + +# Replace paths in Qt frameworks +function replacePathInFrameworks(){ + cd "$1" + for file in * + do + if [ "${file##*.}"x = "framework"x ] + then + executeName=${file%.*} + cd $file + startReplacePathInExecuteFile "$executeName" + cp -f "$executeName" "Versions/A/" + cp -f "$executeName" "Versions/Current/" + cd .. + fi + done +} + +# Replace paths in PAGExporter main executable +replacePathInExporter(){ + cd "$1/Contents/MacOS" + executeName="PAGExporter" + startReplacePathInExecuteFile "$executeName" +} + +# Replace paths in dylib files recursively +replacePathInDylib(){ + cd "$1" + for file in * + do + if [ -d "$file" ]; then + replacePathInDylib $file $2 + elif [ "${file##*.}"x = "dylib"x ]; then + startReplacePathInExecuteFile "$file" $2 + fi + done + cd .. +} + +ExporterPath="$1" +ExporterFrameWorkPath="$ExporterPath/Contents/Frameworks" +ExporterPluginPath="$ExporterPath/Contents/PlugIns" +ExporterResourcePath="$ExporterPath/Contents/Resources/qml" + +replacePathInFrameworks "$ExporterFrameWorkPath" +replacePathInDylib "$ExporterPluginPath" +replacePathInDylib "$ExporterResourcePath" true +replacePathInExporter "$ExporterPath" diff --git a/viewer/src/audio/PAGAudioReader.cpp b/viewer/src/audio/PAGAudioReader.cpp index 744d1311ca..5f1f992b11 100644 --- a/viewer/src/audio/PAGAudioReader.cpp +++ b/viewer/src/audio/PAGAudioReader.cpp @@ -19,6 +19,7 @@ #include "PAGAudioReader.h" #include #include +#include #include #include #include "model/AudioClip.h" diff --git a/viewer/src/main.cpp b/viewer/src/main.cpp index 4a02b9273d..d7ed9a4066 100644 --- a/viewer/src/main.cpp +++ b/viewer/src/main.cpp @@ -22,6 +22,8 @@ #include #include "PAGUpdater.h" #include "PAGViewer.h" +#include "maintenance/PluginInstallerModel.h" +#include "profiling/PAGRunTimeDataModel.h" #include "rendering/PAGView.h" #include "task/PAGTaskFactory.h" #include "version.h" @@ -64,6 +66,7 @@ int main(int argc, char* argv[]) { QApplication::setWindowIcon(QIcon(":/images/window-icon.png")); qmlRegisterType("PAG", 1, 0, "PAGView"); qmlRegisterType("PAG", 1, 0, "PAGTaskFactory"); + qmlRegisterType("PAG", 1, 0, "PluginInstallerModel"); app.openFile(QString::fromLocal8Bit(filePath.data())); pag::InitUpdater(); diff --git a/viewer/src/maintenance/PluginInstallerModel.cpp b/viewer/src/maintenance/PluginInstallerModel.cpp new file mode 100644 index 0000000000..7b33c1b69e --- /dev/null +++ b/viewer/src/maintenance/PluginInstallerModel.cpp @@ -0,0 +1,55 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2025 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include "PluginInstallerModel.h" +#include "platform/PluginInstaller.h" + +namespace pag { + +PluginInstallerModel::PluginInstallerModel(QObject* parent) + : QObject(parent), installer(std::make_unique(this)) { + + connect(installer.get(), &PluginInstaller::updateChecked, this, + &PluginInstallerModel::updateCheckCompleted); + connect(installer.get(), &PluginInstaller::installCompleted, this, + &PluginInstallerModel::installationCompleted); + connect(installer.get(), &PluginInstaller::uninstallCompleted, this, + &PluginInstallerModel::uninstallationCompleted); +} + +bool PluginInstallerModel::hasUpdate() const { + return installer->hasUpdate(); +} + +InstallResult PluginInstallerModel::installPlugin() { + return installer->installPlugin(); +} + +InstallResult PluginInstallerModel::uninstallPlugin() { + return installer->uninstallPlugin(); +} + +QString PluginInstallerModel::getInstalledVersion() const { + return installer->getInstalledVersion(); +} + +bool PluginInstallerModel::isPluginInstalled() const { + return installer->isPluginInstalled(); +} + +} // namespace pag \ No newline at end of file diff --git a/viewer/src/maintenance/PluginInstallerModel.h b/viewer/src/maintenance/PluginInstallerModel.h new file mode 100644 index 0000000000..dcad777fc9 --- /dev/null +++ b/viewer/src/maintenance/PluginInstallerModel.h @@ -0,0 +1,53 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2025 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once +#include +#include +#include "platform/PluginInstaller.h" + +namespace pag { + +class PluginInstaller; + +class PluginInstallerModel : public QObject { + Q_OBJECT + + public: + explicit PluginInstallerModel(QObject* parent = nullptr); + + Q_INVOKABLE bool hasUpdate() const; + + Q_INVOKABLE InstallResult installPlugin(); + + Q_INVOKABLE InstallResult uninstallPlugin(); + + Q_INVOKABLE QString getInstalledVersion() const; + + Q_INVOKABLE bool isPluginInstalled() const; + + Q_SIGNALS: + void updateCheckCompleted(bool hasUpdate); + void installationCompleted(InstallResult result, const QString& message); + void uninstallationCompleted(InstallResult result, const QString& message); + + private: + std::unique_ptr installer = nullptr; +}; + +} // namespace pag diff --git a/viewer/src/platform/PluginInstaller.cpp b/viewer/src/platform/PluginInstaller.cpp new file mode 100644 index 0000000000..d351971e6f --- /dev/null +++ b/viewer/src/platform/PluginInstaller.cpp @@ -0,0 +1,201 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2025 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include "PluginInstaller.h" +#include +#include +#include +#include +#include +#include +#include + +namespace pag { + +PluginInstaller::PluginInstaller(QObject* parent) : QObject(parent) { +} + +bool PluginInstaller::hasUpdate() const { + QStringList plugins = getPluginList(); + + for (const QString& pluginName : plugins) { + QString sourcePath = getPluginSourcePath(pluginName); + QString installPath = getPluginInstallPath(pluginName); + if (!QFile::exists(sourcePath)) { + continue; + } + + auto sourceVersion = getPluginVersion(sourcePath); + auto installedVersion = getPluginVersion(installPath); + if (installedVersion == 0 || sourceVersion > installedVersion) { + return true; + } + } + + return false; +} + +InstallResult PluginInstaller::installPlugin() { + bool pluginInstalled = isPluginInstalled(); + bool hasUpdateAvailable = hasUpdate(); + + if (!pluginInstalled) { + if (!requestConfirmation(tr("Install Adobe After Effects Plug-in"), + tr("Do you want to install the Adobe After Effects plugins?"))) { + return InstallResult::UnknownError; + } + } + + else if (hasUpdateAvailable) { + if (!requestConfirmation( + tr("Update Adobe After Effects Plug-in"), + tr("New plugin versions are available. Do you want to install them?"))) { + return InstallResult::UnknownError; + } + } else { + if (!requestConfirmation(tr("Reinstall Adobe After Effects Plug-in"), + tr("The plugins are already installed with the latest version. " + "Do you want to reinstall them?"))) { + return InstallResult::UnknownError; + } + } + + while (checkAeRunning()) { + if (!requestConfirmation(tr("Please close Adobe After Effects"), + tr("Adobe After Effects is currently running. Please close it and " + "click OK to continue, or Cancel to abort."))) { + return InstallResult::AeRunning; + } + } + + QStringList plugins = getPluginList(); + for (const QString& plugin : plugins) { + QString sourcePath = getPluginSourcePath(plugin); + if (!QFile::exists(sourcePath)) { + showMessage(tr("Installation Failed"), tr("Plugin source not found: %1").arg(sourcePath), + true); + return InstallResult::SourceNotFound; + } + } + + bool success = copyPluginFiles(plugins); + + if (success) { + showMessage(tr("Installation Complete"), + tr("Adobe After Effects plugins have been successfully installed!")); + Q_EMIT installCompleted(InstallResult::Success, + tr("Adobe After Effects plug-in installed successfully.")); + return InstallResult::Success; + } else { + showMessage(tr("Installation Failed"), + tr("Failed to install Adobe After Effects plugins. Please check permissions."), + true); + Q_EMIT installCompleted( + InstallResult::PermissionDenied, + tr("Failed to install Adobe After Effects plug-in due to permission issues.")); + return InstallResult::PermissionDenied; + } +} + +InstallResult PluginInstaller::uninstallPlugin() { + if (!requestConfirmation(tr("Uninstall Plugins"), + tr("Are you sure you want to uninstall the selected plugins?"))) { + return InstallResult::UnknownError; + } + + while (checkAeRunning()) { + if (!requestConfirmation(tr("Please close Adobe After Effects"), + tr("Adobe After Effects is currently running. Please close it and " + "click OK to continue, or Cancel to abort."))) { + return InstallResult::AeRunning; + } + } + + QStringList plugins = getPluginList(); + bool success = removePluginFiles(plugins); + + if (success) { + showMessage(tr("Uninstallation Complete"), tr("Plugins have been successfully removed!")); + Q_EMIT uninstallCompleted(InstallResult::Success, tr("Uninstalled AE Plug-in Successfully")); + return InstallResult::Success; + } else { + showMessage(tr("Uninstallation Failed"), + tr("Failed to uninstall plugins. Please check permissions."), true); + Q_EMIT uninstallCompleted(InstallResult::PermissionDenied, + tr("Failed to uninstall AE Plug-in due to permission issues.")); + return InstallResult::PermissionDenied; + } +} + +QString PluginInstaller::getInstalledVersion() const { + QString pluginName = "PAGExporter"; + QString installPath = getPluginInstallPath(pluginName); + if (!QFile::exists(installPath)) { + return QString(); + } + return getPluginVersionString(installPath); +} + +bool PluginInstaller::isPluginInstalled() const { + QString pluginName = "PAGExporter"; + return QFile::exists(getPluginInstallPath(pluginName)); +} + +QStringList PluginInstaller::getPluginList() const { + return {"PAGExporter", "H264EncoderTools"}; +} + +int64_t PluginInstaller::getPluginVersion(const QString& pluginPath) const { + QString versionString = getPluginVersionString(pluginPath); + if (versionString.isEmpty()) { + return 0; + } + + Version version(versionString); + return (static_cast(version.major) << 48) | (static_cast(version.minor) << 32) | + (static_cast(version.patch) << 16) | (static_cast(version.build) << 0); +} + +PluginInstaller::Version::Version(const QString& versionStr) { + QStringList parts = versionStr.split('.'); + if (parts.size() >= 1) { + major = parts[0].toInt(); + } + if (parts.size() >= 2) { + minor = parts[1].toInt(); + } + if (parts.size() >= 3) { + patch = parts[2].toInt(); + } + if (parts.size() >= 4) { + build = parts[3].toInt(); + } +} + +bool PluginInstaller::Version::operator>(const Version& other) const { + if (major != other.major) return major > other.major; + if (minor != other.minor) return minor > other.minor; + if (patch != other.patch) return patch > other.patch; + return build > other.build; +} + +QString PluginInstaller::Version::toString() const { + return QString("%1.%2.%3.%4").arg(major).arg(minor).arg(patch).arg(build); +} + +} // namespace pag diff --git a/viewer/src/platform/PluginInstaller.h b/viewer/src/platform/PluginInstaller.h new file mode 100644 index 0000000000..b6f363fdbb --- /dev/null +++ b/viewer/src/platform/PluginInstaller.h @@ -0,0 +1,106 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2025 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once +#include +#include +#include +#include + +namespace pag { + +enum class InstallResult { Success, SourceNotFound, PermissionDenied, AeRunning, UnknownError }; + +class PluginInstaller : public QObject { + Q_OBJECT + + public: + static constexpr int DefaultMinYear = 2017; + static constexpr int DefaultMaxYear = 2030; + + explicit PluginInstaller(QObject* parent = nullptr); + + bool hasUpdate() const; + InstallResult installPlugin(); + InstallResult uninstallPlugin(); + + QString getInstalledVersion() const; + bool isPluginInstalled() const; + + void setYearRange(int minYear, int maxYear); + + static QString GetH264EncoderToolsExePath(); + + Q_SIGNALS: + void updateChecked(bool hasUpdate); + void installCompleted(InstallResult result, const QString& message); + void uninstallCompleted(InstallResult result, const QString& message); + + private: + bool checkAeRunning(); + bool requestConfirmation(const QString& title, const QString& message); + void showMessage(const QString& title, const QString& message, bool isWarning = false); + + QStringList getAeInstallPaths(); + QString getPluginSourcePath(const QString& pluginName) const; + QString getPluginInstallPath(const QString& pluginName) const; + QString getPluginFullName(const QString& pluginName) const; + int getAeVersionForPath(const QString& aePath) const; + + QString getPluginVersionString(const QString& pluginPath) const; + bool executeWithPrivileges(const QString& command) const; + + bool copyPluginFiles(const QStringList& plugins) const; + bool removePluginFiles(const QStringList& plugins) const; + + int64_t getPluginVersion(const QString& pluginPath) const; + QStringList getPluginList() const; + + void appendQtResourceCopyCommands(QStringList& commands, const QStringList& aePaths) const; + void appendQtResourceDeleteCommands(QStringList& commands, const QStringList& aePaths) const; + QString getQtResourceDir() const; + bool shouldExcludeFile(const QString& fileName) const; + bool shouldExcludeDir(const QString& dirName) const; + + void appendExporterCopyCommands(QStringList& commands, const QStringList& aePaths) const; + void appendExporterDeleteCommands(QStringList& commands, const QStringList& aePaths) const; + + void storeViewerPathForPlugin() const; + + bool copyH264EncoderToolsWithRetry(int maxRetries = 5) const; + QString getH264EncoderToolsInstallDir() const; + + void CopyQtResource(char cmd[], int cmdSize) const; + void DeleteQtResource(char cmd[], int cmdSize) const; + + struct Version { + int major = 0; + int minor = 0; + int patch = 0; + int build = 0; + + explicit Version(const QString& versionStr); + bool operator>(const Version& other) const; + QString toString() const; + }; + + int minSupportedYear = DefaultMinYear; + int maxSupportedYear = DefaultMaxYear; +}; + +} // namespace pag diff --git a/viewer/src/platform/mac/PluginInstaller.mm b/viewer/src/platform/mac/PluginInstaller.mm new file mode 100644 index 0000000000..3f0134cd82 --- /dev/null +++ b/viewer/src/platform/mac/PluginInstaller.mm @@ -0,0 +1,333 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2025 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include "platform/PluginInstaller.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; +@interface NSString (QtBridge) ++ (NSString*)fromQString:(const QString&)qstr; +@end + +@implementation NSString (QtBridge) ++ (NSString*)fromQString:(const QString&)qstr { + return [NSString stringWithUTF8String:qstr.toUtf8().constData()]; +} +@end + +namespace pag { +static constexpr int cmdBufSize = 4096; + +bool PluginInstaller::checkAeRunning() { + NSArray* runningApps = [[NSWorkspace sharedWorkspace] runningApplications]; + + if (!runningApps) { + return false; + } + for (NSRunningApplication* app in runningApps) { + NSString* bundleIdentifier = [app bundleIdentifier]; + + if (bundleIdentifier) { + QString bundleId = QString::fromNSString(bundleIdentifier); + + if (bundleId.startsWith("com.adobe.AfterEffects")) { + return true; + } + } + } + + return false; +} + +bool PluginInstaller::requestConfirmation(const QString& title, const QString& message) { + return QMessageBox::question(nullptr, title, message, QMessageBox::Yes | QMessageBox::No) == + QMessageBox::Yes; +} + +void PluginInstaller::showMessage(const QString& title, const QString& message, bool isWarning) { + QMessageBox msgBox; + msgBox.setWindowTitle(title); + msgBox.setText(message); + msgBox.setIcon(isWarning ? QMessageBox::Warning : QMessageBox::Information); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.exec(); +} + +QStringList detectSpecialVersion() { + QStringList specialPaths; + + QString latestPath = "/Applications/Adobe After Effects/Adobe After Effects.app"; + if (QDir(latestPath).exists()) { + specialPaths << latestPath; + } + + QString betaPath = "/Applications/Adobe After Effects (Beta)/Adobe After Effects (Beta).app"; + if (QDir(betaPath).exists()) { + specialPaths << betaPath; + } + + return specialPaths; +} + +QStringList PluginInstaller::getAeInstallPaths() { + QStringList paths; + int currentYear = QDate::currentDate().year(); + int startYear = qMax(minSupportedYear, currentYear - 10); + int endYear = qMin(maxSupportedYear, currentYear + 2); + + for (int year = startYear; year <= endYear; ++year) { + QString aePath = + QString("/Applications/Adobe After Effects %1/Adobe After Effects %1.app").arg(year); + if (QDir(aePath).exists()) { + paths << aePath; + } + + QString ccPath = + QString("/Applications/Adobe After Effects CC %1/Adobe After Effects CC %1.app").arg(year); + if (QDir(ccPath).exists()) { + paths << ccPath; + } + } + + paths << detectSpecialVersion(); + return paths; +} + +QString PluginInstaller::getPluginFullName(const QString& pluginName) const { + if (pluginName == "H264EncoderTools") { + return pluginName; + } else { + return pluginName + ".plugin"; + } +} + +QString PluginInstaller::getPluginSourcePath(const QString& pluginName) const { + QString resourcesPath; + NSBundle* bundle = [NSBundle mainBundle]; + NSString* bundleResourcesPath = [bundle resourcePath]; + if (bundleResourcesPath) { + resourcesPath = QString::fromNSString(bundleResourcesPath); + } else { + resourcesPath = QDir(QCoreApplication::applicationDirPath()).filePath("../Resources"); + } + + QString fullName = getPluginFullName(pluginName); + return resourcesPath + "/" + fullName; +} + +QString PluginInstaller::getPluginInstallPath(const QString& pluginName) const { + QString fullName = getPluginFullName(pluginName); + + if (pluginName == "H264EncoderTools") { + QString roaming = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); + return roaming + "/H264EncoderTools/" + fullName; + } else { + return "/Library/Application Support/Adobe/Common/Plug-ins/7.0/MediaCore/" + fullName; + } +} + +QString PluginInstaller::getPluginVersionString(const QString& pluginPath) const { + QString plistPath; + + if (pluginPath.endsWith(".plugin")) { + plistPath = pluginPath + "/Contents/Info.plist"; + } else { + QString pluginDirPath = pluginPath + ".plugin"; + if (QDir(pluginDirPath).exists()) { + plistPath = pluginDirPath + "/Contents/Info.plist"; + } else { + QDir pluginDir(pluginPath); + QStringList entries = pluginDir.entryList(QDir::Dirs); + for (const QString& entry : entries) { + if (entry.endsWith(".plugin")) { + plistPath = pluginPath + "/" + entry + "/Contents/Info.plist"; + break; + } + } + } + } + + if (!QFile::exists(plistPath)) { + return QString(); + } + + QSettings plist(plistPath, QSettings::NativeFormat); + return plist.value("CFBundleVersion").toString(); +} + +bool PluginInstaller::executeWithPrivileges(const QString& command) const { + QProcess process; + QStringList args; + + QString enhancedCommand = command; + enhancedCommand.replace("cp -r ", "ditto "); + + QStringList commandParts = enhancedCommand.split(" && "); + for (int i = 0; i < commandParts.size(); ++i) { + if (commandParts[i].startsWith("ditto ")) { + QRegularExpression re("ditto '([^']+)' '([^']+)'"); + QRegularExpressionMatch match = re.match(commandParts[i]); + if (match.hasMatch()) { + QString sourcePath = match.captured(1); + QString targetDir = match.captured(2); + QString sourceFileName = QFileInfo(sourcePath).fileName(); + QString fullTargetPath = targetDir + "/" + sourceFileName; + commandParts[i] += QString(" && xattr -cr '%1' 2>/dev/null || true").arg(fullTargetPath); + } + } + } + enhancedCommand = commandParts.join(" && "); + + QString escapedCommand = enhancedCommand; + escapedCommand.replace("\"", "\\\""); + escapedCommand.replace("\\", "\\\\"); + + QString appleScriptCommand = + QString("do shell script \"%1\" with administrator privileges").arg(escapedCommand); + + args << "-e" << appleScriptCommand; + + process.start("osascript", args); + process.waitForFinished(300000); + if (process.exitCode() != 0) { + QString error = process.readAllStandardError(); + QString output = process.readAllStandardOutput(); + + QString fallbackCommand = command; + fallbackCommand.replace("cp -r ", "cp -r "); + + escapedCommand = fallbackCommand; + escapedCommand.replace("\"", "\\\""); + escapedCommand.replace("\\", "\\\\"); + + QString fallbackAppleScript = + QString("do shell script \"%1\" with administrator privileges").arg(escapedCommand); + + args.clear(); + args << "-e" << fallbackAppleScript; + + process.start("osascript", args); + process.waitForFinished(300000); // 5 分钟超时 + + if (process.exitCode() != 0) { + error = process.readAllStandardError(); + output = process.readAllStandardOutput(); + return false; + } + } + + return true; +} + +bool PluginInstaller::copyPluginFiles(const QStringList& plugins) const { + QStringList commands; + + for (const QString& plugin : plugins) { + QString source = getPluginSourcePath(plugin); + QString target = getPluginInstallPath(plugin); + + fs::path sourcePath(source.toStdString()); + if (!fs::exists(sourcePath)) { + return false; + } + + QString targetDir = QFileInfo(target).absolutePath(); + commands << QString("mkdir -p '%1'").arg(targetDir); + commands << QString("rm -rf '%1'").arg(target); + commands << QString("ditto '%1' '%2'").arg(source).arg(target); + } + + char qtResourceCmd[cmdBufSize] = {0}; + CopyQtResource(qtResourceCmd, sizeof(qtResourceCmd)); + if (strlen(qtResourceCmd) > 0) { + commands << QString::fromUtf8(qtResourceCmd).trimmed(); + } + + if (commands.isEmpty()) { + return true; + } + + QString fullCommand = commands.join(" && "); + bool result = executeWithPrivileges(fullCommand); + return result; +} + +bool PluginInstaller::removePluginFiles(const QStringList& plugins) const { + QStringList commands; + + for (const QString& plugin : plugins) { + QString target = getPluginInstallPath(plugin); + commands << QString("rm -rf '%1'").arg(target); + } + + char deleteQtResourceCmd[cmdBufSize] = {0}; + DeleteQtResource(deleteQtResourceCmd, sizeof(deleteQtResourceCmd)); + if (strlen(deleteQtResourceCmd) > 0) { + commands << QString::fromUtf8(deleteQtResourceCmd).trimmed(); + } + + if (commands.isEmpty()) { + return true; + } + + QString fullCommand = commands.join(" && "); + return executeWithPrivileges(fullCommand); +} + +void PluginInstaller::CopyQtResource(char cmd[], int cmdSize) const { + NSString* copyQtShellPath = [[NSBundle mainBundle] pathForResource:@"copy_qt_resource" + ofType:@"sh"]; + if (copyQtShellPath != nil) { + snprintf(cmd + strlen(cmd), cmdSize - strlen(cmd), "sh '%s'\n", [copyQtShellPath UTF8String]); + } +} + +void PluginInstaller::DeleteQtResource(char cmd[], int cmdSize) const { + NSString* deleteShellPath = [[NSBundle mainBundle] pathForResource:@"delete_qt_resource" + ofType:@"sh"]; + if (deleteShellPath != nil) { + snprintf(cmd + strlen(cmd), cmdSize - strlen(cmd), "sh '%s'\n", [deleteShellPath UTF8String]); + } +} + +void PluginInstaller::setYearRange(int minYear, int maxYear) { + if (minYear > maxYear) { + qWarning() << "Invalid year range:" << minYear << "-" << maxYear; + return; + } + + minSupportedYear = qMax(2017, minYear); + maxSupportedYear = qMin(2030, maxYear); +} + +} // namespace pag diff --git a/viewer/src/platform/win/PluginInstaller.cpp b/viewer/src/platform/win/PluginInstaller.cpp new file mode 100644 index 0000000000..657d9f522e --- /dev/null +++ b/viewer/src/platform/win/PluginInstaller.cpp @@ -0,0 +1,603 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2025 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include "platform/PluginInstaller.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace pag { + +static constexpr int MaxCopyRetries = 5; +static constexpr int RetryDelayMs = 100; + +QString PluginInstaller::GetH264EncoderToolsExePath() { + QString appDir = QCoreApplication::applicationDirPath(); + QString localPath = appDir + "/H264EncoderTools.exe"; + if (QFile::exists(localPath)) { + return localPath; + } + + QString roaming = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); + return roaming + "/H264EncoderTools/H264EncoderTools.exe"; +} + +bool PluginInstaller::checkAeRunning() { + QProcess process; + process.start("tasklist", QStringList() << "/FI" + << "IMAGENAME eq AfterFX.exe"); + process.waitForFinished(3000); // 3 second timeout + QString output = process.readAllStandardOutput(); + return output.contains("AfterFX.exe"); +} + +bool PluginInstaller::requestConfirmation(const QString& title, const QString& message) { + return QMessageBox::question(nullptr, title, message, QMessageBox::Yes | QMessageBox::No) == + QMessageBox::Yes; +} + +void PluginInstaller::showMessage(const QString& title, const QString& message, bool isWarning) { + QMessageBox msgBox; + msgBox.setWindowTitle(title); + msgBox.setText(message); + msgBox.setIcon(isWarning ? QMessageBox::Warning : QMessageBox::Information); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.exec(); +} + +QStringList PluginInstaller::getAeInstallPaths() { + QStringList paths; + + const QStringList adobeRegPaths = { + "HKEY_LOCAL_MACHINE\\SOFTWARE\\Adobe\\After Effects", + "HKEY_LOCAL_MACHINE\\SOFTWARE\\WOW6432Node\\Adobe\\After Effects", + "HKEY_CURRENT_USER\\SOFTWARE\\Adobe\\After Effects"}; + + for (const QString& regPath : adobeRegPaths) { + QSettings registry(regPath, QSettings::NativeFormat); + const QStringList versionKeys = registry.childGroups(); + + for (const QString& versionKey : versionKeys) { + QSettings aeRegistry(regPath + "\\" + versionKey, QSettings::NativeFormat); + + QString installPath = aeRegistry.value("InstallPath").toString(); + if (installPath.isEmpty()) { + installPath = aeRegistry.value("ApplicationPath").toString(); + } + + if (!installPath.isEmpty()) { + QString supportFilesPath = QDir::cleanPath(installPath + "/Support Files"); + if (!QDir(supportFilesPath).exists()) { + supportFilesPath = QDir::cleanPath(installPath); + } + + if (QDir(supportFilesPath).exists() && QFile::exists(supportFilesPath + "/AfterFX.exe")) { + int version = static_cast(versionKey.split('.').first().toDouble()); + QString pathWithVersion = QString("%1|%2").arg(supportFilesPath).arg(version); + if (!paths.contains(pathWithVersion)) { + paths << pathWithVersion; + } + } + } + } + } + + const QStringList uninstallRegPaths = { + "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall", + "HKEY_LOCAL_MACHINE\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall"}; + + for (const QString& uninstallPath : uninstallRegPaths) { + QSettings uninstallRegistry(uninstallPath, QSettings::NativeFormat); + const QStringList uninstallKeys = uninstallRegistry.childGroups(); + + for (const QString& key : uninstallKeys) { + QSettings appRegistry(uninstallPath + "\\" + key, QSettings::NativeFormat); + QString displayName = appRegistry.value("DisplayName").toString(); + + if (displayName.contains("Adobe After Effects", Qt::CaseInsensitive)) { + QString installLocation = appRegistry.value("InstallLocation").toString(); + if (!installLocation.isEmpty()) { + QString supportFilesPath = QDir::cleanPath(installLocation + "/Support Files"); + if (!QDir(supportFilesPath).exists()) { + supportFilesPath = QDir::cleanPath(installLocation); + } + + if (QDir(supportFilesPath).exists() && QFile::exists(supportFilesPath + "/AfterFX.exe")) { + QRegularExpression versionRegex("After Effects(?: CC)? (\\d+)"); + QRegularExpressionMatch match = versionRegex.match(displayName); + QString pathWithVersion; + if (match.hasMatch()) { + int version = match.captured(1).toInt(); + pathWithVersion = QString("%1|%2").arg(supportFilesPath).arg(version); + } else { + pathWithVersion = supportFilesPath; + } + + if (!paths.contains(pathWithVersion)) { + paths << pathWithVersion; + } + } + } + } + } + } + + std::sort(paths.begin(), paths.end(), [](const QString& a, const QString& b) { + int versionA = 0, versionB = 0; + if (a.contains('|')) { + versionA = a.split('|').last().toInt(); + } else { + QRegularExpression versionRegex("After Effects(?: CC)? (\\d+)"); + QRegularExpressionMatch matchA = versionRegex.match(a); + versionA = matchA.hasMatch() ? matchA.captured(1).toInt() : 0; + } + + if (b.contains('|')) { + versionB = b.split('|').last().toInt(); + } else { + QRegularExpression versionRegex("After Effects(?: CC)? (\\d+)"); + QRegularExpressionMatch matchB = versionRegex.match(b); + versionB = matchB.hasMatch() ? matchB.captured(1).toInt() : 0; + } + + return versionA > versionB; + }); + + QStringList cleanPaths; + for (const QString& path : paths) { + if (path.contains('|')) { + cleanPaths << path.split('|').first(); + } else { + cleanPaths << path; + } + } + + return cleanPaths; +} + +QString PluginInstaller::getPluginFullName(const QString& pluginName) const { + if (pluginName == "H264EncoderTools") { + return pluginName + ".exe"; + } else { + return pluginName + ".aex"; + } +} + +QString PluginInstaller::getPluginSourcePath(const QString& pluginName) const { + QString appDir = QCoreApplication::applicationDirPath(); + QString fullName = getPluginFullName(pluginName); + return appDir + "/" + fullName; +} + +QString PluginInstaller::getPluginInstallPath(const QString& pluginName) const { + QString fullName = getPluginFullName(pluginName); + + if (pluginName == "H264EncoderTools") { + QString roaming = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); + return roaming + "/H264EncoderTools/" + fullName; + } else { + return "C:/Program Files/Adobe/Common/Plug-ins/7.0/MediaCore/" + fullName; + } +} + +QString PluginInstaller::getPluginVersionString(const QString& pluginPath) const { + QString targetPath = pluginPath; + if (!targetPath.endsWith(".aex") && !targetPath.endsWith(".exe")) { + if (QFile::exists(targetPath + ".aex")) { + targetPath = targetPath + ".aex"; + } else if (QFile::exists(targetPath + ".exe")) { + targetPath = targetPath + ".exe"; + } else { + QDir pluginDir(pluginPath); + if (pluginDir.exists()) { + QStringList entries = pluginDir.entryList(QStringList() << "*.aex" + << "*.exe", + QDir::Files); + if (!entries.isEmpty()) { + targetPath = pluginPath + "/" + entries.first(); + } else { + return QString(); + } + } else { + return QString(); + } + } + } + + if (!QFile::exists(targetPath)) { + return QString(); + } + + std::wstring filePathW = targetPath.toStdWString(); + DWORD handle = 0; + DWORD size = GetFileVersionInfoSizeW(filePathW.c_str(), &handle); + + if (size == 0) { + return QString(); + } + + std::vector buffer(size); + if (!GetFileVersionInfoW(filePathW.c_str(), handle, size, buffer.data())) { + return QString(); + } + + VS_FIXEDFILEINFO* fileInfo = nullptr; + UINT fileInfoSize = 0; + if (!VerQueryValueW(buffer.data(), L"\\", reinterpret_cast(&fileInfo), &fileInfoSize)) { + return QString(); + } + + if (fileInfo == nullptr || fileInfoSize == 0) { + return QString(); + } + + int major = HIWORD(fileInfo->dwFileVersionMS); + int minor = LOWORD(fileInfo->dwFileVersionMS); + int patch = HIWORD(fileInfo->dwFileVersionLS); + int build = LOWORD(fileInfo->dwFileVersionLS); + + QString versionStr = QString::number(major); + if (minor > 0 || patch > 0 || build > 0) { + versionStr += "." + QString::number(minor); + } + if (patch > 0 || build > 0) { + versionStr += "." + QString::number(patch); + } + if (build > 0) { + versionStr += "." + QString::number(build); + } + + return versionStr; +} + +int PluginInstaller::getAeVersionForPath(const QString& aePath) const { + const QStringList registryPaths = {"HKEY_LOCAL_MACHINE\\SOFTWARE\\Adobe", + "HKEY_LOCAL_MACHINE\\SOFTWARE\\WOW6432Node\\Adobe", + "HKEY_CURRENT_USER\\SOFTWARE\\Adobe"}; + + for (const QString& regPath : registryPaths) { + QSettings registry(regPath, QSettings::NativeFormat); + const QStringList adobeKeys = registry.childGroups(); + + for (const QString& key : adobeKeys) { + if (key.contains("After Effects", Qt::CaseInsensitive)) { + QSettings aeRegistry(regPath + "\\" + key, QSettings::NativeFormat); + QString installPath = aeRegistry.value("InstallPath").toString(); + if (!installPath.isEmpty()) { + QString supportFilesPath = QDir::cleanPath(installPath + "/Support Files"); + if (supportFilesPath == aePath) { + QRegularExpression versionRegex("After Effects(?: CC)? (\\d+)"); + QRegularExpressionMatch match = versionRegex.match(key); + if (match.hasMatch()) { + return match.captured(1).toInt(); + } + } + } + } + } + } + + QSettings ccRegistry( + "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall", + QSettings::NativeFormat); + const QStringList uninstallKeys = ccRegistry.childGroups(); + + for (const QString& key : uninstallKeys) { + QSettings appRegistry( + "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\" + key, + QSettings::NativeFormat); + QString displayName = appRegistry.value("DisplayName").toString(); + if (displayName.contains("Adobe After Effects", Qt::CaseInsensitive)) { + QString installLocation = appRegistry.value("InstallLocation").toString(); + if (!installLocation.isEmpty()) { + QString supportFilesPath = QDir::cleanPath(installLocation + "/Support Files"); + if (supportFilesPath == aePath) { + QRegularExpression versionRegex("After Effects(?: CC)? (\\d+)"); + QRegularExpressionMatch match = versionRegex.match(displayName); + if (match.hasMatch()) { + return match.captured(1).toInt(); + } + } + } + } + } + + return 0; +} + +bool PluginInstaller::executeWithPrivileges(const QString& command) const { + QTemporaryFile tempFile; + tempFile.setFileTemplate(QDir::tempPath() + "/install_plugin_XXXXXX.bat"); + tempFile.setAutoRemove(false); + + if (!tempFile.open()) { + return false; + } + + QString batPath = tempFile.fileName(); + tempFile.write("@echo off\n"); + tempFile.write("chcp 65001 > nul\n"); + tempFile.write(command.toUtf8()); + tempFile.write("\necho.\necho Press any key to continue...\npause > nul\n"); + tempFile.close(); + + HINSTANCE result = ShellExecuteW( + nullptr, L"runas", L"cmd.exe", + QString("/c \"%1\"").arg(QDir::toNativeSeparators(batPath)).toStdWString().c_str(), nullptr, + SW_SHOW); + QFile::remove(batPath); + + return reinterpret_cast(result) > 32; +} + +bool PluginInstaller::copyPluginFiles(const QStringList& plugins) const { + QStringList commands; + QStringList aePaths = const_cast(this)->getAeInstallPaths(); + + storeViewerPathForPlugin(); + + if (plugins.contains("H264EncoderTools")) { + copyH264EncoderToolsWithRetry(MaxCopyRetries); + } + + if (plugins.contains("PAGExporter") && !aePaths.isEmpty()) { + appendExporterCopyCommands(commands, aePaths); + appendQtResourceCopyCommands(commands, aePaths); + } + + for (const QString& plugin : plugins) { + if (plugin == "H264EncoderTools" || plugin == "PAGExporter") { + continue; + } + + QString source = getPluginSourcePath(plugin); + QString target = getPluginInstallPath(plugin); + + if (!QFile::exists(source)) { + continue; + } + + QString targetDir = QFileInfo(target).absolutePath(); + commands + << QString("if not exist \"%1\" mkdir \"%1\"").arg(QDir::toNativeSeparators(targetDir)); + + commands << QString("copy /Y \"%1\" \"%2\"") + .arg(QDir::toNativeSeparators(source)) + .arg(QDir::toNativeSeparators(target)); + } + + if (commands.isEmpty()) { + return true; + } + + QString fullCommand = commands.join("\n"); + return executeWithPrivileges(fullCommand); +} + +bool PluginInstaller::removePluginFiles(const QStringList& plugins) const { + QStringList commands; + QStringList aePaths = const_cast(this)->getAeInstallPaths(); + + if (plugins.contains("PAGExporter") && !aePaths.isEmpty()) { + appendExporterDeleteCommands(commands, aePaths); + appendQtResourceDeleteCommands(commands, aePaths); + } + + if (plugins.contains("H264EncoderTools")) { + QString h264Path = getPluginInstallPath("H264EncoderTools"); + if (QFile::exists(h264Path)) { + QFile::remove(h264Path); + } + } + + for (const QString& plugin : plugins) { + if (plugin == "H264EncoderTools" || plugin == "PAGExporter") { + continue; + } + + QString target = getPluginInstallPath(plugin); + commands << QString("if exist \"%1\" del /F /Q \"%1\"").arg(QDir::toNativeSeparators(target)); + } + + if (commands.isEmpty()) { + return true; + } + + QString fullCommand = commands.join("\n"); + return executeWithPrivileges(fullCommand); +} + +QString PluginInstaller::getQtResourceDir() const { + return QCoreApplication::applicationDirPath(); +} + +bool PluginInstaller::shouldExcludeFile(const QString& fileName) const { + // Files that should NOT be copied to AE directory + static const QStringList excludedFiles = { + "PAGViewer.exe", "PAGExporter.aex", "H264EncoderTools.exe", "ffmovie.dll", "WinSparkle.dll", + }; + return excludedFiles.contains(fileName, Qt::CaseInsensitive); +} + +bool PluginInstaller::shouldExcludeDir(const QString& dirName) const { + static const QStringList excludedDirs = { + "scripts", + }; + return excludedDirs.contains(dirName, Qt::CaseInsensitive); +} + +void PluginInstaller::appendQtResourceCopyCommands(QStringList& commands, + const QStringList& aePaths) const { + QString appDir = getQtResourceDir(); + QDir dir(appDir); + QStringList dllFiles = dir.entryList(QStringList() << "*.dll", QDir::Files); + QStringList subDirs = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + + for (const QString& aePath : aePaths) { + for (const QString& dllFile : dllFiles) { + if (shouldExcludeFile(dllFile)) { + continue; + } + QString sourcePath = appDir + "/" + dllFile; + commands << QString("copy /Y \"%1\" \"%2\\\"") + .arg(QDir::toNativeSeparators(sourcePath)) + .arg(QDir::toNativeSeparators(aePath)); + } + + for (const QString& subDir : subDirs) { + if (shouldExcludeDir(subDir)) { + continue; + } + QString sourcePath = appDir + "/" + subDir; + commands << QString("xcopy /Y /E /I /R \"%1\" \"%2\\%3\\\"") + .arg(QDir::toNativeSeparators(sourcePath)) + .arg(QDir::toNativeSeparators(aePath)) + .arg(subDir); + } + } +} + +void PluginInstaller::appendQtResourceDeleteCommands(QStringList& commands, + const QStringList& aePaths) const { + QString appDir = getQtResourceDir(); + QDir dir(appDir); + + QStringList dllFiles = dir.entryList(QStringList() << "*.dll", QDir::Files); + + QStringList subDirs = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + + for (const QString& aePath : aePaths) { + for (const QString& dllFile : dllFiles) { + if (shouldExcludeFile(dllFile)) { + continue; + } + QString dllPath = aePath + "/" + dllFile; + commands + << QString("if exist \"%1\" del /F /Q \"%1\"").arg(QDir::toNativeSeparators(dllPath)); + } + + for (const QString& subDir : subDirs) { + if (shouldExcludeDir(subDir)) { + continue; + } + commands << QString("if exist \"%1\\%2\" rmdir /S /Q \"%1\\%2\"") + .arg(QDir::toNativeSeparators(aePath)) + .arg(subDir); + } + } +} + +void PluginInstaller::appendExporterCopyCommands(QStringList& commands, + const QStringList& aePaths) const { + QString sourcePath = getPluginSourcePath("PAGExporter"); + if (!QFile::exists(sourcePath)) { + return; + } + + for (const QString& aePath : aePaths) { + QString pluginsDir = aePath + "/Plug-ins"; + commands + << QString("if not exist \"%1\" mkdir \"%1\"").arg(QDir::toNativeSeparators(pluginsDir)); + commands << QString("copy /Y \"%1\" \"%2\\PAGExporter.aex\"") + .arg(QDir::toNativeSeparators(sourcePath)) + .arg(QDir::toNativeSeparators(pluginsDir)); + } +} + +void PluginInstaller::appendExporterDeleteCommands(QStringList& commands, + const QStringList& aePaths) const { + for (const QString& aePath : aePaths) { + QString pluginPath = aePath + "/Plug-ins/PAGExporter.aex"; + commands + << QString("if exist \"%1\" del /F /Q \"%1\"").arg(QDir::toNativeSeparators(pluginPath)); + } +} + +QString PluginInstaller::getH264EncoderToolsInstallDir() const { + QString roaming = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); + return roaming + "/H264EncoderTools"; +} + +bool PluginInstaller::copyH264EncoderToolsWithRetry(int maxRetries) const { + QString sourcePath = getPluginSourcePath("H264EncoderTools"); + if (!QFile::exists(sourcePath)) { + return false; + } + + QString installDir = getH264EncoderToolsInstallDir(); + QString targetPath = installDir + "/H264EncoderTools.exe"; + + QDir dir; + if (!dir.exists(installDir)) { + if (!dir.mkpath(installDir)) { + return false; + } + } + + for (int attempt = 0; attempt < maxRetries; ++attempt) { + if (QFile::exists(targetPath)) { + QFile::remove(targetPath); + } + + if (QFile::copy(sourcePath, targetPath)) { + return true; + } + + if (attempt < maxRetries - 1) { + QThread::msleep(RetryDelayMs); + } + } + + return false; +} + +void PluginInstaller::storeViewerPathForPlugin() const { + QString roaming = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); + QString pagExporterDir = roaming + "/PAGExporter"; + + QDir dir; + if (!dir.exists(pagExporterDir)) { + if (!dir.mkpath(pagExporterDir)) { + return; + } + } + + QString viewerPathFile = pagExporterDir + "/PAGViewerPath.txt"; + QString viewerExePath = QCoreApplication::applicationFilePath(); + + QFile file(viewerPathFile); + if (file.open(QIODevice::WriteOnly | QIODevice::Text)) { + QTextStream stream(&file); + stream << viewerExePath; + file.close(); + } +} + +} // namespace pag diff --git a/viewer/src/rendering/PAGView.cpp b/viewer/src/rendering/PAGView.cpp index 970bfd7610..23e0f2852e 100644 --- a/viewer/src/rendering/PAGView.cpp +++ b/viewer/src/rendering/PAGView.cpp @@ -318,6 +318,8 @@ QSGNode* PAGView::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) { } setProgressInternal(progress, false); } + } else { + QMetaObject::invokeMethod(renderThread.get(), "flush", Qt::QueuedConnection); } return node; }