Merge branch 'develop' of https://github.com/PrismLauncher/PrismLauncher into refresh_cats

This commit is contained in:
Trial97 2024-01-20 00:27:30 +02:00
commit 0303b1cc7f
No known key found for this signature in database
GPG Key ID: 55EF5DA53DB36318
30 changed files with 279 additions and 252 deletions

View File

@ -21,8 +21,23 @@ on:
WINDOWS_CODESIGN_PASSWORD: WINDOWS_CODESIGN_PASSWORD:
description: Password for signing Windows builds description: Password for signing Windows builds
required: false required: false
CACHIX_AUTH_TOKEN: APPLE_CODESIGN_CERT:
description: Private token for authenticating against Cachix cache description: Certificate for signing macOS builds
required: false
APPLE_CODESIGN_PASSWORD:
description: Password for signing macOS builds
required: false
APPLE_CODESIGN_ID:
description: Certificate ID for signing macOS builds
required: false
APPLE_NOTARIZE_APPLE_ID:
description: Apple ID used for notarizing macOS builds
required: false
APPLE_NOTARIZE_TEAM_ID:
description: Team ID used for notarizing macOS builds
required: false
APPLE_NOTARIZE_PASSWORD:
description: Password used for notarizing macOS builds
required: false required: false
GPG_PRIVATE_KEY: GPG_PRIVATE_KEY:
description: Private key for AppImage signing description: Private key for AppImage signing
@ -151,7 +166,7 @@ jobs:
- 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@v3.3.2 uses: actions/cache@v4.0.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 }}
@ -336,6 +351,20 @@ jobs:
# PACKAGE BUILDS # PACKAGE BUILDS
## ##
- name: Fetch codesign certificate (macOS)
if: runner.os == 'macOS'
run: |
echo '${{ secrets.APPLE_CODESIGN_CERT }}' | base64 --decode > codesign.p12
if [ -n '${{ secrets.APPLE_CODESIGN_ID }}' ]; then
security create-keychain -p '${{ secrets.APPLE_CODESIGN_PASSWORD }}' build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p '${{ secrets.APPLE_CODESIGN_PASSWORD }}' build.keychain
security import codesign.p12 -k build.keychain -P '${{ secrets.APPLE_CODESIGN_PASSWORD }}' -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k '${{ secrets.APPLE_CODESIGN_PASSWORD }}' build.keychain
else
echo ":warning: Using ad-hoc code signing for macOS, as certificate was not present." >> $GITHUB_STEP_SUMMARY
fi
- name: Package (macOS) - name: Package (macOS)
if: runner.os == 'macOS' if: runner.os == 'macOS'
run: | run: |
@ -343,9 +372,34 @@ jobs:
cd ${{ env.INSTALL_DIR }} cd ${{ env.INSTALL_DIR }}
chmod +x "PrismLauncher.app/Contents/MacOS/prismlauncher" chmod +x "PrismLauncher.app/Contents/MacOS/prismlauncher"
sudo codesign --sign - --deep --force --entitlements "../program_info/App.entitlements" --options runtime "PrismLauncher.app/Contents/MacOS/prismlauncher"
if [ -n '${{ secrets.APPLE_CODESIGN_ID }}' ]; then
APPLE_CODESIGN_ID='${{ secrets.APPLE_CODESIGN_ID }}'
else
APPLE_CODESIGN_ID='-'
fi
sudo codesign --sign "$APPLE_CODESIGN_ID" --deep --force --entitlements "../program_info/App.entitlements" --options runtime "PrismLauncher.app/Contents/MacOS/prismlauncher"
mv "PrismLauncher.app" "Prism Launcher.app" mv "PrismLauncher.app" "Prism Launcher.app"
tar -czf ../PrismLauncher.tar.gz *
- name: Notarize (macOS)
if: runner.os == 'macOS'
run: |
cd ${{ env.INSTALL_DIR }}
if [ -n '${{ secrets.APPLE_NOTARIZE_PASSWORD }}' ]; then
ditto -c -k --sequesterRsrc --keepParent "Prism Launcher.app" ../PrismLauncher.zip
xcrun notarytool submit ../PrismLauncher.zip \
--wait --progress \
--apple-id '${{ secrets.APPLE_NOTARIZE_APPLE_ID }}' \
--team-id '${{ secrets.APPLE_NOTARIZE_TEAM_ID }}' \
--password '${{ secrets.APPLE_NOTARIZE_PASSWORD }}'
xcrun stapler staple "Prism Launcher.app"
else
echo ":warning: Skipping notarization as credentials are not present." >> $GITHUB_STEP_SUMMARY
fi
ditto -c -k --sequesterRsrc --keepParent "Prism Launcher.app" ../PrismLauncher.zip
- name: Make Sparkle signature (macOS) - name: Make Sparkle signature (macOS)
if: matrix.name == 'macOS' if: matrix.name == 'macOS'
@ -520,7 +574,7 @@ jobs:
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: PrismLauncher-${{ matrix.name }}-${{ env.VERSION }}-${{ inputs.build_type }} name: PrismLauncher-${{ matrix.name }}-${{ env.VERSION }}-${{ inputs.build_type }}
path: PrismLauncher.tar.gz path: PrismLauncher.zip
- name: Upload binary zip (Windows) - name: Upload binary zip (Windows)
if: runner.os == 'Windows' if: runner.os == 'Windows'

View File

@ -32,6 +32,11 @@ jobs:
SPARKLE_ED25519_KEY: ${{ secrets.SPARKLE_ED25519_KEY }} SPARKLE_ED25519_KEY: ${{ secrets.SPARKLE_ED25519_KEY }}
WINDOWS_CODESIGN_CERT: ${{ secrets.WINDOWS_CODESIGN_CERT }} WINDOWS_CODESIGN_CERT: ${{ secrets.WINDOWS_CODESIGN_CERT }}
WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }} WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }}
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} APPLE_CODESIGN_CERT: ${{ secrets.APPLE_CODESIGN_CERT }}
APPLE_CODESIGN_PASSWORD: ${{ secrets.APPLE_CODESIGN_PASSWORD }}
APPLE_CODESIGN_ID: ${{ secrets.APPLE_CODESIGN_ID }}
APPLE_NOTARIZE_APPLE_ID: ${{ secrets.APPLE_NOTARIZE_APPLE_ID }}
APPLE_NOTARIZE_TEAM_ID: ${{ secrets.APPLE_NOTARIZE_TEAM_ID }}
APPLE_NOTARIZE_PASSWORD: ${{ secrets.APPLE_NOTARIZE_PASSWORD }}
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
GPG_PRIVATE_KEY_ID: ${{ secrets.GPG_PRIVATE_KEY_ID }} GPG_PRIVATE_KEY_ID: ${{ secrets.GPG_PRIVATE_KEY_ID }}

View File

@ -16,7 +16,12 @@ jobs:
SPARKLE_ED25519_KEY: ${{ secrets.SPARKLE_ED25519_KEY }} SPARKLE_ED25519_KEY: ${{ secrets.SPARKLE_ED25519_KEY }}
WINDOWS_CODESIGN_CERT: ${{ secrets.WINDOWS_CODESIGN_CERT }} WINDOWS_CODESIGN_CERT: ${{ secrets.WINDOWS_CODESIGN_CERT }}
WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }} WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }}
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} APPLE_CODESIGN_CERT: ${{ secrets.APPLE_CODESIGN_CERT }}
APPLE_CODESIGN_PASSWORD: ${{ secrets.APPLE_CODESIGN_PASSWORD }}
APPLE_CODESIGN_ID: ${{ secrets.APPLE_CODESIGN_ID }}
APPLE_NOTARIZE_APPLE_ID: ${{ secrets.APPLE_NOTARIZE_APPLE_ID }}
APPLE_NOTARIZE_TEAM_ID: ${{ secrets.APPLE_NOTARIZE_TEAM_ID }}
APPLE_NOTARIZE_PASSWORD: ${{ secrets.APPLE_NOTARIZE_PASSWORD }}
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
GPG_PRIVATE_KEY_ID: ${{ secrets.GPG_PRIVATE_KEY_ID }} GPG_PRIVATE_KEY_ID: ${{ secrets.GPG_PRIVATE_KEY_ID }}
@ -46,8 +51,8 @@ jobs:
mv PrismLauncher-Linux-Qt5*/PrismLauncher.tar.gz PrismLauncher-Linux-Qt5-${{ env.VERSION }}.tar.gz mv PrismLauncher-Linux-Qt5*/PrismLauncher.tar.gz PrismLauncher-Linux-Qt5-${{ env.VERSION }}.tar.gz
mv PrismLauncher-*.AppImage/PrismLauncher-*.AppImage PrismLauncher-Linux-x86_64.AppImage mv PrismLauncher-*.AppImage/PrismLauncher-*.AppImage PrismLauncher-Linux-x86_64.AppImage
mv PrismLauncher-*.AppImage.zsync/PrismLauncher-*.AppImage.zsync PrismLauncher-Linux-x86_64.AppImage.zsync mv PrismLauncher-*.AppImage.zsync/PrismLauncher-*.AppImage.zsync PrismLauncher-Linux-x86_64.AppImage.zsync
mv PrismLauncher-macOS-Legacy*/PrismLauncher.tar.gz PrismLauncher-macOS-Legacy-${{ env.VERSION }}.tar.gz mv PrismLauncher-macOS-Legacy*/PrismLauncher.zip PrismLauncher-macOS-Legacy-${{ env.VERSION }}.zip
mv PrismLauncher-macOS*/PrismLauncher.tar.gz PrismLauncher-macOS-${{ env.VERSION }}.tar.gz mv PrismLauncher-macOS*/PrismLauncher.zip PrismLauncher-macOS-${{ env.VERSION }}.zip
tar --exclude='.git' -czf PrismLauncher-${{ env.VERSION }}.tar.gz PrismLauncher-${{ env.VERSION }} tar --exclude='.git' -czf PrismLauncher-${{ env.VERSION }}.tar.gz PrismLauncher-${{ env.VERSION }}
@ -102,6 +107,6 @@ jobs:
PrismLauncher-Windows-MSVC-${{ env.VERSION }}.zip PrismLauncher-Windows-MSVC-${{ env.VERSION }}.zip
PrismLauncher-Windows-MSVC-Portable-${{ env.VERSION }}.zip PrismLauncher-Windows-MSVC-Portable-${{ env.VERSION }}.zip
PrismLauncher-Windows-MSVC-Setup-${{ env.VERSION }}.exe PrismLauncher-Windows-MSVC-Setup-${{ env.VERSION }}.exe
PrismLauncher-macOS-${{ env.VERSION }}.tar.gz PrismLauncher-macOS-${{ env.VERSION }}.zip
PrismLauncher-macOS-Legacy-${{ env.VERSION }}.tar.gz PrismLauncher-macOS-Legacy-${{ env.VERSION }}.zip
PrismLauncher-${{ env.VERSION }}.tar.gz PrismLauncher-${{ env.VERSION }}.tar.gz

View File

@ -17,7 +17,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: cachix/install-nix-action@7ac1ec25491415c381d9b62f0657c7a028df52a7 # v24 - uses: cachix/install-nix-action@6004951b182f8860210c8d6f0d808ec5b1a33d28 # v25
- uses: DeterminateSystems/update-flake-lock@v20 - uses: DeterminateSystems/update-flake-lock@v20
with: with:

40
flake.lock generated
View File

@ -18,14 +18,16 @@
}, },
"flake-parts": { "flake-parts": {
"inputs": { "inputs": {
"nixpkgs-lib": "nixpkgs-lib" "nixpkgs-lib": [
"nixpkgs"
]
}, },
"locked": { "locked": {
"lastModified": 1704152458, "lastModified": 1704982712,
"narHash": "sha256-DS+dGw7SKygIWf9w4eNBUZsK+4Ug27NwEWmn2tnbycg=", "narHash": "sha256-2Ptt+9h8dczgle2Oo6z5ni5rt/uLMG47UFTR1ry/wgg=",
"owner": "hercules-ci", "owner": "hercules-ci",
"repo": "flake-parts", "repo": "flake-parts",
"rev": "88a2cd8166694ba0b6cb374700799cec53aef527", "rev": "07f6395285469419cf9d078f59b5b49993198c00",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -106,11 +108,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1704161960, "lastModified": 1704842529,
"narHash": "sha256-QGua89Pmq+FBAro8NriTuoO/wNaUtugt29/qqA8zeeM=", "narHash": "sha256-OTeQA+F8d/Evad33JMfuXC89VMetQbsU4qcaePchGr4=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "63143ac2c9186be6d9da6035fa22620018c85932", "rev": "eabe8d3eface69f5bb16c18f8662a702f50c20d5",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -120,24 +122,6 @@
"type": "github" "type": "github"
} }
}, },
"nixpkgs-lib": {
"locked": {
"dir": "lib",
"lastModified": 1703961334,
"narHash": "sha256-M1mV/Cq+pgjk0rt6VxoyyD+O8cOUiai8t9Q6Yyq4noY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b0d36bd0a420ecee3bc916c91886caca87c894e9",
"type": "github"
},
"original": {
"dir": "lib",
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"pre-commit-hooks": { "pre-commit-hooks": {
"inputs": { "inputs": {
"flake-compat": [ "flake-compat": [
@ -153,11 +137,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1703939133, "lastModified": 1705072518,
"narHash": "sha256-Gxe+mfOT6bL7wLC/tuT2F+V+Sb44jNr8YsJ3cyIl4Mo=", "narHash": "sha256-90dERRuG781f0EWjn2AOtScZqsTcpIFLpY8TN2VbkL8=",
"owner": "cachix", "owner": "cachix",
"repo": "pre-commit-hooks.nix", "repo": "pre-commit-hooks.nix",
"rev": "9d3d7e18c6bc4473d7520200d4ddab12f8402d38", "rev": "274ae3979a0eacae422e1bbcf63b8b7a335e1114",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@ -1,15 +1,25 @@
{ {
description = "A custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once (Fork of MultiMC)"; description = "A custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once (Fork of MultiMC)";
nixConfig = {
extra-substituters = ["https://cache.garnix.io"];
extra-trusted-public-keys = ["cache.garnix.io:CTFPyKSLcx5RMJKfLo5EEPUObbA78b0YQ2DTCJXqr9g="];
};
inputs = { inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
flake-parts.url = "github:hercules-ci/flake-parts"; flake-parts = {
url = "github:hercules-ci/flake-parts";
inputs.nixpkgs-lib.follows = "nixpkgs";
};
nix-filter.url = "github:numtide/nix-filter"; nix-filter.url = "github:numtide/nix-filter";
pre-commit-hooks = { pre-commit-hooks = {
url = "github:cachix/pre-commit-hooks.nix"; url = "github:cachix/pre-commit-hooks.nix";
inputs.nixpkgs.follows = "nixpkgs"; inputs = {
inputs.nixpkgs-stable.follows = "nixpkgs"; nixpkgs.follows = "nixpkgs";
inputs.flake-compat.follows = "flake-compat"; nixpkgs-stable.follows = "nixpkgs";
flake-compat.follows = "flake-compat";
};
}; };
flake-compat = { flake-compat = {
url = "github:edolstra/flake-compat"; url = "github:edolstra/flake-compat";

View File

@ -751,6 +751,9 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_settings->registerSetting("ModrinthToken", ""); m_settings->registerSetting("ModrinthToken", "");
m_settings->registerSetting("UserAgentOverride", ""); m_settings->registerSetting("UserAgentOverride", "");
// FTBApp instances
m_settings->registerSetting("FTBAppInstancesPath", "");
// Init page provider // Init page provider
{ {
m_globalSettingsProvider = std::make_shared<GenericPageProvider>(tr("Settings")); m_globalSettingsProvider = std::make_shared<GenericPageProvider>(tr("Settings"));

View File

@ -37,140 +37,33 @@
#include <QDesktopServices> #include <QDesktopServices>
#include <QDir> #include <QDir>
#include <QProcess> #include <QProcess>
#include "FileSystem.h"
/**
* This shouldn't exist, but until QTBUG-9328 and other unreported bugs are fixed, it needs to be a thing.
*/
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
template <typename T>
bool IndirectOpen(T callable, qint64* pid_forked = nullptr)
{
auto pid = fork();
if (pid_forked) {
if (pid > 0)
*pid_forked = pid;
else
*pid_forked = 0;
}
if (pid == -1) {
qWarning() << "IndirectOpen failed to fork: " << errno;
return false;
}
// child - do the stuff
if (pid == 0) {
// unset all this garbage so it doesn't get passed to the child process
qunsetenv("LD_PRELOAD");
qunsetenv("LD_LIBRARY_PATH");
qunsetenv("LD_DEBUG");
qunsetenv("QT_PLUGIN_PATH");
qunsetenv("QT_FONTPATH");
// open the URL
auto status = callable();
// detach from the parent process group.
setsid();
// die. now. do not clean up anything, it would just hang forever.
_exit(status ? 0 : 1);
} else {
// parent - assume it worked.
int status;
while (waitpid(pid, &status, 0)) {
if (WIFEXITED(status)) {
return WEXITSTATUS(status) == 0;
}
if (WIFSIGNALED(status)) {
return false;
}
}
return true;
}
}
#endif
namespace DesktopServices { namespace DesktopServices {
bool openDirectory(const QString& path, [[maybe_unused]] bool ensureExists) bool openPath(const QFileInfo& path, bool ensureFolderPathExists)
{ {
qDebug() << "Opening directory" << path; qDebug() << "Opening path" << path;
QDir parentPath; if (ensureFolderPathExists) {
QDir dir(path); FS::ensureFolderPathExists(path);
if (ensureExists && !dir.exists()) {
parentPath.mkpath(dir.absolutePath());
} }
auto f = [&]() { return QDesktopServices::openUrl(QUrl::fromLocalFile(dir.absolutePath())); }; return openUrl(QUrl::fromLocalFile(QFileInfo(path).absolutePath()));
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
if (!isSandbox()) {
return IndirectOpen(f);
}
#endif
return f();
} }
bool openFile(const QString& path) bool openPath(const QString& path, bool ensureFolderPathExists)
{ {
qDebug() << "Opening file" << path; return openPath(QFileInfo(path), ensureFolderPathExists);
auto f = [&]() { return QDesktopServices::openUrl(QUrl::fromLocalFile(path)); };
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
if (!isSandbox()) {
return IndirectOpen(f);
} else {
return f();
}
#else
return f();
#endif
}
bool openFile(const QString& application, const QString& path, const QString& workingDirectory, qint64* pid)
{
qDebug() << "Opening file" << path << "using" << application;
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
// FIXME: the pid here is fake. So if something depends on it, it will likely misbehave
if (!isSandbox()) {
return IndirectOpen([&]() { return QProcess::startDetached(application, QStringList() << path, workingDirectory); }, pid);
} else {
return QProcess::startDetached(application, QStringList() << path, workingDirectory, pid);
}
#else
return QProcess::startDetached(application, QStringList() << path, workingDirectory, pid);
#endif
} }
bool run(const QString& application, const QStringList& args, const QString& workingDirectory, qint64* pid) bool run(const QString& application, const QStringList& args, const QString& workingDirectory, qint64* pid)
{ {
qDebug() << "Running" << application << "with args" << args.join(' '); qDebug() << "Running" << application << "with args" << args.join(' ');
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
if (!isSandbox()) {
// FIXME: the pid here is fake. So if something depends on it, it will likely misbehave
return IndirectOpen([&]() { return QProcess::startDetached(application, args, workingDirectory); }, pid);
} else {
return QProcess::startDetached(application, args, workingDirectory, pid);
}
#else
return QProcess::startDetached(application, args, workingDirectory, pid); return QProcess::startDetached(application, args, workingDirectory, pid);
#endif
} }
bool openUrl(const QUrl& url) bool openUrl(const QUrl& url)
{ {
qDebug() << "Opening URL" << url.toString(); qDebug() << "Opening URL" << url.toString();
auto f = [&]() { return QDesktopServices::openUrl(url); }; return QDesktopServices::openUrl(url);
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
if (!isSandbox()) {
return IndirectOpen(f);
} else {
return f();
}
#else
return f();
#endif
} }
bool isFlatpak() bool isFlatpak()
@ -191,9 +84,4 @@ bool isSnap()
#endif #endif
} }
bool isSandbox()
{
return isSnap() || isFlatpak();
}
} // namespace DesktopServices } // namespace DesktopServices

View File

@ -3,31 +3,30 @@
#include <QString> #include <QString>
#include <QUrl> #include <QUrl>
class QFileInfo;
/** /**
* This wraps around QDesktopServices and adds workarounds where needed * This wraps around QDesktopServices and adds workarounds where needed
* Use this instead of QDesktopServices! * Use this instead of QDesktopServices!
*/ */
namespace DesktopServices { namespace DesktopServices {
/** /**
* Open a file in whatever application is applicable * Open a path in whatever application is applicable.
* @param ensureFolderPathExists Make sure the path exists
*/ */
bool openFile(const QString& path); bool openPath(const QFileInfo& path, bool ensureFolderPathExists = false);
/** /**
* Open a file in the specified application * Open a path in whatever application is applicable.
* @param ensureFolderPathExists Make sure the path exists
*/ */
bool openFile(const QString& application, const QString& path, const QString& workingDirectory = QString(), qint64* pid = 0); bool openPath(const QString& path, bool ensureFolderPathExists = false);
/** /**
* Run an application * Run an application
*/ */
bool run(const QString& application, const QStringList& args, const QString& workingDirectory = QString(), qint64* pid = 0); bool run(const QString& application, const QStringList& args, const QString& workingDirectory = QString(), qint64* pid = 0);
/**
* Open a directory
*/
bool openDirectory(const QString& path, bool ensureExists = false);
/** /**
* Open the URL, most likely in a browser. Maybe. * Open the URL, most likely in a browser. Maybe.
*/ */
@ -42,9 +41,4 @@ bool isFlatpak();
* Determine whether the launcher is running in a Snap environment * Determine whether the launcher is running in a Snap environment
*/ */
bool isSnap(); bool isSnap();
/**
* Determine whether the launcher is running in a sandboxed (Flatpak or Snap) environment
*/
bool isSandbox();
} // namespace DesktopServices } // namespace DesktopServices

View File

@ -272,15 +272,19 @@ bool ensureFilePathExists(QString filenamepath)
return success; return success;
} }
bool ensureFolderPathExists(QString foldernamepath) bool ensureFolderPathExists(const QFileInfo folderPath)
{ {
QFileInfo a(foldernamepath);
QDir dir; QDir dir;
QString ensuredPath = a.filePath(); QString ensuredPath = folderPath.filePath();
bool success = dir.mkpath(ensuredPath); bool success = dir.mkpath(ensuredPath);
return success; return success;
} }
bool ensureFolderPathExists(const QString folderPathName)
{
return ensureFolderPathExists(QFileInfo(folderPathName));
}
bool copyFileAttributes(QString src, QString dst) bool copyFileAttributes(QString src, QString dst)
{ {
#ifdef Q_OS_WIN32 #ifdef Q_OS_WIN32

View File

@ -91,7 +91,13 @@ bool ensureFilePathExists(QString filenamepath);
* Creates all the folders in a path for the specified path * Creates all the folders in a path for the specified path
* last segment of the path is treated as a folder name and is created! * last segment of the path is treated as a folder name and is created!
*/ */
bool ensureFolderPathExists(QString filenamepath); bool ensureFolderPathExists(const QFileInfo folderPath);
/**
* Creates all the folders in a path for the specified path
* last segment of the path is treated as a folder name and is created!
*/
bool ensureFolderPathExists(const QString folderPathName);
/** /**
* @brief Copies a directory and it's contents from src to dest * @brief Copies a directory and it's contents from src to dest

View File

@ -2,6 +2,8 @@
#include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/CustomMessageBox.h"
#include <QPushButton>
InstanceNameChange askForChangingInstanceName(QWidget* parent, const QString& old_name, const QString& new_name) InstanceNameChange askForChangingInstanceName(QWidget* parent, const QString& old_name, const QString& new_name)
{ {
auto dialog = auto dialog =
@ -27,16 +29,15 @@ ShouldUpdate askIfShouldUpdate(QWidget* parent, QString original_version_name)
"separate instance, or update the existing one?\n\nNOTE: Make sure you made a backup of your important instance data before " "separate instance, or update the existing one?\n\nNOTE: Make sure you made a backup of your important instance data before "
"updating, as worlds can be corrupted and some configuration may be lost (due to pack overrides).") "updating, as worlds can be corrupted and some configuration may be lost (due to pack overrides).")
.arg(original_version_name), .arg(original_version_name),
QMessageBox::Information, QMessageBox::Ok | QMessageBox::Reset | QMessageBox::Abort); QMessageBox::Information, QMessageBox::Cancel);
info->setButtonText(QMessageBox::Ok, QObject::tr("Update existing instance")); QAbstractButton* update = info->addButton(QObject::tr("Update existing instance"), QMessageBox::AcceptRole);
info->setButtonText(QMessageBox::Abort, QObject::tr("Create new instance")); QAbstractButton* skip = info->addButton(QObject::tr("Create new instance"), QMessageBox::ResetRole);
info->setButtonText(QMessageBox::Reset, QObject::tr("Cancel"));
info->exec(); info->exec();
if (info->clickedButton() == info->button(QMessageBox::Ok)) if (info->clickedButton() == update)
return ShouldUpdate::Update; return ShouldUpdate::Update;
if (info->clickedButton() == info->button(QMessageBox::Abort)) if (info->clickedButton() == skip)
return ShouldUpdate::SkipUpdating; return ShouldUpdate::SkipUpdating;
return ShouldUpdate::Cancel; return ShouldUpdate::Cancel;
} }

View File

@ -42,7 +42,6 @@
#include "ui/InstanceWindow.h" #include "ui/InstanceWindow.h"
#include "ui/MainWindow.h" #include "ui/MainWindow.h"
#include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/CustomMessageBox.h"
#include "ui/dialogs/EditAccountDialog.h"
#include "ui/dialogs/ProfileSelectDialog.h" #include "ui/dialogs/ProfileSelectDialog.h"
#include "ui/dialogs/ProfileSetupDialog.h" #include "ui/dialogs/ProfileSetupDialog.h"
#include "ui/dialogs/ProgressDialog.h" #include "ui/dialogs/ProgressDialog.h"
@ -144,6 +143,12 @@ void LaunchController::login()
bool tryagain = true; bool tryagain = true;
unsigned int tries = 0; unsigned int tries = 0;
if (m_accountToUse->accountType() != AccountType::Offline && m_accountToUse->accountState() == AccountState::Offline) {
// Force account refresh on the account used to launch the instance updating the AccountState
// only on first try and if it is not meant to be offline
auto accounts = APPLICATION->accounts();
accounts->requestRefresh(m_accountToUse->internalId());
}
while (tryagain) { while (tryagain) {
if (tries > 0 && tries % 3 == 0) { if (tries > 0 && tries % 3 == 0) {
auto result = auto result =
@ -250,12 +255,6 @@ void LaunchController::login()
progDialog.execWithTask(task.get()); progDialog.execWithTask(task.get());
continue; continue;
} }
// FIXME: this is missing - the meaning is that the account is queued for refresh and we should wait for that
/*
case AccountState::Queued: {
return;
}
*/
case AccountState::Expired: { case AccountState::Expired: {
auto errorString = tr("The account has expired and needs to be logged into manually again."); auto errorString = tr("The account has expired and needs to be logged into manually again.");
QMessageBox::warning(m_parentWidget, tr("Account refresh failed"), errorString, QMessageBox::StandardButton::Ok, QMessageBox::warning(m_parentWidget, tr("Account refresh failed"), errorString, QMessageBox::StandardButton::Ok,

View File

@ -52,8 +52,6 @@
#include <FileSystem.h> #include <FileSystem.h>
#include <QSaveFile> #include <QSaveFile>
#include <chrono>
enum AccountListVersion { MojangMSA = 3 }; enum AccountListVersion { MojangMSA = 3 };
AccountList::AccountList(QObject* parent) : QAbstractListModel(parent) AccountList::AccountList(QObject* parent) : QAbstractListModel(parent)

View File

@ -1197,43 +1197,43 @@ void MainWindow::undoTrashInstance()
void MainWindow::on_actionViewLauncherRootFolder_triggered() void MainWindow::on_actionViewLauncherRootFolder_triggered()
{ {
DesktopServices::openDirectory("."); DesktopServices::openPath(".");
} }
void MainWindow::on_actionViewInstanceFolder_triggered() void MainWindow::on_actionViewInstanceFolder_triggered()
{ {
QString str = APPLICATION->settings()->get("InstanceDir").toString(); QString str = APPLICATION->settings()->get("InstanceDir").toString();
DesktopServices::openDirectory(str); DesktopServices::openPath(str);
} }
void MainWindow::on_actionViewCentralModsFolder_triggered() void MainWindow::on_actionViewCentralModsFolder_triggered()
{ {
DesktopServices::openDirectory(APPLICATION->settings()->get("CentralModsDir").toString(), true); DesktopServices::openPath(APPLICATION->settings()->get("CentralModsDir").toString(), true);
} }
void MainWindow::on_actionViewIconThemeFolder_triggered() void MainWindow::on_actionViewIconThemeFolder_triggered()
{ {
DesktopServices::openDirectory(APPLICATION->themeManager()->getIconThemesFolder().path(), true); DesktopServices::openPath(APPLICATION->themeManager()->getIconThemesFolder().path(), true);
} }
void MainWindow::on_actionViewWidgetThemeFolder_triggered() void MainWindow::on_actionViewWidgetThemeFolder_triggered()
{ {
DesktopServices::openDirectory(APPLICATION->themeManager()->getApplicationThemesFolder().path(), true); DesktopServices::openPath(APPLICATION->themeManager()->getApplicationThemesFolder().path(), true);
} }
void MainWindow::on_actionViewCatPackFolder_triggered() void MainWindow::on_actionViewCatPackFolder_triggered()
{ {
DesktopServices::openDirectory(APPLICATION->themeManager()->getCatPacksFolder().path(), true); DesktopServices::openPath(APPLICATION->themeManager()->getCatPacksFolder().path(), true);
} }
void MainWindow::on_actionViewIconsFolder_triggered() void MainWindow::on_actionViewIconsFolder_triggered()
{ {
DesktopServices::openDirectory(APPLICATION->icons()->getDirectory(), true); DesktopServices::openPath(APPLICATION->icons()->getDirectory(), true);
} }
void MainWindow::on_actionViewLogsFolder_triggered() void MainWindow::on_actionViewLogsFolder_triggered()
{ {
DesktopServices::openDirectory("logs", true); DesktopServices::openPath("logs", true);
} }
void MainWindow::refreshInstances() void MainWindow::refreshInstances()
@ -1452,7 +1452,7 @@ void MainWindow::on_actionViewSelectedInstFolder_triggered()
{ {
if (m_selectedInstance) { if (m_selectedInstance) {
QString str = m_selectedInstance->instanceRoot(); QString str = m_selectedInstance->instanceRoot();
DesktopServices::openDirectory(QDir(str).absolutePath()); DesktopServices::openPath(QFileInfo(str));
} }
} }

View File

@ -159,5 +159,5 @@ IconPickerDialog::~IconPickerDialog()
void IconPickerDialog::openFolder() void IconPickerDialog::openFolder()
{ {
DesktopServices::openDirectory(APPLICATION->icons()->getDirectory(), true); DesktopServices::openPath(APPLICATION->icons()->getDirectory(), true);
} }

View File

@ -443,6 +443,9 @@ void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info, QStri
reqItem->insertChildren(i++, { reqItem }); reqItem->insertChildren(i++, { reqItem });
} }
} }
ui->toggleDepsButton->show();
m_deps << item_top;
} }
auto changelog_item = new QTreeWidgetItem(item_top); auto changelog_item = new QTreeWidgetItem(item_top);

View File

@ -13,6 +13,7 @@ ReviewMessageBox::ReviewMessageBox(QWidget* parent, [[maybe_unused]] QString con
auto back_button = ui->buttonBox->button(QDialogButtonBox::Cancel); auto back_button = ui->buttonBox->button(QDialogButtonBox::Cancel);
back_button->setText(tr("Back")); back_button->setText(tr("Back"));
ui->toggleDepsButton->hide();
ui->modTreeWidget->header()->setSectionResizeMode(0, QHeaderView::Stretch); ui->modTreeWidget->header()->setSectionResizeMode(0, QHeaderView::Stretch);
ui->modTreeWidget->header()->setStretchLastSection(false); ui->modTreeWidget->header()->setStretchLastSection(false);
ui->modTreeWidget->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents); ui->modTreeWidget->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
@ -75,6 +76,8 @@ void ReviewMessageBox::appendResource(ResourceInformation&& info)
} }
itemTop->insertChildren(childIndx++, { requiredByItem }); itemTop->insertChildren(childIndx++, { requiredByItem });
ui->toggleDepsButton->show();
m_deps << itemTop;
} }
auto versionTypeItem = new QTreeWidgetItem(itemTop); auto versionTypeItem = new QTreeWidgetItem(itemTop);
@ -108,3 +111,10 @@ void ReviewMessageBox::retranslateUi(QString resources_name)
ui->explainLabel->setText(tr("You're about to download the following %1:").arg(resources_name)); ui->explainLabel->setText(tr("You're about to download the following %1:").arg(resources_name));
ui->onlyCheckedLabel->setText(tr("Only %1 with a check will be downloaded!").arg(resources_name)); ui->onlyCheckedLabel->setText(tr("Only %1 with a check will be downloaded!").arg(resources_name));
} }
void ReviewMessageBox::on_toggleDepsButton_clicked()
{
m_deps_checked = !m_deps_checked;
auto state = m_deps_checked ? Qt::Checked : Qt::Unchecked;
for (auto dep : m_deps)
dep->setCheckState(0, state);
};

View File

@ -1,6 +1,7 @@
#pragma once #pragma once
#include <QDialog> #include <QDialog>
#include <QTreeWidgetItem>
namespace Ui { namespace Ui {
class ReviewMessageBox; class ReviewMessageBox;
@ -28,8 +29,14 @@ class ReviewMessageBox : public QDialog {
~ReviewMessageBox() override; ~ReviewMessageBox() override;
protected slots:
void on_toggleDepsButton_clicked();
protected: protected:
ReviewMessageBox(QWidget* parent, const QString& title, const QString& icon); ReviewMessageBox(QWidget* parent, const QString& title, const QString& icon);
Ui::ReviewMessageBox* ui; Ui::ReviewMessageBox* ui;
QList<QTreeWidgetItem*> m_deps;
bool m_deps_checked = true;
}; };

View File

@ -44,15 +44,20 @@
</widget> </widget>
</item> </item>
<item row="1" column="0"> <item row="1" column="0">
<widget class="QLabel" name="explainLabel"> <widget class="QLabel" name="explainLabel"/>
</widget>
</item> </item>
<item row="5" column="0" rowspan="2"> <item row="5" column="0" rowspan="2">
<layout class="QHBoxLayout" name="horizontalLayout"> <layout class="QHBoxLayout" name="horizontalLayout">
<item> <item>
<widget class="QLabel" name="onlyCheckedLabel"> <widget class="QPushButton" name="toggleDepsButton">
<property name="text">
<string>Toggle Dependencies</string>
</property>
</widget> </widget>
</item> </item>
<item>
<widget class="QLabel" name="onlyCheckedLabel"/>
</item>
<item> <item>
<widget class="QDialogButtonBox" name="buttonBox"> <widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons"> <property name="standardButtons">

View File

@ -290,12 +290,12 @@ void ExternalResourcesPage::disableItem()
void ExternalResourcesPage::viewConfigs() void ExternalResourcesPage::viewConfigs()
{ {
DesktopServices::openDirectory(m_instance->instanceConfigFolder(), true); DesktopServices::openPath(m_instance->instanceConfigFolder(), true);
} }
void ExternalResourcesPage::viewFolder() void ExternalResourcesPage::viewFolder()
{ {
DesktopServices::openDirectory(m_model->dir().absolutePath(), true); DesktopServices::openPath(m_model->dir().absolutePath(), true);
} }
bool ExternalResourcesPage::current(const QModelIndex& current, const QModelIndex& previous) bool ExternalResourcesPage::current(const QModelIndex& current, const QModelIndex& previous)

View File

@ -324,8 +324,7 @@ void ScreenshotsPage::onItemActivated(QModelIndex index)
if (!index.isValid()) if (!index.isValid())
return; return;
auto info = m_model->fileInfo(index); auto info = m_model->fileInfo(index);
QString fileName = info.absoluteFilePath(); DesktopServices::openPath(info);
DesktopServices::openFile(info.absoluteFilePath());
} }
void ScreenshotsPage::onCurrentSelectionChanged(const QItemSelection& selected) void ScreenshotsPage::onCurrentSelectionChanged(const QItemSelection& selected)
@ -352,7 +351,7 @@ void ScreenshotsPage::onCurrentSelectionChanged(const QItemSelection& selected)
void ScreenshotsPage::on_actionView_Folder_triggered() void ScreenshotsPage::on_actionView_Folder_triggered()
{ {
DesktopServices::openDirectory(m_folder, true); DesktopServices::openPath(m_folder, true);
} }
void ScreenshotsPage::on_actionUpload_triggered() void ScreenshotsPage::on_actionUpload_triggered()

View File

@ -447,12 +447,12 @@ void VersionPage::on_actionAdd_Empty_triggered()
void VersionPage::on_actionLibrariesFolder_triggered() void VersionPage::on_actionLibrariesFolder_triggered()
{ {
DesktopServices::openDirectory(m_inst->getLocalLibraryPath(), true); DesktopServices::openPath(m_inst->getLocalLibraryPath(), true);
} }
void VersionPage::on_actionMinecraftFolder_triggered() void VersionPage::on_actionMinecraftFolder_triggered()
{ {
DesktopServices::openDirectory(m_inst->gameRoot(), true); DesktopServices::openPath(m_inst->gameRoot(), true);
} }
void VersionPage::versionCurrent(const QModelIndex& current, [[maybe_unused]] const QModelIndex& previous) void VersionPage::versionCurrent(const QModelIndex& current, [[maybe_unused]] const QModelIndex& previous)

View File

@ -207,7 +207,7 @@ void WorldListPage::on_actionRemove_triggered()
void WorldListPage::on_actionView_Folder_triggered() void WorldListPage::on_actionView_Folder_triggered()
{ {
DesktopServices::openDirectory(m_worlds->dir().absolutePath(), true); DesktopServices::openPath(m_worlds->dir().absolutePath(), true);
} }
void WorldListPage::on_actionDatapacks_triggered() void WorldListPage::on_actionDatapacks_triggered()
@ -223,7 +223,7 @@ void WorldListPage::on_actionDatapacks_triggered()
auto fullPath = m_worlds->data(index, WorldList::FolderRole).toString(); auto fullPath = m_worlds->data(index, WorldList::FolderRole).toString();
DesktopServices::openDirectory(FS::PathCombine(fullPath, "datapacks"), true); DesktopServices::openPath(FS::PathCombine(fullPath, "datapacks"), true);
} }
void WorldListPage::on_actionReset_Icon_triggered() void WorldListPage::on_actionReset_Icon_triggered()

View File

@ -20,6 +20,7 @@
#include "ui/widgets/ProjectItem.h" #include "ui/widgets/ProjectItem.h"
#include "ui_ImportFTBPage.h" #include "ui_ImportFTBPage.h"
#include <QFileDialog>
#include <QWidget> #include <QWidget>
#include "FileSystem.h" #include "FileSystem.h"
#include "ListModel.h" #include "ListModel.h"
@ -56,6 +57,13 @@ ImportFTBPage::ImportFTBPage(NewInstanceDialog* dialog, QWidget* parent) : QWidg
connect(ui->searchEdit, &QLineEdit::textChanged, this, &ImportFTBPage::triggerSearch); connect(ui->searchEdit, &QLineEdit::textChanged, this, &ImportFTBPage::triggerSearch);
connect(ui->browseButton, &QPushButton::clicked, this, [this] {
auto path = listModel->getPath();
QString dir = QFileDialog::getExistingDirectory(this, tr("Select FTBApp instances directory"), path, QFileDialog::ShowDirsOnly);
if (!dir.isEmpty())
listModel->setPath(dir);
});
ui->modpackList->setItemDelegate(new ProjectItemDelegate(this)); ui->modpackList->setItemDelegate(new ProjectItemDelegate(this));
ui->modpackList->selectionModel()->reset(); ui->modpackList->selectionModel()->reset();
} }

View File

@ -11,7 +11,7 @@
</rect> </rect>
</property> </property>
<layout class="QGridLayout" name="gridLayout"> <layout class="QGridLayout" name="gridLayout">
<item row="1" column="1"> <item row="2" column="1">
<widget class="QTreeView" name="modpackList"> <widget class="QTreeView" name="modpackList">
<property name="maximumSize"> <property name="maximumSize">
<size> <size>
@ -21,28 +21,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="1"> <item row="3" column="1">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLineEdit" name="searchEdit">
<property name="placeholderText">
<string>Search and filter...</string>
</property>
<property name="clearButtonEnabled">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton">
<property name="text">
<string>Search</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_2"> <layout class="QHBoxLayout" name="horizontalLayout_2">
<item> <item>
<widget class="QComboBox" name="sortByBox"> <widget class="QComboBox" name="sortByBox">
@ -69,6 +48,54 @@
</item> </item>
</layout> </layout>
</item> </item>
<item row="1" column="1">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLineEdit" name="searchEdit">
<property name="placeholderText">
<string>Search and filter...</string>
</property>
<property name="clearButtonEnabled">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton">
<property name="text">
<string>Search</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="browseButton">
<property name="toolTip">
<string>Select FTBApp instances directory</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="viewfolder">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="flat">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item row="0" column="1">
<widget class="QLabel" name="label">
<property name="text">
<string>Note: If your FTB instances are not in the default location, select it using the button next to search.</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<resources/> <resources/>

View File

@ -17,11 +17,13 @@
*/ */
#include "ListModel.h" #include "ListModel.h"
#include <qfileinfo.h>
#include <QDir> #include <QDir>
#include <QDirIterator> #include <QDirIterator>
#include <QFileInfo> #include <QFileInfo>
#include <QIcon> #include <QIcon>
#include <QProcessEnvironment> #include <QProcessEnvironment>
#include "Application.h"
#include "FileSystem.h" #include "FileSystem.h"
#include "StringUtils.h" #include "StringUtils.h"
#include "modplatform/import_ftb/PackHelpers.h" #include "modplatform/import_ftb/PackHelpers.h"
@ -29,7 +31,7 @@
namespace FTBImportAPP { namespace FTBImportAPP {
QString getPath() QString getStaticPath()
{ {
QString partialPath; QString partialPath;
#if defined(Q_OS_OSX) #if defined(Q_OS_OSX)
@ -42,14 +44,14 @@ QString getPath()
return FS::PathCombine(partialPath, ".ftba"); return FS::PathCombine(partialPath, ".ftba");
} }
const QString ListModel::FTB_APP_PATH = getPath(); static const QString FTB_APP_PATH = FS::PathCombine(getStaticPath(), "instances");
void ListModel::update() void ListModel::update()
{ {
beginResetModel(); beginResetModel();
modpacks.clear(); modpacks.clear();
QString instancesPath = FS::PathCombine(FTB_APP_PATH, "instances"); QString instancesPath = getPath();
if (auto instancesInfo = QFileInfo(instancesPath); instancesInfo.exists() && instancesInfo.isDir()) { if (auto instancesInfo = QFileInfo(instancesPath); instancesInfo.exists() && instancesInfo.isDir()) {
QDirIterator directoryIterator(instancesPath, QDir::Dirs | QDir::NoDotAndDotDot | QDir::Readable | QDir::Hidden, QDirIterator directoryIterator(instancesPath, QDir::Dirs | QDir::NoDotAndDotDot | QDir::Readable | QDir::Hidden,
QDirIterator::FollowSymlinks); QDirIterator::FollowSymlinks);
@ -168,4 +170,17 @@ FilterModel::Sorting FilterModel::getCurrentSorting()
{ {
return currentSorting; return currentSorting;
} }
void ListModel::setPath(QString path)
{
APPLICATION->settings()->set("FTBAppInstancesPath", path);
update();
}
QString ListModel::getPath()
{
auto path = APPLICATION->settings()->get("FTBAppInstancesPath").toString();
if (path.isEmpty() || !QFileInfo(path).exists())
path = FTB_APP_PATH;
return path;
}
} // namespace FTBImportAPP } // namespace FTBImportAPP

View File

@ -60,7 +60,8 @@ class ListModel : public QAbstractListModel {
void update(); void update();
static const QString FTB_APP_PATH; QString getPath();
void setPath(QString path);
private: private:
ModpackList modpacks; ModpackList modpacks;

View File

@ -35,11 +35,11 @@ ThemeCustomizationWidget::ThemeCustomizationWidget(QWidget* parent) : QWidget(pa
connect(ui->backgroundCatComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ThemeCustomizationWidget::applyCatTheme); connect(ui->backgroundCatComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ThemeCustomizationWidget::applyCatTheme);
connect(ui->iconsFolder, &QPushButton::clicked, this, connect(ui->iconsFolder, &QPushButton::clicked, this,
[] { DesktopServices::openDirectory(APPLICATION->themeManager()->getIconThemesFolder().path()); }); [] { DesktopServices::openPath(APPLICATION->themeManager()->getIconThemesFolder().path()); });
connect(ui->widgetStyleFolder, &QPushButton::clicked, this, connect(ui->widgetStyleFolder, &QPushButton::clicked, this,
[] { DesktopServices::openDirectory(APPLICATION->themeManager()->getApplicationThemesFolder().path()); }); [] { DesktopServices::openPath(APPLICATION->themeManager()->getApplicationThemesFolder().path()); });
connect(ui->catPackFolder, &QPushButton::clicked, this, connect(ui->catPackFolder, &QPushButton::clicked, this,
[] { DesktopServices::openDirectory(APPLICATION->themeManager()->getCatPacksFolder().path()); }); [] { DesktopServices::openPath(APPLICATION->themeManager()->getCatPacksFolder().path()); });
connect(ui->refreshButton, &QPushButton::clicked, this, &ThemeCustomizationWidget::refresh); connect(ui->refreshButton, &QPushButton::clicked, this, &ThemeCustomizationWidget::refresh);
} }

View File

@ -9,7 +9,8 @@ See [Package variants](#package-variants) for a list of available packages.
## Installing a development release (flake) ## Installing a development release (flake)
We use [garnix](https://garnix.io/) to build and cache our development builds. We use [garnix](https://garnix.io/) to build and cache our development builds.
If you want to avoid rebuilds you may add the garnix cache to your substitutors. If you want to avoid rebuilds you may add the garnix cache to your substitutors, or use `--accept-flake-config`
to temporarily enable it when using `nix` commands.
Example (NixOS): Example (NixOS):