Compare commits

..

84 commits

Author SHA1 Message Date
Alexandru Ionut Tripon 031015b332
chore(deps): update korthout/backport-action action to v4.4 (#5441)
Some checks are pending
Nix / Build (${{ matrix.system }}) (macos-26, aarch64-darwin) (push) Waiting to run
Nix / Build (${{ matrix.system }}) (ubuntu-22.04, x86_64-linux) (push) Waiting to run
Nix / Build (${{ matrix.system }}) (ubuntu-22.04-arm, aarch64-linux) (push) Waiting to run
2026-04-22 20:08:16 +00:00
Alexandru Ionut Tripon b4f34b87d6
chore(deps): update cachix/install-nix-action digest to 6165592 (#5325) 2026-04-22 20:07:58 +00:00
Alexandru Ionut Tripon de8ad56e60
chore(nix): update lockfile (#5307) 2026-04-22 20:07:36 +00:00
Alexandru Ionut Tripon b65f25fcfe
chore(deps): update hendrikmuhs/ccache-action action to v1.2.23 (#5440) 2026-04-22 19:03:01 +00:00
renovate[bot] f14701ffb7
chore(deps): update korthout/backport-action action to v4.4 2026-04-21 21:05:28 +00:00
renovate[bot] 672cd4d59c
chore(deps): update cachix/install-nix-action digest to 6165592 2026-04-21 21:05:23 +00:00
renovate[bot] a7c91796b3
chore(deps): update hendrikmuhs/ccache-action action to v1.2.23 2026-04-21 09:23:37 +00:00
Alexandru Ionut Tripon 5a9fdffd7d
chore(deps): update softprops/action-gh-release action to v3 (#5369) 2026-04-20 10:43:38 +00:00
Alexandru Ionut Tripon e154413b1d
ci(container): actually use amd64 runner for amd64 (#5436) 2026-04-20 06:35:22 +00:00
Seth Flynn 541e5ca9fe
ci(container): actually use amd64 runner for amd64
Signed-off-by: Seth Flynn <getchoo@tuta.io>
2026-04-20 01:06:38 -04:00
Alexandru Ionut Tripon 48f240703f
feat: add Manage Skins menu item to accounts button in MainWindow (#5414) 2026-04-19 20:44:25 +00:00
Alexandru Ionut Tripon b595488487
NetJob: do not automatically retry on 404 Not Found response (#5416) 2026-04-19 20:44:13 +00:00
Alexandru Ionut Tripon e7322a4507
fix world size uninitialized memory and UI refresh signal (#5418) 2026-04-19 20:44:06 +00:00
github-actions[bot] c67de94b3d chore(nix): update lockfile
Flake lock file updates:

• Updated input 'nixpkgs':
    'https://releases.nixos.org/nixos/unstable/nixos-26.05pre971119.8110df5ad7ab/nixexprs.tar.xz?narHash=sha256-D4ely1FsBcvtj/qSrNhSWpq%2BCUZKNiKwJIxpxnfy9o4%3D' (2026-03-28)
  → 'https://releases.nixos.org/nixos/unstable/nixos-26.05pre980183.4bd9165a9165/nixexprs.tar.xz?narHash=sha256-Gk2T0tDDDAs319hp/ak%2BbAIUG5bPMvnNEjPV8CS86Fg%3D' (2026-04-14)
2026-04-19 00:47:57 +00:00
Alexandru Ionut Tripon e7a03d311c
ProgressDialog: allow finished tasks to be re-displayed once restarted (#5412) 2026-04-17 14:00:07 +00:00
Alexandru Ionut Tripon af8225e2da
Improve checksum mismatch logging (#5413) 2026-04-17 13:59:58 +00:00
Alexandru Ionut Tripon 49e9f96327
Fixes for task abort logic (#5415) 2026-04-17 13:59:42 +00:00
captivator cbaf45084e fix world size uninitialized memory and UI refresh signal
Signed-off-by: captivator <84224501+qaptivator@users.noreply.github.com>
2026-04-17 15:28:12 +03:00
captivator 03799bf258
apply reviewer suggestion: use explicit MSA check again
Co-authored-by: Alexandru Ionut Tripon <alexandru.tripon97@gmail.com>
Signed-off-by: captivator <84224501+qaptivator@users.noreply.github.com>
2026-04-17 13:47:08 +03:00
captivator 4344f5eef9
apply reviewer suggestion: use explicit MSA check
Co-authored-by: Alexandru Ionut Tripon <alexandru.tripon97@gmail.com>
Signed-off-by: captivator <84224501+qaptivator@users.noreply.github.com>
2026-04-17 13:46:43 +03:00
Alexandru Ionut Tripon 4872ec634c
chore: bump develop version to 12.0.0 (#5339) 2026-04-17 07:12:35 +00:00
Octol1ttle 85613cfadc
Don't use new Qt method
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-17 11:12:46 +05:00
0x189D7997 4a59e6012d
NetJob: do not automatically retry on 404 response
Signed-off-by: 0x189D7997 <199489335+0x189D7997@users.noreply.github.com>
2026-04-17 03:45:33 +00:00
0x189D7997 ffded2ccac
Fix(NetJob): do not call emitAborted() when not running
Signed-off-by: 0x189D7997 <199489335+0x189D7997@users.noreply.github.com>
2026-04-17 02:56:04 +00:00
0x189D7997 15b39af92e
Fix(Task): check if task is still running before calling emitAborted()
Signed-off-by: 0x189D7997 <199489335+0x189D7997@users.noreply.github.com>
2026-04-17 02:11:06 +00:00
0x189D7997 4ed3aa1f1c
Fix(InstanceCreationTask): propagate abort signal to super
Signed-off-by: 0x189D7997 <199489335+0x189D7997@users.noreply.github.com>
2026-04-17 02:07:09 +00:00
captivator 7d0d9a3827 feat: add Manage Skins menu item to accounts button in MainWindow
Assisted-by: Gemini:3-Flash
Signed-off-by: captivator <84224501+qaptivator@users.noreply.github.com>
2026-04-17 04:15:48 +03:00
Octol1ttle b9fa4ffc00
fix(ProgressDialog): allow finished tasks to be re-displayed once restarted
Cherry-picked from libcurl (lmao)

Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-16 22:04:43 +05:00
Tayou f40cbf816e
fix text overlap in project item views (#5406) 2026-04-16 09:45:45 +00:00
Octol1ttle 3ee45691ab
change: improve checksum mismatch logging
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-16 11:32:58 +05:00
Alexandru Ionut Tripon 8a68c625fb
Windows installer: Disable skipping files (#5385) 2026-04-15 21:08:25 +00:00
Alexandru Ionut Tripon 8eb9a9971b
Search by project id (#) improvement (#5303) 2026-04-15 20:41:57 +00:00
Alexandru Ionut Tripon 44e3ae59e4
Low RAM warning fixes (#5392) 2026-04-15 20:39:08 +00:00
Alexandru Ionut Tripon 8901da68c7
chore(deps): update actions/cache action to v5.0.5 (#5386) 2026-04-15 20:38:24 +00:00
so5iso4ka fa54329711
fix text overlap in project item views
Signed-off-by: so5iso4ka <so5iso4ka@icloud.com>
2026-04-15 22:44:17 +03:00
renovate[bot] cddbb0e970
chore(deps): update softprops/action-gh-release action to v3 2026-04-15 09:43:32 +00:00
renovate[bot] da50f0e9e3
chore(deps): update actions/cache action to v5.0.5 2026-04-15 09:43:28 +00:00
0x189D7997 28c42d04b6
Limit normal search fallback to 404 respnse
Signed-off-by: 0x189D7997 <199489335+0x189D7997@users.noreply.github.com>
2026-04-14 23:51:34 +00:00
Octol1ttle 5d9622db21
Use newlines more often in macOS dialog for a nicer look
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-14 23:24:10 +05:00
Alexandru Ionut Tripon 7e8db63882
Fix infinite loop in SkinManageDialog (#5388) 2026-04-14 16:30:03 +00:00
Alexandru Ionut Tripon 519d8f7385
add a option to skip meta refresh on launch (#5267) 2026-04-14 16:29:30 +00:00
Alexandru Ionut Tripon ece83eb637
fix: force metadata version list refreshes to reload (#5349) 2026-04-14 16:29:05 +00:00
Alexandru Ionut Tripon 06282c0363
fix pessimizing-move warning (#5361) 2026-04-14 16:28:01 +00:00
Alexandru Ionut Tripon a0c5893a98
Task: Warn when disposing while running (#5371) 2026-04-14 16:27:24 +00:00
Alexandru Ionut Tripon fbec685eb5
Fix Copy/Upload buttons not working in ScreenshotsPage (#5387) 2026-04-14 16:26:12 +00:00
Octol1ttle 0b578fa767
fix(EnsureAvailableMemory/macOS): warn based on memory pressure rather than available RAM
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-14 18:58:47 +05:00
Octol1ttle ae331cfc9a
change(EnsureAvailableMemory): rephrase warning message
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-14 18:43:52 +05:00
Octol1ttle 575be16d3e
fix(EnsureAvailableMemory): do not warn if available memory could not be read
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-14 12:47:48 +05:00
Octol1ttle d5db0c6c1b
fix(SkinList): do not consider non-png files correctly
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-14 00:39:34 +05:00
Octol1ttle 1fec781251
fix(ScreenshotsPage): fix QString::arg in string with no arguments
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-13 23:44:13 +05:00
Octol1ttle 08de904e21
fix(ScreenshotsPage): disable "Copy Image" when selecting multiple screenshots
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-13 23:44:13 +05:00
Octol1ttle 7a1d2e41a1
refactor(ScreenshotsPage): clang-tidy
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-13 23:44:13 +05:00
Octol1ttle 1b650622ea
fix(ScreenshotsPage): use correct selection collection
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-13 22:57:13 +05:00
Octol1ttle 88035b9815
change(Windows installer): disable skipping files
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-13 21:05:30 +05:00
Octol1ttle ae7e143537
change(Task): warn when disposing while running
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-12 13:48:15 +05:00
DioEgizio b230645d53
Updater: Do not reset current task in finished signal (#5370) 2026-04-12 08:22:19 +00:00
Octol1ttle 9b270f783e
fix(updater): do not reset current task in finished signal
The order of signals in case of a success is "succeeded"->"finished"

The "succeeded" signal may launch another download if the updater needs to fetch more pages
But if we reset the task then the newly started download will be disposed and the updater will softlock

Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-12 13:02:46 +05:00
Andrei Damian ac54df366b fix pessimizing-move warning
Signed-off-by: Andrei Damian <andreidaamian@gmail.com>
2026-04-11 12:08:06 +03:00
Alexandru Ionut Tripon a17a45c748
enable modpack changelog for modrinth page (#5354) 2026-04-11 05:19:09 +00:00
Alexandru Ionut Tripon a488eb6d5d
fix pack upgrade (#5345) 2026-04-10 17:04:55 +00:00
Trial97 f3ff0a730a enable modpack changelog for modrinth page
Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
2026-04-10 19:29:56 +03:00
Alexandru Ionut Tripon 966ecd00bd
Allow disabling low RAM warning (#5333) 2026-04-10 09:14:47 +00:00
Octol1ttle 4b3aedd5d0
Change LowMemWarning default to always enabled
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-10 11:53:37 +05:00
morsz 2219c37d7f
fix: force metadata version list refreshes to reload
manual refreshes on version selection screens could reuse cached metadata and skip downloading updated manifests, so new versions would not appear until Prism was restarted. pass an explicit forced reload through the shared version list loading path and use it from refresh actions so manual refresh always reloads metadata

Signed-off-by: morsz <morsz@morsz.dev>
2026-04-10 02:19:28 +02:00
Trial97 b7344af313 fix pack upgrade
Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
2026-04-10 00:36:05 +03:00
Trial97 9bccda0a79 chore: bump develop version to 12.0.0
Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
2026-04-09 23:32:02 +03:00
Alexandru Ionut Tripon 013bb5cac3
fix McClient (#5332) 2026-04-09 20:23:53 +00:00
Alexandru Ionut Tripon e8afd48c67
Don't count JAR mods when checking offline libraries (#5334) 2026-04-09 20:18:41 +00:00
Alexandru Ionut Tripon 2ef22124cd
CI/Nix: Bump macOS (#5335) 2026-04-09 20:16:54 +00:00
Alexandru Ionut Tripon 6b9d2dbb64
fix(PrintInstanceInfo): add break before OS info (#5336) 2026-04-09 20:16:13 +00:00
Octol1ttle 658a1391f8
change(EnsureAvailableMemory): add lenience
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-09 22:35:32 +05:00
Octol1ttle 4cf8cf7d18
fix(PrintInstanceInfo): add break before OS info
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-09 20:53:42 +05:00
Octol1ttle 724c9a4a2c
fix(CI/nix): bump macOS
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-09 20:40:22 +05:00
Octol1ttle ec4484282c
fix: don't count JAR mods when checking offline libraries
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-09 20:31:39 +05:00
Octol1ttle c044ed36af
feat: allow disabling low RAM warning
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-09 19:34:11 +05:00
Octol1ttle 91616ae9b6
refactor: McClient
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-09 19:03:35 +05:00
Octol1ttle 2fe0569bd6
fix(McClient): do not use unsigned type for response length
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-09 17:41:25 +05:00
0x189D7997 364968a6b4
Use network_error_code from callbacks
Signed-off-by: 0x189D7997 <199489335+0x189D7997@users.noreply.github.com>
2026-04-07 12:57:09 +00:00
0x189D7997 fdd1a5dde8
oops forgot again
Signed-off-by: 0x189D7997 <199489335+0x189D7997@users.noreply.github.com>
2026-04-04 13:35:06 +00:00
0x189D7997 4151db6c94
Fallback to normal search on error and apply same changes to ResourceModel
Signed-off-by: 0x189D7997 <199489335+0x189D7997@users.noreply.github.com>
2026-04-04 13:12:23 +00:00
0x189D7997 4706f894e3
Activate search by project id only for numarical values for CurseForge
Signed-off-by: 0x189D7997 <199489335+0x189D7997@users.noreply.github.com>
2026-04-04 07:29:19 +00:00
0x189D7997 bf75d50baf
Make search by id fail quietly
Signed-off-by: 0x189D7997 <199489335+0x189D7997@users.noreply.github.com>
2026-04-04 07:27:29 +00:00
0x189D7997 983bf34807
Allow requesting project info without manual retry on fail
Signed-off-by: 0x189D7997 <199489335+0x189D7997@users.noreply.github.com>
2026-04-04 07:17:58 +00:00
Trial97 40b7cab3ed
add a option to skip meta refresh on launch
related to https://github.com/PrismLauncher/PrismLauncher/issues/3785
It doesn't fix it but it should at least allow users to skip the
redownload of the meta files.
So in a previous PR I added an automated way to refresh all the meta
from the original index, to the component index to the actual index.

Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
2026-03-27 00:23:23 +02:00
67 changed files with 750 additions and 522 deletions

View file

@ -55,7 +55,7 @@ runs:
# TODO(@getchoo): Get this working on MSYS2!
- name: Setup ccache
if: ${{ (runner.os != 'Windows' || inputs.msystem == '') && inputs.build-type == 'Debug' }}
uses: hendrikmuhs/ccache-action@v1.2.22
uses: hendrikmuhs/ccache-action@v1.2.23
with:
variant: sccache
create-symlink: ${{ runner.os != 'Windows' }}

View file

@ -91,7 +91,7 @@ runs:
- name: Retrieve ccache cache (MinGW)
if: ${{ inputs.msystem != '' && inputs.build-type == 'Debug' }}
uses: actions/cache@v5.0.4
uses: actions/cache@v5.0.5
with:
path: '${{ github.workspace }}\.ccache'
key: ${{ runner.os }}-mingw-w64-ccache-${{ github.run_id }}

View file

@ -24,7 +24,7 @@ jobs:
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Create backport PRs
uses: korthout/backport-action@v4.3.0
uses: korthout/backport-action@v4.4
with:
# Config README: https://github.com/korthout/backport-action#backport-action
pull_description: |-

View file

@ -29,7 +29,7 @@ jobs:
submodules: "true"
- name: Setup sccache
uses: hendrikmuhs/ccache-action@v1.2.22
uses: hendrikmuhs/ccache-action@v1.2.23
with:
variant: sccache

View file

@ -33,7 +33,7 @@ jobs:
- arch: arm64
os: ubuntu-24.04-arm
- arch: amd64
os: ubuntu-24.04-arm
os: ubuntu-24.04
runs-on: ${{ matrix.os }}

View file

@ -88,7 +88,7 @@ jobs:
- os: ubuntu-22.04-arm
system: aarch64-linux
- os: macos-14
- os: macos-26
system: aarch64-darwin
runs-on: ${{ matrix.os }}

View file

@ -94,7 +94,7 @@ jobs:
- name: Create release
id: create_release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
with:
token: ${{ secrets.GITHUB_TOKEN }}
tag_name: ${{ github.ref }}

View file

@ -20,7 +20,7 @@ jobs:
steps:
- uses: actions/checkout@v6
- uses: cachix/install-nix-action@96951a368ba55167b55f1c916f7d416bac6505fe # v31
- uses: cachix/install-nix-action@616559265b40713947b9c190a8ff4b507b5df49b # v31
- uses: DeterminateSystems/update-flake-lock@v28
with:

View file

@ -179,7 +179,7 @@ set(Launcher_LOGIN_CALLBACK_URL "https://prismlauncher.org/successful-login" CAC
set(Launcher_LEGACY_FMLLIBS_BASE_URL "https://files.prismlauncher.org/fmllibs/" CACHE STRING "URL for legacy (<=1.5.2) FML Libraries.")
######## Set version numbers ########
set(Launcher_VERSION_MAJOR 11)
set(Launcher_VERSION_MAJOR 12)
set(Launcher_VERSION_MINOR 0)
set(Launcher_VERSION_PATCH 0)

View file

@ -18,11 +18,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1774709303,
"narHash": "sha256-D4ely1FsBcvtj/qSrNhSWpq+CUZKNiKwJIxpxnfy9o4=",
"rev": "8110df5ad7abf5d4c0f6fb0f8f978390e77f9685",
"lastModified": 1776169885,
"narHash": "sha256-Gk2T0tDDDAs319hp/ak+bAIUG5bPMvnNEjPV8CS86Fg=",
"rev": "4bd9165a9165d7b5e33ae57f3eecbcb28fb231c9",
"type": "tarball",
"url": "https://releases.nixos.org/nixos/unstable/nixos-26.05pre971119.8110df5ad7ab/nixexprs.tar.xz"
"url": "https://releases.nixos.org/nixos/unstable/nixos-26.05pre980183.4bd9165a9165/nixexprs.tar.xz"
},
"original": {
"type": "tarball",

View file

@ -735,6 +735,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_settings->registerSetting({ "MinMemAlloc", "MinMemoryAlloc" }, 512);
m_settings->registerSetting({ "MaxMemAlloc", "MaxMemoryAlloc" }, SysInfo::defaultMaxJvmMem());
m_settings->registerSetting("PermGen", 128);
m_settings->registerSetting("LowMemWarning", true);
// Java Settings
m_settings->registerSetting("JavaPath", "");
@ -870,6 +871,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
resetIfInvalid(m_settings->registerSetting("LegacyFMLLibsURLOverride", "").get());
}
m_settings->registerSetting("MetaRefreshOnLaunch", true);
m_settings->registerSetting("CloseAfterLaunch", false);
m_settings->registerSetting("QuitAfterGameStop", false);

View file

@ -63,7 +63,7 @@ class BaseVersionList : public QAbstractListModel {
* The task returned by this function should reset the model when it's done.
* \return A pointer to a task that reloads the version list.
*/
virtual Task::Ptr getLoadTask() = 0;
virtual Task::Ptr getLoadTask(bool forceReload = false) = 0;
//! Checks whether or not the list is loaded. If this returns false, the list should be
// loaded.

View file

@ -141,7 +141,6 @@ uint64_t HardwareInfo::availableRamMiB()
}
#elif defined(Q_OS_MACOS)
#include "mach/mach.h"
#include "sys/sysctl.h"
QString HardwareInfo::cpuInfo()
@ -171,18 +170,32 @@ uint64_t HardwareInfo::totalRamMiB()
uint64_t HardwareInfo::availableRamMiB()
{
mach_port_t host_port = mach_host_self();
mach_msg_type_number_t count = HOST_VM_INFO64_COUNT;
return 0;
}
vm_statistics64_data_t vm_stats;
if (host_statistics64(host_port, HOST_VM_INFO64, reinterpret_cast<host_info64_t>(&vm_stats), &count) == KERN_SUCCESS) {
// transforming bytes -> mib
return (vm_stats.free_count + vm_stats.inactive_count) * vm_page_size / 1024 / 1024;
MacOSHardwareInfo::MemoryPressureLevel MacOSHardwareInfo::memoryPressureLevel()
{
uint32_t level;
size_t levelSize = sizeof level;
if (sysctlbyname("kern.memorystatus_vm_pressure_level", &level, &levelSize, nullptr, 0) == 0) {
return static_cast<MemoryPressureLevel>(level);
}
qWarning() << "Could not get available RAM: host_statistics64";
return 0;
qWarning() << "Could not get memory pressure level: sysctlbyname";
return MemoryPressureLevel::Normal;
}
QString MacOSHardwareInfo::memoryPressureLevelName()
{
// The names are internal, users refer to levels by their graph colors in Activity Monitor
switch (memoryPressureLevel()) {
case MemoryPressureLevel::Normal:
return "Green";
case MemoryPressureLevel::Warning:
return "Yellow";
case MemoryPressureLevel::Critical:
return "Red";
}
}
#elif defined(Q_OS_LINUX)

View file

@ -27,3 +27,16 @@ uint64_t totalRamMiB();
uint64_t availableRamMiB();
QStringList gpuInfo();
} // namespace HardwareInfo
#ifdef Q_OS_MACOS
namespace MacOSHardwareInfo {
enum class MemoryPressureLevel : uint8_t {
Normal = 1,
Warning = 2,
Critical = 4,
};
MemoryPressureLevel memoryPressureLevel();
QString memoryPressureLevelName();
} // namespace MacOSHardwareInfo
#endif

View file

@ -18,7 +18,7 @@ bool InstanceCreationTask::abort()
return m_gameFilesTask->abort();
}
return true;
return InstanceTask::abort();
}
void InstanceCreationTask::executeTask()

View file

@ -924,23 +924,23 @@ class InstanceStaging : public Task {
connect(child, &Task::progress, this, &InstanceStaging::setProgress);
connect(child, &Task::stepProgress, this, &InstanceStaging::propagateStepProgress);
connect(&m_backoffTimer, &QTimer::timeout, this, &InstanceStaging::childSucceeded);
m_backoffTimer.setSingleShot(true);
}
virtual ~InstanceStaging() {}
~InstanceStaging() override = default;
// FIXME/TODO: add ability to abort during instance commit retries
bool abort() override
{
if (!canAbort())
if (!canAbort()) {
return false;
}
return m_child->abort();
}
bool canAbort() const override { return (m_child && m_child->canAbort()); }
protected:
virtual void executeTask() override
void executeTask() override
{
if (m_stagingPath.isNull()) {
emitFailed(tr("Could not create staging folder"));
@ -954,10 +954,8 @@ class InstanceStaging : public Task {
private slots:
void childSucceeded()
{
if (!isRunning())
return;
unsigned sleepTime = backoff();
if (m_parent->commitStagedInstance(m_stagingPath, *m_child.get(), m_child->group(), *m_child.get())) {
if (m_parent->commitStagedInstance(m_stagingPath, *m_child, m_child->group(), *m_child)) {
m_backoffTimer.stop();
emitSucceeded();
return;

View file

@ -51,8 +51,9 @@ JavaInstallList::JavaInstallList(QObject* parent, bool onlyManagedVersions)
: BaseVersionList(parent), m_only_managed_versions(onlyManagedVersions)
{}
Task::Ptr JavaInstallList::getLoadTask()
Task::Ptr JavaInstallList::getLoadTask(bool forceReload)
{
Q_UNUSED(forceReload)
load();
return getCurrentTask();
}

View file

@ -35,7 +35,7 @@ class JavaInstallList : public BaseVersionList {
public:
explicit JavaInstallList(QObject* parent = 0, bool onlyManagedVersions = false);
Task::Ptr getLoadTask() override;
Task::Ptr getLoadTask(bool forceReload = false) override;
bool isLoaded() override;
const BaseVersion::Ptr at(int i) const override;
int count() const override;

View file

@ -82,12 +82,12 @@ QUrl BaseEntity::url() const
return QUrl(metaOverride).resolved(localFilename());
}
Task::Ptr BaseEntity::loadTask(Net::Mode mode)
Task::Ptr BaseEntity::loadTask(Net::Mode mode, bool forceReload)
{
if (m_task && m_task->isRunning()) {
return m_task;
}
m_task.reset(new BaseEntityLoadTask(this, mode));
m_task.reset(new BaseEntityLoadTask(this, mode, forceReload));
return m_task;
}
@ -107,7 +107,9 @@ BaseEntity::LoadStatus BaseEntity::status() const
return m_load_status;
}
BaseEntityLoadTask::BaseEntityLoadTask(BaseEntity* parent, Net::Mode mode) : m_entity(parent), m_mode(mode) {}
BaseEntityLoadTask::BaseEntityLoadTask(BaseEntity* parent, Net::Mode mode, bool forceReload)
: m_entity(parent), m_mode(mode), m_force_reload(forceReload)
{}
void BaseEntityLoadTask::executeTask()
{
@ -125,9 +127,11 @@ void BaseEntityLoadTask::executeTask()
}
// on online the hash needs to match
hashMatches = m_entity->m_sha256 == m_entity->m_file_sha256;
const auto& expected = m_entity->m_sha256;
const auto& actual = m_entity->m_file_sha256;
hashMatches = expected == actual;
if (m_mode == Net::Mode::Online && !m_entity->m_sha256.isEmpty() && !hashMatches) {
throw Exception("mismatched checksum");
throw Exception(QString("Checksum mismatch, expected sha256: %1, got: %2").arg(expected, actual));
}
// load local file
@ -149,13 +153,18 @@ void BaseEntityLoadTask::executeTask()
auto wasLoadedOffline = m_entity->m_load_status != BaseEntity::LoadStatus::NotLoaded && m_mode == Net::Mode::Offline;
// if has is not present allways fetch from remote(e.g. the main index file), else only fetch if hash doesn't match
auto wasLoadedRemote = m_entity->m_sha256.isEmpty() ? m_entity->m_load_status == BaseEntity::LoadStatus::Remote : hashMatches;
if (wasLoadedOffline || wasLoadedRemote) {
if (wasLoadedOffline || (wasLoadedRemote && !m_force_reload)) {
emitSucceeded();
return;
}
m_task.reset(new NetJob(QObject::tr("Download of meta file %1").arg(m_entity->localFilename()), APPLICATION->network()));
auto url = m_entity->url();
auto entry = APPLICATION->metacache()->resolveEntry("meta", m_entity->localFilename());
if (m_force_reload) {
// clear validators so manual refreshes fetch a fresh body
entry->setETag({});
entry->setRemoteChangedTimestamp({});
}
entry->setStale(true);
auto dl = Net::ApiDownload::makeCached(url, entry);
/*

View file

@ -43,7 +43,7 @@ class BaseEntity {
void setSha256(QString sha256);
virtual void parse(const QJsonObject& obj) = 0;
[[nodiscard]] Task::Ptr loadTask(Net::Mode loadType = Net::Mode::Online);
[[nodiscard]] Task::Ptr loadTask(Net::Mode loadType = Net::Mode::Online, bool forceReload = false);
protected:
QString m_sha256; // the expected sha256
@ -58,7 +58,7 @@ class BaseEntityLoadTask : public Task {
Q_OBJECT
public:
explicit BaseEntityLoadTask(BaseEntity* parent, Net::Mode mode);
explicit BaseEntityLoadTask(BaseEntity* parent, Net::Mode mode, bool forceReload);
~BaseEntityLoadTask() override = default;
virtual void executeTask() override;
@ -68,6 +68,7 @@ class BaseEntityLoadTask : public Task {
private:
BaseEntity* m_entity;
Net::Mode m_mode;
bool m_force_reload = false;
NetJob::Ptr m_task;
};
} // namespace Meta

View file

@ -15,6 +15,7 @@
#include "Index.h"
#include "Application.h"
#include "JsonFormat.h"
#include "QObjectPtr.h"
#include "VersionList.h"
@ -135,7 +136,7 @@ void Index::connectVersionList(const int row, const VersionList::Ptr& list)
Task::Ptr Index::loadVersion(const QString& uid, const QString& version, Net::Mode mode, bool force)
{
if (mode == Net::Mode::Offline) {
if (mode == Net::Mode::Offline || !APPLICATION->settings()->get("MetaRefreshOnLaunch").toBool()) {
return get(uid, version)->loadTask(mode);
}

View file

@ -32,11 +32,11 @@ VersionList::VersionList(const QString& uid, QObject* parent) : BaseVersionList(
setObjectName("Version list: " + uid);
}
Task::Ptr VersionList::getLoadTask()
Task::Ptr VersionList::getLoadTask(bool forceReload)
{
auto loadTask = makeShared<SequentialTask>(tr("Load meta for %1", "This is for the task name that loads the meta index.").arg(m_uid));
loadTask->addTask(APPLICATION->metadataIndex()->loadTask(Net::Mode::Online));
loadTask->addTask(this->loadTask(Net::Mode::Online));
loadTask->addTask(APPLICATION->metadataIndex()->loadTask(Net::Mode::Online, forceReload));
loadTask->addTask(this->loadTask(Net::Mode::Online, forceReload));
return loadTask;
}

View file

@ -37,7 +37,7 @@ class VersionList : public BaseVersionList, public BaseEntity {
enum Roles { UidRole = Qt::UserRole + 100, TimeRole, RequiresRole, VersionPtrRole };
bool isLoaded() override;
Task::Ptr getLoadTask() override;
Task::Ptr getLoadTask(bool forceReload = false) override;
const BaseVersion::Ptr at(int i) const override;
int count() const override;
void sortVersions() override;

View file

@ -349,7 +349,8 @@ void LaunchProfile::getLibraryFiles(const RuntimeContext& runtimeContext,
QStringList& jars,
QStringList& nativeJars,
const QString& overridePath,
const QString& tempPath) const
const QString& tempPath,
bool addJarMods) const
{
QStringList native32, native64;
jars.clear();
@ -360,7 +361,7 @@ void LaunchProfile::getLibraryFiles(const RuntimeContext& runtimeContext,
// NOTE: order is important here, add main jar last to the lists
if (m_mainJar) {
// FIXME: HACK!! jar modding is weird and unsystematic!
if (m_jarMods.size()) {
if (m_jarMods.size() && addJarMods) {
QDir tempDir(tempPath);
jars.append(tempDir.absoluteFilePath("minecraft.jar"));
} else {

View file

@ -87,7 +87,8 @@ class LaunchProfile : public ProblemProvider {
QStringList& jars,
QStringList& nativeJars,
const QString& overridePath,
const QString& tempPath) const;
const QString& tempPath,
bool addJarMods = true) const;
bool hasTrait(const QString& trait) const;
ProblemSeverity getProblemSeverity() const override;
const QList<PatchProblem> getProblems() const override;

View file

@ -149,7 +149,7 @@ QList<Net::NetRequest::Ptr> Library::getDownloads(const RuntimeContext& runtimeC
if (sha1.size()) {
auto dl = Net::ApiDownload::makeCached(url, entry, options);
dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, sha1));
qDebug() << "Checksummed Download for:" << rawName().serialize() << "storage:" << storage << "url:" << url;
qDebug() << "Checksummed Download for:" << rawName().serialize() << "storage:" << storage << "url:" << url << "expected sha1:" << sha1;
out.append(dl);
} else {
out.append(Net::ApiDownload::makeCached(url, entry, options));

View file

@ -209,6 +209,7 @@ void MinecraftInstance::loadSpecificSettings()
m_settings->registerOverride(global_settings->getSetting("MinMemAlloc"), memorySetting);
m_settings->registerOverride(global_settings->getSetting("MaxMemAlloc"), memorySetting);
m_settings->registerOverride(global_settings->getSetting("PermGen"), memorySetting);
m_settings->registerOverride(global_settings->getSetting("LowMemWarning"), memorySetting);
// Native library workarounds
auto nativeLibraryWorkaroundsOverride = m_settings->registerSetting("OverrideNativeWorkarounds", false);

View file

@ -157,7 +157,8 @@ bool WorldList::resetIcon(int row)
return false;
World& m = m_worlds[row];
if (m.resetIcon()) {
emit dataChanged(index(row), index(row), { WorldList::IconFileRole });
QModelIndex modelIndex = index(row, NameColumn);
emit dataChanged(modelIndex, modelIndex, { WorldList::IconFileRole });
return true;
}
return false;
@ -426,7 +427,7 @@ void WorldList::loadWorldsAsync()
m_worlds[row].setSize(size);
// Notify views
QModelIndex modelIndex = index(row);
QModelIndex modelIndex = index(row, SizeColumn);
emit dataChanged(modelIndex, modelIndex, { SizeRole });
}
},

View file

@ -25,26 +25,81 @@ EnsureAvailableMemory::EnsureAvailableMemory(LaunchTask* parent, MinecraftInstan
void EnsureAvailableMemory::executeTask()
{
const uint64_t available = HardwareInfo::availableRamMiB();
const uint64_t min = m_instance->settings()->get("MinMemAlloc").toUInt();
const uint64_t max = m_instance->settings()->get("MaxMemAlloc").toUInt();
const uint64_t required = std::max(min, max);
#ifdef Q_OS_MACOS
QString text;
switch (MacOSHardwareInfo::memoryPressureLevel()) {
case MacOSHardwareInfo::MemoryPressureLevel::Normal:
emitSucceeded();
return;
case MacOSHardwareInfo::MemoryPressureLevel::Warning:
text =
tr("The system is under increased memory pressure.\n"
"This may lead to lag or slowdowns.\n"
"If possible, close other applications before continuing.\n\n"
"Launch anyway?");
break;
case MacOSHardwareInfo::MemoryPressureLevel::Critical:
text =
tr("Your system is under critical memory pressure.\n"
"This may lead to severe slowdowns, crashes or system instability.\n"
"It is recommended to close other applications or restart your system.\n\n"
"Launch anyway?");
break;
}
if (required > available) {
auto* dialog = CustomMessageBox::selectable(
nullptr, tr("Not enough RAM"),
tr("There is not enough RAM available to launch this instance with the current memory settings.\n\n"
"Required: %1 MiB\nAvailable: %2 MiB\n\n"
"Continue anyway? This may cause slowdowns in the game and your system.")
.arg(required)
.arg(available),
QMessageBox::Icon::Warning, QMessageBox::StandardButton::Yes | QMessageBox::StandardButton::No,
QMessageBox::StandardButton::No);
const auto response = dialog->exec();
bool shouldAbort = false;
if (m_instance->settings()->get("LowMemWarning").toBool()) {
auto* dialog = CustomMessageBox::selectable(nullptr, tr("High memory pressure"), text, QMessageBox::Icon::Warning,
QMessageBox::StandardButton::Yes | QMessageBox::StandardButton::No,
QMessageBox::StandardButton::No);
shouldAbort = dialog->exec() == QMessageBox::No;
dialog->deleteLater();
}
const auto message = tr("The system is under high memory pressure");
if (shouldAbort) {
emit logLine(message, MessageLevel::Fatal);
emitFailed(message);
return;
}
emit logLine(message, MessageLevel::Warning);
emitSucceeded();
#else
const uint64_t available = HardwareInfo::availableRamMiB();
if (available == 0) {
// could not read
emitSucceeded();
return;
}
const uint64_t settingMin = m_instance->settings()->get("MinMemAlloc").toUInt();
const uint64_t settingMax = m_instance->settings()->get("MaxMemAlloc").toUInt();
const uint64_t max = std::max(settingMin, settingMax);
if (static_cast<double>(max) * 0.9 > static_cast<double>(available)) {
bool shouldAbort = false;
if (m_instance->settings()->get("LowMemWarning").toBool()) {
auto* dialog = CustomMessageBox::selectable(
nullptr, tr("Low free memory"),
tr("There might not be enough free RAM to launch this instance with the current memory settings.\n\n"
"Maximum allocated: %1 MiB\nFree: %2 MiB (out of %3 MiB total)\n\n"
"Launch anyway? This may cause slowdowns in the game and your system.")
.arg(max)
.arg(available)
.arg(HardwareInfo::totalRamMiB()),
QMessageBox::Icon::Warning, QMessageBox::StandardButton::Yes | QMessageBox::StandardButton::No,
QMessageBox::StandardButton::No);
shouldAbort = dialog->exec() == QMessageBox::No;
dialog->deleteLater();
}
const auto message = tr("Not enough RAM available to launch this instance");
if (response == QMessageBox::No) {
if (shouldAbort) {
emit logLine(message, MessageLevel::Fatal);
emitFailed(message);
return;
@ -54,4 +109,5 @@ void EnsureAvailableMemory::executeTask()
}
emitSucceeded();
#endif
}

View file

@ -27,16 +27,27 @@ void EnsureOfflineLibraries::executeTask()
{
const auto profile = m_instance->getPackProfile()->getProfile();
QStringList allJars;
profile->getLibraryFiles(m_instance->runtimeContext(), allJars, allJars, m_instance->getLocalLibraryPath(), m_instance->binRoot());
profile->getLibraryFiles(m_instance->runtimeContext(), allJars, allJars, m_instance->getLocalLibraryPath(), m_instance->binRoot(),
false);
QStringList missing;
for (const auto& jar : allJars) {
if (!QFileInfo::exists(jar)) {
emit logLine(tr("This instance cannot be launched because some libraries are missing or have not been downloaded yet. Please "
"try again in online mode with a working Internet connection"),
MessageLevel::Fatal);
emitFailed("Required libraries are missing");
return;
missing.append(jar);
}
}
emitSucceeded();
if (missing.isEmpty()) {
emitSucceeded();
return;
}
emit logLine("Missing libraries:", MessageLevel::Error);
for (const auto& jar : missing) {
emit logLine(" " + jar, MessageLevel::Error);
}
emit logLine(tr("\nThis instance cannot be launched because some libraries are missing or have not been downloaded yet. Please "
"try again in online mode with a working Internet connection"),
MessageLevel::Fatal);
emitFailed("Required libraries are missing");
}

View file

@ -61,13 +61,19 @@ void PrintInstanceInfo::executeTask()
auto instance = m_parent->instance();
QStringList log;
log << "";
log << "OS: " + QString("%1 | %2 | %3").arg(QSysInfo::prettyProductName(), QSysInfo::kernelType(), QSysInfo::kernelVersion());
#ifdef Q_OS_FREEBSD
::runSysctlHwModel(log);
::runPciconf(log);
#else
log << "CPU: " + HardwareInfo::cpuInfo();
#ifdef Q_OS_MACOS
log << "Memory pressure level: " + MacOSHardwareInfo::memoryPressureLevelName();
#else
log << QString("RAM: %1 MiB (available: %2 MiB)").arg(HardwareInfo::totalRamMiB()).arg(HardwareInfo::availableRamMiB());
#endif
#endif
log.append(HardwareInfo::gpuInfo());
log << "";

View file

@ -121,7 +121,7 @@ bool SkinList::update()
auto folderContents = m_dir.entryInfoList();
// if there are any untracked files...
for (QFileInfo entry : folderContents) {
if (!entry.isFile() && entry.suffix() != "png")
if (!entry.isFile() || entry.suffix() != "png")
continue;
SkinModel w(entry.absoluteFilePath());

View file

@ -148,9 +148,9 @@ Task::Ptr ResourceAPI::getProjectVersions(VersionSearchArgs&& args, Callback<QVe
return netJob;
}
Task::Ptr ResourceAPI::getProjectInfo(ProjectInfoArgs&& args, Callback<ModPlatform::IndexedPack::Ptr>&& callbacks) const
Task::Ptr ResourceAPI::getProjectInfo(ProjectInfoArgs&& args, Callback<ModPlatform::IndexedPack::Ptr>&& callbacks, bool askRetry) const
{
auto [job, response] = getProject(args.pack->addonId.toString());
auto [job, response] = getProject(args.pack->addonId.toString(), askRetry);
QObject::connect(job.get(), &NetJob::succeeded, [this, response, callbacks, args] {
auto pack = args.pack;
@ -284,7 +284,7 @@ QString ResourceAPI::mapMCVersionToModrinth(Version v) const
return verStr;
}
std::pair<Task::Ptr, QByteArray*> ResourceAPI::getProject(QString addonId) const
std::pair<Task::Ptr, QByteArray*> ResourceAPI::getProject(QString addonId, bool askRetry) const
{
auto project_url_optional = getInfoURL(addonId);
if (!project_url_optional.has_value())
@ -293,6 +293,7 @@ std::pair<Task::Ptr, QByteArray*> ResourceAPI::getProject(QString addonId) const
auto project_url = project_url_optional.value();
auto netJob = makeShared<NetJob>(QString("%1::GetProject").arg(addonId), APPLICATION->network());
netJob->setAskRetry(askRetry);
auto [action, response] = Net::ApiDownload::makeByteArray(QUrl(project_url));
netJob->addNetAction(action);

View file

@ -115,10 +115,10 @@ class ResourceAPI {
public slots:
virtual Task::Ptr searchProjects(SearchArgs&&, Callback<QList<ModPlatform::IndexedPack::Ptr>>&&) const;
virtual std::pair<Task::Ptr, QByteArray*> getProject(QString addonId) const;
virtual std::pair<Task::Ptr, QByteArray*> getProject(QString addonId, bool askRetry = true) const;
virtual std::pair<Task::Ptr, QByteArray*> getProjects(QStringList addonIds) const = 0;
virtual Task::Ptr getProjectInfo(ProjectInfoArgs&&, Callback<ModPlatform::IndexedPack::Ptr>&&) const;
virtual Task::Ptr getProjectInfo(ProjectInfoArgs&&, Callback<ModPlatform::IndexedPack::Ptr>&&, bool askRetry = true) const;
Task::Ptr getProjectVersions(VersionSearchArgs&& args, Callback<QVector<ModPlatform::IndexedVersion>>&& callbacks) const;
virtual Task::Ptr getDependencyVersion(DependencySearchArgs&&, Callback<ModPlatform::IndexedVersion>&&) const;

View file

@ -38,7 +38,6 @@
#include "Validator.h"
#include <QCryptographicHash>
#include <QFile>
namespace Net {
class ChecksumValidator : public Validator {
@ -69,10 +68,10 @@ class ChecksumValidator : public Validator {
return true;
}
auto validate(QNetworkReply&) -> bool override
auto validate(QNetworkReply& reply) -> bool override
{
if (m_expected.size() && m_expected != hash()) {
qWarning() << "Checksum mismatch, download is bad.";
if (!m_expected.isEmpty() && m_expected != hash()) {
qWarning() << "Checksum mismatch for URL:" << reply.url().toString() << "expected:" << m_expected << "got:" << hash();
return false;
}
return true;

View file

@ -69,11 +69,15 @@ void NetJob::executeNextSubTask()
// We're finished, check for failures and retry if we can (up to 3 times)
if (isRunning() && m_queue.isEmpty() && m_doing.isEmpty() && !m_failed.isEmpty() && m_try < 3) {
m_try += 1;
while (!m_failed.isEmpty()) {
auto task = m_failed.take(*m_failed.keyBegin());
m_done.remove(task.get());
m_queue.enqueue(task);
}
m_failed.removeIf([this](QHash<Task*, Task::Ptr>::iterator task) {
// there is no point in retying on 404 Not Found
if (static_cast<Net::NetRequest*>(task->get())->replyStatusCode() == 404) {
return false;
}
m_done.remove(task->get());
m_queue.enqueue(*task);
return true;
});
}
ConcurrentTask::executeNextSubTask();
}
@ -100,13 +104,18 @@ auto NetJob::canAbort() const -> bool
auto NetJob::abort() -> bool
{
bool fullyAborted = true;
// fail all downloads on the queue
for (auto task : m_queue)
m_failed.insert(task.get(), task);
m_queue.clear();
if (m_doing.isEmpty()) {
// no downloads to abort, NetJob is not running
return true;
}
bool fullyAborted = true;
// abort active downloads
auto toKill = m_doing.values();
for (auto part : toKill) {

View file

@ -118,7 +118,7 @@ auto ImgurUpload::Sink::finalize(QNetworkReply&) -> Task::State
Net::NetRequest::Ptr ImgurUpload::make(ScreenShot::Ptr m_shot)
{
auto up = makeShared<ImgurUpload>(m_shot->m_file);
up->m_url = std::move(BuildConfig.IMGUR_BASE_URL + "image");
up->m_url = BuildConfig.IMGUR_BASE_URL + "image";
up->m_sink.reset(new Sink(m_shot));
up->addHeaderProxy(std::make_unique<Net::RawHeaderProxy>(QList<Net::HeaderPair>{
{ "Authorization", QString("Client-ID %1").arg(BuildConfig.IMGUR_CLIENT_ID).toUtf8() }, { "Accept", "application/json" } }));

View file

@ -48,6 +48,13 @@ Task::Task(bool show_debug) : m_show_debug(show_debug)
setAutoDelete(false);
}
Task::~Task()
{
if (isRunning()) {
qCWarning(taskLogC) << "Task" << describe() << "disposed while running!";
}
}
void Task::setStatus(const QString& new_status)
{
if (m_status != new_status) {

View file

@ -94,7 +94,7 @@ class Task : public QObject, public QRunnable {
public:
explicit Task(bool show_debug_log = true);
virtual ~Task() = default;
~Task() override;
bool isRunning() const;
bool isFinished() const;
@ -165,7 +165,7 @@ class Task : public QObject, public QRunnable {
//! used by external code to ask the task to abort
virtual bool abort()
{
if (canAbort())
if (canAbort() && isRunning())
emitAborted();
return canAbort();
}

View file

@ -105,6 +105,7 @@
#include "ui/dialogs/NewInstanceDialog.h"
#include "ui/dialogs/NewsDialog.h"
#include "ui/dialogs/ProgressDialog.h"
#include "ui/dialogs/skins/SkinManageDialog.h"
#include "ui/instanceview/InstanceDelegate.h"
#include "ui/instanceview/InstanceProxyModel.h"
#include "ui/instanceview/InstanceView.h"
@ -180,7 +181,7 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi
ui->instanceToolBar->insertSeparator(ui->actionLaunchInstance);
// restore the instance toolbar settings
auto const setting_name = QString("WideBarVisibility_%1").arg(ui->instanceToolBar->objectName());
const auto setting_name = QString("WideBarVisibility_%1").arg(ui->instanceToolBar->objectName());
instanceToolbarSetting = APPLICATION->settings()->getOrRegisterSetting(setting_name);
ui->instanceToolBar->setVisibilityState(QByteArray::fromBase64(instanceToolbarSetting->get().toString().toUtf8()));
@ -396,6 +397,7 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi
// Shouldn't have to use lambdas here like this, but if I don't, the compiler throws a fit.
// Template hell sucks...
connect(APPLICATION->accounts(), &AccountList::defaultAccountChanged, [this] { defaultAccountChanged(); });
connect(APPLICATION->accounts(), &AccountList::listActivityChanged, [this] { defaultAccountChanged(); });
connect(APPLICATION->accounts(), &AccountList::listChanged, [this] { defaultAccountChanged(); });
// Show initial account
@ -653,6 +655,9 @@ void MainWindow::repopulateAccountsMenu()
auto accounts = APPLICATION->accounts();
MinecraftAccountPtr defaultAccount = accounts->defaultAccount();
bool canChangeSkin = defaultAccount && (defaultAccount->accountType() == AccountType::MSA) && !defaultAccount->isActive();
ui->actionManageSkins->setEnabled(canChangeSkin);
QString active_profileId = "";
if (defaultAccount) {
@ -709,6 +714,7 @@ void MainWindow::repopulateAccountsMenu()
connect(ui->actionNoDefaultAccount, &QAction::triggered, this, &MainWindow::changeActiveAccount);
ui->accountsMenu->addSeparator();
ui->accountsMenu->addAction(ui->actionManageSkins);
ui->accountsMenu->addAction(ui->actionManageAccounts);
accountsButtonMenu->addActions(ui->accountsMenu->actions());
@ -942,9 +948,7 @@ void MainWindow::processURLs(QList<QUrl> urls)
QUrl local_url;
if (!url.isLocalFile()) { // download the remote resource and identify
const bool isExternalURLImport =
(url.host().toLower() == "import") ||
(url.path().startsWith("/import", Qt::CaseInsensitive));
const bool isExternalURLImport = (url.host().toLower() == "import") || (url.path().startsWith("/import", Qt::CaseInsensitive));
QUrl dl_url;
if (url.scheme() == "curseforge" || (url.scheme() == BuildConfig.LAUNCHER_APP_BINARY_NAME && url.host() == "install")) {
@ -952,7 +956,7 @@ void MainWindow::processURLs(QList<QUrl> urls)
// format of url curseforge://install?addonId=IDHERE&fileId=IDHERE
// format of url binaryname://install?platform=curseforge&addonId=IDHERE&fileId=IDHERE
QUrlQuery query(url);
// check if this is a binaryname:// url
if (url.scheme() == BuildConfig.LAUNCHER_APP_BINARY_NAME) {
// check this is an curseforge platform request
@ -1015,8 +1019,7 @@ void MainWindow::processURLs(QList<QUrl> urls)
receivedData.insert(it->first, it->second);
emit APPLICATION->oauthReplyRecieved(receivedData);
continue;
} else if ((url.scheme() == "prismlauncher" || url.scheme() == BuildConfig.LAUNCHER_APP_BINARY_NAME)
&& isExternalURLImport) {
} else if ((url.scheme() == "prismlauncher" || url.scheme() == BuildConfig.LAUNCHER_APP_BINARY_NAME) && isExternalURLImport) {
// PrismLauncher URL protocol modpack import
// works for any prism fork
// preferred import format: prismlauncher://import?url=ENCODED
@ -1035,7 +1038,6 @@ void MainWindow::processURLs(QList<QUrl> urls)
// alternative import format: prismlauncher://import/ENCODED
if (encodedTarget.isEmpty()) {
QString p = path;
if (p.startsWith("/import/", Qt::CaseInsensitive)) {
@ -1050,12 +1052,9 @@ void MainWindow::processURLs(QList<QUrl> urls)
}
if (encodedTarget.isEmpty()) {
CustomMessageBox::selectable(
this,
tr("Error"),
tr("Invalid import link: missing 'url' parameter."),
QMessageBox::Critical
)->show();
CustomMessageBox::selectable(this, tr("Error"), tr("Invalid import link: missing 'url' parameter."),
QMessageBox::Critical)
->show();
continue;
}
@ -1065,23 +1064,15 @@ void MainWindow::processURLs(QList<QUrl> urls)
// Validate: only allow http(s)
if (!target.isValid() || (target.scheme() != "https" && target.scheme() != "http")) {
CustomMessageBox::selectable(
this,
tr("Error"),
tr("Invalid import link: URL must be http(s)."),
QMessageBox::Critical
)->show();
CustomMessageBox::selectable(this, tr("Error"), tr("Invalid import link: URL must be http(s)."), QMessageBox::Critical)
->show();
continue;
}
const auto res = QMessageBox::question(
this,
tr("Install modpack"),
tr("Do you want to download and import a modpack from:\n%1\n\nURL:\n%2")
.arg(target.host(), target.toString()),
QMessageBox::Yes | QMessageBox::No,
QMessageBox::Yes
);
this, tr("Install modpack"),
tr("Do you want to download and import a modpack from:\n%1\n\nURL:\n%2").arg(target.host(), target.toString()),
QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
if (res != QMessageBox::Yes) {
continue;
}
@ -1396,6 +1387,16 @@ void MainWindow::on_actionEditInstance_triggered()
}
}
void MainWindow::on_actionManageSkins_triggered()
{
auto account = APPLICATION->accounts()->defaultAccount();
if (account && (account->accountType() == AccountType::MSA) && !account->isActive()) {
SkinManageDialog dialog(this, account);
dialog.exec();
}
}
void MainWindow::on_actionManageAccounts_triggered()
{
APPLICATION->ShowGlobalSettings(this, "accounts");

View file

@ -130,6 +130,8 @@ class MainWindow : public QMainWindow {
void on_actionSettings_triggered();
void on_actionManageSkins_triggered();
void on_actionManageAccounts_triggered();
void on_actionReportBug_triggered();

View file

@ -322,6 +322,14 @@
<enum>QAction::PreferencesRole</enum>
</property>
</action>
<action name="actionManageSkins">
<property name="icon">
<iconset theme="settings"/>
</property>
<property name="text">
<string>Manage &amp;Skins...</string>
</property>
</action>
<action name="actionManageAccounts">
<property name="icon">
<iconset theme="accounts"/>

View file

@ -101,7 +101,7 @@ InstallLoaderDialog::InstallLoaderDialog(PackProfile* profile, const QString& ui
buttonLayout->setContentsMargins(0, 0, 6, 6);
#endif
auto refreshButton = new QPushButton(tr("&Refresh"), this);
connect(refreshButton, &QPushButton::clicked, this, [this] { pageCast(container->selectedPage())->loadList(); });
connect(refreshButton, &QPushButton::clicked, this, [this] { pageCast(container->selectedPage())->loadList(true); });
buttonLayout->addWidget(refreshButton);
buttons->setOrientation(Qt::Horizontal);

View file

@ -252,10 +252,7 @@ void ProgressDialog::changeStepProgress(TaskStepProgress const& task_progress)
task_bar->setValue(mapped_current);
task_bar->setStatus(task_progress.status);
task_bar->setDetails(task_progress.details);
if (task_progress.isDone()) {
task_bar->setVisible(false);
}
task_bar->setVisible(!task_progress.isDone());
}
void ProgressDialog::changeProgress(qint64 current, qint64 total)

View file

@ -144,7 +144,7 @@ BaseVersion::Ptr VersionSelectDialog::selectedVersion() const
void VersionSelectDialog::on_refreshButton_clicked()
{
m_versionWidget->loadList();
m_versionWidget->loadList(true);
}
void VersionSelectDialog::setExactFilter(BaseVersionList::ModelRoles role, QString filter)

View file

@ -121,8 +121,8 @@ class InstallJavaPage : public QWidget, public BasePage {
void selectSearch() { javaVersionSelect->selectSearch(); }
void loadList()
{
majorVersionSelect->loadList();
javaVersionSelect->loadList();
majorVersionSelect->loadList(true);
javaVersionSelect->loadList(true);
}
public slots:

View file

@ -33,9 +33,9 @@ VersionList::VersionList(Meta::Version::Ptr version, QObject* parent) : BaseVers
sortVersions();
}
Task::Ptr VersionList::getLoadTask()
Task::Ptr VersionList::getLoadTask(bool forceReload)
{
auto task = m_version->loadTask(Net::Mode::Online);
auto task = m_version->loadTask(Net::Mode::Online, forceReload);
connect(task.get(), &Task::finished, this, &VersionList::sortVersions);
return task;
}

View file

@ -30,7 +30,7 @@ class VersionList : public BaseVersionList {
public:
explicit VersionList(Meta::Version::Ptr m_version, QObject* parent = 0);
Task::Ptr getLoadTask() override;
Task::Ptr getLoadTask(bool forceReload = false) override;
bool isLoaded() override;
const BaseVersion::Ptr at(int i) const override;
int count() const override;

View file

@ -143,6 +143,7 @@ void APIPage::loadSettings()
ui->msaClientID->setText(msaClientID);
QString metaURL = s->get("MetaURLOverride").toString();
ui->metaURL->setText(metaURL);
ui->metaRefreshOnLaunchCB->setCheckState(s->get("MetaRefreshOnLaunch").toBool() ? Qt::Checked : Qt::Unchecked);
QString resourceURL = s->get("ResourceURLOverride").toString();
ui->resourceURL->setText(resourceURL);
QString fmlLibsURL = s->get("LegacyFMLLibsURLOverride").toString();
@ -194,6 +195,7 @@ void APIPage::applySettings()
s->set("FallbackMRBlockedMods", ui->FallbackMRBlockedMods->checkState());
s->set("MetaURLOverride", metaURL.toString());
s->set("MetaRefreshOnLaunch", ui->metaRefreshOnLaunchCB->checkState() == Qt::Checked);
s->set("ResourceURLOverride", resourceURL.toString());
s->set("LegacyFMLLibsURLOverride", fmlLibsURL.toString());
QString flameKey = ui->flameKey->text();

View file

@ -32,9 +32,9 @@
<property name="geometry">
<rect>
<x>0</x>
<y>-262</y>
<width>820</width>
<height>908</height>
<y>0</y>
<width>825</width>
<height>1236</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
@ -126,6 +126,13 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="metaRefreshOnLaunchCB">
<property name="text">
<string>Refresh on launch</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>

View file

@ -202,23 +202,24 @@ bool ManagedPackPage::runUpdateTask(InstanceTask* task)
unique_qobject_ptr<Task> wrapped_task(APPLICATION->instances()->wrapInstanceTask(task));
connect(task, &Task::failed,
[this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); });
connect(task, &Task::succeeded, [this, task]() {
connect(wrapped_task.get(), &Task::failed,
[this](const QString& reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); });
connect(wrapped_task.get(), &Task::succeeded, [this, task]() {
QStringList warnings = task->warnings();
if (warnings.count())
if (warnings.count()) {
CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show();
}
});
connect(task, &Task::aborted, [this] {
connect(wrapped_task.get(), &Task::aborted, [this] {
CustomMessageBox::selectable(this, tr("Task aborted"), tr("The task has been aborted by the user."), QMessageBox::Information)
->show();
});
ProgressDialog loadDialog(this);
loadDialog.setSkipButton(true, tr("Abort"));
loadDialog.execWithTask(task);
loadDialog.execWithTask(wrapped_task.get());
return task->wasSuccessful();
return wrapped_task->wasSuccessful();
}
void ManagedPackPage::suggestVersion()
@ -260,14 +261,16 @@ void ModrinthManagedPackPage::parseManagedPack()
qDebug() << "Parsing Modrinth pack";
// No need for the extra work because we already have everything we need.
if (m_loaded)
if (m_loaded) {
return;
}
if (m_fetch_job && m_fetch_job->isRunning())
if (m_fetch_job && m_fetch_job->isRunning()) {
m_fetch_job->abort();
}
ResourceAPI::Callback<QVector<ModPlatform::IndexedVersion>> callbacks{};
m_pack = { m_inst->getManagedPackID() };
m_pack = { .addonId = m_inst->getManagedPackID() };
// Use default if no callbacks are set
callbacks.on_succeed = [this](auto& doc) {
@ -284,8 +287,9 @@ void ModrinthManagedPackPage::parseManagedPack()
// NOTE: the id from version isn't the same id in the modpack format spec...
// e.g. HexMC's 4.4.0 has versionId 4.0.0 in the modpack index..............
if (version.version == m_inst->getManagedPackVersionName())
if (version.version == m_inst->getManagedPackVersionName()) {
name = tr("%1 (Current)").arg(name);
}
ui->versionsComboBox->addItem(name, version.fileId);
}
@ -294,10 +298,14 @@ void ModrinthManagedPackPage::parseManagedPack()
m_loaded = true;
};
callbacks.on_fail = [this](QString reason, int) { setFailState(); };
callbacks.on_fail = [this](const QString& /*reason*/, int) { setFailState(); };
callbacks.on_abort = [this]() { setFailState(); };
m_fetch_job = m_api.getProjectVersions(
{ std::make_shared<ModPlatform::IndexedPack>(m_pack), {}, {}, ModPlatform::ResourceType::Modpack }, std::move(callbacks));
m_fetch_job = m_api.getProjectVersions({ .pack = std::make_shared<ModPlatform::IndexedPack>(m_pack),
.mcVersions = {},
.loaders = {},
.resourceType = ModPlatform::ResourceType::Modpack,
.includeChangelog = true },
std::move(callbacks));
ui->changelogTextBrowser->setText(tr("Fetching changelogs..."));
@ -406,14 +414,16 @@ void FlameManagedPackPage::parseManagedPack()
}
// No need for the extra work because we already have everything we need.
if (m_loaded)
if (m_loaded) {
return;
}
if (m_fetch_job && m_fetch_job->isRunning())
if (m_fetch_job && m_fetch_job->isRunning()) {
m_fetch_job->abort();
}
QString id = m_inst->getManagedPackID();
m_pack = { id };
m_pack = { .addonId = id };
ResourceAPI::Callback<QVector<ModPlatform::IndexedVersion>> callbacks{};
@ -430,8 +440,9 @@ void FlameManagedPackPage::parseManagedPack()
for (const auto& version : m_pack.versions) {
QString name = version.getVersionDisplayString();
if (version.fileId == m_inst->getManagedPackVersionID().toInt())
if (version.fileId == m_inst->getManagedPackVersionID().toInt()) {
name = tr("%1 (Current)").arg(name);
}
ui->versionsComboBox->addItem(name, QVariant(version.fileId));
}
@ -440,10 +451,14 @@ void FlameManagedPackPage::parseManagedPack()
m_loaded = true;
};
callbacks.on_fail = [this](QString reason, int) { setFailState(); };
callbacks.on_fail = [this](const QString& /*reason*/, int) { setFailState(); };
callbacks.on_abort = [this]() { setFailState(); };
m_fetch_job = m_api.getProjectVersions(
{ std::make_shared<ModPlatform::IndexedPack>(m_pack), {}, {}, ModPlatform::ResourceType::Modpack }, std::move(callbacks));
m_fetch_job = m_api.getProjectVersions({ .pack = std::make_shared<ModPlatform::IndexedPack>(m_pack),
.mcVersions = {},
.loaders = {},
.resourceType = ModPlatform::ResourceType::Modpack,
.includeChangelog = true },
std::move(callbacks));
m_fetch_job->start();
}

View file

@ -1,18 +1,17 @@
#include "McClient.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QObject>
#include <QTcpSocket>
#include <utility>
#include <Exception.h>
#include "Exception.h"
#include "Json.h"
#include "McClient.h"
// 7 first bits
#define SEGMENT_BITS 0x7F
// last bit
#define CONTINUE_BIT 0x80
McClient::McClient(QObject* parent, QString domain, QString ip, short port) : QObject(parent), m_domain(domain), m_ip(ip), m_port(port) {}
McClient::McClient(QObject* parent, QString domain, QString ip, const uint16_t port)
: QObject(parent), m_domain(std::move(domain)), m_ip(std::move(ip)), m_port(port)
{}
void McClient::getStatusData()
{
@ -33,13 +32,12 @@ void McClient::getStatusData()
void McClient::sendRequest()
{
QByteArray data;
writeVarInt(data, 0x00); // packet ID
writeVarInt(data, 763); // hardcoded protocol version (763 = 1.20.1)
writeVarInt(data, m_domain.size()); // server address length
writeString(data, m_domain.toStdString()); // server address
writeFixedInt(data, m_port, 2); // server port
writeVarInt(data, 0x01); // next state
writePacketToSocket(data); // send handshake packet
writeVarInt(data, 0x00); // packet ID
writeVarInt(data, 763); // hardcoded protocol version (763 = 1.20.1)
writeString(data, m_domain); // server address
writeUInt16(data, m_port); // server port
writeVarInt(data, 0x01); // next state
writePacketToSocket(data); // send handshake packet
writeVarInt(data, 0x00); // packet ID
writePacketToSocket(data); // send status packet
@ -47,17 +45,17 @@ void McClient::sendRequest()
void McClient::readRawResponse()
{
if (m_responseReadState == 2) {
if (m_responseReadState == ResponseReadState::Finished) {
return;
}
m_resp.append(m_socket.readAll());
if (m_responseReadState == 0 && m_resp.size() >= 5) {
if (m_responseReadState == ResponseReadState::Waiting && m_resp.size() >= 5) {
m_wantedRespLength = readVarInt(m_resp);
m_responseReadState = 1;
m_responseReadState = ResponseReadState::GotLength;
}
if (m_responseReadState == 1 && m_resp.size() >= m_wantedRespLength) {
if (m_responseReadState == ResponseReadState::GotLength && m_resp.size() >= m_wantedRespLength) {
if (m_resp.size() > m_wantedRespLength) {
qDebug().nospace() << "Warning: Packet length doesn't match actual packet size (" << m_wantedRespLength << " expected vs "
<< m_resp.size() << " received)";
@ -67,7 +65,7 @@ void McClient::readRawResponse()
} catch (const Exception& e) {
emitFail(e.cause());
}
m_responseReadState = 2;
m_responseReadState = ResponseReadState::Finished;
}
}
@ -75,7 +73,7 @@ void McClient::parseResponse()
{
qDebug() << "Received response successfully";
int packetID = readVarInt(m_resp);
const int packetID = readVarInt(m_resp);
if (packetID != 0x00) {
throw Exception(QString("Packet ID doesn't match expected value (0x00 vs 0x%1)").arg(packetID, 0, 16));
}
@ -84,7 +82,7 @@ void McClient::parseResponse()
// 'resp' should now be the JSON string
QJsonParseError parseError;
QJsonDocument doc = Json::parseUntilGarbage(m_resp, &parseError);
const QJsonDocument doc = Json::parseUntilGarbage(m_resp, &parseError);
if (parseError.error != QJsonParseError::NoError) {
qDebug() << "Failed to parse JSON:" << parseError.errorString();
emitFail(parseError.errorString());
@ -93,18 +91,23 @@ void McClient::parseResponse()
emitSucceed(doc.object());
}
// NOLINTBEGIN(*-signed-bitwise)
// From https://wiki.vg/Protocol#VarInt_and_VarLong
constexpr uint8_t g_varIntValueMask = 0x7F;
constexpr uint8_t g_varIntContinue = 0x80;
void McClient::writeVarInt(QByteArray& data, int value)
{
while ((value & ~SEGMENT_BITS)) { // check if the value is too big to fit in 7 bits
while ((value & ~g_varIntValueMask) != 0) { // check if the value is too big to fit in 7 bits
// Write 7 bits
data.append((value & SEGMENT_BITS) | CONTINUE_BIT);
data.append(static_cast<uint8_t>((value & ~g_varIntValueMask) | g_varIntContinue)); // NOLINT(*-narrowing-conversions)
// Erase theses 7 bits from the value to write
// Note: >>> means that the sign bit is shifted with the rest of the number rather than being left alone
value >>= 7;
}
data.append(value);
data.append(static_cast<uint8_t>(value)); // NOLINT(*-narrowing-conversions)
}
// From https://wiki.vg/Protocol#VarInt_and_VarLong
@ -112,53 +115,56 @@ int McClient::readVarInt(QByteArray& data)
{
int value = 0;
int position = 0;
char currentByte;
while (position < 32) {
currentByte = readByte(data);
value |= (currentByte & SEGMENT_BITS) << position;
const uint8_t currentByte = readByte(data);
value |= (currentByte & g_varIntValueMask) << position;
if ((currentByte & CONTINUE_BIT) == 0)
if ((currentByte & g_varIntContinue) == 0) {
break;
}
position += 7;
}
if (position >= 32)
if (position >= 32) {
throw Exception("VarInt is too big");
}
return value;
}
char McClient::readByte(QByteArray& data)
// NOLINTEND(*-signed-bitwise)
uint8_t McClient::readByte(QByteArray& data)
{
if (data.isEmpty()) {
throw Exception("No more bytes to read");
}
char byte = data.at(0);
const uint8_t byte = data.at(0);
data.remove(0, 1);
return byte;
}
// write number with specified size in big endian format
void McClient::writeFixedInt(QByteArray& data, int value, int size)
void McClient::writeUInt16(QByteArray& data, const uint16_t value)
{
for (int i = size - 1; i >= 0; i--) {
data.append((value >> (i * 8)) & 0xFF);
}
QDataStream stream(&data, QIODeviceBase::Append);
stream.setByteOrder(QDataStream::BigEndian);
stream << value;
}
void McClient::writeString(QByteArray& data, const std::string& value)
void McClient::writeString(QByteArray& data, const QString& value)
{
data.append(value.c_str());
writeVarInt(data, static_cast<int32_t>(value.size()));
data.append(value.toUtf8());
}
void McClient::writePacketToSocket(QByteArray& data)
{
// we prefix the packet with its length
QByteArray dataWithSize;
writeVarInt(dataWithSize, data.size());
writeVarInt(dataWithSize, static_cast<int32_t>(data.size()));
dataWithSize.append(data);
// write it to the socket
@ -168,7 +174,7 @@ void McClient::writePacketToSocket(QByteArray& data)
data.clear();
}
void McClient::emitFail(QString error)
void McClient::emitFail(const QString& error)
{
qDebug() << "Minecraft server ping for status error:" << error;
emit failed(error);
@ -177,6 +183,6 @@ void McClient::emitFail(QString error)
void McClient::emitSucceed(QJsonObject data)
{
emit succeeded(data);
emit succeeded(std::move(data));
emit finished();
}

View file

@ -1,53 +1,54 @@
#pragma once
#include <QFuture>
#include <QJsonDocument>
#include <QJsonObject>
#include <QObject>
#include <QTcpSocket>
#include <Exception.h>
// Client for the Minecraft protocol
class McClient : public QObject {
Q_OBJECT
QString m_domain;
QString m_ip;
short m_port;
QTcpSocket m_socket;
// 0: did not start reading the response yet
// 1: read the response length, still reading the response
// 2: finished reading the response
unsigned m_responseReadState = 0;
unsigned m_wantedRespLength = 0;
QByteArray m_resp;
public:
explicit McClient(QObject* parent, QString domain, QString ip, short port);
explicit McClient(QObject* parent, QString domain, QString ip, uint16_t port);
//! Read status data of the server, and calls the succeeded() signal with the parsed JSON data
void getStatusData();
signals:
void succeeded(QJsonObject data);
void failed(QString error);
void finished();
private:
static uint8_t readByte(QByteArray& data);
static int readVarInt(QByteArray& data);
static void writeUInt16(QByteArray& data, uint16_t value);
static void writeString(QByteArray& data, const QString& value);
static void writeVarInt(QByteArray& data, int value);
private:
void sendRequest();
//! Accumulate data until we have a full response, then call parseResponse() once
void readRawResponse();
void parseResponse();
void writeVarInt(QByteArray& data, int value);
int readVarInt(QByteArray& data);
char readByte(QByteArray& data);
//! write number with specified size in big endian format
void writeFixedInt(QByteArray& data, int value, int size);
void writeString(QByteArray& data, const std::string& value);
void writePacketToSocket(QByteArray& data);
void emitFail(QString error);
void emitFail(const QString& error);
void emitSucceed(QJsonObject data);
signals:
void succeeded(QJsonObject data);
void failed(QString error);
void finished();
private:
enum class ResponseReadState : uint8_t {
Waiting,
GotLength,
Finished
};
QString m_domain;
QString m_ip;
uint16_t m_port;
QTcpSocket m_socket;
ResponseReadState m_responseReadState = ResponseReadState::Waiting;
int32_t m_wantedRespLength = 0;
QByteArray m_resp;
};

View file

@ -45,7 +45,6 @@
#include <QFileSystemModel>
#include <QKeyEvent>
#include <QLineEdit>
#include <QMap>
#include <QMenu>
#include <QModelIndex>
#include <QMutableListIterator>
@ -53,6 +52,8 @@
#include <QRegularExpression>
#include <QSet>
#include <QStyledItemDelegate>
#include <memory>
#include <utility>
#include <Application.h>
#include "settings/SettingsObject.h"
@ -70,9 +71,14 @@
#include "RWStorage.h"
class ScreenshotsFSModel : public QFileSystemModel {
bool canDropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) const override
public:
bool canDropMimeData(const QMimeData* data,
const Qt::DropAction action,
const int row,
const int column,
const QModelIndex& parent) const override
{
QUrl root = QUrl::fromLocalFile(rootPath());
const QUrl root = QUrl::fromLocalFile(rootPath());
// this disables reordering items inside the model
// by rejecting drops if the file is already inside the folder
if (data->hasUrls()) {
@ -92,8 +98,8 @@ using SharedIconCachePtr = std::shared_ptr<SharedIconCache>;
class ThumbnailingResult : public QObject {
Q_OBJECT
public slots:
inline void emitResultsReady(const QString& path) { emit resultsReady(path); }
inline void emitResultsFailed(const QString& path) { emit resultsFailed(path); }
void emitResultsReady(const QString& path) { emit resultsReady(path); }
void emitResultsFailed(const QString& path) { emit resultsFailed(path); }
signals:
void resultsReady(const QString& path);
void resultsFailed(const QString& path);
@ -101,32 +107,32 @@ class ThumbnailingResult : public QObject {
class ThumbnailRunnable : public QRunnable {
public:
ThumbnailRunnable(QString path, SharedIconCachePtr cache)
ThumbnailRunnable(QString path, SharedIconCachePtr cache) : m_path(std::move(path)), m_cache(std::move(cache)) {}
void run() override
{
m_path = path;
m_cache = cache;
}
void run()
{
QFileInfo info(m_path);
if (info.isDir())
const QFileInfo info(m_path);
if (info.isDir()) {
return;
if ((info.suffix().compare("png", Qt::CaseInsensitive) != 0))
}
if (info.suffix().compare("png", Qt::CaseInsensitive) != 0) {
return;
if (!m_cache->stale(m_path))
}
if (!m_cache->stale(m_path)) {
return;
QImage image(m_path);
}
const QImage image(m_path);
if (image.isNull()) {
m_resultEmitter.emitResultsFailed(m_path);
qDebug() << "Error loading screenshot (perhaps too large?):" + m_path;
return;
}
QImage small;
if (image.width() > image.height())
if (image.width() > image.height()) {
small = image.scaledToWidth(512).scaledToWidth(256, Qt::SmoothTransformation);
else
} else {
small = image.scaledToHeight(512).scaledToHeight(256, Qt::SmoothTransformation);
QPoint offset((256 - small.width()) / 2, (256 - small.height()) / 2);
}
const QPoint offset((256 - small.width()) / 2, (256 - small.height()) / 2);
QImage square(QSize(256, 256), QImage::Format_ARGB32);
square.fill(Qt::transparent);
@ -134,7 +140,7 @@ class ThumbnailRunnable : public QRunnable {
painter.drawImage(offset, small);
painter.end();
QIcon icon(QPixmap::fromImage(square));
const QIcon icon(QPixmap::fromImage(square));
m_cache->add(m_path, icon);
m_resultEmitter.emitResultsReady(m_path);
}
@ -148,59 +154,62 @@ class ThumbnailRunnable : public QRunnable {
class FilterModel : public QIdentityProxyModel {
Q_OBJECT
public:
explicit FilterModel(QObject* parent = 0) : QIdentityProxyModel(parent)
explicit FilterModel(QObject* parent = nullptr) : QIdentityProxyModel(parent)
{
m_thumbnailingPool.setMaxThreadCount(4);
m_thumbnailCache = std::make_shared<SharedIconCache>();
m_thumbnailCache->add("placeholder", QIcon::fromTheme("screenshot-placeholder"));
connect(&watcher, &QFileSystemWatcher::fileChanged, this, &FilterModel::fileChanged);
}
virtual ~FilterModel()
~FilterModel() override
{
m_thumbnailingPool.clear();
if (!m_thumbnailingPool.waitForDone(500))
if (!m_thumbnailingPool.waitForDone(500)) {
qDebug() << "Thumbnail pool took longer than 500ms to finish";
}
}
virtual QVariant data(const QModelIndex& proxyIndex, int role = Qt::DisplayRole) const
QVariant data(const QModelIndex& proxyIndex, const int role = Qt::DisplayRole) const override // NOLINT(*-default-arguments)
{
auto model = sourceModel();
if (!model)
return QVariant();
const auto* model = sourceModel();
if (!model) {
return {};
}
if (role == Qt::DisplayRole || role == Qt::EditRole) {
QVariant result = sourceModel()->data(mapToSource(proxyIndex), role);
const QVariant result = model->data(mapToSource(proxyIndex), role);
static const QRegularExpression s_removeChars("\\.png$");
return result.toString().remove(s_removeChars);
}
if (role == Qt::DecorationRole) {
QVariant result = sourceModel()->data(mapToSource(proxyIndex), QFileSystemModel::FilePathRole);
QString filePath = result.toString();
QIcon temp;
const QVariant result = model->data(mapToSource(proxyIndex), QFileSystemModel::FilePathRole);
const QString filePath = result.toString();
if (!watched.contains(filePath)) {
((QFileSystemWatcher&)watcher).addPath(filePath);
((QSet<QString>&)watched).insert(filePath);
const_cast<QFileSystemWatcher&>(watcher).addPath(filePath);
const_cast<QSet<QString>&>(watched).insert(filePath);
}
if (m_thumbnailCache->get(filePath, temp)) {
if (QIcon temp; m_thumbnailCache->get(filePath, temp)) {
return temp;
}
if (!m_failed.contains(filePath)) {
((FilterModel*)this)->thumbnailImage(filePath);
const_cast<FilterModel*>(this)->thumbnailImage(filePath);
}
return (m_thumbnailCache->get("placeholder"));
}
return sourceModel()->data(mapToSource(proxyIndex), role);
return model->data(mapToSource(proxyIndex), role);
}
virtual bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole)
bool setData(const QModelIndex& index, const QVariant& value, const int role = Qt::EditRole) override // NOLINT(*-default-arguments)
{
auto model = sourceModel();
if (!model)
auto* model = sourceModel();
if (!model) {
return false;
if (role != Qt::EditRole)
}
if (role != Qt::EditRole) {
return false;
}
// FIXME: this is a workaround for a bug in QFileSystemModel, where it doesn't
// sort after renames
{
((QFileSystemModel*)model)->setNameFilterDisables(true);
((QFileSystemModel*)model)->setNameFilterDisables(false);
static_cast<QFileSystemModel*>(model)->setNameFilterDisables(true);
static_cast<QFileSystemModel*>(model)->setNameFilterDisables(false);
}
return model->setData(mapToSource(index), value.toString() + ".png", role);
}
@ -208,15 +217,15 @@ class FilterModel : public QIdentityProxyModel {
private:
void thumbnailImage(QString path)
{
auto runnable = new ThumbnailRunnable(path, m_thumbnailCache);
auto* runnable = new ThumbnailRunnable(std::move(path), m_thumbnailCache);
connect(&runnable->m_resultEmitter, &ThumbnailingResult::resultsReady, this, &FilterModel::thumbnailReady);
connect(&runnable->m_resultEmitter, &ThumbnailingResult::resultsFailed, this, &FilterModel::thumbnailFailed);
m_thumbnailingPool.start(runnable);
}
private slots:
void thumbnailReady(QString path) { emit layoutChanged(); }
void thumbnailFailed(QString path) { m_failed.insert(path); }
void fileChanged(QString filepath)
void thumbnailReady(const QString& /*path*/) { emit layoutChanged(); }
void thumbnailFailed(const QString& path) { m_failed.insert(path); }
void fileChanged(const QString& filepath)
{
m_thumbnailCache->setStale(filepath);
// reinsert the path...
@ -237,13 +246,12 @@ class FilterModel : public QIdentityProxyModel {
class CenteredEditingDelegate : public QStyledItemDelegate {
public:
explicit CenteredEditingDelegate(QObject* parent = 0) : QStyledItemDelegate(parent) {}
virtual ~CenteredEditingDelegate() {}
virtual QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const
explicit CenteredEditingDelegate(QObject* parent = nullptr) : QStyledItemDelegate(parent) {}
~CenteredEditingDelegate() override = default;
QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const override
{
auto widget = QStyledItemDelegate::createEditor(parent, option, index);
auto foo = dynamic_cast<QLineEdit*>(widget);
if (foo) {
auto* widget = QStyledItemDelegate::createEditor(parent, option, index);
if (auto* foo = dynamic_cast<QLineEdit*>(widget)) {
foo->setAlignment(Qt::AlignHCenter);
foo->setFrame(true);
foo->setMaximumWidth(192);
@ -252,10 +260,11 @@ class CenteredEditingDelegate : public QStyledItemDelegate {
}
};
ScreenshotsPage::ScreenshotsPage(QString path, QWidget* parent) : QMainWindow(parent), ui(new Ui::ScreenshotsPage)
ScreenshotsPage::ScreenshotsPage(QString path, QWidget* parent)
: QMainWindow(parent), ui(new Ui::ScreenshotsPage), m_folder(std::move(path))
{
m_model.reset(new ScreenshotsFSModel());
m_filterModel.reset(new FilterModel());
m_model = std::make_shared<ScreenshotsFSModel>();
m_filterModel = std::make_shared<FilterModel>();
m_filterModel->setSourceModel(m_model.get());
m_model->setFilter(QDir::Files);
m_model->setReadOnly(false);
@ -266,7 +275,6 @@ ScreenshotsPage::ScreenshotsPage(QString path, QWidget* parent) : QMainWindow(pa
constexpr int file_modified_column_index = 3;
m_model->sort(file_modified_column_index, Qt::DescendingOrder);
m_folder = path;
m_valid = FS::ensureFolderPathExists(m_folder);
ui->setupUi(this);
@ -283,18 +291,19 @@ ScreenshotsPage::ScreenshotsPage(QString path, QWidget* parent) : QMainWindow(pa
ui->listView->setEditTriggers(QAbstractItemView::NoEditTriggers);
ui->listView->setItemDelegate(new CenteredEditingDelegate(this));
ui->listView->setContextMenuPolicy(Qt::CustomContextMenu);
connect(ui->listView, &QListView::customContextMenuRequested, this, &ScreenshotsPage::ShowContextMenu);
connect(ui->listView, &QListView::customContextMenuRequested, this, &ScreenshotsPage::showContextMenu);
connect(ui->listView, &QAbstractItemView::activated, this, &ScreenshotsPage::onItemActivated);
}
bool ScreenshotsPage::eventFilter(QObject* obj, QEvent* evt)
{
if (obj != ui->listView)
if (obj != ui->listView) {
return QWidget::eventFilter(obj, evt);
}
if (evt->type() != QEvent::KeyPress) {
return QWidget::eventFilter(obj, evt);
}
QKeyEvent* keyEvent = static_cast<QKeyEvent*>(evt);
const auto* keyEvent = static_cast<QKeyEvent*>(evt);
if (keyEvent->matches(QKeySequence::Copy)) {
on_actionCopy_File_s_triggered();
@ -324,11 +333,11 @@ ScreenshotsPage::~ScreenshotsPage()
delete ui;
}
void ScreenshotsPage::ShowContextMenu(const QPoint& pos)
void ScreenshotsPage::showContextMenu(const QPoint& pos)
{
auto menu = ui->toolBar->createContextMenu(this, tr("Context menu"));
auto* menu = ui->toolBar->createContextMenu(this, tr("Context menu"));
if (ui->listView->selectionModel()->selectedRows().size() > 1) {
if (ui->listView->selectionModel()->selectedIndexes().size() > 1) {
menu->removeAction(ui->actionCopy_Image);
}
@ -343,66 +352,75 @@ QMenu* ScreenshotsPage::createPopupMenu()
return filteredMenu;
}
void ScreenshotsPage::onItemActivated(QModelIndex index)
void ScreenshotsPage::onItemActivated(QModelIndex index) const
{
if (!index.isValid())
if (!index.isValid()) {
return;
auto info = m_model->fileInfo(index);
}
const auto info = m_model->fileInfo(index);
DesktopServices::openPath(info);
}
void ScreenshotsPage::onCurrentSelectionChanged(const QItemSelection& selected)
void ScreenshotsPage::onCurrentSelectionChanged(const QItemSelection& /*selected*/) const
{
const auto selected = ui->listView->selectionModel()->selectedIndexes();
bool allReadable = !selected.isEmpty();
bool allWritable = !selected.isEmpty();
for (auto index : selected.indexes()) {
if (!index.isValid())
for (auto index : selected) {
if (!index.isValid()) {
break;
}
auto info = m_model->fileInfo(index);
if (!info.isReadable())
if (!info.isReadable()) {
allReadable = false;
if (!info.isWritable())
}
if (!info.isWritable()) {
allWritable = false;
}
}
ui->actionUpload->setEnabled(allReadable);
ui->actionCopy_Image->setEnabled(allReadable);
ui->actionCopy_Image->setEnabled(allReadable && selected.size() == 1);
ui->actionCopy_File_s->setEnabled(allReadable);
ui->actionDelete->setEnabled(allWritable);
ui->actionRename->setEnabled(allWritable);
}
void ScreenshotsPage::on_actionView_Folder_triggered()
void ScreenshotsPage::on_actionView_Folder_triggered() const
{
DesktopServices::openPath(m_folder, true);
}
void ScreenshotsPage::on_actionUpload_triggered()
{
auto selection = ui->listView->selectionModel()->selectedRows();
if (selection.isEmpty())
auto selection = ui->listView->selectionModel()->selectedIndexes();
if (selection.isEmpty()) {
return;
}
QString text;
QUrl baseUrl(BuildConfig.IMGUR_BASE_URL);
if (selection.size() > 1)
const QUrl baseUrl(BuildConfig.IMGUR_BASE_URL);
if (selection.size() > 1) {
text = tr("You are about to upload %1 screenshots to %2.\n"
"You should double-check for personal information.\n\n"
"Are you sure?")
.arg(QString::number(selection.size()), baseUrl.host());
else
} else {
text = tr("You are about to upload the selected screenshot to %1.\n"
"You should double-check for personal information.\n\n"
"Are you sure?")
.arg(baseUrl.host());
}
auto response = CustomMessageBox::selectable(this, "Confirm Upload", text, QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No,
QMessageBox::No)
->exec();
if (response != QMessageBox::Yes)
if (response != QMessageBox::Yes) {
return;
}
QList<ScreenShot::Ptr> uploaded;
auto job = NetJob::Ptr(new NetJob("Screenshot Upload", APPLICATION->network()));
@ -416,7 +434,7 @@ void ScreenshotsPage::on_actionUpload_triggered()
auto screenshot = std::make_shared<ScreenShot>(info);
job->addNetAction(ImgurUpload::make(screenshot));
connect(job.get(), &Task::failed, [this](QString reason) {
connect(job.get(), &Task::failed, [this](const QString& reason) {
CustomMessageBox::selectable(this, tr("Failed to upload screenshots!"), reason, QMessageBox::Critical)->show();
});
connect(job.get(), &Task::aborted, [this] {
@ -457,7 +475,7 @@ void ScreenshotsPage::on_actionUpload_triggered()
task.addTask(job);
task.addTask(albumTask);
connect(&task, &Task::failed, [this](QString reason) {
connect(&task, &Task::failed, [this](const QString& reason) {
CustomMessageBox::selectable(this, tr("Failed to upload screenshots!"), reason, QMessageBox::Critical)->show();
});
connect(&task, &Task::aborted, [this] {
@ -485,24 +503,24 @@ void ScreenshotsPage::on_actionUpload_triggered()
m_uploadActive = false;
}
void ScreenshotsPage::on_actionCopy_Image_triggered()
void ScreenshotsPage::on_actionCopy_Image_triggered() const
{
auto selection = ui->listView->selectionModel()->selectedRows();
auto selection = ui->listView->selectionModel()->selectedIndexes();
if (selection.size() < 1) {
return;
}
// You can only copy one image to the clipboard. In the case of multiple selected files, only the first one gets copied.
auto item = selection[0];
auto info = m_model->fileInfo(item);
QImage image(info.absoluteFilePath());
const auto item = selection.first();
const auto info = m_model->fileInfo(item);
const QImage image(info.absoluteFilePath());
Q_ASSERT(!image.isNull());
QApplication::clipboard()->setImage(image, QClipboard::Clipboard);
}
void ScreenshotsPage::on_actionCopy_File_s_triggered()
void ScreenshotsPage::on_actionCopy_File_s_triggered() const
{
auto selection = ui->listView->selectionModel()->selectedRows();
auto selection = ui->listView->selectionModel()->selectedIndexes();
if (selection.size() < 1) {
// Don't do anything so we don't empty the users clipboard
return;
@ -513,7 +531,7 @@ void ScreenshotsPage::on_actionCopy_File_s_triggered()
auto info = m_model->fileInfo(item);
buf += "file:///" + info.absoluteFilePath() + "\r\n";
}
QMimeData* mimeData = new QMimeData();
auto* mimeData = new QMimeData();
mimeData->setData("text/uri-list", buf.toLocal8Bit());
QApplication::clipboard()->setMimeData(mimeData);
}
@ -522,39 +540,43 @@ void ScreenshotsPage::on_actionDelete_triggered()
{
auto selected = ui->listView->selectionModel()->selectedIndexes();
int count = ui->listView->selectionModel()->selectedRows().size();
const qsizetype count = selected.size();
QString text;
if (count > 1)
if (count > 1) {
text = tr("You are about to delete %1 screenshots.\n"
"This may be permanent and they will be gone from the folder.\n\n"
"Are you sure?")
.arg(count);
else
text = tr("You are about to delete the selected screenshot.\n"
"This may be permanent and it will be gone from the folder.\n\n"
"Are you sure?")
.arg(count);
} else {
text =
tr("You are about to delete the selected screenshot.\n"
"This may be permanent and it will be gone from the folder.\n\n"
"Are you sure?");
}
auto response =
const auto response =
CustomMessageBox::selectable(this, tr("Confirm Deletion"), text, QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No)->exec();
if (response != QMessageBox::Yes)
if (response != QMessageBox::Yes) {
return;
}
for (auto item : selected) {
if (FS::trash(m_model->filePath(item)))
if (FS::trash(m_model->filePath(item))) {
continue;
}
m_model->remove(item);
}
}
void ScreenshotsPage::on_actionRename_triggered()
void ScreenshotsPage::on_actionRename_triggered() const
{
auto selection = ui->listView->selectionModel()->selectedIndexes();
if (selection.isEmpty())
if (selection.isEmpty()) {
return;
ui->listView->edit(selection[0]);
}
ui->listView->edit(selection.first());
// TODO: mass renaming
}
@ -564,8 +586,8 @@ void ScreenshotsPage::openedImpl()
m_valid = FS::ensureFolderPathExists(m_folder);
}
if (m_valid) {
QString path = QDir(m_folder).absolutePath();
auto idx = m_model->setRootPath(path);
const QString path = QDir(m_folder).absolutePath();
const auto idx = m_model->setRootPath(path);
if (idx.isValid()) {
ui->listView->setModel(m_filterModel.get());
connect(ui->listView->selectionModel(), &QItemSelectionModel::selectionChanged, this,
@ -577,7 +599,7 @@ void ScreenshotsPage::openedImpl()
}
}
auto const setting_name = QString("WideBarVisibility_%1").arg(id());
const auto setting_name = QString("WideBarVisibility_%1").arg(id());
m_wide_bar_setting = APPLICATION->settings()->getOrRegisterSetting(setting_name);
ui->toolBar->setVisibilityState(QByteArray::fromBase64(m_wide_bar_setting->get().toString().toUtf8()));

View file

@ -78,14 +78,14 @@ class ScreenshotsPage : public QMainWindow, public BasePage {
private slots:
void on_actionUpload_triggered();
void on_actionCopy_Image_triggered();
void on_actionCopy_File_s_triggered();
void on_actionCopy_Image_triggered() const;
void on_actionCopy_File_s_triggered() const;
void on_actionDelete_triggered();
void on_actionRename_triggered();
void on_actionView_Folder_triggered();
void onItemActivated(QModelIndex);
void onCurrentSelectionChanged(const QItemSelection& selected);
void ShowContextMenu(const QPoint& pos);
void on_actionRename_triggered() const;
void on_actionView_Folder_triggered() const;
void onItemActivated(QModelIndex) const;
void onCurrentSelectionChanged(const QItemSelection& selected) const;
void showContextMenu(const QPoint& pos);
private:
Ui::ScreenshotsPage* ui;

View file

@ -80,14 +80,14 @@ void CustomPage::openedImpl()
void CustomPage::refresh()
{
ui->versionList->loadList();
ui->versionList->loadList(true);
}
void CustomPage::loaderRefresh()
{
if (ui->noneFilter->isChecked())
return;
ui->loaderVersionList->loadList();
ui->loaderVersionList->loadList(true);
}
void CustomPage::filterChanged()

View file

@ -139,15 +139,18 @@ void ResourceModel::search()
if (hasActiveSearchJob())
return;
if (m_search_term.startsWith("#")) {
if (m_search_state != SearchState::ResetRequested && m_search_term.startsWith("#")) {
auto projectId = m_search_term.mid(1);
if (!projectId.isEmpty()) {
ResourceAPI::Callback<ModPlatform::IndexedPack::Ptr> callbacks;
callbacks.on_fail = [this](QString reason, int) {
callbacks.on_fail = [this](QString reason, int network_error_code) {
if (!s_running_models.constFind(this).value())
return;
searchRequestFailed(reason, -1);
if (network_error_code == 404) {
m_search_state = SearchState::ResetRequested;
}
searchRequestFailed(reason, network_error_code);
};
callbacks.on_abort = [this] {
if (!s_running_models.constFind(this).value())
@ -162,7 +165,7 @@ void ResourceModel::search()
};
auto project = std::make_shared<ModPlatform::IndexedPack>();
project->addonId = projectId;
if (auto job = m_api->getProjectInfo({ project }, std::move(callbacks)); job)
if (auto job = m_api->getProjectInfo({ project }, std::move(callbacks), false); job)
runSearchJob(job);
return;
}
@ -407,6 +410,9 @@ void ResourceModel::searchRequestFailed([[maybe_unused]] QString reason, int net
// Network error
QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load mods."));
break;
case 404:
// 404 Not Found, some APIs return this when nothing is found, no need to bother the user
break;
case 409:
// 409 Gone, notify user to update
QMessageBox::critical(nullptr, tr("Error"),
@ -414,7 +420,14 @@ void ResourceModel::searchRequestFailed([[maybe_unused]] QString reason, int net
break;
}
m_search_state = SearchState::Finished;
if (m_search_state == SearchState::ResetRequested) {
clearData();
m_next_search_offset = 0;
search();
} else {
m_search_state = SearchState::Finished;
}
}
void ResourceModel::searchRequestAborted()

View file

@ -165,12 +165,20 @@ void ListModel::fetchMore(const QModelIndex& parent)
void ListModel::performPaginatedSearch()
{
static const FlameAPI api;
if (m_currentSearchTerm.startsWith("#")) {
// activate search by id only for numerical values because all CurseForge ids are numerical
static const QRegularExpression s_projectIdExpr("^\\#[0-9]+$");
if (m_searchState != ResetRequested && s_projectIdExpr.match(m_currentSearchTerm).hasMatch()) {
auto projectId = m_currentSearchTerm.mid(1);
if (!projectId.isEmpty()) {
ResourceAPI::Callback<ModPlatform::IndexedPack::Ptr> callbacks;
callbacks.on_fail = [this](QString reason, int) { searchRequestFailed(reason); };
callbacks.on_fail = [this](QString reason, int network_error_code) {
if (network_error_code == 404) {
m_searchState = ResetRequested;
}
searchRequestFailed(reason);
};
callbacks.on_succeed = [this](auto& pack) { searchRequestForOneSucceeded(pack); };
callbacks.on_abort = [this] {
qCritical() << "Search task aborted by an unknown reason!";
@ -178,7 +186,7 @@ void ListModel::performPaginatedSearch()
};
auto project = std::make_shared<ModPlatform::IndexedPack>();
project->addonId = projectId;
if (auto job = api.getProjectInfo({ project }, std::move(callbacks)); job) {
if (auto job = api.getProjectInfo({ project }, std::move(callbacks), false); job) {
m_jobPtr = job;
m_jobPtr->start();
}

View file

@ -135,20 +135,26 @@ void ModpackListModel::performPaginatedSearch()
return;
static const ModrinthAPI api;
if (m_currentSearchTerm.startsWith("#")) {
// Modrinth ids are not limited to numbers and can be any length
if (m_searchState != ResetRequested && m_currentSearchTerm.startsWith("#")) {
auto projectId = m_currentSearchTerm.mid(1);
if (!projectId.isEmpty()) {
ResourceAPI::Callback<ModPlatform::IndexedPack::Ptr> callbacks;
callbacks.on_fail = [this](QString reason, int) { searchRequestFailed(reason); };
callbacks.on_fail = [this](QString reason, int network_error_code) {
if (network_error_code == 404) {
m_searchState = ResetRequested;
}
searchRequestFailed(reason, network_error_code);
};
callbacks.on_succeed = [this](auto& pack) { searchRequestForOneSucceeded(pack); };
callbacks.on_abort = [this] {
qCritical() << "Search task aborted by an unknown reason!";
searchRequestFailed("Aborted");
searchRequestFailed("Aborted", 0);
};
auto project = std::make_shared<ModPlatform::IndexedPack>();
project->addonId = projectId;
if (auto job = api.getProjectInfo({ project }, std::move(callbacks)); job) {
if (auto job = api.getProjectInfo({ project }, std::move(callbacks), false); job) {
m_jobPtr = job;
m_jobPtr->start();
}
@ -161,10 +167,10 @@ void ModpackListModel::performPaginatedSearch()
ResourceAPI::Callback<QList<ModPlatform::IndexedPack::Ptr>> callbacks{};
callbacks.on_succeed = [this](auto& doc) { searchRequestFinished(doc); };
callbacks.on_fail = [this](QString reason, int) { searchRequestFailed(reason); };
callbacks.on_fail = [this](QString reason, int network_error_code) { searchRequestFailed(reason, network_error_code); };
callbacks.on_abort = [this] {
qCritical() << "Search task aborted by an unknown reason!";
searchRequestFailed("Aborted");
searchRequestFailed("Aborted", 0);
};
auto netJob = api.searchProjects({ ModPlatform::ResourceType::Modpack, m_nextSearchOffset, m_currentSearchTerm, sort, m_filter->loaders,
@ -316,13 +322,12 @@ void ModpackListModel::searchRequestForOneSucceeded(ModPlatform::IndexedPack::Pt
endInsertRows();
}
void ModpackListModel::searchRequestFailed(QString)
void ModpackListModel::searchRequestFailed(QString reason, int network_error_code)
{
auto failed_action = dynamic_cast<NetJob*>(m_jobPtr.get())->getFailedActions().at(0);
if (failed_action->replyStatusCode() == -1) {
// Network error
if (network_error_code == -1) {
// Unknown error in network stack
QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load modpacks."));
} else if (failed_action->replyStatusCode() == 409) {
} else if (network_error_code == 409) {
// 409 Gone, notify user to update
QMessageBox::critical(nullptr, tr("Error"),
//: %1 refers to the launcher itself

View file

@ -85,7 +85,7 @@ class ModpackListModel : public QAbstractListModel {
public slots:
void searchRequestFinished(QList<ModPlatform::IndexedPack::Ptr>& doc_all);
void searchRequestFailed(QString reason);
void searchRequestFailed(QString reason, int network_error_code);
void searchRequestForOneSucceeded(ModPlatform::IndexedPack::Ptr);
protected slots:

View file

@ -151,6 +151,7 @@ void JavaSettingsWidget::loadSettings()
m_ui->maxMemSpinBox->setValue(min);
}
m_ui->permGenSpinBox->setValue(settings->get("PermGen").toInt());
m_ui->lowMemWarningCheckBox->setChecked(settings->get("LowMemWarning").toBool());
// Java arguments
m_ui->javaArgumentsGroupBox->setChecked(m_instance == nullptr || settings->get("OverrideJavaArgs").toBool());
@ -205,10 +206,12 @@ void JavaSettingsWidget::saveSettings()
settings->set("MaxMemAlloc", min);
}
settings->set("PermGen", m_ui->permGenSpinBox->value());
settings->set("LowMemWarning", m_ui->lowMemWarningCheckBox->isChecked());
} else {
settings->reset("MinMemAlloc");
settings->reset("MaxMemAlloc");
settings->reset("PermGen");
settings->reset("LowMemWarning");
}
// Java arguments

View file

@ -55,7 +55,7 @@
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -86,7 +86,7 @@
<item>
<spacer name="horizontalSpacer_7">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -101,10 +101,10 @@
<item row="9" column="0">
<spacer name="verticalSpacer_4">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
<enum>QSizePolicy::Policy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -160,10 +160,10 @@
<item row="3" column="0">
<spacer name="verticalSpacer_5">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
<enum>QSizePolicy::Policy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -190,156 +190,166 @@
<property name="checked">
<bool>false</bool>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="2" column="2">
<widget class="QLabel" name="label_3">
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="labelMinMem">
<property name="text">
<string>M&amp;inimum Memory Usage:</string>
</property>
<property name="buddy">
<cstring>minMemSpinBox</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QSpinBox" name="minMemSpinBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>The amount of memory Minecraft is started with.</string>
</property>
<property name="suffix">
<string notr="true"> MiB</string>
</property>
<property name="minimum">
<number>8</number>
</property>
<property name="maximum">
<number>1048576</number>
</property>
<property name="singleStep">
<number>128</number>
</property>
<property name="value">
<number>256</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>(-Xms)</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QLabel" name="labelMaxMem">
<property name="text">
<string>Ma&amp;ximum Memory Usage:</string>
</property>
<property name="buddy">
<cstring>maxMemSpinBox</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QSpinBox" name="maxMemSpinBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>The maximum amount of memory Minecraft is allowed to use.</string>
</property>
<property name="suffix">
<string notr="true"> MiB</string>
</property>
<property name="minimum">
<number>8</number>
</property>
<property name="maximum">
<number>1048576</number>
</property>
<property name="singleStep">
<number>128</number>
</property>
<property name="value">
<number>1024</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>(-Xmx)</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_11">
<property name="text">
<string>&amp;PermGen Size:</string>
</property>
<property name="buddy">
<cstring>permGenSpinBox</cstring>
</property>
</widget>
</item>
<item row="2" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_6">
<item>
<widget class="QSpinBox" name="permGenSpinBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>The amount of memory available to store loaded Java classes.</string>
</property>
<property name="suffix">
<string notr="true"> MiB</string>
</property>
<property name="minimum">
<number>4</number>
</property>
<property name="maximum">
<number>1048576</number>
</property>
<property name="singleStep">
<number>8</number>
</property>
<property name="value">
<number>64</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>(-XX:PermSize)</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="lowMemWarningCheckBox">
<property name="text">
<string>(-XX:PermSize)</string>
<string>Warn when there is not enough memory available</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QSpinBox" name="permGenSpinBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>The amount of memory available to store loaded Java classes.</string>
</property>
<property name="suffix">
<string notr="true"> MiB</string>
</property>
<property name="minimum">
<number>4</number>
</property>
<property name="maximum">
<number>1048576</number>
</property>
<property name="singleStep">
<number>8</number>
</property>
<property name="value">
<number>64</number>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="maxMemSpinBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>The maximum amount of memory Minecraft is allowed to use.</string>
</property>
<property name="suffix">
<string notr="true"> MiB</string>
</property>
<property name="minimum">
<number>8</number>
</property>
<property name="maximum">
<number>1048576</number>
</property>
<property name="singleStep">
<number>128</number>
</property>
<property name="value">
<number>1024</number>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QLabel" name="label_2">
<property name="text">
<string>(-Xmx)</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QSpinBox" name="minMemSpinBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>The amount of memory Minecraft is started with.</string>
</property>
<property name="suffix">
<string notr="true"> MiB</string>
</property>
<property name="minimum">
<number>8</number>
</property>
<property name="maximum">
<number>1048576</number>
</property>
<property name="singleStep">
<number>128</number>
</property>
<property name="value">
<number>256</number>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_11">
<property name="text">
<string>&amp;PermGen Size:</string>
</property>
<property name="buddy">
<cstring>permGenSpinBox</cstring>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QLabel" name="label">
<property name="text">
<string>(-Xms)</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="labelMaxMem">
<property name="text">
<string>Ma&amp;ximum Memory Usage:</string>
</property>
<property name="buddy">
<cstring>maxMemSpinBox</cstring>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="labelMinMem">
<property name="text">
<string>M&amp;inimum Memory Usage:</string>
</property>
<property name="buddy">
<cstring>minMemSpinBox</cstring>
</property>
</widget>
</item>
<item row="0" column="3">
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
<item row="3" column="0" colspan="4">
<item>
<widget class="QLabel" name="labelMaxMemNotice">
<property name="text">
<string>Memory Notice</string>
@ -382,9 +392,7 @@
<tabstop>autodownloadJavaCheckBox</tabstop>
<tabstop>javaTestBtn</tabstop>
<tabstop>javaDownloadBtn</tabstop>
<tabstop>minMemSpinBox</tabstop>
<tabstop>maxMemSpinBox</tabstop>
<tabstop>permGenSpinBox</tabstop>
<tabstop>jvmArgsTextBox</tabstop>
</tabstops>
<resources/>

View file

@ -142,7 +142,7 @@ void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& o
description_y -= opt.fontMetrics.height();
// On the bottom, aligned to the left after the icon, and featuring at most two lines of text (with some margin space to spare)
painter->drawText(description_x, description_y, remaining_width, cut_text.size() * opt.fontMetrics.height(), Qt::TextWordWrap,
painter->drawText(description_x, description_y, remaining_width, num_lines * opt.fontMetrics.height(), Qt::TextWordWrap,
description);
}

View file

@ -127,9 +127,9 @@ void VersionSelectWidget::closeEvent(QCloseEvent* event)
QWidget::closeEvent(event);
}
void VersionSelectWidget::loadList()
void VersionSelectWidget::loadList(bool forceReload)
{
m_load_task = m_vlist->getLoadTask();
m_load_task = m_vlist->getLoadTask(forceReload);
connect(m_load_task.get(), &Task::succeeded, this, &VersionSelectWidget::onTaskSucceeded);
connect(m_load_task.get(), &Task::failed, this, &VersionSelectWidget::onTaskFailed);
connect(m_load_task.get(), &Task::progress, this, &VersionSelectWidget::changeProgress);

View file

@ -57,7 +57,7 @@ class VersionSelectWidget : public QWidget {
void initialize(BaseVersionList* vlist, bool forceLoad = false);
//! Starts a task that loads the list.
void loadList();
void loadList(bool forceReload = false);
bool hasVersions() const;
BaseVersion::Ptr selectedVersion() const;

View file

@ -1160,8 +1160,6 @@ void PrismUpdaterApp::downloadReleasePage(const QString& api_url, int page)
m_current_task.reset(download);
connect(download.get(), &Net::Download::finished, this, [this]() {
qDebug() << "Download" << m_current_task->getUid().toString() << "finished";
m_current_task.reset();
m_current_url = "";
});
QCoreApplication::processEvents();

View file

@ -4,6 +4,7 @@
!include "x64.nsh"
AllowSkipFiles off
Unicode true
Name "@Launcher_DisplayName@"