Merge remote-tracking branch 'upstream/develop' into unify-mc-settings

Signed-off-by: TheKodeToad <TheKodeToad@proton.me>
This commit is contained in:
TheKodeToad 2025-01-27 17:02:50 +00:00
commit cc504f4a6c
No known key found for this signature in database
GPG Key ID: 5E39D70B4C93C38E
58 changed files with 738 additions and 640 deletions

View File

@ -62,7 +62,7 @@ jobs:
qt_version: "5.15.2" qt_version: "5.15.2"
qt_modules: "qtnetworkauth" qt_modules: "qtnetworkauth"
- os: ubuntu-20.04 - os: ubuntu-22.04
qt_ver: 6 qt_ver: 6
qt_host: linux qt_host: linux
qt_arch: "" qt_arch: ""
@ -80,9 +80,9 @@ jobs:
architecture: "x64" architecture: "x64"
vcvars_arch: "amd64" vcvars_arch: "amd64"
qt_ver: 6 qt_ver: 6
qt_host: windows qt_host: "windows"
qt_arch: "" qt_arch: "win64_msvc2022_64"
qt_version: "6.7.3" qt_version: "6.8.1"
qt_modules: "qt5compat qtimageformats qtnetworkauth" qt_modules: "qt5compat qtimageformats qtnetworkauth"
nscurl_tag: "v24.9.26.122" nscurl_tag: "v24.9.26.122"
nscurl_sha256: "AEE6C4BE3CB6455858E9C1EE4B3AFE0DB9960FA03FE99CCDEDC28390D57CCBB0" nscurl_sha256: "AEE6C4BE3CB6455858E9C1EE4B3AFE0DB9960FA03FE99CCDEDC28390D57CCBB0"
@ -93,9 +93,9 @@ jobs:
architecture: "arm64" architecture: "arm64"
vcvars_arch: "amd64_arm64" vcvars_arch: "amd64_arm64"
qt_ver: 6 qt_ver: 6
qt_host: windows qt_host: "windows"
qt_arch: "win64_msvc2019_arm64" qt_arch: "win64_msvc2022_arm64_cross_compiled"
qt_version: "6.7.3" qt_version: "6.8.1"
qt_modules: "qt5compat qtimageformats qtnetworkauth" qt_modules: "qt5compat qtimageformats qtnetworkauth"
nscurl_tag: "v24.9.26.122" nscurl_tag: "v24.9.26.122"
nscurl_sha256: "AEE6C4BE3CB6455858E9C1EE4B3AFE0DB9960FA03FE99CCDEDC28390D57CCBB0" nscurl_sha256: "AEE6C4BE3CB6455858E9C1EE4B3AFE0DB9960FA03FE99CCDEDC28390D57CCBB0"
@ -106,7 +106,7 @@ jobs:
qt_ver: 6 qt_ver: 6
qt_host: mac qt_host: mac
qt_arch: "" qt_arch: ""
qt_version: "6.7.3" qt_version: "6.8.1"
qt_modules: "qt5compat qtimageformats qtnetworkauth" qt_modules: "qt5compat qtimageformats qtnetworkauth"
- os: macos-14 - os: macos-14
@ -167,13 +167,13 @@ jobs:
- name: Setup ccache - name: Setup ccache
if: (runner.os != 'Windows' || matrix.msystem == '') && inputs.build_type == 'Debug' if: (runner.os != 'Windows' || matrix.msystem == '') && inputs.build_type == 'Debug'
uses: hendrikmuhs/ccache-action@v1.2.14 uses: hendrikmuhs/ccache-action@v1.2.16
with: with:
key: ${{ matrix.os }}-qt${{ matrix.qt_ver }}-${{ matrix.architecture }} key: ${{ matrix.os }}-qt${{ matrix.qt_ver }}-${{ matrix.architecture }}
- name: Retrieve ccache cache (Windows MinGW-w64) - name: Retrieve ccache cache (Windows MinGW-w64)
if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug' if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug'
uses: actions/cache@v4.1.2 uses: actions/cache@v4.2.0
with: with:
path: '${{ github.workspace }}\.ccache' path: '${{ github.workspace }}\.ccache'
key: ${{ matrix.os }}-mingw-w64-ccache-${{ github.run_id }} key: ${{ matrix.os }}-mingw-w64-ccache-${{ github.run_id }}
@ -216,14 +216,14 @@ jobs:
- name: Install host Qt (Windows MSVC arm64) - name: Install host Qt (Windows MSVC arm64)
if: runner.os == 'Windows' && matrix.architecture == 'arm64' if: runner.os == 'Windows' && matrix.architecture == 'arm64'
uses: jurplel/install-qt-action@v3 uses: jurplel/install-qt-action@v4
with: with:
aqtversion: "==3.1.*" aqtversion: "==3.1.*"
py7zrversion: ">=0.20.2" py7zrversion: ">=0.20.2"
version: ${{ matrix.qt_version }} version: ${{ matrix.qt_version }}
host: "windows" host: "windows"
target: "desktop" target: "desktop"
arch: "" arch: ${{ matrix.qt_arch }}
modules: ${{ matrix.qt_modules }} modules: ${{ matrix.qt_modules }}
cache: ${{ inputs.is_qt_cached }} cache: ${{ inputs.is_qt_cached }}
cache-key-prefix: host-qt-arm64-windows cache-key-prefix: host-qt-arm64-windows
@ -232,7 +232,7 @@ jobs:
- name: Install Qt (macOS, Linux & Windows MSVC) - name: Install Qt (macOS, Linux & Windows MSVC)
if: matrix.msystem == '' if: matrix.msystem == ''
uses: jurplel/install-qt-action@v3 uses: jurplel/install-qt-action@v4
with: with:
aqtversion: "==3.1.*" aqtversion: "==3.1.*"
py7zrversion: ">=0.20.2" py7zrversion: ">=0.20.2"
@ -259,12 +259,12 @@ jobs:
wget "https://github.com/AppImageCommunity/AppImageUpdate/releases/download/continuous/AppImageUpdate-x86_64.AppImage" wget "https://github.com/AppImageCommunity/AppImageUpdate/releases/download/continuous/AppImageUpdate-x86_64.AppImage"
sudo apt install libopengl0 sudo apt install libopengl0 libfuse2
- name: Add QT_HOST_PATH var (Windows MSVC arm64) - name: Add QT_HOST_PATH var (Windows MSVC arm64)
if: runner.os == 'Windows' && matrix.architecture == 'arm64' if: runner.os == 'Windows' && matrix.architecture == 'arm64'
run: | run: |
echo "QT_HOST_PATH=${{ github.workspace }}\HostQt\Qt\${{ matrix.qt_version }}\msvc2019_64" >> $env:GITHUB_ENV echo "QT_HOST_PATH=${{ github.workspace }}\HostQt\Qt\${{ matrix.qt_version }}\msvc2022_64" >> $env:GITHUB_ENV
- name: Setup java (macOS) - name: Setup java (macOS)
if: runner.os == 'macOS' if: runner.os == 'macOS'
@ -380,11 +380,13 @@ jobs:
if [ -n '${{ secrets.APPLE_CODESIGN_ID }}' ]; then if [ -n '${{ secrets.APPLE_CODESIGN_ID }}' ]; then
APPLE_CODESIGN_ID='${{ secrets.APPLE_CODESIGN_ID }}' APPLE_CODESIGN_ID='${{ secrets.APPLE_CODESIGN_ID }}'
ENTITLEMENTS_FILE='../program_info/App.entitlements'
else else
APPLE_CODESIGN_ID='-' APPLE_CODESIGN_ID='-'
ENTITLEMENTS_FILE='../program_info/AdhocSignedApp.entitlements'
fi fi
sudo codesign --sign "$APPLE_CODESIGN_ID" --deep --force --entitlements "../program_info/App.entitlements" --options runtime "PrismLauncher.app/Contents/MacOS/prismlauncher" sudo codesign --sign "$APPLE_CODESIGN_ID" --deep --force --entitlements "$ENTITLEMENTS_FILE" --options runtime "PrismLauncher.app/Contents/MacOS/prismlauncher"
mv "PrismLauncher.app" "Prism Launcher.app" mv "PrismLauncher.app" "Prism Launcher.app"
- name: Notarize (macOS) - name: Notarize (macOS)
@ -519,8 +521,8 @@ jobs:
cp -r ${{ runner.workspace }}/Qt/${{ matrix.qt_version }}/gcc_64/plugins/iconengines/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins/iconengines cp -r ${{ runner.workspace }}/Qt/${{ matrix.qt_version }}/gcc_64/plugins/iconengines/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins/iconengines
cp /usr/lib/x86_64-linux-gnu/libcrypto.so.1.1 ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/ cp /usr/lib/x86_64-linux-gnu/libcrypto.so.* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/
cp /usr/lib/x86_64-linux-gnu/libssl.so.1.1 ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/ cp /usr/lib/x86_64-linux-gnu/libssl.so.* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/
cp /usr/lib/x86_64-linux-gnu/libOpenGL.so.0* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/ cp /usr/lib/x86_64-linux-gnu/libOpenGL.so.0* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/
LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib" LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib"
@ -555,9 +557,9 @@ jobs:
mkdir ${{ env.INSTALL_PORTABLE_DIR }}/lib mkdir ${{ env.INSTALL_PORTABLE_DIR }}/lib
cp /lib/x86_64-linux-gnu/libbz2.so.1.0 ${{ env.INSTALL_PORTABLE_DIR }}/lib cp /lib/x86_64-linux-gnu/libbz2.so.1.0 ${{ env.INSTALL_PORTABLE_DIR }}/lib
cp /usr/lib/x86_64-linux-gnu/libgobject-2.0.so.0 ${{ env.INSTALL_PORTABLE_DIR }}/lib cp /usr/lib/x86_64-linux-gnu/libgobject-2.0.so.0 ${{ env.INSTALL_PORTABLE_DIR }}/lib
cp /usr/lib/x86_64-linux-gnu/libcrypto.so.1.1 ${{ env.INSTALL_PORTABLE_DIR }}/lib cp /usr/lib/x86_64-linux-gnu/libcrypto.so.* ${{ env.INSTALL_PORTABLE_DIR }}/lib
cp /usr/lib/x86_64-linux-gnu/libssl.so.1.1 ${{ env.INSTALL_PORTABLE_DIR }}/lib cp /usr/lib/x86_64-linux-gnu/libssl.so.* ${{ env.INSTALL_PORTABLE_DIR }}/lib
cp /usr/lib/x86_64-linux-gnu/libffi.so.7 ${{ env.INSTALL_PORTABLE_DIR }}/lib cp /usr/lib/x86_64-linux-gnu/libffi.so.*.* ${{ env.INSTALL_PORTABLE_DIR }}/lib
mv ${{ env.INSTALL_PORTABLE_DIR }}/bin/*.so* ${{ env.INSTALL_PORTABLE_DIR }}/lib mv ${{ env.INSTALL_PORTABLE_DIR }}/bin/*.so* ${{ env.INSTALL_PORTABLE_DIR }}/lib
for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f); do l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_PORTABLE_DIR }}/}; l=${l#./}; echo $l; done > ${{ env.INSTALL_PORTABLE_DIR }}/manifest.txt for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f); do l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_PORTABLE_DIR }}/}; l=${l#./}; echo $l; done > ${{ env.INSTALL_PORTABLE_DIR }}/manifest.txt
@ -631,22 +633,42 @@ jobs:
ccache -s ccache -s
flatpak: flatpak:
runs-on: ubuntu-latest name: Flatpak (${{ matrix.arch }})
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-22.04
arch: x86_64
- os: ubuntu-22.04-arm
arch: aarch64
runs-on: ${{ matrix.os }}
container: container:
image: bilelmoussaoui/flatpak-github-actions:kde-6.7 image: ghcr.io/flathub-infra/flatpak-github-actions:kde-6.8
options: --privileged options: --privileged
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
if: inputs.build_type == 'Debug' if: inputs.build_type == 'Debug'
with: with:
submodules: "true" submodules: true
- name: Set short version
shell: bash
run: echo "VERSION=${GITHUB_SHA::7}" >> $GITHUB_ENV
- name: Build Flatpak (Linux) - name: Build Flatpak (Linux)
if: inputs.build_type == 'Debug' if: inputs.build_type == 'Debug'
uses: flatpak/flatpak-github-actions/flatpak-builder@v6 uses: flatpak/flatpak-github-actions/flatpak-builder@v6
with: with:
bundle: "Prism Launcher.flatpak" bundle: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-Flatpak.flatpak
manifest-path: flatpak/org.prismlauncher.PrismLauncher.yml manifest-path: flatpak/org.prismlauncher.PrismLauncher.yml
arch: ${{ matrix.arch }}
nix: nix:
name: Nix (${{ matrix.system }}) name: Nix (${{ matrix.system }})
@ -658,6 +680,9 @@ jobs:
- os: ubuntu-22.04 - os: ubuntu-22.04
system: x86_64-linux system: x86_64-linux
- os: ubuntu-22.04-arm
system: aarch64-linux
- os: macos-13 - os: macos-13
system: x86_64-darwin system: x86_64-darwin

View File

@ -78,6 +78,13 @@ else()
# ATL's pack list needs more than the default 1 Mib stack on windows # ATL's pack list needs more than the default 1 Mib stack on windows
if(WIN32) if(WIN32)
set(CMAKE_EXE_LINKER_FLAGS "-Wl,--stack,8388608 ${CMAKE_EXE_LINKER_FLAGS}") set(CMAKE_EXE_LINKER_FLAGS "-Wl,--stack,8388608 ${CMAKE_EXE_LINKER_FLAGS}")
# -ffunction-sections and -fdata-sections help reduce binary size
# -mguard=cf enables Control Flow Guard
# TODO: Look into -gc-sections to further reduce binary size
foreach(lang C CXX)
set("CMAKE_${lang}_FLAGS_RELEASE" "-ffunction-sections -fdata-sections -mguard=cf")
endforeach()
endif() endif()
endif() endif()
@ -401,8 +408,8 @@ if(UNIX AND APPLE)
set(MACOSX_SPARKLE_UPDATE_PUBLIC_KEY "v55ZWWD6QlPoXGV6VLzOTZxZUggWeE51X8cRQyQh6vA=" CACHE STRING "Public key for Sparkle update feed") set(MACOSX_SPARKLE_UPDATE_PUBLIC_KEY "v55ZWWD6QlPoXGV6VLzOTZxZUggWeE51X8cRQyQh6vA=" CACHE STRING "Public key for Sparkle update feed")
set(MACOSX_SPARKLE_UPDATE_FEED_URL "https://prismlauncher.org/feed/appcast.xml" CACHE STRING "URL for Sparkle update feed") set(MACOSX_SPARKLE_UPDATE_FEED_URL "https://prismlauncher.org/feed/appcast.xml" CACHE STRING "URL for Sparkle update feed")
set(MACOSX_SPARKLE_DOWNLOAD_URL "https://github.com/sparkle-project/Sparkle/releases/download/2.5.2/Sparkle-2.5.2.tar.xz" CACHE STRING "URL to Sparkle release archive") set(MACOSX_SPARKLE_DOWNLOAD_URL "https://github.com/sparkle-project/Sparkle/releases/download/2.6.4/Sparkle-2.6.4.tar.xz" CACHE STRING "URL to Sparkle release archive")
set(MACOSX_SPARKLE_SHA256 "572dd67ae398a466f19f343a449e1890bac1ef74885b4739f68f979a8a89884b" CACHE STRING "SHA256 checksum for Sparkle release archive") set(MACOSX_SPARKLE_SHA256 "50612a06038abc931f16011d7903b8326a362c1074dabccb718404ce8e585f0b" CACHE STRING "SHA256 checksum for Sparkle release archive")
set(MACOSX_SPARKLE_DIR "${CMAKE_BINARY_DIR}/frameworks/Sparkle") set(MACOSX_SPARKLE_DIR "${CMAKE_BINARY_DIR}/frameworks/Sparkle")
# directories to look for dependencies # directories to look for dependencies

View File

@ -8,6 +8,8 @@
<string>A Minecraft mod wants to access your microphone.</string> <string>A Minecraft mod wants to access your microphone.</string>
<key>NSDownloadsFolderUsageDescription</key> <key>NSDownloadsFolderUsageDescription</key>
<string>Prism uses access to your Downloads folder to help you more quickly add mods that can't be automatically downloaded to your instance. You can change where Prism scans for downloaded mods in Settings or the prompt that appears.</string> <string>Prism uses access to your Downloads folder to help you more quickly add mods that can't be automatically downloaded to your instance. You can change where Prism scans for downloaded mods in Settings or the prompt that appears.</string>
<key>NSLocalNetworkUsageDescription</key>
<string>Minecraft uses the local network to find and connect to LAN servers.</string>
<key>NSPrincipalClass</key> <key>NSPrincipalClass</key>
<string>NSApplication</string> <string>NSApplication</string>
<key>NSHighResolutionCapable</key> <key>NSHighResolutionCapable</key>

12
flake.lock generated
View File

@ -3,11 +3,11 @@
"flake-compat": { "flake-compat": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1696426674, "lastModified": 1733328505,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=",
"owner": "edolstra", "owner": "edolstra",
"repo": "flake-compat", "repo": "flake-compat",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -49,11 +49,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1732014248, "lastModified": 1737062831,
"narHash": "sha256-y/MEyuJ5oBWrWAic/14LaIr/u5E0wRVzyYsouYY3W6w=", "narHash": "sha256-Tbk1MZbtV2s5aG+iM99U8FqwxU/YNArMcWAv6clcsBc=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "23e89b7da85c3640bbc2173fe04f4bd114342367", "rev": "5df43628fdf08d642be8ba5b3625a6c70731c19c",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@ -1,6 +1,6 @@
id: org.prismlauncher.PrismLauncher id: org.prismlauncher.PrismLauncher
runtime: org.kde.Platform runtime: org.kde.Platform
runtime-version: '6.7' runtime-version: '6.8'
sdk: org.kde.Sdk sdk: org.kde.Sdk
sdk-extensions: sdk-extensions:
- org.freedesktop.Sdk.Extension.openjdk17 - org.freedesktop.Sdk.Extension.openjdk17
@ -75,8 +75,8 @@ modules:
buildsystem: autotools buildsystem: autotools
sources: sources:
- type: archive - type: archive
url: https://xorg.freedesktop.org/archive/individual/app/xrandr-1.5.2.tar.xz url: https://xorg.freedesktop.org/archive/individual/app/xrandr-1.5.3.tar.xz
sha256: c8bee4790d9058bacc4b6246456c58021db58a87ddda1a9d0139bf5f18f1f240 sha256: f8dd7566adb74147fab9964680b6bbadee87cf406a7fcff51718a5e6949b841c
x-checker-data: x-checker-data:
type: anitya type: anitya
project-id: 14957 project-id: 14957

View File

@ -614,6 +614,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_settings->registerSetting("IconsDir", "icons"); m_settings->registerSetting("IconsDir", "icons");
m_settings->registerSetting("DownloadsDir", QStandardPaths::writableLocation(QStandardPaths::DownloadLocation)); m_settings->registerSetting("DownloadsDir", QStandardPaths::writableLocation(QStandardPaths::DownloadLocation));
m_settings->registerSetting("DownloadsDirWatchRecursive", false); m_settings->registerSetting("DownloadsDirWatchRecursive", false);
m_settings->registerSetting("MoveModsFromDownloadsDir", false);
m_settings->registerSetting("SkinsDir", "skins"); m_settings->registerSetting("SkinsDir", "skins");
m_settings->registerSetting("JavaDir", "java"); m_settings->registerSetting("JavaDir", "java");

View File

@ -1024,8 +1024,6 @@ SET(LAUNCHER_SOURCES
ui/dialogs/CopyInstanceDialog.h ui/dialogs/CopyInstanceDialog.h
ui/dialogs/CustomMessageBox.cpp ui/dialogs/CustomMessageBox.cpp
ui/dialogs/CustomMessageBox.h ui/dialogs/CustomMessageBox.h
ui/dialogs/EditAccountDialog.cpp
ui/dialogs/EditAccountDialog.h
ui/dialogs/ExportInstanceDialog.cpp ui/dialogs/ExportInstanceDialog.cpp
ui/dialogs/ExportInstanceDialog.h ui/dialogs/ExportInstanceDialog.h
ui/dialogs/ExportPackDialog.cpp ui/dialogs/ExportPackDialog.cpp
@ -1079,8 +1077,6 @@ SET(LAUNCHER_SOURCES
ui/widgets/CustomCommands.h ui/widgets/CustomCommands.h
ui/widgets/EnvironmentVariables.cpp ui/widgets/EnvironmentVariables.cpp
ui/widgets/EnvironmentVariables.h ui/widgets/EnvironmentVariables.h
ui/widgets/DropLabel.cpp
ui/widgets/DropLabel.h
ui/widgets/FocusLineEdit.cpp ui/widgets/FocusLineEdit.cpp
ui/widgets/FocusLineEdit.h ui/widgets/FocusLineEdit.h
ui/widgets/IconLabel.cpp ui/widgets/IconLabel.cpp
@ -1215,7 +1211,6 @@ qt_wrap_ui(LAUNCHER_UI
ui/dialogs/MSALoginDialog.ui ui/dialogs/MSALoginDialog.ui
ui/dialogs/OfflineLoginDialog.ui ui/dialogs/OfflineLoginDialog.ui
ui/dialogs/AboutDialog.ui ui/dialogs/AboutDialog.ui
ui/dialogs/EditAccountDialog.ui
ui/dialogs/ReviewMessageBox.ui ui/dialogs/ReviewMessageBox.ui
ui/dialogs/ScrollMessageBox.ui ui/dialogs/ScrollMessageBox.ui
ui/dialogs/BlockedModsDialog.ui ui/dialogs/BlockedModsDialog.ui

View File

@ -47,24 +47,24 @@
#define MAX_SIZE 1024 #define MAX_SIZE 1024
IconList::IconList(const QStringList& builtinPaths, QString path, QObject* parent) : QAbstractListModel(parent) IconList::IconList(const QStringList& builtinPaths, const QString& path, QObject* parent) : QAbstractListModel(parent)
{ {
QSet<QString> builtinNames; QSet<QString> builtinNames;
// add builtin icons // add builtin icons
for (auto& builtinPath : builtinPaths) { for (const auto& builtinPath : builtinPaths) {
QDir instance_icons(builtinPath); QDir instanceIcons(builtinPath);
auto file_info_list = instance_icons.entryInfoList(QDir::Files, QDir::Name); auto fileInfoList = instanceIcons.entryInfoList(QDir::Files, QDir::Name);
for (auto file_info : file_info_list) { for (const auto& fileInfo : fileInfoList) {
builtinNames.insert(file_info.completeBaseName()); builtinNames.insert(fileInfo.baseName());
} }
} }
for (auto& builtinName : builtinNames) { for (const auto& builtinName : builtinNames) {
addThemeIcon(builtinName); addThemeIcon(builtinName);
} }
m_watcher.reset(new QFileSystemWatcher()); m_watcher.reset(new QFileSystemWatcher());
is_watching = false; m_isWatching = false;
connect(m_watcher.get(), &QFileSystemWatcher::directoryChanged, this, &IconList::directoryChanged); connect(m_watcher.get(), &QFileSystemWatcher::directoryChanged, this, &IconList::directoryChanged);
connect(m_watcher.get(), &QFileSystemWatcher::fileChanged, this, &IconList::fileChanged); connect(m_watcher.get(), &QFileSystemWatcher::fileChanged, this, &IconList::fileChanged);
@ -77,91 +77,131 @@ IconList::IconList(const QStringList& builtinPaths, QString path, QObject* paren
void IconList::sortIconList() void IconList::sortIconList()
{ {
qDebug() << "Sorting icon list..."; qDebug() << "Sorting icon list...";
std::sort(icons.begin(), icons.end(), [](const MMCIcon& a, const MMCIcon& b) { return a.m_key.localeAwareCompare(b.m_key) < 0; }); std::sort(m_icons.begin(), m_icons.end(), [](const MMCIcon& a, const MMCIcon& b) {
bool aIsSubdir = a.m_key.contains(QDir::separator());
bool bIsSubdir = b.m_key.contains(QDir::separator());
if (aIsSubdir != bIsSubdir) {
return !aIsSubdir; // root-level icons come first
}
return a.m_key.localeAwareCompare(b.m_key) < 0;
});
reindex(); reindex();
} }
// Helper function to add directories recursively
bool IconList::addPathRecursively(const QString& path)
{
QDir dir(path);
if (!dir.exists())
return false;
// Add the directory itself
bool watching = m_watcher->addPath(path);
// Add all subdirectories
QFileInfoList entries = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot);
for (const QFileInfo& entry : entries) {
if (addPathRecursively(entry.absoluteFilePath())) {
watching = true;
}
}
return watching;
}
QStringList IconList::getIconFilePaths() const
{
QStringList iconFiles{};
QStringList directories{ m_dir.absolutePath() };
while (!directories.isEmpty()) {
QString first = directories.takeFirst();
QDir dir(first);
for (QFileInfo& fileInfo : dir.entryInfoList(QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot, QDir::Name)) {
if (fileInfo.isDir())
directories.push_back(fileInfo.absoluteFilePath());
else
iconFiles.push_back(fileInfo.absoluteFilePath());
}
}
return iconFiles;
}
QString formatName(const QDir& iconsDir, const QFileInfo& iconFile)
{
if (iconFile.dir() == iconsDir)
return iconFile.baseName();
constexpr auto delimiter = " » ";
QString relativePathWithoutExtension = iconsDir.relativeFilePath(iconFile.dir().path()) + QDir::separator() + iconFile.baseName();
return relativePathWithoutExtension.replace(QDir::separator(), delimiter);
}
/// Split into a separate function because the preprocessing impedes readability
QSet<QString> toStringSet(const QList<QString>& list)
{
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
QSet<QString> set(list.begin(), list.end());
#else
QSet<QString> set = list.toSet();
#endif
return set;
}
void IconList::directoryChanged(const QString& path) void IconList::directoryChanged(const QString& path)
{ {
QDir new_dir(path); QDir newDir(path);
if (m_dir.absolutePath() != new_dir.absolutePath()) { if (m_dir.absolutePath() != newDir.absolutePath()) {
if (!path.startsWith(m_dir.absolutePath()))
m_dir.setPath(path); m_dir.setPath(path);
m_dir.refresh(); m_dir.refresh();
if (is_watching) if (m_isWatching)
stopWatching(); stopWatching();
startWatching(); startWatching();
} }
if (!m_dir.exists()) if (!m_dir.exists() && !FS::ensureFolderPathExists(m_dir.absolutePath()))
if (!FS::ensureFolderPathExists(m_dir.absolutePath()))
return; return;
m_dir.refresh(); m_dir.refresh();
auto new_list = m_dir.entryList(QDir::Files, QDir::Name); const QStringList newFileNamesList = getIconFilePaths();
for (auto it = new_list.begin(); it != new_list.end(); it++) { const QSet<QString> newSet = toStringSet(newFileNamesList);
QString& foo = (*it); QSet<QString> currentSet;
foo = m_dir.filePath(foo); for (const MMCIcon& it : m_icons) {
}
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
QSet<QString> new_set(new_list.begin(), new_list.end());
#else
auto new_set = new_list.toSet();
#endif
QList<QString> current_list;
for (auto& it : icons) {
if (!it.has(IconType::FileBased)) if (!it.has(IconType::FileBased))
continue; continue;
current_list.push_back(it.m_images[IconType::FileBased].filename); currentSet.insert(it.m_images[IconType::FileBased].filename);
} }
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) QSet<QString> toRemove = currentSet - newSet;
QSet<QString> current_set(current_list.begin(), current_list.end()); QSet<QString> toAdd = newSet - currentSet;
#else
QSet<QString> current_set = current_list.toSet();
#endif
QSet<QString> to_remove = current_set; for (const QString& removedPath : toRemove) {
to_remove -= new_set; qDebug() << "Removing icon " << removedPath;
QFileInfo removedFile(removedPath);
QSet<QString> to_add = new_set; QString key = m_dir.relativeFilePath(removedFile.absoluteFilePath());
to_add -= current_set;
for (auto remove : to_remove) {
qDebug() << "Removing " << remove;
QFileInfo rmfile(remove);
QString key = rmfile.completeBaseName();
QString suffix = rmfile.suffix();
// The icon doesnt have a suffix, but it can have other .s in the name, so we account for those as well
if (!IconUtils::isIconSuffix(suffix))
key = rmfile.fileName();
int idx = getIconIndex(key); int idx = getIconIndex(key);
if (idx == -1) if (idx == -1)
continue; continue;
icons[idx].remove(IconType::FileBased); m_icons[idx].remove(FileBased);
if (icons[idx].type() == IconType::ToBeDeleted) { if (m_icons[idx].type() == ToBeDeleted) {
beginRemoveRows(QModelIndex(), idx, idx); beginRemoveRows(QModelIndex(), idx, idx);
icons.remove(idx); m_icons.remove(idx);
reindex(); reindex();
endRemoveRows(); endRemoveRows();
} else { } else {
dataChanged(index(idx), index(idx)); dataChanged(index(idx), index(idx));
} }
m_watcher->removePath(remove); m_watcher->removePath(removedPath);
emit iconUpdated(key); emit iconUpdated(key);
} }
for (auto add : to_add) { for (const QString& addedPath : toAdd) {
qDebug() << "Adding " << add; qDebug() << "Adding icon " << addedPath;
QFileInfo addfile(add); QFileInfo addfile(addedPath);
QString key = addfile.completeBaseName(); QString relativePath = m_dir.relativeFilePath(addfile.absoluteFilePath());
QString key = QFileInfo(relativePath).completeBaseName();
QString name = formatName(m_dir, addfile);
QString suffix = addfile.suffix(); if (addIcon(key, name, addfile.filePath(), IconType::FileBased)) {
// The icon doesnt have a suffix, but it can have other .s in the name, so we account for those as well m_watcher->addPath(addedPath);
if (!IconUtils::isIconSuffix(suffix))
key = addfile.fileName();
if (addIcon(key, QString(), addfile.filePath(), IconType::FileBased)) {
m_watcher->addPath(add);
emit iconUpdated(key); emit iconUpdated(key);
} }
} }
@ -171,24 +211,24 @@ void IconList::directoryChanged(const QString& path)
void IconList::fileChanged(const QString& path) void IconList::fileChanged(const QString& path)
{ {
qDebug() << "Checking " << path; qDebug() << "Checking icon " << path;
QFileInfo checkfile(path); QFileInfo checkfile(path);
if (!checkfile.exists()) if (!checkfile.exists())
return; return;
QString key = checkfile.completeBaseName(); QString key = m_dir.relativeFilePath(checkfile.absoluteFilePath());
int idx = getIconIndex(key); int idx = getIconIndex(key);
if (idx == -1) if (idx == -1)
return; return;
QIcon icon(path); QIcon icon(path);
if (!icon.availableSizes().size()) if (icon.availableSizes().empty())
return; return;
icons[idx].m_images[IconType::FileBased].icon = icon; m_icons[idx].m_images[IconType::FileBased].icon = icon;
dataChanged(index(idx), index(idx)); dataChanged(index(idx), index(idx));
emit iconUpdated(key); emit iconUpdated(key);
} }
void IconList::SettingChanged(const Setting& setting, QVariant value) void IconList::SettingChanged(const Setting& setting, const QVariant& value)
{ {
if (setting.id() != "IconsDir") if (setting.id() != "IconsDir")
return; return;
@ -200,8 +240,8 @@ void IconList::startWatching()
{ {
auto abs_path = m_dir.absolutePath(); auto abs_path = m_dir.absolutePath();
FS::ensureFolderPathExists(abs_path); FS::ensureFolderPathExists(abs_path);
is_watching = m_watcher->addPath(abs_path); m_isWatching = addPathRecursively(abs_path);
if (is_watching) { if (m_isWatching) {
qDebug() << "Started watching " << abs_path; qDebug() << "Started watching " << abs_path;
} else { } else {
qDebug() << "Failed to start watching " << abs_path; qDebug() << "Failed to start watching " << abs_path;
@ -212,7 +252,7 @@ void IconList::stopWatching()
{ {
m_watcher->removePaths(m_watcher->files()); m_watcher->removePaths(m_watcher->files());
m_watcher->removePaths(m_watcher->directories()); m_watcher->removePaths(m_watcher->directories());
is_watching = false; m_isWatching = false;
} }
QStringList IconList::mimeTypes() const QStringList IconList::mimeTypes() const
@ -242,7 +282,7 @@ bool IconList::dropMimeData(const QMimeData* data,
if (data->hasUrls()) { if (data->hasUrls()) {
auto urls = data->urls(); auto urls = data->urls();
QStringList iconFiles; QStringList iconFiles;
for (auto url : urls) { for (const auto& url : urls) {
// only local files may be dropped... // only local files may be dropped...
if (!url.isLocalFile()) if (!url.isLocalFile())
continue; continue;
@ -263,33 +303,33 @@ Qt::ItemFlags IconList::flags(const QModelIndex& index) const
QVariant IconList::data(const QModelIndex& index, int role) const QVariant IconList::data(const QModelIndex& index, int role) const
{ {
if (!index.isValid()) if (!index.isValid())
return QVariant(); return {};
int row = index.row(); int row = index.row();
if (row < 0 || row >= icons.size()) if (row < 0 || row >= m_icons.size())
return QVariant(); return {};
switch (role) { switch (role) {
case Qt::DecorationRole: case Qt::DecorationRole:
return icons[row].icon(); return m_icons[row].icon();
case Qt::DisplayRole: case Qt::DisplayRole:
return icons[row].name(); return m_icons[row].name();
case Qt::UserRole: case Qt::UserRole:
return icons[row].m_key; return m_icons[row].m_key;
default: default:
return QVariant(); return {};
} }
} }
int IconList::rowCount(const QModelIndex& parent) const int IconList::rowCount(const QModelIndex& parent) const
{ {
return parent.isValid() ? 0 : icons.size(); return parent.isValid() ? 0 : m_icons.size();
} }
void IconList::installIcons(const QStringList& iconFiles) void IconList::installIcons(const QStringList& iconFiles)
{ {
for (QString file : iconFiles) for (const QString& file : iconFiles)
installIcon(file, {}); installIcon(file, {});
} }
@ -312,12 +352,13 @@ bool IconList::iconFileExists(const QString& key) const
return iconEntry && iconEntry->has(IconType::FileBased); return iconEntry && iconEntry->has(IconType::FileBased);
} }
/// Returns the icon with the given key or nullptr if it doesn't exist.
const MMCIcon* IconList::icon(const QString& key) const const MMCIcon* IconList::icon(const QString& key) const
{ {
int iconIdx = getIconIndex(key); int iconIdx = getIconIndex(key);
if (iconIdx == -1) if (iconIdx == -1)
return nullptr; return nullptr;
return &icons[iconIdx]; return &m_icons[iconIdx];
} }
bool IconList::deleteIcon(const QString& key) bool IconList::deleteIcon(const QString& key)
@ -332,22 +373,22 @@ bool IconList::trashIcon(const QString& key)
bool IconList::addThemeIcon(const QString& key) bool IconList::addThemeIcon(const QString& key)
{ {
auto iter = name_index.find(key); auto iter = m_nameIndex.find(key);
if (iter != name_index.end()) { if (iter != m_nameIndex.end()) {
auto& oldOne = icons[*iter]; auto& oldOne = m_icons[*iter];
oldOne.replace(Builtin, key); oldOne.replace(Builtin, key);
dataChanged(index(*iter), index(*iter)); dataChanged(index(*iter), index(*iter));
return true; return true;
} }
// add a new icon // add a new icon
beginInsertRows(QModelIndex(), icons.size(), icons.size()); beginInsertRows(QModelIndex(), m_icons.size(), m_icons.size());
{ {
MMCIcon mmc_icon; MMCIcon mmc_icon;
mmc_icon.m_name = key; mmc_icon.m_name = key;
mmc_icon.m_key = key; mmc_icon.m_key = key;
mmc_icon.replace(Builtin, key); mmc_icon.replace(Builtin, key);
icons.push_back(mmc_icon); m_icons.push_back(mmc_icon);
name_index[key] = icons.size() - 1; m_nameIndex[key] = m_icons.size() - 1;
} }
endInsertRows(); endInsertRows();
return true; return true;
@ -359,22 +400,22 @@ bool IconList::addIcon(const QString& key, const QString& name, const QString& p
QIcon icon(path); QIcon icon(path);
if (icon.isNull()) if (icon.isNull())
return false; return false;
auto iter = name_index.find(key); auto iter = m_nameIndex.find(key);
if (iter != name_index.end()) { if (iter != m_nameIndex.end()) {
auto& oldOne = icons[*iter]; auto& oldOne = m_icons[*iter];
oldOne.replace(type, icon, path); oldOne.replace(type, icon, path);
dataChanged(index(*iter), index(*iter)); dataChanged(index(*iter), index(*iter));
return true; return true;
} }
// add a new icon // add a new icon
beginInsertRows(QModelIndex(), icons.size(), icons.size()); beginInsertRows(QModelIndex(), m_icons.size(), m_icons.size());
{ {
MMCIcon mmc_icon; MMCIcon mmc_icon;
mmc_icon.m_name = name; mmc_icon.m_name = name;
mmc_icon.m_key = key; mmc_icon.m_key = key;
mmc_icon.replace(type, icon, path); mmc_icon.replace(type, icon, path);
icons.push_back(mmc_icon); m_icons.push_back(mmc_icon);
name_index[key] = icons.size() - 1; m_nameIndex[key] = m_icons.size() - 1;
} }
endInsertRows(); endInsertRows();
return true; return true;
@ -389,33 +430,32 @@ void IconList::saveIcon(const QString& key, const QString& path, const char* for
void IconList::reindex() void IconList::reindex()
{ {
name_index.clear(); m_nameIndex.clear();
int i = 0; for (int i = 0; i < m_icons.size(); i++) {
for (auto& iter : icons) { m_nameIndex[m_icons[i].m_key] = i;
name_index[iter.m_key] = i; emit iconUpdated(m_icons[i].m_key); // prevents incorrect indices with proxy model
i++;
} }
} }
QIcon IconList::getIcon(const QString& key) const QIcon IconList::getIcon(const QString& key) const
{ {
int icon_index = getIconIndex(key); int iconIndex = getIconIndex(key);
if (icon_index != -1) if (iconIndex != -1)
return icons[icon_index].icon(); return m_icons[iconIndex].icon();
// Fallback for icons that don't exist. // Fallback for icons that don't exist.b
icon_index = getIconIndex("grass"); iconIndex = getIconIndex("grass");
if (icon_index != -1) if (iconIndex != -1)
return icons[icon_index].icon(); return m_icons[iconIndex].icon();
return QIcon(); return {};
} }
int IconList::getIconIndex(const QString& key) const int IconList::getIconIndex(const QString& key) const
{ {
auto iter = name_index.find(key == "default" ? "grass" : key); auto iter = m_nameIndex.find(key == "default" ? "grass" : key);
if (iter != name_index.end()) if (iter != m_nameIndex.end())
return *iter; return *iter;
return -1; return -1;
@ -425,3 +465,15 @@ QString IconList::getDirectory() const
{ {
return m_dir.absolutePath(); return m_dir.absolutePath();
} }
/// Returns the directory of the icon with the given key or the default directory if it's a builtin icon.
QString IconList::iconDirectory(const QString& key) const
{
for (const auto& mmcIcon : m_icons) {
if (mmcIcon.m_key == key && mmcIcon.has(IconType::FileBased)) {
QFileInfo iconFile(mmcIcon.getFilePath());
return iconFile.dir().path();
}
}
return getDirectory();
}

View File

@ -51,7 +51,7 @@ class QFileSystemWatcher;
class IconList : public QAbstractListModel { class IconList : public QAbstractListModel {
Q_OBJECT Q_OBJECT
public: public:
explicit IconList(const QStringList& builtinPaths, QString path, QObject* parent = 0); explicit IconList(const QStringList& builtinPaths, const QString& path, QObject* parent = 0);
virtual ~IconList() {}; virtual ~IconList() {};
QIcon getIcon(const QString& key) const; QIcon getIcon(const QString& key) const;
@ -72,6 +72,7 @@ class IconList : public QAbstractListModel {
bool deleteIcon(const QString& key); bool deleteIcon(const QString& key);
bool trashIcon(const QString& key); bool trashIcon(const QString& key);
bool iconFileExists(const QString& key) const; bool iconFileExists(const QString& key) const;
QString iconDirectory(const QString& key) const;
void installIcons(const QStringList& iconFiles); void installIcons(const QStringList& iconFiles);
void installIcon(const QString& file, const QString& name); void installIcon(const QString& file, const QString& name);
@ -91,18 +92,20 @@ class IconList : public QAbstractListModel {
IconList& operator=(const IconList&) = delete; IconList& operator=(const IconList&) = delete;
void reindex(); void reindex();
void sortIconList(); void sortIconList();
bool addPathRecursively(const QString& path);
QStringList getIconFilePaths() const;
public slots: public slots:
void directoryChanged(const QString& path); void directoryChanged(const QString& path);
protected slots: protected slots:
void fileChanged(const QString& path); void fileChanged(const QString& path);
void SettingChanged(const Setting& setting, QVariant value); void SettingChanged(const Setting& setting, const QVariant& value);
private: private:
shared_qobject_ptr<QFileSystemWatcher> m_watcher; shared_qobject_ptr<QFileSystemWatcher> m_watcher;
bool is_watching; bool m_isWatching;
QMap<QString, int> name_index; QMap<QString, int> m_nameIndex;
QVector<MMCIcon> icons; QVector<MMCIcon> m_icons;
QDir m_dir; QDir m_dir;
}; };

View File

@ -86,11 +86,10 @@ void ManifestDownloadTask::downloadJava(const QJsonDocument& doc)
if (type == "directory") { if (type == "directory") {
FS::ensureFolderPathExists(file); FS::ensureFolderPathExists(file);
} else if (type == "link") { } else if (type == "link") {
// this is linux only ! // this is *nix only !
auto path = Json::ensureString(meta, "target"); auto path = Json::ensureString(meta, "target");
if (!path.isEmpty()) { if (!path.isEmpty()) {
auto target = FS::PathCombine(file, "../" + path); QFile::link(path, file);
QFile(target).link(file);
} }
} else if (type == "file") { } else if (type == "file") {
// TODO download compressed version if it exists ? // TODO download compressed version if it exists ?

View File

@ -254,20 +254,60 @@ void LaunchTask::emitFailed(QString reason)
Task::emitFailed(reason); Task::emitFailed(reason);
} }
void LaunchTask::substituteVariables(QStringList& args) const QString expandVariables(const QString& input, QProcessEnvironment dict)
{ {
auto env = m_instance->createEnvironment(); QString result = input;
for (auto key : env.keys()) { enum { base, maybeBrace, variable, brace } state = base;
args.replaceInStrings("$" + key, env.value(key)); int startIdx = -1;
for (int i = 0; i < result.length();) {
QChar c = result.at(i++);
switch (state) {
case base:
if (c == '$')
state = maybeBrace;
break;
case maybeBrace:
if (c == '{') {
state = brace;
startIdx = i;
} else if (c.isLetterOrNumber() || c == '_') {
state = variable;
startIdx = i - 1;
} else {
state = base;
} }
break;
case brace:
if (c == '}') {
const auto res = dict.value(result.mid(startIdx, i - 1 - startIdx), "");
if (!res.isEmpty()) {
result.replace(startIdx - 2, i - startIdx + 2, res);
i = startIdx - 2 + res.length();
}
state = base;
}
break;
case variable:
if (!c.isLetterOrNumber() && c != '_') {
const auto res = dict.value(result.mid(startIdx, i - startIdx - 1), "");
if (!res.isEmpty()) {
result.replace(startIdx - 1, i - startIdx, res);
i = startIdx - 1 + res.length();
}
state = base;
}
break;
}
}
if (state == variable) {
if (const auto res = dict.value(result.mid(startIdx), ""); !res.isEmpty())
result.replace(startIdx - 1, result.length() - startIdx + 1, res);
}
return result;
} }
void LaunchTask::substituteVariables(QString& cmd) const QString LaunchTask::substituteVariables(QString& cmd, bool isLaunch) const
{ {
auto env = m_instance->createEnvironment(); return expandVariables(cmd, isLaunch ? m_instance->createLaunchEnvironment() : m_instance->createEnvironment());
for (auto key : env.keys()) {
cmd.replace("$" + key, env.value(key));
}
} }

View File

@ -87,8 +87,7 @@ class LaunchTask : public Task {
shared_qobject_ptr<LogModel> getLogModel(); shared_qobject_ptr<LogModel> getLogModel();
public: public:
void substituteVariables(QStringList& args) const; QString substituteVariables(QString& cmd, bool isLaunch = false) const;
void substituteVariables(QString& cmd) const;
QString censorPrivateInfo(QString in); QString censorPrivateInfo(QString in);
protected: /* methods */ protected: /* methods */

View File

@ -47,19 +47,15 @@ PostLaunchCommand::PostLaunchCommand(LaunchTask* parent) : LaunchStep(parent)
void PostLaunchCommand::executeTask() void PostLaunchCommand::executeTask()
{ {
// FIXME: where to put this? auto cmd = m_parent->substituteVariables(m_command);
emit logLine(tr("Running Post-Launch command: %1").arg(cmd), MessageLevel::Launcher);
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
auto args = QProcess::splitCommand(m_command); auto args = QProcess::splitCommand(cmd);
m_parent->substituteVariables(args);
emit logLine(tr("Running Post-Launch command: %1").arg(args.join(' ')), MessageLevel::Launcher);
const QString program = args.takeFirst(); const QString program = args.takeFirst();
m_process.start(program, args); m_process.start(program, args);
#else #else
m_parent->substituteVariables(m_command); m_process.start(cmd);
emit logLine(tr("Running Post-Launch command: %1").arg(m_command), MessageLevel::Launcher);
m_process.start(m_command);
#endif #endif
} }

View File

@ -47,19 +47,14 @@ PreLaunchCommand::PreLaunchCommand(LaunchTask* parent) : LaunchStep(parent)
void PreLaunchCommand::executeTask() void PreLaunchCommand::executeTask()
{ {
// FIXME: where to put this? auto cmd = m_parent->substituteVariables(m_command);
emit logLine(tr("Running Pre-Launch command: %1").arg(cmd), MessageLevel::Launcher);
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
auto args = QProcess::splitCommand(m_command); auto args = QProcess::splitCommand(cmd);
m_parent->substituteVariables(args);
emit logLine(tr("Running Pre-Launch command: %1").arg(args.join(' ')), MessageLevel::Launcher);
const QString program = args.takeFirst(); const QString program = args.takeFirst();
m_process.start(program, args); m_process.start(program, args);
#else #else
m_parent->substituteVariables(m_command); m_process.start(cmd);
emit logLine(tr("Running Pre-Launch command: %1").arg(m_command), MessageLevel::Launcher);
m_process.start(m_command);
#endif #endif
} }

View File

@ -594,6 +594,13 @@ QMap<QString, QString> MinecraftInstance::getVariables()
out.insert("INST_JAVA", settings()->get("JavaPath").toString()); out.insert("INST_JAVA", settings()->get("JavaPath").toString());
out.insert("INST_JAVA_ARGS", javaArguments().join(' ')); out.insert("INST_JAVA_ARGS", javaArguments().join(' '));
out.insert("NO_COLOR", "1"); out.insert("NO_COLOR", "1");
#ifdef Q_OS_MACOS
// get library for Steam overlay support
QString steamDyldInsertLibraries = qEnvironmentVariable("STEAM_DYLD_INSERT_LIBRARIES");
if (!steamDyldInsertLibraries.isEmpty()) {
out.insert("DYLD_INSERT_LIBRARIES", steamDyldInsertLibraries);
}
#endif
return out; return out;
} }
@ -1151,13 +1158,6 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt
process->appendStep(step); process->appendStep(step);
} }
// run pre-launch command if that's needed
if (getPreLaunchCommand().size()) {
auto step = makeShared<PreLaunchCommand>(pptr);
step->setWorkingDirectory(gameRoot());
process->appendStep(step);
}
// load meta // load meta
{ {
auto mode = session->status != AuthSession::PlayableOffline ? Net::Mode::Online : Net::Mode::Offline; auto mode = session->status != AuthSession::PlayableOffline ? Net::Mode::Online : Net::Mode::Offline;
@ -1170,6 +1170,13 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt
process->appendStep(makeShared<CheckJava>(pptr)); process->appendStep(makeShared<CheckJava>(pptr));
} }
// run pre-launch command if that's needed
if (getPreLaunchCommand().size()) {
auto step = makeShared<PreLaunchCommand>(pptr);
step->setWorkingDirectory(gameRoot());
process->appendStep(step);
}
// if we aren't in offline mode,. // if we aren't in offline mode,.
if (session->status != AuthSession::PlayableOffline) { if (session->status != AuthSession::PlayableOffline) {
if (!session->demo) { if (!session->demo) {

View File

@ -8,7 +8,10 @@ void MinecraftLoadAndCheck::executeTask()
{ {
// add offline metadata load task // add offline metadata load task
auto components = m_inst->getPackProfile(); auto components = m_inst->getPackProfile();
components->reload(m_netmode); if (auto result = components->reload(m_netmode); !result) {
emitFailed(result.error);
return;
}
m_task = components->getCurrentTask(); m_task = components->getCurrentTask();
if (!m_task) { if (!m_task) {

View File

@ -173,29 +173,32 @@ static bool savePackProfile(const QString& filename, const ComponentContainer& c
} }
// Read the given file into component containers // Read the given file into component containers
static bool loadPackProfile(PackProfile* parent, static PackProfile::Result loadPackProfile(PackProfile* parent,
const QString& filename, const QString& filename,
const QString& componentJsonPattern, const QString& componentJsonPattern,
ComponentContainer& container) ComponentContainer& container)
{ {
QFile componentsFile(filename); QFile componentsFile(filename);
if (!componentsFile.exists()) { if (!componentsFile.exists()) {
qCWarning(instanceProfileC) << "Components file" << filename << "doesn't exist. This should never happen."; auto message = QObject::tr("Components file %1 doesn't exist. This should never happen.").arg(filename);
return false; qCWarning(instanceProfileC) << message;
return PackProfile::Result::Error(message);
} }
if (!componentsFile.open(QFile::ReadOnly)) { if (!componentsFile.open(QFile::ReadOnly)) {
qCCritical(instanceProfileC) << "Couldn't open" << componentsFile.fileName() << " for reading:" << componentsFile.errorString(); auto message = QObject::tr("Couldn't open %1 for reading: %2").arg(componentsFile.fileName(), componentsFile.errorString());
qCCritical(instanceProfileC) << message;
qCWarning(instanceProfileC) << "Ignoring overridden order"; qCWarning(instanceProfileC) << "Ignoring overridden order";
return false; return PackProfile::Result::Error(message);
} }
// and it's valid JSON // and it's valid JSON
QJsonParseError error; QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(componentsFile.readAll(), &error); QJsonDocument doc = QJsonDocument::fromJson(componentsFile.readAll(), &error);
if (error.error != QJsonParseError::NoError) { if (error.error != QJsonParseError::NoError) {
qCCritical(instanceProfileC) << "Couldn't parse" << componentsFile.fileName() << ":" << error.errorString(); auto message = QObject::tr("Couldn't parse %1 as json: %2").arg(componentsFile.fileName(), error.errorString());
qCCritical(instanceProfileC) << message;
qCWarning(instanceProfileC) << "Ignoring overridden order"; qCWarning(instanceProfileC) << "Ignoring overridden order";
return false; return PackProfile::Result::Error(message);
} }
// and then read it and process it if all above is true. // and then read it and process it if all above is true.
@ -212,11 +215,13 @@ static bool loadPackProfile(PackProfile* parent,
container.append(componentFromJsonV1(parent, componentJsonPattern, comp_obj)); container.append(componentFromJsonV1(parent, componentJsonPattern, comp_obj));
} }
} catch ([[maybe_unused]] const JSONValidationError& err) { } catch ([[maybe_unused]] const JSONValidationError& err) {
qCCritical(instanceProfileC) << "Couldn't parse" << componentsFile.fileName() << ": bad file format"; auto message = QObject::tr("Couldn't parse %1 : bad file format").arg(componentsFile.fileName());
qCCritical(instanceProfileC) << message;
qCWarning(instanceProfileC) << "error:" << err.what();
container.clear(); container.clear();
return false; return PackProfile::Result::Error(message);
} }
return true; return PackProfile::Result::Success();
} }
// END: component file format // END: component file format
@ -283,16 +288,16 @@ void PackProfile::save_internal()
d->dirty = false; d->dirty = false;
} }
bool PackProfile::load() PackProfile::Result PackProfile::load()
{ {
auto filename = componentsFilePath(); auto filename = componentsFilePath();
// load the new component list and swap it with the current one... // load the new component list and swap it with the current one...
ComponentContainer newComponents; ComponentContainer newComponents;
if (!loadPackProfile(this, filename, patchesPattern(), newComponents)) { if (auto result = loadPackProfile(this, filename, patchesPattern(), newComponents); !result) {
qCritical() << d->m_instance->name() << "|" << "Failed to load the component config"; qCritical() << d->m_instance->name() << "|" << "Failed to load the component config";
return false; return result;
} else { }
// FIXME: actually use fine-grained updates, not this... // FIXME: actually use fine-grained updates, not this...
beginResetModel(); beginResetModel();
// disconnect all the old components // disconnect all the old components
@ -312,15 +317,14 @@ bool PackProfile::load()
} }
endResetModel(); endResetModel();
d->loaded = true; d->loaded = true;
return true; return Result::Success();
}
} }
void PackProfile::reload(Net::Mode netmode) PackProfile::Result PackProfile::reload(Net::Mode netmode)
{ {
// Do not reload when the update/resolve task is running. It is in control. // Do not reload when the update/resolve task is running. It is in control.
if (d->m_updateTask) { if (d->m_updateTask) {
return; return Result::Success();
} }
// flush any scheduled saves to not lose state // flush any scheduled saves to not lose state
@ -329,9 +333,11 @@ void PackProfile::reload(Net::Mode netmode)
// FIXME: differentiate when a reapply is required by propagating state from components // FIXME: differentiate when a reapply is required by propagating state from components
invalidateLaunchProfile(); invalidateLaunchProfile();
if (load()) { if (auto result = load(); !result) {
resolve(netmode); return result;
} }
resolve(netmode);
return Result::Success();
} }
Task::Ptr PackProfile::getCurrentTask() Task::Ptr PackProfile::getCurrentTask()

View File

@ -62,6 +62,19 @@ class PackProfile : public QAbstractListModel {
public: public:
enum Columns { NameColumn = 0, VersionColumn, NUM_COLUMNS }; enum Columns { NameColumn = 0, VersionColumn, NUM_COLUMNS };
struct Result {
bool success;
QString error;
// Implicit conversion to bool
operator bool() const { return success; }
// Factory methods for convenience
static Result Success() { return { true, "" }; }
static Result Error(const QString& errorMessage) { return { false, errorMessage }; }
};
explicit PackProfile(MinecraftInstance* instance); explicit PackProfile(MinecraftInstance* instance);
virtual ~PackProfile(); virtual ~PackProfile();
@ -102,7 +115,7 @@ class PackProfile : public QAbstractListModel {
bool revertToBase(int index); bool revertToBase(int index);
/// reload the list, reload all components, resolve dependencies /// reload the list, reload all components, resolve dependencies
void reload(Net::Mode netmode); Result reload(Net::Mode netmode);
// reload all components, resolve dependencies // reload all components, resolve dependencies
void resolve(Net::Mode netmode); void resolve(Net::Mode netmode);
@ -169,7 +182,7 @@ class PackProfile : public QAbstractListModel {
void disableInteraction(bool disable); void disableInteraction(bool disable);
private: private:
bool load(); Result load();
bool installJarMods_internal(QStringList filepaths); bool installJarMods_internal(QStringList filepaths);
bool installCustomJar_internal(QString filepath); bool installCustomJar_internal(QString filepath);
bool installAgents_internal(QStringList filepaths); bool installAgents_internal(QStringList filepaths);

View File

@ -131,6 +131,7 @@ void LauncherPartLaunch::executeTask()
QString wrapperCommandStr = instance->getWrapperCommand().trimmed(); QString wrapperCommandStr = instance->getWrapperCommand().trimmed();
if (!wrapperCommandStr.isEmpty()) { if (!wrapperCommandStr.isEmpty()) {
wrapperCommandStr = m_parent->substituteVariables(wrapperCommandStr);
auto wrapperArgs = Commandline::splitArgs(wrapperCommandStr); auto wrapperArgs = Commandline::splitArgs(wrapperCommandStr);
auto wrapperCommand = wrapperArgs.takeFirst(); auto wrapperCommand = wrapperArgs.takeFirst();
auto realWrapperCommand = QStandardPaths::findExecutable(wrapperCommand); auto realWrapperCommand = QStandardPaths::findExecutable(wrapperCommand);

View File

@ -48,6 +48,7 @@
#include "Version.h" #include "Version.h"
#include "minecraft/mod/ModDetails.h" #include "minecraft/mod/ModDetails.h"
#include "minecraft/mod/tasks/LocalModParseTask.h" #include "minecraft/mod/tasks/LocalModParseTask.h"
#include "modplatform/ModIndex.h"
Mod::Mod(const QFileInfo& file) : Resource(file), m_local_details() Mod::Mod(const QFileInfo& file) : Resource(file), m_local_details()
{ {
@ -157,12 +158,9 @@ auto Mod::loaders() const -> QString
if (metadata()) { if (metadata()) {
QStringList loaders; QStringList loaders;
auto modLoaders = metadata()->loaders; auto modLoaders = metadata()->loaders;
for (auto loader : { ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Cauldron, ModPlatform::LiteLoader, ModPlatform::Fabric, for (auto loader : ModPlatform::modLoaderTypesToList(modLoaders)) {
ModPlatform::Quilt }) {
if (modLoaders & loader) {
loaders << getModLoaderAsString(loader); loaders << getModLoaderAsString(loader);
} }
}
return loaders.join(", "); return loaders.join(", ");
} }

View File

@ -19,27 +19,28 @@ static ModrinthAPI modrinth_api;
static FlameAPI flame_api; static FlameAPI flame_api;
EnsureMetadataTask::EnsureMetadataTask(Resource* resource, QDir dir, ModPlatform::ResourceProvider prov) EnsureMetadataTask::EnsureMetadataTask(Resource* resource, QDir dir, ModPlatform::ResourceProvider prov)
: Task(), m_index_dir(dir), m_provider(prov), m_hashing_task(nullptr), m_current_task(nullptr) : Task(), m_index_dir(dir), m_provider(prov), m_hashingTask(nullptr), m_current_task(nullptr)
{ {
auto hash_task = createNewHash(resource); auto hashTask = createNewHash(resource);
if (!hash_task) if (!hashTask)
return; return;
connect(hash_task.get(), &Hashing::Hasher::resultsReady, [this, resource](QString hash) { m_resources.insert(hash, resource); }); connect(hashTask.get(), &Hashing::Hasher::resultsReady, [this, resource](QString hash) { m_resources.insert(hash, resource); });
connect(hash_task.get(), &Task::failed, [this, resource] { emitFail(resource, "", RemoveFromList::No); }); connect(hashTask.get(), &Task::failed, [this, resource] { emitFail(resource, "", RemoveFromList::No); });
hash_task->start(); m_hashingTask = hashTask;
} }
EnsureMetadataTask::EnsureMetadataTask(QList<Resource*>& resources, QDir dir, ModPlatform::ResourceProvider prov) EnsureMetadataTask::EnsureMetadataTask(QList<Resource*>& resources, QDir dir, ModPlatform::ResourceProvider prov)
: Task(), m_index_dir(dir), m_provider(prov), m_current_task(nullptr) : Task(), m_index_dir(dir), m_provider(prov), m_current_task(nullptr)
{ {
m_hashing_task.reset(new ConcurrentTask("MakeHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())); auto hashTask = makeShared<ConcurrentTask>("MakeHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt());
m_hashingTask = hashTask;
for (auto* resource : resources) { for (auto* resource : resources) {
auto hash_task = createNewHash(resource); auto hash_task = createNewHash(resource);
if (!hash_task) if (!hash_task)
continue; continue;
connect(hash_task.get(), &Hashing::Hasher::resultsReady, [this, resource](QString hash) { m_resources.insert(hash, resource); }); connect(hash_task.get(), &Hashing::Hasher::resultsReady, [this, resource](QString hash) { m_resources.insert(hash, resource); });
connect(hash_task.get(), &Task::failed, [this, resource] { emitFail(resource, "", RemoveFromList::No); }); connect(hash_task.get(), &Task::failed, [this, resource] { emitFail(resource, "", RemoveFromList::No); });
m_hashing_task->addTask(hash_task); hashTask->addTask(hash_task);
} }
} }

View File

@ -21,7 +21,7 @@ class EnsureMetadataTask : public Task {
~EnsureMetadataTask() = default; ~EnsureMetadataTask() = default;
Task::Ptr getHashingTask() { return m_hashing_task; } Task::Ptr getHashingTask() { return m_hashingTask; }
public slots: public slots:
bool abort() override; bool abort() override;
@ -59,6 +59,6 @@ class EnsureMetadataTask : public Task {
ModPlatform::ResourceProvider m_provider; ModPlatform::ResourceProvider m_provider;
QHash<QString, ModPlatform::IndexedVersion> m_temp_versions; QHash<QString, ModPlatform::IndexedVersion> m_temp_versions;
ConcurrentTask::Ptr m_hashing_task; Task::Ptr m_hashingTask;
Task::Ptr m_current_task; Task::Ptr m_current_task;
}; };

View File

@ -31,6 +31,19 @@ static const QMap<QString, IndexedVersionType::VersionType> s_indexed_version_ty
{ "alpha", IndexedVersionType::VersionType::Alpha } { "alpha", IndexedVersionType::VersionType::Alpha }
}; };
static const QList<ModLoaderType> loaderList = { NeoForge, Forge, Cauldron, LiteLoader, Quilt, Fabric };
QList<ModLoaderType> modLoaderTypesToList(ModLoaderTypes flags)
{
QList<ModLoaderType> flagList;
for (auto flag : loaderList) {
if (flags.testFlag(flag)) {
flagList.append(flag);
}
}
return flagList;
}
IndexedVersionType::IndexedVersionType(const QString& type) : IndexedVersionType(enumFromString(type)) {} IndexedVersionType::IndexedVersionType(const QString& type) : IndexedVersionType(enumFromString(type)) {}
IndexedVersionType::IndexedVersionType(const IndexedVersionType::VersionType& type) IndexedVersionType::IndexedVersionType(const IndexedVersionType::VersionType& type)

View File

@ -32,6 +32,7 @@ namespace ModPlatform {
enum ModLoaderType { NeoForge = 1 << 0, Forge = 1 << 1, Cauldron = 1 << 2, LiteLoader = 1 << 3, Fabric = 1 << 4, Quilt = 1 << 5 }; enum ModLoaderType { NeoForge = 1 << 0, Forge = 1 << 1, Cauldron = 1 << 2, LiteLoader = 1 << 3, Fabric = 1 << 4, Quilt = 1 << 5 };
Q_DECLARE_FLAGS(ModLoaderTypes, ModLoaderType) Q_DECLARE_FLAGS(ModLoaderTypes, ModLoaderType)
QList<ModLoaderType> modLoaderTypesToList(ModLoaderTypes flags);
enum class ResourceProvider { MODRINTH, FLAME }; enum class ResourceProvider { MODRINTH, FLAME };

View File

@ -87,6 +87,30 @@ void Flame::FileResolvingTask::executeTask()
m_task->start(); m_task->start();
} }
PackedResourceType getResourceType(int classId)
{
switch (classId) {
case 17: // Worlds
return PackedResourceType::WorldSave;
case 6: // Mods
return PackedResourceType::Mod;
case 12: // Resource Packs
// return PackedResourceType::ResourcePack; // not really a resourcepack
/* fallthrough */
case 4546: // Customization
// return PackedResourceType::ShaderPack; // not really a shaderPack
/* fallthrough */
case 4471: // Modpacks
/* fallthrough */
case 5: // Bukkit Plugins
/* fallthrough */
case 4559: // Addons
/* fallthrough */
default:
return PackedResourceType::UNKNOWN;
}
}
void Flame::FileResolvingTask::netJobFinished() void Flame::FileResolvingTask::netJobFinished()
{ {
setProgress(1, 3); setProgress(1, 3);
@ -144,7 +168,7 @@ void Flame::FileResolvingTask::netJobFinished()
<< " reason: " << parse_error.errorString(); << " reason: " << parse_error.errorString();
qWarning() << *m_result; qWarning() << *m_result;
failed(parse_error.errorString()); getFlameProjects();
return; return;
} }
@ -232,6 +256,10 @@ void Flame::FileResolvingTask::getFlameProjects()
setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(file->version.fileName)); setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(file->version.fileName));
FlameMod::loadIndexedPack(file->pack, entry_obj); FlameMod::loadIndexedPack(file->pack, entry_obj);
file->resourceType = getResourceType(Json::requireInteger(entry_obj, "classId", "modClassId"));
if (file->resourceType == PackedResourceType::WorldSave) {
file->targetFolder = "saves";
}
} }
} catch (Json::JsonException& e) { } catch (Json::JsonException& e) {
qDebug() << e.cause(); qDebug() << e.cause();

View File

@ -270,21 +270,44 @@ std::optional<ModPlatform::IndexedVersion> FlameAPI::getLatestVersion(QList<ModP
QList<ModPlatform::ModLoaderType> instanceLoaders, QList<ModPlatform::ModLoaderType> instanceLoaders,
ModPlatform::ModLoaderTypes modLoaders) ModPlatform::ModLoaderTypes modLoaders)
{ {
// edge case: mod has installed for forge but the instance is fabric => fabric version will be prioritizated on update static const auto noLoader = ModPlatform::ModLoaderType(0);
auto bestVersion = [&versions](ModPlatform::ModLoaderTypes loader) { QHash<ModPlatform::ModLoaderType, ModPlatform::IndexedVersion> bestMatch;
std::optional<ModPlatform::IndexedVersion> ver; auto checkVersion = [&bestMatch](const ModPlatform::IndexedVersion& version, const ModPlatform::ModLoaderType& loader) {
for (auto file_tmp : versions) { if (bestMatch.contains(loader)) {
if (file_tmp.loaders & loader && (!ver.has_value() || file_tmp.date > ver->date)) { auto best = bestMatch.value(loader);
ver = file_tmp; if (version.date > best.date) {
bestMatch[loader] = version;
} }
} else {
bestMatch[loader] = version;
} }
return ver;
}; };
for (auto l : instanceLoaders) { for (auto file_tmp : versions) {
auto ver = bestVersion(l); auto loaders = ModPlatform::modLoaderTypesToList(file_tmp.loaders);
if (ver.has_value()) { if (loaders.isEmpty()) {
return ver; checkVersion(file_tmp, noLoader);
} else {
for (auto loader : loaders) {
checkVersion(file_tmp, loader);
} }
} }
return bestVersion(modLoaders); }
// edge case: mod has installed for forge but the instance is fabric => fabric version will be prioritizated on update
auto currentLoaders = instanceLoaders + ModPlatform::modLoaderTypesToList(modLoaders);
currentLoaders.append(noLoader); // add a fallback in case the versions do not define a loader
for (auto loader : currentLoaders) {
if (bestMatch.contains(loader)) {
auto bestForLoader = bestMatch.value(loader);
// awkward case where the mod has only two loaders and one of them is not specified
if (loader != noLoader && bestMatch.contains(noLoader) && bestMatch.size() == 2) {
auto bestForNoLoader = bestMatch.value(noLoader);
if (bestForNoLoader.date > bestForLoader.date) {
return bestForNoLoader;
}
}
return bestForLoader;
}
}
return {};
} }

View File

@ -1,6 +1,5 @@
#pragma once #pragma once
#include "Application.h"
#include "modplatform/CheckUpdateTask.h" #include "modplatform/CheckUpdateTask.h"
#include "net/NetJob.h" #include "net/NetJob.h"

View File

@ -75,12 +75,12 @@ bool FlameCreationTask::abort()
return false; return false;
m_abort = true; m_abort = true;
if (m_process_update_file_info_job) if (m_processUpdateFileInfoJob)
m_process_update_file_info_job->abort(); m_processUpdateFileInfoJob->abort();
if (m_files_job) if (m_filesJob)
m_files_job->abort(); m_filesJob->abort();
if (m_mod_id_resolver) if (m_modIdResolver)
m_mod_id_resolver->abort(); m_modIdResolver->abort();
return Task::abort(); return Task::abort();
} }
@ -232,12 +232,12 @@ bool FlameCreationTask::updateInstance()
connect(job.get(), &Task::failed, this, [](QString reason) { qCritical() << "Failed to get files: " << reason; }); connect(job.get(), &Task::failed, this, [](QString reason) { qCritical() << "Failed to get files: " << reason; });
connect(job.get(), &Task::finished, &loop, &QEventLoop::quit); connect(job.get(), &Task::finished, &loop, &QEventLoop::quit);
m_process_update_file_info_job = job; m_processUpdateFileInfoJob = job;
job->start(); job->start();
loop.exec(); loop.exec();
m_process_update_file_info_job = nullptr; m_processUpdateFileInfoJob = nullptr;
} else { } else {
// We don't have an old index file, so we may duplicate stuff! // We don't have an old index file, so we may duplicate stuff!
auto dialog = CustomMessageBox::selectable(m_parent, tr("No index file."), auto dialog = CustomMessageBox::selectable(m_parent, tr("No index file."),
@ -430,26 +430,26 @@ bool FlameCreationTask::createInstance()
} }
// Don't add managed info to packs without an ID (most likely imported from ZIP) // Don't add managed info to packs without an ID (most likely imported from ZIP)
if (!m_managed_id.isEmpty()) if (!m_managedId.isEmpty())
instance.setManagedPack("flame", m_managed_id, m_pack.name, m_managed_version_id, m_pack.version); instance.setManagedPack("flame", m_managedId, m_pack.name, m_managedVersionId, m_pack.version);
else else
instance.setManagedPack("flame", "", name(), "", ""); instance.setManagedPack("flame", "", name(), "", "");
instance.setName(name()); instance.setName(name());
m_mod_id_resolver.reset(new Flame::FileResolvingTask(APPLICATION->network(), m_pack)); m_modIdResolver.reset(new Flame::FileResolvingTask(APPLICATION->network(), m_pack));
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::succeeded, this, [this, &loop] { idResolverSucceeded(loop); }); connect(m_modIdResolver.get(), &Flame::FileResolvingTask::succeeded, this, [this, &loop] { idResolverSucceeded(loop); });
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::failed, [this, &loop](QString reason) { connect(m_modIdResolver.get(), &Flame::FileResolvingTask::failed, [this, &loop](QString reason) {
m_mod_id_resolver.reset(); m_modIdResolver.reset();
setError(tr("Unable to resolve mod IDs:\n") + reason); setError(tr("Unable to resolve mod IDs:\n") + reason);
loop.quit(); loop.quit();
}); });
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::aborted, &loop, &QEventLoop::quit); connect(m_modIdResolver.get(), &Flame::FileResolvingTask::aborted, &loop, &QEventLoop::quit);
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::progress, this, &FlameCreationTask::setProgress); connect(m_modIdResolver.get(), &Flame::FileResolvingTask::progress, this, &FlameCreationTask::setProgress);
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::status, this, &FlameCreationTask::setStatus); connect(m_modIdResolver.get(), &Flame::FileResolvingTask::status, this, &FlameCreationTask::setStatus);
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::stepProgress, this, &FlameCreationTask::propagateStepProgress); connect(m_modIdResolver.get(), &Flame::FileResolvingTask::stepProgress, this, &FlameCreationTask::propagateStepProgress);
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::details, this, &FlameCreationTask::setDetails); connect(m_modIdResolver.get(), &Flame::FileResolvingTask::details, this, &FlameCreationTask::setDetails);
m_mod_id_resolver->start(); m_modIdResolver->start();
loop.exec(); loop.exec();
@ -468,14 +468,14 @@ bool FlameCreationTask::createInstance()
void FlameCreationTask::idResolverSucceeded(QEventLoop& loop) void FlameCreationTask::idResolverSucceeded(QEventLoop& loop)
{ {
auto results = m_mod_id_resolver->getResults(); auto results = m_modIdResolver->getResults();
// first check for blocked mods // first check for blocked mods
QList<BlockedMod> blocked_mods; QList<BlockedMod> blocked_mods;
auto anyBlocked = false; auto anyBlocked = false;
for (const auto& result : results.files.values()) { for (const auto& result : results.files.values()) {
if (result.version.fileName.endsWith(".zip")) { if (result.resourceType != PackedResourceType::Mod) {
m_ZIP_resources.append(std::make_pair(result.version.fileName, result.targetFolder)); m_otherResources.append(std::make_pair(result.version.fileName, result.targetFolder));
} }
if (result.version.downloadUrl.isEmpty()) { if (result.version.downloadUrl.isEmpty()) {
@ -507,7 +507,7 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop)
copyBlockedMods(blocked_mods); copyBlockedMods(blocked_mods);
setupDownloadJob(loop); setupDownloadJob(loop);
} else { } else {
m_mod_id_resolver.reset(); m_modIdResolver.reset();
setError("Canceled"); setError("Canceled");
loop.quit(); loop.quit();
} }
@ -518,8 +518,8 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop)
void FlameCreationTask::setupDownloadJob(QEventLoop& loop) void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
{ {
m_files_job.reset(new NetJob(tr("Mod Download Flame"), APPLICATION->network())); m_filesJob.reset(new NetJob(tr("Mod Download Flame"), APPLICATION->network()));
auto results = m_mod_id_resolver->getResults().files; auto results = m_modIdResolver->getResults().files;
QStringList optionalFiles; QStringList optionalFiles;
for (auto& result : results) { for (auto& result : results) {
@ -554,26 +554,26 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
if (!result.version.downloadUrl.isEmpty()) { if (!result.version.downloadUrl.isEmpty()) {
qDebug() << "Will download" << result.version.downloadUrl << "to" << path; qDebug() << "Will download" << result.version.downloadUrl << "to" << path;
auto dl = Net::ApiDownload::makeFile(result.version.downloadUrl, path); auto dl = Net::ApiDownload::makeFile(result.version.downloadUrl, path);
m_files_job->addNetAction(dl); m_filesJob->addNetAction(dl);
} }
} }
connect(m_files_job.get(), &NetJob::finished, this, [this, &loop]() { connect(m_filesJob.get(), &NetJob::finished, this, [this, &loop]() {
m_files_job.reset(); m_filesJob.reset();
validateZIPResources(loop); validateOtherResources(loop);
}); });
connect(m_files_job.get(), &NetJob::failed, [this](QString reason) { connect(m_filesJob.get(), &NetJob::failed, [this](QString reason) {
m_files_job.reset(); m_filesJob.reset();
setError(reason); setError(reason);
}); });
connect(m_files_job.get(), &NetJob::progress, this, [this](qint64 current, qint64 total) { connect(m_filesJob.get(), &NetJob::progress, this, [this](qint64 current, qint64 total) {
setDetails(tr("%1 out of %2 complete").arg(current).arg(total)); setDetails(tr("%1 out of %2 complete").arg(current).arg(total));
setProgress(current, total); setProgress(current, total);
}); });
connect(m_files_job.get(), &NetJob::stepProgress, this, &FlameCreationTask::propagateStepProgress); connect(m_filesJob.get(), &NetJob::stepProgress, this, &FlameCreationTask::propagateStepProgress);
setStatus(tr("Downloading mods...")); setStatus(tr("Downloading mods..."));
m_files_job->start(); m_filesJob->start();
} }
/// @brief copy the matched blocked mods to the instance staging area /// @brief copy the matched blocked mods to the instance staging area
@ -597,9 +597,15 @@ void FlameCreationTask::copyBlockedMods(QList<BlockedMod> const& blocked_mods)
qDebug() << "Will try to copy" << mod.localPath << "to" << destPath; qDebug() << "Will try to copy" << mod.localPath << "to" << destPath;
if (mod.move) {
if (!FS::move(mod.localPath, destPath)) {
qDebug() << "Move of" << mod.localPath << "to" << destPath << "Failed";
}
} else {
if (!FS::copy(mod.localPath, destPath)()) { if (!FS::copy(mod.localPath, destPath)()) {
qDebug() << "Copy of" << mod.localPath << "to" << destPath << "Failed"; qDebug() << "Copy of" << mod.localPath << "to" << destPath << "Failed";
} }
}
i++; i++;
setProgress(i, total); setProgress(i, total);
@ -608,11 +614,11 @@ void FlameCreationTask::copyBlockedMods(QList<BlockedMod> const& blocked_mods)
setAbortable(true); setAbortable(true);
} }
void FlameCreationTask::validateZIPResources(QEventLoop& loop) void FlameCreationTask::validateOtherResources(QEventLoop& loop)
{ {
qDebug() << "Validating whether resources stored as .zip are in the right place"; qDebug() << "Validating whether other resources are in the right place";
QStringList zipMods; QStringList zipMods;
for (auto [fileName, targetFolder] : m_ZIP_resources) { for (auto [fileName, targetFolder] : m_otherResources) {
qDebug() << "Checking" << fileName << "..."; qDebug() << "Checking" << fileName << "...";
auto localPath = FS::PathCombine(m_stagingPath, "minecraft", targetFolder, fileName); auto localPath = FS::PathCombine(m_stagingPath, "minecraft", targetFolder, fileName);
@ -672,6 +678,7 @@ void FlameCreationTask::validateZIPResources(QEventLoop& loop)
installWorld(worldPath); installWorld(worldPath);
break; break;
case PackedResourceType::UNKNOWN: case PackedResourceType::UNKNOWN:
/* fallthrough */
default: default:
qDebug() << "Can't Identify" << fileName << "at" << localPath << ", leaving it where it is."; qDebug() << "Can't Identify" << fileName << "at" << localPath << ", leaving it where it is.";
break; break;
@ -679,7 +686,7 @@ void FlameCreationTask::validateZIPResources(QEventLoop& loop)
} }
// TODO make this work with other sorts of resource // TODO make this work with other sorts of resource
auto task = makeShared<ConcurrentTask>("CreateModMetadata", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()); auto task = makeShared<ConcurrentTask>("CreateModMetadata", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt());
auto results = m_mod_id_resolver->getResults().files; auto results = m_modIdResolver->getResults().files;
auto folder = FS::PathCombine(m_stagingPath, "minecraft", "mods", ".index"); auto folder = FS::PathCombine(m_stagingPath, "minecraft", "mods", ".index");
for (auto file : results) { for (auto file : results) {
if (file.targetFolder != "mods" || (file.version.fileName.endsWith(".zip") && !zipMods.contains(file.version.fileName))) { if (file.targetFolder != "mods" || (file.version.fileName.endsWith(".zip") && !zipMods.contains(file.version.fileName))) {
@ -688,6 +695,6 @@ void FlameCreationTask::validateZIPResources(QEventLoop& loop)
task->addTask(makeShared<LocalResourceUpdateTask>(folder, file.pack, file.version)); task->addTask(makeShared<LocalResourceUpdateTask>(folder, file.pack, file.version));
} }
connect(task.get(), &Task::finished, &loop, &QEventLoop::quit); connect(task.get(), &Task::finished, &loop, &QEventLoop::quit);
m_process_update_file_info_job = task; m_processUpdateFileInfoJob = task;
task->start(); task->start();
} }

View File

@ -57,7 +57,7 @@ class FlameCreationTask final : public InstanceCreationTask {
QString id, QString id,
QString version_id, QString version_id,
QString original_instance_id = {}) QString original_instance_id = {})
: InstanceCreationTask(), m_parent(parent), m_managed_id(std::move(id)), m_managed_version_id(std::move(version_id)) : InstanceCreationTask(), m_parent(parent), m_managedId(std::move(id)), m_managedVersionId(std::move(version_id))
{ {
setStagingPath(staging_path); setStagingPath(staging_path);
setParentSettings(global_settings); setParentSettings(global_settings);
@ -74,22 +74,22 @@ class FlameCreationTask final : public InstanceCreationTask {
void idResolverSucceeded(QEventLoop&); void idResolverSucceeded(QEventLoop&);
void setupDownloadJob(QEventLoop&); void setupDownloadJob(QEventLoop&);
void copyBlockedMods(QList<BlockedMod> const& blocked_mods); void copyBlockedMods(QList<BlockedMod> const& blocked_mods);
void validateZIPResources(QEventLoop& loop); void validateOtherResources(QEventLoop& loop);
QString getVersionForLoader(QString uid, QString loaderType, QString version, QString mcVersion); QString getVersionForLoader(QString uid, QString loaderType, QString version, QString mcVersion);
private: private:
QWidget* m_parent = nullptr; QWidget* m_parent = nullptr;
shared_qobject_ptr<Flame::FileResolvingTask> m_mod_id_resolver; shared_qobject_ptr<Flame::FileResolvingTask> m_modIdResolver;
Flame::Manifest m_pack; Flame::Manifest m_pack;
// Handle to allow aborting // Handle to allow aborting
Task::Ptr m_process_update_file_info_job = nullptr; Task::Ptr m_processUpdateFileInfoJob = nullptr;
NetJob::Ptr m_files_job = nullptr; NetJob::Ptr m_filesJob = nullptr;
QString m_managed_id, m_managed_version_id; QString m_managedId, m_managedVersionId;
QList<std::pair<QString, QString>> m_ZIP_resources; QList<std::pair<QString, QString>> m_otherResources;
std::optional<InstancePtr> m_instance; std::optional<InstancePtr> m_instance;
}; };

View File

@ -40,6 +40,7 @@
#include <QString> #include <QString>
#include <QUrl> #include <QUrl>
#include <QVector> #include <QVector>
#include "minecraft/mod/tasks/LocalResourceParse.h"
#include "modplatform/ModIndex.h" #include "modplatform/ModIndex.h"
namespace Flame { namespace Flame {
@ -54,6 +55,7 @@ struct File {
// our // our
QString targetFolder = QStringLiteral("mods"); QString targetFolder = QStringLiteral("mods");
PackedResourceType resourceType;
}; };
struct Modloader { struct Modloader {

View File

@ -71,13 +71,15 @@ class ModrinthAPI : public NetworkResourceAPI {
static auto getSideFilters(QString side) -> const QString static auto getSideFilters(QString side) -> const QString
{ {
if (side.isEmpty() || side == "both") { if (side.isEmpty()) {
return {}; return {};
} }
if (side == "both")
return QString("\"client_side:required\"],[\"server_side:required\"");
if (side == "client") if (side == "client")
return QString("\"client_side:required\",\"client_side:optional\""); return QString("\"client_side:required\",\"client_side:optional\"],[\"server_side:optional\",\"server_side:unsupported\"");
if (side == "server") if (side == "server")
return QString("\"server_side:required\",\"server_side:optional\""); return QString("\"server_side:required\",\"server_side:optional\"],[\"client_side:optional\",\"client_side:unsupported\"");
return {}; return {};
} }

View File

@ -8,6 +8,7 @@
#include "QObjectPtr.h" #include "QObjectPtr.h"
#include "ResourceDownloadTask.h" #include "ResourceDownloadTask.h"
#include "modplatform/ModIndex.h"
#include "modplatform/helpers/HashUtils.h" #include "modplatform/helpers/HashUtils.h"
#include "tasks/ConcurrentTask.h" #include "tasks/ConcurrentTask.h"
@ -107,10 +108,8 @@ void ModrinthCheckUpdate::checkVersionsResponse(std::shared_ptr<QByteArray> resp
// Sometimes a version may have multiple files, one with "forge" and one with "fabric", // Sometimes a version may have multiple files, one with "forge" and one with "fabric",
// so we may want to filter it // so we may want to filter it
QString loader_filter; QString loader_filter;
static auto flags = { ModPlatform::ModLoaderType::NeoForge, ModPlatform::ModLoaderType::Forge, if (loader.has_value()) {
ModPlatform::ModLoaderType::Quilt, ModPlatform::ModLoaderType::Fabric }; for (auto flag : ModPlatform::modLoaderTypesToList(*loader)) {
for (auto flag : flags) {
if (loader.has_value() && loader->testFlag(flag)) {
loader_filter = ModPlatform::getModLoaderAsString(flag); loader_filter = ModPlatform::getModLoaderAsString(flag);
break; break;
} }

View File

@ -262,12 +262,14 @@ bool ModrinthCreationTask::createInstance()
mod->setDetails(d); mod->setDetails(d);
resources[file.hash.toHex()] = mod; resources[file.hash.toHex()] = mod;
} }
if (file.downloads.empty()) {
setError(tr("The file '%1' is missing a download link. This is invalid in the pack format.").arg(fileName));
return false;
}
qDebug() << "Will try to download" << file.downloads.front() << "to" << file_path; qDebug() << "Will try to download" << file.downloads.front() << "to" << file_path;
auto dl = Net::ApiDownload::makeFile(file.downloads.dequeue(), file_path); auto dl = Net::ApiDownload::makeFile(file.downloads.dequeue(), file_path);
dl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash)); dl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash));
downloadMods->addNetAction(dl); downloadMods->addNetAction(dl);
if (!file.downloads.empty()) { if (!file.downloads.empty()) {
// FIXME: This really needs to be put into a ConcurrentTask of // FIXME: This really needs to be put into a ConcurrentTask of
// MultipleOptionsTask's , once those exist :) // MultipleOptionsTask's , once those exist :)

View File

@ -190,12 +190,9 @@ void V1::updateModIndex(const QDir& index_dir, Mod& mod)
} }
toml::array loaders; toml::array loaders;
for (auto loader : { ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Cauldron, ModPlatform::LiteLoader, ModPlatform::Fabric, for (auto loader : ModPlatform::modLoaderTypesToList(mod.loaders)) {
ModPlatform::Quilt }) {
if (mod.loaders & loader) {
loaders.push_back(getModLoaderAsString(loader).toStdString()); loaders.push_back(getModLoaderAsString(loader).toStdString());
} }
}
toml::array mcVersions; toml::array mcVersions;
for (auto version : mod.mcVersions) { for (auto version : mod.mcVersions) {
mcVersions.push_back(version.toStdString()); mcVersions.push_back(version.toStdString());

View File

@ -1,7 +1,6 @@
[Icon Theme] [Icon Theme]
Name=Legacy Name=Legacy
Comment=Default Icons Comment=Default Icons
Inherits=default
Directories=8x8,16x16,22x22,24x24,32x32,32x32/instances,48x48,50x50/instances,64x64,128x128/instances,256x256,scalable,scalable/instances Directories=8x8,16x16,22x22,24x24,32x32,32x32/instances,48x48,50x50/instances,64x64,128x128/instances,256x256,scalable,scalable/instances
[8x8] [8x8]

View File

@ -51,11 +51,35 @@
#include <settings/SettingsObject.h> #include <settings/SettingsObject.h>
#include "Application.h" #include "Application.h"
constexpr int MaxMclogsLines = 25000;
constexpr int InitialMclogsLines = 10000;
constexpr int FinalMclogsLines = 14900;
QString truncateLogForMclogs(const QString& logContent)
{
QStringList lines = logContent.split("\n");
if (lines.size() > MaxMclogsLines) {
QString truncatedLog = lines.mid(0, InitialMclogsLines).join("\n");
truncatedLog +=
"\n\n\n\n\n\n\n\n\n\n"
"------------------------------------------------------------\n"
"----------------------- Log truncated ----------------------\n"
"------------------------------------------------------------\n"
"----- Middle portion omitted to fit mclo.gs size limits ----\n"
"------------------------------------------------------------\n"
"\n\n\n\n\n\n\n\n\n\n";
truncatedLog += lines.mid(lines.size() - FinalMclogsLines - 1).join("\n");
return truncatedLog;
}
return logContent;
}
std::optional<QString> GuiUtil::uploadPaste(const QString& name, const QString& text, QWidget* parentWidget) std::optional<QString> GuiUtil::uploadPaste(const QString& name, const QString& text, QWidget* parentWidget)
{ {
ProgressDialog dialog(parentWidget); ProgressDialog dialog(parentWidget);
auto pasteTypeSetting = static_cast<PasteUpload::PasteType>(APPLICATION->settings()->get("PastebinType").toInt()); auto pasteTypeSetting = static_cast<PasteUpload::PasteType>(APPLICATION->settings()->get("PastebinType").toInt());
auto pasteCustomAPIBaseSetting = APPLICATION->settings()->get("PastebinCustomAPIBase").toString(); auto pasteCustomAPIBaseSetting = APPLICATION->settings()->get("PastebinCustomAPIBase").toString();
bool shouldTruncate = false;
{ {
QUrl baseUrl; QUrl baseUrl;
@ -75,10 +99,36 @@ std::optional<QString> GuiUtil::uploadPaste(const QString& name, const QString&
if (response != QMessageBox::Yes) if (response != QMessageBox::Yes)
return {}; return {};
if (baseUrl.toString() == "https://api.mclo.gs" && text.count("\n") > MaxMclogsLines) {
auto truncateResponse = CustomMessageBox::selectable(
parentWidget, QObject::tr("Confirm Truncation"),
QObject::tr("The log has %1 lines, exceeding mclo.gs' limit of %2.\n"
"The launcher can keep the first %3 and last %4 lines, trimming the middle.\n\n"
"If you choose 'No', mclo.gs will only keep the first %2 lines, cutting off "
"potentially useful info like crashes at the end.\n\n"
"Proceed with truncation?")
.arg(text.count("\n"))
.arg(MaxMclogsLines)
.arg(InitialMclogsLines)
.arg(FinalMclogsLines),
QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::No)
->exec();
if (truncateResponse == QMessageBox::Cancel) {
return {};
}
shouldTruncate = truncateResponse == QMessageBox::Yes;
}
} }
} }
std::unique_ptr<PasteUpload> paste(new PasteUpload(parentWidget, text, pasteCustomAPIBaseSetting, pasteTypeSetting)); QString textToUpload = text;
if (shouldTruncate) {
textToUpload = truncateLogForMclogs(text);
}
std::unique_ptr<PasteUpload> paste(new PasteUpload(parentWidget, textToUpload, pasteCustomAPIBaseSetting, pasteTypeSetting));
dialog.execWithTask(paste.get()); dialog.execWithTask(paste.get());
if (!paste->wasSuccessful()) { if (!paste->wasSuccessful()) {

View File

@ -288,6 +288,8 @@ void BlockedModsDialog::checkMatchHash(QString hash, QString path)
qDebug() << "[Blocked Mods Dialog] Checking for match on hash: " << hash << "| From path:" << path; qDebug() << "[Blocked Mods Dialog] Checking for match on hash: " << hash << "| From path:" << path;
auto downloadDir = QFileInfo(APPLICATION->settings()->get("DownloadsDir").toString()).absoluteFilePath();
auto moveFiles = APPLICATION->settings()->get("MoveModsFromDownloadsDir").toBool();
for (auto& mod : m_mods) { for (auto& mod : m_mods) {
if (mod.matched) { if (mod.matched) {
continue; continue;
@ -295,6 +297,9 @@ void BlockedModsDialog::checkMatchHash(QString hash, QString path)
if (mod.hash.compare(hash, Qt::CaseInsensitive) == 0) { if (mod.hash.compare(hash, Qt::CaseInsensitive) == 0) {
mod.matched = true; mod.matched = true;
mod.localPath = path; mod.localPath = path;
if (moveFiles) {
mod.move = QFileInfo(path).absoluteFilePath().startsWith(downloadDir);
}
match = true; match = true;
qDebug() << "[Blocked Mods Dialog] Hash match found:" << mod.name << hash << "| From path:" << path; qDebug() << "[Blocked Mods Dialog] Hash match found:" << mod.name << hash << "| From path:" << path;
@ -346,6 +351,8 @@ bool BlockedModsDialog::checkValidPath(QString path)
return fsName.compare(metaName) == 0; return fsName.compare(metaName) == 0;
}; };
auto downloadDir = QFileInfo(APPLICATION->settings()->get("DownloadsDir").toString()).absoluteFilePath();
auto moveFiles = APPLICATION->settings()->get("MoveModsFromDownloadsDir").toBool();
for (auto& mod : m_mods) { for (auto& mod : m_mods) {
if (compare(filename, mod.name)) { if (compare(filename, mod.name)) {
// if the mod is not yet matched and doesn't have a hash then // if the mod is not yet matched and doesn't have a hash then
@ -353,6 +360,9 @@ bool BlockedModsDialog::checkValidPath(QString path)
if (!mod.matched && mod.hash.isEmpty()) { if (!mod.matched && mod.hash.isEmpty()) {
mod.matched = true; mod.matched = true;
mod.localPath = path; mod.localPath = path;
if (moveFiles) {
mod.move = QFileInfo(path).absoluteFilePath().startsWith(downloadDir);
}
return false; return false;
} }
qDebug() << "[Blocked Mods Dialog] Name match found:" << mod.name << "| From path:" << path; qDebug() << "[Blocked Mods Dialog] Name match found:" << mod.name << "| From path:" << path;

View File

@ -42,6 +42,7 @@ struct BlockedMod {
bool matched; bool matched;
QString localPath; QString localPath;
QString targetFolder; QString targetFolder;
bool move = false;
}; };
QT_BEGIN_NAMESPACE QT_BEGIN_NAMESPACE

View File

@ -1,64 +0,0 @@
/* Copyright 2013-2021 MultiMC Contributors
*
* 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 "EditAccountDialog.h"
#include <DesktopServices.h>
#include <QPushButton>
#include <QUrl>
#include "ui_EditAccountDialog.h"
EditAccountDialog::EditAccountDialog(const QString& text, QWidget* parent, int flags) : QDialog(parent), ui(new Ui::EditAccountDialog)
{
ui->setupUi(this);
ui->label->setText(text);
ui->label->setVisible(!text.isEmpty());
ui->userTextBox->setEnabled(flags & UsernameField);
ui->passTextBox->setEnabled(flags & PasswordField);
ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel"));
ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK"));
}
EditAccountDialog::~EditAccountDialog()
{
delete ui;
}
void EditAccountDialog::on_label_linkActivated(const QString& link)
{
DesktopServices::openUrl(QUrl(link));
}
void EditAccountDialog::setUsername(const QString& user) const
{
ui->userTextBox->setText(user);
}
QString EditAccountDialog::username() const
{
return ui->userTextBox->text();
}
void EditAccountDialog::setPassword(const QString& pass) const
{
ui->passTextBox->setText(pass);
}
QString EditAccountDialog::password() const
{
return ui->passTextBox->text();
}

View File

@ -1,52 +0,0 @@
/* Copyright 2013-2021 MultiMC Contributors
*
* 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 <QDialog>
namespace Ui {
class EditAccountDialog;
}
class EditAccountDialog : public QDialog {
Q_OBJECT
public:
explicit EditAccountDialog(const QString& text = "", QWidget* parent = 0, int flags = UsernameField | PasswordField);
~EditAccountDialog();
void setUsername(const QString& user) const;
void setPassword(const QString& pass) const;
QString username() const;
QString password() const;
enum Flags {
NoFlags = 0,
//! Specifies that the dialog should have a username field.
UsernameField,
//! Specifies that the dialog should have a password field.
PasswordField,
};
private slots:
void on_label_linkActivated(const QString& link);
private:
Ui::EditAccountDialog* ui;
};

View File

@ -1,94 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>EditAccountDialog</class>
<widget class="QDialog" name="EditAccountDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>148</height>
</rect>
</property>
<property name="windowTitle">
<string>Login</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string notr="true">Message label placeholder.</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="userTextBox">
<property name="placeholderText">
<string>Email</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="passTextBox">
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
<property name="placeholderText">
<string>Password</string>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>EditAccountDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>EditAccountDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -15,7 +15,9 @@
#include <QFileDialog> #include <QFileDialog>
#include <QKeyEvent> #include <QKeyEvent>
#include <QLineEdit>
#include <QPushButton> #include <QPushButton>
#include <QSortFilterProxyModel>
#include "Application.h" #include "Application.h"
@ -33,6 +35,15 @@ IconPickerDialog::IconPickerDialog(QWidget* parent) : QDialog(parent), ui(new Ui
ui->setupUi(this); ui->setupUi(this);
setWindowModality(Qt::WindowModal); setWindowModality(Qt::WindowModal);
searchBar = new QLineEdit(this);
searchBar->setPlaceholderText(tr("Search..."));
ui->verticalLayout->insertWidget(0, searchBar);
proxyModel = new QSortFilterProxyModel(this);
proxyModel->setSourceModel(APPLICATION->icons().get());
proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
ui->iconView->setModel(proxyModel);
auto contentsWidget = ui->iconView; auto contentsWidget = ui->iconView;
contentsWidget->setViewMode(QListView::IconMode); contentsWidget->setViewMode(QListView::IconMode);
contentsWidget->setFlow(QListView::LeftToRight); contentsWidget->setFlow(QListView::LeftToRight);
@ -57,7 +68,7 @@ IconPickerDialog::IconPickerDialog(QWidget* parent) : QDialog(parent), ui(new Ui
contentsWidget->installEventFilter(this); contentsWidget->installEventFilter(this);
contentsWidget->setModel(APPLICATION->icons().get()); contentsWidget->setModel(proxyModel);
// NOTE: ResetRole forces the button to be on the left, while the OK/Cancel ones are on the right. We win. // NOTE: ResetRole forces the button to be on the left, while the OK/Cancel ones are on the right. We win.
auto buttonAdd = ui->buttonBox->addButton(tr("Add Icon"), QDialogButtonBox::ResetRole); auto buttonAdd = ui->buttonBox->addButton(tr("Add Icon"), QDialogButtonBox::ResetRole);
@ -76,6 +87,9 @@ IconPickerDialog::IconPickerDialog(QWidget* parent) : QDialog(parent), ui(new Ui
auto buttonFolder = ui->buttonBox->addButton(tr("Open Folder"), QDialogButtonBox::ResetRole); auto buttonFolder = ui->buttonBox->addButton(tr("Open Folder"), QDialogButtonBox::ResetRole);
connect(buttonFolder, &QPushButton::clicked, this, &IconPickerDialog::openFolder); connect(buttonFolder, &QPushButton::clicked, this, &IconPickerDialog::openFolder);
connect(searchBar, &QLineEdit::textChanged, this, &IconPickerDialog::filterIcons);
// Prevent incorrect indices from e.g. filesystem changes
connect(APPLICATION->icons().get(), &IconList::iconUpdated, this, [this]() { proxyModel->invalidate(); });
} }
bool IconPickerDialog::eventFilter(QObject* obj, QEvent* evt) bool IconPickerDialog::eventFilter(QObject* obj, QEvent* evt)
@ -162,5 +176,10 @@ IconPickerDialog::~IconPickerDialog()
void IconPickerDialog::openFolder() void IconPickerDialog::openFolder()
{ {
DesktopServices::openPath(APPLICATION->icons()->getDirectory(), true); DesktopServices::openPath(APPLICATION->icons()->iconDirectory(selectedIconKey), true);
}
void IconPickerDialog::filterIcons(const QString& query)
{
proxyModel->setFilterFixedString(query);
} }

View File

@ -16,6 +16,8 @@
#pragma once #pragma once
#include <QDialog> #include <QDialog>
#include <QItemSelection> #include <QItemSelection>
#include <QLineEdit>
#include <QSortFilterProxyModel>
namespace Ui { namespace Ui {
class IconPickerDialog; class IconPickerDialog;
@ -36,6 +38,8 @@ class IconPickerDialog : public QDialog {
private: private:
Ui::IconPickerDialog* ui; Ui::IconPickerDialog* ui;
QPushButton* buttonRemove; QPushButton* buttonRemove;
QLineEdit* searchBar;
QSortFilterProxyModel* proxyModel;
private slots: private slots:
void selectionChanged(QItemSelection, QItemSelection); void selectionChanged(QItemSelection, QItemSelection);
@ -44,4 +48,5 @@ class IconPickerDialog : public QDialog {
void addNewIcon(); void addNewIcon();
void removeSelectedIcon(); void removeSelectedIcon();
void openFolder(); void openFolder();
void filterIcons(const QString& text);
}; };

View File

@ -30,6 +30,9 @@ Choose your name carefully:</string>
<property name="wordWrap"> <property name="wordWrap">
<bool>true</bool> <bool>true</bool>
</property> </property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
<property name="buddy"> <property name="buddy">
<cstring>nameEdit</cstring> <cstring>nameEdit</cstring>
</property> </property>

View File

@ -1,4 +1,5 @@
#include "ResourceUpdateDialog.h" #include "ResourceUpdateDialog.h"
#include "Application.h"
#include "ChooseProviderDialog.h" #include "ChooseProviderDialog.h"
#include "CustomMessageBox.h" #include "CustomMessageBox.h"
#include "ProgressDialog.h" #include "ProgressDialog.h"
@ -7,6 +8,7 @@
#include "minecraft/mod/tasks/GetModDependenciesTask.h" #include "minecraft/mod/tasks/GetModDependenciesTask.h"
#include "modplatform/ModIndex.h" #include "modplatform/ModIndex.h"
#include "modplatform/flame/FlameAPI.h" #include "modplatform/flame/FlameAPI.h"
#include "tasks/SequentialTask.h"
#include "ui_ReviewMessageBox.h" #include "ui_ReviewMessageBox.h"
#include "Markdown.h" #include "Markdown.h"
@ -411,8 +413,14 @@ void ResourceUpdateDialog::onMetadataFailed(Resource* resource, bool try_others,
connect(task.get(), &EnsureMetadataTask::metadataFailed, [this](Resource* candidate) { onMetadataFailed(candidate, false); }); connect(task.get(), &EnsureMetadataTask::metadataFailed, [this](Resource* candidate) { onMetadataFailed(candidate, false); });
connect(task.get(), &EnsureMetadataTask::failed, connect(task.get(), &EnsureMetadataTask::failed,
[this](const QString& reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); [this](const QString& reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); });
if (task->getHashingTask()) {
auto seq = makeShared<SequentialTask>();
seq->addTask(task->getHashingTask());
seq->addTask(task);
m_second_try_metadata->addTask(seq);
} else {
m_second_try_metadata->addTask(task); m_second_try_metadata->addTask(task);
}
} else { } else {
QString reason{ tr("Couldn't find a valid version on the selected mod provider(s)") }; QString reason{ tr("Couldn't find a valid version on the selected mod provider(s)") };

View File

@ -207,7 +207,7 @@
<item row="0" column="0"> <item row="0" column="0">
<widget class="QLabel" name="label_8"> <widget class="QLabel" name="label_8">
<property name="text"> <property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Note: you only need to set this to access private data. Read the &lt;a href=&quot;https://docs.modrinth.com/#section/Authentication&quot;&gt;documentation&lt;/a&gt; for more information.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string> <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Note: you only need to set this to access private data. Read the &lt;a href=&quot;https://docs.modrinth.com/api/#authentication&quot;&gt;documentation&lt;/a&gt; for more information.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property> </property>
<property name="openExternalLinks"> <property name="openExternalLinks">
<bool>true</bool> <bool>true</bool>

View File

@ -232,6 +232,7 @@ void LauncherPage::applySettings()
s->set("SkinsDir", ui->skinsDirTextBox->text()); s->set("SkinsDir", ui->skinsDirTextBox->text());
s->set("JavaDir", ui->javaDirTextBox->text()); s->set("JavaDir", ui->javaDirTextBox->text());
s->set("DownloadsDirWatchRecursive", ui->downloadsDirWatchRecursiveCheckBox->isChecked()); s->set("DownloadsDirWatchRecursive", ui->downloadsDirWatchRecursiveCheckBox->isChecked());
s->set("MoveModsFromDownloadsDir", ui->downloadsDirMoveCheckBox->isChecked());
auto sortMode = (InstSortMode)ui->sortingModeGroup->checkedId(); auto sortMode = (InstSortMode)ui->sortingModeGroup->checkedId();
switch (sortMode) { switch (sortMode) {
@ -296,6 +297,7 @@ void LauncherPage::loadSettings()
ui->skinsDirTextBox->setText(s->get("SkinsDir").toString()); ui->skinsDirTextBox->setText(s->get("SkinsDir").toString());
ui->javaDirTextBox->setText(s->get("JavaDir").toString()); ui->javaDirTextBox->setText(s->get("JavaDir").toString());
ui->downloadsDirWatchRecursiveCheckBox->setChecked(s->get("DownloadsDirWatchRecursive").toBool()); ui->downloadsDirWatchRecursiveCheckBox->setChecked(s->get("DownloadsDirWatchRecursive").toBool());
ui->downloadsDirMoveCheckBox->setChecked(s->get("MoveModsFromDownloadsDir").toBool());
QString sortMode = s->get("InstSortMode").toString(); QString sortMode = s->get("InstSortMode").toString();

View File

@ -6,7 +6,7 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>511</width> <width>562</width>
<height>726</height> <height>726</height>
</rect> </rect>
</property> </property>
@ -38,7 +38,7 @@
<enum>QTabWidget::Rounded</enum> <enum>QTabWidget::Rounded</enum>
</property> </property>
<property name="currentIndex"> <property name="currentIndex">
<number>0</number> <number>2</number>
</property> </property>
<widget class="QWidget" name="featuresTab"> <widget class="QWidget" name="featuresTab">
<attribute name="title"> <attribute name="title">
@ -48,7 +48,7 @@
<item> <item>
<widget class="QScrollArea" name="scrollArea"> <widget class="QScrollArea" name="scrollArea">
<property name="horizontalScrollBarPolicy"> <property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum> <enum>Qt::ScrollBarAsNeeded</enum>
</property> </property>
<property name="widgetResizable"> <property name="widgetResizable">
<bool>true</bool> <bool>true</bool>
@ -58,8 +58,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>473</width> <width>570</width>
<height>690</height> <height>692</height>
</rect> </rect>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_8"> <layout class="QVBoxLayout" name="verticalLayout_8">
@ -156,6 +156,8 @@
</widget> </widget>
</item> </item>
<item row="9" column="1" colspan="2"> <item row="9" column="1" colspan="2">
<layout class="QHBoxLayout" name="downloadModsCheckLayout">
<item>
<widget class="QCheckBox" name="downloadsDirWatchRecursiveCheckBox"> <widget class="QCheckBox" name="downloadsDirWatchRecursiveCheckBox">
<property name="toolTip"> <property name="toolTip">
<string>When enabled, in addition to the downloads folder, its sub folders will also be searched when looking for resources (e.g. when looking for blocked mods on CurseForge).</string> <string>When enabled, in addition to the downloads folder, its sub folders will also be searched when looking for resources (e.g. when looking for blocked mods on CurseForge).</string>
@ -165,6 +167,18 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="QCheckBox" name="downloadsDirMoveCheckBox">
<property name="toolTip">
<string>When enabled, it will move blocked resources instead of copying them.</string>
</property>
<property name="text">
<string>Move blocked resources</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="8" column="1"> <item row="8" column="1">
<widget class="QLineEdit" name="downloadsDirTextBox"/> <widget class="QLineEdit" name="downloadsDirTextBox"/>
</item> </item>
@ -585,7 +599,7 @@
</sizepolicy> </sizepolicy>
</property> </property>
<property name="horizontalScrollBarPolicy"> <property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum> <enum>Qt::ScrollBarAsNeeded</enum>
</property> </property>
<property name="undoRedoEnabled"> <property name="undoRedoEnabled">
<bool>false</bool> <bool>false</bool>
@ -637,15 +651,33 @@
</customwidgets> </customwidgets>
<tabstops> <tabstops>
<tabstop>tabWidget</tabstop> <tabstop>tabWidget</tabstop>
<tabstop>scrollArea</tabstop>
<tabstop>autoUpdateCheckBox</tabstop> <tabstop>autoUpdateCheckBox</tabstop>
<tabstop>updateIntervalSpinBox</tabstop>
<tabstop>instDirTextBox</tabstop> <tabstop>instDirTextBox</tabstop>
<tabstop>instDirBrowseBtn</tabstop> <tabstop>instDirBrowseBtn</tabstop>
<tabstop>modsDirTextBox</tabstop> <tabstop>modsDirTextBox</tabstop>
<tabstop>modsDirBrowseBtn</tabstop> <tabstop>modsDirBrowseBtn</tabstop>
<tabstop>iconsDirTextBox</tabstop> <tabstop>iconsDirTextBox</tabstop>
<tabstop>iconsDirBrowseBtn</tabstop> <tabstop>iconsDirBrowseBtn</tabstop>
<tabstop>javaDirTextBox</tabstop>
<tabstop>javaDirBrowseBtn</tabstop>
<tabstop>skinsDirTextBox</tabstop>
<tabstop>skinsDirBrowseBtn</tabstop>
<tabstop>downloadsDirTextBox</tabstop>
<tabstop>downloadsDirBrowseBtn</tabstop>
<tabstop>downloadsDirWatchRecursiveCheckBox</tabstop>
<tabstop>metadataDisableBtn</tabstop>
<tabstop>dependenciesDisableBtn</tabstop>
<tabstop>skipModpackUpdatePromptBtn</tabstop>
<tabstop>numberOfConcurrentTasksSpinBox</tabstop>
<tabstop>numberOfConcurrentDownloadsSpinBox</tabstop>
<tabstop>numberOfManualRetriesSpinBox</tabstop>
<tabstop>timeoutSecondsSpinBox</tabstop>
<tabstop>sortLastLaunchedBtn</tabstop> <tabstop>sortLastLaunchedBtn</tabstop>
<tabstop>sortByNameBtn</tabstop> <tabstop>sortByNameBtn</tabstop>
<tabstop>catOpacitySpinBox</tabstop>
<tabstop>preferMenuBarCheckBox</tabstop>
<tabstop>lineLimitSpinBox</tabstop> <tabstop>lineLimitSpinBox</tabstop>
<tabstop>checkStopLogging</tabstop> <tabstop>checkStopLogging</tabstop>
<tabstop>consoleFont</tabstop> <tabstop>consoleFont</tabstop>

View File

@ -60,7 +60,7 @@
<bool>true</bool> <bool>true</bool>
</property> </property>
<property name="dragDropMode"> <property name="dragDropMode">
<enum>QAbstractItemView::NoDragDrop</enum> <enum>QAbstractItemView::DropOnly</enum>
</property> </property>
<property name="uniformRowHeights"> <property name="uniformRowHeights">
<bool>true</bool> <bool>true</bool>

View File

@ -252,8 +252,11 @@ void VersionPage::updateButtons(int row)
bool VersionPage::reloadPackProfile() bool VersionPage::reloadPackProfile()
{ {
try { try {
m_profile->reload(Net::Mode::Online); auto result = m_profile->reload(Net::Mode::Online);
return true; if (!result) {
QMessageBox::critical(this, tr("Error"), result.error);
}
return result;
} catch (const Exception& e) { } catch (const Exception& e) {
QMessageBox::critical(this, tr("Error"), e.cause()); QMessageBox::critical(this, tr("Error"), e.cause());
return false; return false;

View File

@ -13,6 +13,11 @@
<layout class="QVBoxLayout" name="verticalLayout"> <layout class="QVBoxLayout" name="verticalLayout">
<item> <item>
<widget class="QLabel" name="label"> <widget class="QLabel" name="label">
<property name="font">
<font>
<italic>true</italic>
</font>
</property>
<property name="text"> <property name="text">
<string>Note: If your FTB instances are not in the default location, select it using the button next to search.</string> <string>Note: If your FTB instances are not in the default location, select it using the button next to search.</string>
</property> </property>

View File

@ -36,6 +36,9 @@
ThemeManager::ThemeManager() ThemeManager::ThemeManager()
{ {
QIcon::setFallbackThemeName(QIcon::themeName());
QIcon::setThemeSearchPaths(QIcon::themeSearchPaths() << m_iconThemeFolder.path());
themeDebugLog() << "Determining System Widget Theme..."; themeDebugLog() << "Determining System Widget Theme...";
const auto& style = QApplication::style(); const auto& style = QApplication::style();
m_defaultStyle = style->objectName(); m_defaultStyle = style->objectName();
@ -93,10 +96,6 @@ void ThemeManager::initializeIcons()
// set icon theme search path! // set icon theme search path!
themeDebugLog() << "<> Initializing Icon Themes"; themeDebugLog() << "<> Initializing Icon Themes";
auto searchPaths = QIcon::themeSearchPaths();
searchPaths.append(m_iconThemeFolder.path());
QIcon::setThemeSearchPaths(searchPaths);
for (const QString& id : builtinIcons) { for (const QString& id : builtinIcons) {
IconTheme theme(id, QString(":/icons/%1").arg(id)); IconTheme theme(id, QString(":/icons/%1").arg(id));
if (!theme.load()) { if (!theme.load()) {

View File

@ -1,40 +0,0 @@
#include "DropLabel.h"
#include <QDropEvent>
#include <QMimeData>
DropLabel::DropLabel(QWidget* parent) : QLabel(parent)
{
setAcceptDrops(true);
}
void DropLabel::dragEnterEvent(QDragEnterEvent* event)
{
event->acceptProposedAction();
}
void DropLabel::dragMoveEvent(QDragMoveEvent* event)
{
event->acceptProposedAction();
}
void DropLabel::dragLeaveEvent(QDragLeaveEvent* event)
{
event->accept();
}
void DropLabel::dropEvent(QDropEvent* event)
{
const QMimeData* mimeData = event->mimeData();
if (!mimeData) {
return;
}
if (mimeData->hasUrls()) {
auto urls = mimeData->urls();
emit droppedURLs(urls);
}
event->acceptProposedAction();
}

View File

@ -1,19 +0,0 @@
#pragma once
#include <QLabel>
class DropLabel : public QLabel {
Q_OBJECT
public:
explicit DropLabel(QWidget* parent = nullptr);
signals:
void droppedURLs(QList<QUrl> urls);
protected:
void dropEvent(QDropEvent* event) override;
void dragEnterEvent(QDragEnterEvent* event) override;
void dragMoveEvent(QDragMoveEvent* event) override;
void dragLeaveEvent(QDragLeaveEvent* event) override;
};

View File

@ -279,16 +279,17 @@ void ModFilterWidget::onSideFilterChanged()
{ {
QString side; QString side;
if (ui->clientSide->isChecked() != ui->serverSide->isChecked()) { if (ui->clientSide->isChecked() && !ui->serverSide->isChecked()) {
if (ui->clientSide->isChecked())
side = "client"; side = "client";
else } else if (!ui->clientSide->isChecked() && ui->serverSide->isChecked()) {
side = "server"; side = "server";
} else if (ui->clientSide->isChecked() && ui->serverSide->isChecked()) {
side = "both";
} else { } else {
// both are checked or none are checked; in either case no filtering will happen
side = ""; side = "";
} }
m_filter_changed = side != m_filter->side; m_filter_changed = side != m_filter->side;
m_filter->side = side; m_filter->side = side;
if (m_filter_changed) if (m_filter_changed)

View File

@ -47,6 +47,9 @@
<property name="wordWrap"> <property name="wordWrap">
<bool>true</bool> <bool>true</bool>
</property> </property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
</widget> </widget>
</item> </item>
<item> <item>
@ -68,6 +71,9 @@
<property name="alignment"> <property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property> </property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
</widget> </widget>
</item> </item>
</layout> </layout>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
</dict>
</plist>

View File

@ -2,10 +2,6 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
<key>com.apple.security.device.audio-input</key> <key>com.apple.security.device.audio-input</key>
<true/> <true/>
<key>com.apple.security.device.camera</key> <key>com.apple.security.device.camera</key>