aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/build.yml13
-rw-r--r--.github/workflows/winget.yml2
-rw-r--r--.gitmodules3
-rw-r--r--CMakeLists.txt46
-rw-r--r--COPYING.md41
-rw-r--r--README.md73
-rw-r--r--buildconfig/BuildConfig.cpp.in4
-rw-r--r--flake.lock31
-rw-r--r--flake.nix7
-rw-r--r--flatpak/org.prismlauncher.PrismLauncher.yml1
-rw-r--r--launcher/Application.cpp101
-rw-r--r--launcher/Application.h7
-rw-r--r--launcher/ApplicationMessage.cpp8
-rw-r--r--launcher/ApplicationMessage.h4
-rw-r--r--launcher/CMakeLists.txt101
-rw-r--r--launcher/FileSystem.cpp26
-rw-r--r--launcher/FileSystem.h13
-rw-r--r--launcher/HoeDown.h76
-rw-r--r--launcher/InstanceImportTask.cpp74
-rw-r--r--launcher/LaunchController.cpp10
-rw-r--r--launcher/Markdown.h34
-rw-r--r--launcher/ModDownloadTask.cpp72
-rw-r--r--launcher/QObjectPtr.h4
-rw-r--r--launcher/ResourceDownloadTask.cpp90
-rw-r--r--launcher/ResourceDownloadTask.h (renamed from launcher/ModDownloadTask.h)24
-rw-r--r--launcher/minecraft/MinecraftInstance.cpp4
-rw-r--r--launcher/minecraft/PackProfile.cpp28
-rw-r--r--launcher/minecraft/PackProfile.h4
-rw-r--r--launcher/minecraft/World.cpp7
-rw-r--r--launcher/minecraft/auth/Parsers.cpp25
-rw-r--r--launcher/minecraft/auth/steps/EntitlementsStep.cpp5
-rw-r--r--launcher/minecraft/auth/steps/LauncherLoginStep.cpp15
-rw-r--r--launcher/minecraft/auth/steps/MSAStep.cpp7
-rw-r--r--launcher/minecraft/auth/steps/MinecraftProfileStep.cpp5
-rw-r--r--launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp5
-rw-r--r--launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp5
-rw-r--r--launcher/minecraft/auth/steps/XboxProfileStep.cpp10
-rw-r--r--launcher/minecraft/mod/DataPack.cpp108
-rw-r--r--launcher/minecraft/mod/DataPack.h73
-rw-r--r--launcher/minecraft/mod/Mod.cpp24
-rw-r--r--launcher/minecraft/mod/Mod.h6
-rw-r--r--launcher/minecraft/mod/ModDetails.h6
-rw-r--r--launcher/minecraft/mod/ModFolderModel.cpp15
-rw-r--r--launcher/minecraft/mod/ModFolderModel.h1
-rw-r--r--launcher/minecraft/mod/Resource.cpp4
-rw-r--r--launcher/minecraft/mod/Resource.h3
-rw-r--r--launcher/minecraft/mod/ResourceFolderModel.cpp7
-rw-r--r--launcher/minecraft/mod/ResourceFolderModel.h2
-rw-r--r--launcher/minecraft/mod/ResourcePack.cpp13
-rw-r--r--launcher/minecraft/mod/ShaderPack.cpp37
-rw-r--r--launcher/minecraft/mod/ShaderPack.h62
-rw-r--r--launcher/minecraft/mod/WorldSave.cpp43
-rw-r--r--launcher/minecraft/mod/WorldSave.h61
-rw-r--r--launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp177
-rw-r--r--launcher/minecraft/mod/tasks/LocalDataPackParseTask.h65
-rw-r--r--launcher/minecraft/mod/tasks/LocalModParseTask.cpp152
-rw-r--r--launcher/minecraft/mod/tasks/LocalModParseTask.h34
-rw-r--r--launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp110
-rw-r--r--launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h8
-rw-r--r--launcher/minecraft/mod/tasks/LocalResourceParse.cpp78
-rw-r--r--launcher/minecraft/mod/tasks/LocalResourceParse.h37
-rw-r--r--launcher/minecraft/mod/tasks/LocalShaderPackParseTask.cpp113
-rw-r--r--launcher/minecraft/mod/tasks/LocalShaderPackParseTask.h62
-rw-r--r--launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp59
-rw-r--r--launcher/minecraft/mod/tasks/LocalTexturePackParseTask.h8
-rw-r--r--launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.cpp190
-rw-r--r--launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.h62
-rw-r--r--launcher/modplatform/CheckUpdateTask.h14
-rw-r--r--launcher/modplatform/EnsureMetadataTask.cpp94
-rw-r--r--launcher/modplatform/EnsureMetadataTask.h16
-rw-r--r--launcher/modplatform/ModAPI.h118
-rw-r--r--launcher/modplatform/ModIndex.cpp24
-rw-r--r--launcher/modplatform/ModIndex.h40
-rw-r--r--launcher/modplatform/ResourceAPI.h177
-rw-r--r--launcher/modplatform/flame/FlameAPI.cpp41
-rw-r--r--launcher/modplatform/flame/FlameAPI.h110
-rw-r--r--launcher/modplatform/flame/FlameCheckUpdate.cpp14
-rw-r--r--launcher/modplatform/flame/FlameCheckUpdate.h2
-rw-r--r--launcher/modplatform/flame/FlameInstanceCreationTask.cpp156
-rw-r--r--launcher/modplatform/flame/FlameInstanceCreationTask.h5
-rw-r--r--launcher/modplatform/flame/FlameModIndex.cpp8
-rw-r--r--launcher/modplatform/flame/FlameModIndex.h2
-rw-r--r--launcher/modplatform/helpers/HashUtils.cpp16
-rw-r--r--launcher/modplatform/helpers/HashUtils.h10
-rw-r--r--launcher/modplatform/helpers/NetworkModAPI.cpp97
-rw-r--r--launcher/modplatform/helpers/NetworkModAPI.h17
-rw-r--r--launcher/modplatform/helpers/NetworkResourceAPI.cpp128
-rw-r--r--launcher/modplatform/helpers/NetworkResourceAPI.h22
-rw-r--r--launcher/modplatform/modrinth/ModrinthAPI.cpp74
-rw-r--r--launcher/modplatform/modrinth/ModrinthAPI.h134
-rw-r--r--launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp27
-rw-r--r--launcher/modplatform/modrinth/ModrinthCheckUpdate.h4
-rw-r--r--launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp50
-rw-r--r--launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h4
-rw-r--r--launcher/modplatform/modrinth/ModrinthPackIndex.cpp13
-rw-r--r--launcher/modplatform/modrinth/ModrinthPackIndex.h2
-rw-r--r--launcher/modplatform/packwiz/Packwiz.cpp8
-rw-r--r--launcher/modplatform/packwiz/Packwiz.h2
-rw-r--r--launcher/modplatform/technic/TechnicPackProcessor.cpp2
-rw-r--r--launcher/net/NetAction.h4
-rw-r--r--launcher/net/NetJob.cpp9
-rw-r--r--launcher/resources/backgrounds/backgrounds.qrc12
-rw-r--r--launcher/resources/backgrounds/teawie-bday.pngbin0 -> 190586 bytes
-rw-r--r--launcher/resources/backgrounds/teawie-spooky.pngbin0 -> 204756 bytes
-rw-r--r--launcher/resources/backgrounds/teawie-xmas.pngbin0 -> 200137 bytes
-rw-r--r--launcher/resources/backgrounds/teawie.pngbin0 -> 187972 bytes
-rw-r--r--launcher/tasks/ConcurrentTask.cpp34
-rw-r--r--launcher/tasks/ConcurrentTask.h9
-rw-r--r--launcher/tasks/MultipleOptionsTask.cpp4
-rw-r--r--launcher/tasks/SequentialTask.cpp4
-rw-r--r--launcher/ui/GuiUtil.cpp28
-rw-r--r--launcher/ui/GuiUtil.h3
-rw-r--r--launcher/ui/MainWindow.cpp130
-rw-r--r--launcher/ui/MainWindow.h4
-rw-r--r--launcher/ui/WinDarkmode.cpp32
-rw-r--r--launcher/ui/WinDarkmode.h60
-rw-r--r--launcher/ui/dialogs/AboutDialog.cpp6
-rw-r--r--launcher/ui/dialogs/BlockedModsDialog.cpp2
-rw-r--r--launcher/ui/dialogs/ChooseProviderDialog.cpp6
-rw-r--r--launcher/ui/dialogs/ChooseProviderDialog.h6
-rw-r--r--launcher/ui/dialogs/IconPickerDialog.cpp1
-rw-r--r--launcher/ui/dialogs/ImportResourceDialog.cpp (renamed from launcher/ui/dialogs/ImportResourcePackDialog.cpp)20
-rw-r--r--launcher/ui/dialogs/ImportResourceDialog.h30
-rw-r--r--launcher/ui/dialogs/ImportResourceDialog.ui (renamed from launcher/ui/dialogs/ImportResourcePackDialog.ui)17
-rw-r--r--launcher/ui/dialogs/ImportResourcePackDialog.h27
-rw-r--r--launcher/ui/dialogs/ModDownloadDialog.cpp202
-rw-r--r--launcher/ui/dialogs/ModDownloadDialog.h78
-rw-r--r--launcher/ui/dialogs/ModUpdateDialog.cpp55
-rw-r--r--launcher/ui/dialogs/ModUpdateDialog.h8
-rw-r--r--launcher/ui/dialogs/ResourceDownloadDialog.cpp232
-rw-r--r--launcher/ui/dialogs/ResourceDownloadDialog.h111
-rw-r--r--launcher/ui/dialogs/ReviewMessageBox.cpp28
-rw-r--r--launcher/ui/dialogs/ReviewMessageBox.h11
-rw-r--r--launcher/ui/dialogs/ReviewMessageBox.ui14
-rw-r--r--launcher/ui/pages/global/LauncherPage.cpp108
-rw-r--r--launcher/ui/pages/global/LauncherPage.ui161
-rw-r--r--launcher/ui/pages/instance/ExternalResourcesPage.cpp49
-rw-r--r--launcher/ui/pages/instance/ExternalResourcesPage.h3
-rw-r--r--launcher/ui/pages/instance/InstanceSettingsPage.cpp76
-rw-r--r--launcher/ui/pages/instance/InstanceSettingsPage.h14
-rw-r--r--launcher/ui/pages/instance/InstanceSettingsPage.ui42
-rw-r--r--launcher/ui/pages/instance/LogPage.cpp29
-rw-r--r--launcher/ui/pages/instance/ManagedPackPage.cpp8
-rw-r--r--launcher/ui/pages/instance/ManagedPackPage.h4
-rw-r--r--launcher/ui/pages/instance/ModFolderPage.cpp18
-rw-r--r--launcher/ui/pages/instance/ModFolderPage.h5
-rw-r--r--launcher/ui/pages/instance/NotesPage.ui6
-rw-r--r--launcher/ui/pages/instance/OtherLogsPage.cpp31
-rw-r--r--launcher/ui/pages/instance/OtherLogsPage.ui3
-rw-r--r--launcher/ui/pages/instance/ResourcePackPage.h1
-rw-r--r--launcher/ui/pages/instance/ScreenshotsPage.cpp48
-rw-r--r--launcher/ui/pages/instance/ServersPage.cpp15
-rw-r--r--launcher/ui/pages/instance/VersionPage.cpp41
-rw-r--r--launcher/ui/pages/instance/VersionPage.ui3
-rw-r--r--launcher/ui/pages/instance/WorldListPage.cpp18
-rw-r--r--launcher/ui/pages/instance/WorldListPage.ui2
-rw-r--r--launcher/ui/pages/modplatform/ModModel.cpp348
-rw-r--r--launcher/ui/pages/modplatform/ModModel.h94
-rw-r--r--launcher/ui/pages/modplatform/ModPage.cpp362
-rw-r--r--launcher/ui/pages/modplatform/ModPage.h97
-rw-r--r--launcher/ui/pages/modplatform/ResourceModel.cpp444
-rw-r--r--launcher/ui/pages/modplatform/ResourceModel.h147
-rw-r--r--launcher/ui/pages/modplatform/ResourcePage.cpp411
-rw-r--r--launcher/ui/pages/modplatform/ResourcePage.h111
-rw-r--r--launcher/ui/pages/modplatform/ResourcePage.ui (renamed from launcher/ui/pages/modplatform/ModPage.ui)18
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameModModel.cpp31
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameModModel.h26
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp37
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameResourceModels.h30
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp (renamed from launcher/ui/pages/modplatform/flame/FlameModPage.cpp)44
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameResourcePages.h (renamed from launcher/ui/pages/modplatform/flame/FlameModPage.h)43
-rw-r--r--launcher/ui/pages/modplatform/ftb/FtbPage.cpp5
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp1
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp6
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp (renamed from launcher/ui/pages/modplatform/modrinth/ModrinthModModel.cpp)26
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h (renamed from launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h)26
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp (renamed from launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp)60
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h (renamed from launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h)41
-rw-r--r--launcher/ui/setupwizard/SetupWizard.cpp3
-rw-r--r--launcher/ui/setupwizard/ThemeWizardPage.cpp70
-rw-r--r--launcher/ui/setupwizard/ThemeWizardPage.h43
-rw-r--r--launcher/ui/setupwizard/ThemeWizardPage.ui371
-rw-r--r--launcher/ui/themes/CustomTheme.cpp31
-rw-r--r--launcher/ui/themes/ITheme.cpp40
-rw-r--r--launcher/ui/themes/ITheme.h48
-rw-r--r--launcher/ui/themes/SystemTheme.cpp21
-rw-r--r--launcher/ui/themes/SystemTheme.h44
-rw-r--r--launcher/ui/themes/ThemeManager.cpp310
-rw-r--r--launcher/ui/themes/ThemeManager.h109
-rw-r--r--launcher/ui/widgets/ModListView.cpp17
-rw-r--r--launcher/ui/widgets/ProgressWidget.cpp11
-rw-r--r--launcher/ui/widgets/ProgressWidget.h6
-rw-r--r--launcher/ui/widgets/ThemeCustomizationWidget.cpp150
-rw-r--r--launcher/ui/widgets/ThemeCustomizationWidget.h77
-rw-r--r--launcher/ui/widgets/ThemeCustomizationWidget.ui132
-rw-r--r--launcher/ui/widgets/VariableSizedImageObject.cpp2
-rw-r--r--libraries/README.md8
m---------libraries/cmark0
-rw-r--r--libraries/hoedown/CMakeLists.txt26
-rw-r--r--libraries/hoedown/LICENSE15
-rw-r--r--libraries/hoedown/README.md9
-rw-r--r--libraries/hoedown/include/hoedown/autolink.h46
-rw-r--r--libraries/hoedown/include/hoedown/buffer.h134
-rw-r--r--libraries/hoedown/include/hoedown/document.h172
-rw-r--r--libraries/hoedown/include/hoedown/escape.h28
-rw-r--r--libraries/hoedown/include/hoedown/html.h84
-rw-r--r--libraries/hoedown/include/hoedown/stack.h52
-rw-r--r--libraries/hoedown/include/hoedown/version.h33
-rw-r--r--libraries/hoedown/src/autolink.c281
-rw-r--r--libraries/hoedown/src/buffer.c308
-rw-r--r--libraries/hoedown/src/document.c2958
-rw-r--r--libraries/hoedown/src/escape.c188
-rw-r--r--libraries/hoedown/src/html.c754
-rw-r--r--libraries/hoedown/src/html_blocks.c240
-rw-r--r--libraries/hoedown/src/html_smartypants.c435
-rw-r--r--libraries/hoedown/src/stack.c79
-rw-r--r--libraries/hoedown/src/version.c9
-rw-r--r--libraries/katabasis/CMakeLists.txt9
-rw-r--r--libraries/katabasis/include/katabasis/DeviceFlow.h1
-rw-r--r--libraries/katabasis/src/DeviceFlow.cpp6
-rw-r--r--nix/default.nix16
-rw-r--r--program_info/org.prismlauncher.PrismLauncher.metainfo.xml.in29
-rw-r--r--program_info/prismlauncher.6.scd18
-rw-r--r--tests/CMakeLists.txt12
-rw-r--r--tests/DataPackParse_test.cpp79
-rw-r--r--tests/DummyResourceAPI.h47
-rw-r--r--tests/Packwiz_test.cpp4
-rw-r--r--tests/ResourceModel_test.cpp88
-rw-r--r--tests/ResourcePackParse_test.cpp9
-rw-r--r--tests/ShaderPackParse_test.cpp77
-rw-r--r--tests/Task_test.cpp60
-rw-r--r--tests/TexturePackParse_test.cpp9
-rw-r--r--tests/WorldSaveParse_test.cpp94
-rw-r--r--tests/testdata/DataPackParse/another_test_folder/data/dummy/tags/item/foo_proof/bar.json1
-rw-r--r--tests/testdata/DataPackParse/another_test_folder/pack.mcmeta6
-rw-r--r--tests/testdata/DataPackParse/test_data_pack_boogaloo.zipbin0 -> 898 bytes
-rw-r--r--tests/testdata/DataPackParse/test_folder/data/dummy/tags/item/foo_proof/bar.json1
-rw-r--r--tests/testdata/DataPackParse/test_folder/pack.mcmeta6
-rw-r--r--tests/testdata/ResourcePackParse/test_resource_pack_idk.zipbin322 -> 804 bytes
-rw-r--r--tests/testdata/ShaderPackParse/shaderpack1.zipbin0 -> 242 bytes
-rw-r--r--tests/testdata/ShaderPackParse/shaderpack2/shaders/shaders.properties0
-rw-r--r--tests/testdata/ShaderPackParse/shaderpack3.zipbin0 -> 128 bytes
-rw-r--r--tests/testdata/WorldSaveParse/minecraft_save_1.zipbin0 -> 184 bytes
-rw-r--r--tests/testdata/WorldSaveParse/minecraft_save_2.zipbin0 -> 352 bytes
-rw-r--r--tests/testdata/WorldSaveParse/minecraft_save_3/world_3/level.dat0
-rw-r--r--tests/testdata/WorldSaveParse/minecraft_save_4/saves/world_4/level.dat0
246 files changed, 7091 insertions, 9141 deletions
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 1ba5d0e4..1373815c 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -61,7 +61,7 @@ jobs:
qt_ver: 6
qt_host: windows
qt_arch: ''
- qt_version: '6.4.0'
+ qt_version: '6.4.2'
qt_modules: 'qt5compat qtimageformats'
qt_tools: ''
@@ -73,7 +73,7 @@ jobs:
qt_ver: 6
qt_host: windows
qt_arch: 'win64_msvc2019_arm64'
- qt_version: '6.4.0'
+ qt_version: '6.4.2'
qt_modules: 'qt5compat qtimageformats'
qt_tools: ''
@@ -105,6 +105,7 @@ jobs:
INSTALL_APPIMAGE_DIR: "install-appdir"
BUILD_DIR: "build"
CCACHE_VAR: ""
+ HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1
steps:
##
@@ -135,6 +136,7 @@ jobs:
quazip-qt6:p
ccache:p
qt6-5compat:p
+ cmark:p
- name: Force newer ccache
if: runner.os == 'Windows' && matrix.msystem == '' && inputs.build_type == 'Debug'
@@ -143,7 +145,7 @@ jobs:
- name: Setup ccache
if: (runner.os != 'Windows' || matrix.msystem == '') && inputs.build_type == 'Debug'
- uses: hendrikmuhs/ccache-action@v1.2.5
+ uses: hendrikmuhs/ccache-action@v1.2.8
with:
key: ${{ matrix.os }}-qt${{ matrix.qt_ver }}-${{ matrix.architecture }}
@@ -165,7 +167,7 @@ jobs:
- name: Retrieve ccache cache (Windows MinGW-w64)
if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug'
- uses: actions/cache@v3.0.11
+ uses: actions/cache@v3.2.3
with:
path: '${{ github.workspace }}\.ccache'
key: ${{ matrix.os }}-mingw-w64
@@ -546,11 +548,10 @@ jobs:
submodules: 'true'
- name: Build Flatpak (Linux)
if: inputs.build_type == 'Debug'
- uses: flatpak/flatpak-github-actions/flatpak-builder@v4
+ uses: flatpak/flatpak-github-actions/flatpak-builder@v5
with:
bundle: "Prism Launcher.flatpak"
manifest-path: flatpak/org.prismlauncher.PrismLauncher.yml
- cache-key: flatpak-${{ github.sha }}-x86_64
nix:
runs-on: ubuntu-latest
diff --git a/.github/workflows/winget.yml b/.github/workflows/winget.yml
index 5c34040f..ef9561cf 100644
--- a/.github/workflows/winget.yml
+++ b/.github/workflows/winget.yml
@@ -11,5 +11,5 @@ jobs:
with:
identifier: PrismLauncher.PrismLauncher
version: ${{ github.event.release.tag_name }}
- installers-regex: 'PrismLauncher-Windows-Setup-.+\.exe$'
+ installers-regex: 'PrismLauncher-Windows-MSVC(:?-arm64|-Legacy)?-Setup-.+\.exe$'
token: ${{ secrets.WINGET_TOKEN }}
diff --git a/.gitmodules b/.gitmodules
index 95274f15..87703fee 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -16,3 +16,6 @@
[submodule "libraries/extra-cmake-modules"]
path = libraries/extra-cmake-modules
url = https://github.com/KDE/extra-cmake-modules
+[submodule "libraries/cmark"]
+ path = libraries/cmark
+ url = https://github.com/commonmark/cmark.git
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 75360f86..f32a73d1 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -28,19 +28,28 @@ set(CMAKE_CXX_STANDARD 17)
set(CMAKE_C_STANDARD 11)
include(GenerateExportHeader)
if(MSVC)
- # Use /W4 as /Wall includes unnesserey warnings such as added padding to structs
- # /permissive- specify standards-conforming compiler behavior, also enabled by Qt6, default on with std:c++20
# /GS Adds buffer security checks, default on but incuded anyway to mirror gcc's fstack-protector flag
- set(CMAKE_CXX_FLAGS "/W4 /permissive- /GS ${CMAKE_CXX_FLAGS}")
+ # /permissive- specify standards-conforming compiler behavior, also enabled by Qt6, default on with std:c++20
+ # Use /W4 as /Wall includes unnesserey warnings such as added padding to structs
+ set(CMAKE_CXX_FLAGS "/GS /permissive- /W4 ${CMAKE_CXX_FLAGS}")
# LINK accepts /SUBSYSTEM whics sets if we are a WINDOWS (gui) or a CONSOLE programs
# This implicitly selects an entrypoint specific to the subsystem selected
# qtmain/QtEntryPointLib provides the correct entrypoint (wWinMain) for gui programs
# Additinaly LINK autodetects we use a GUI so we can omit /SUBSYSTEM
# This allows tests to still use have console without using seperate linker flags
+ # /LTCG allows for linking wholy optimizated programs
# /MANIFEST:NO disables generating a manifest file, we instead provide our own
# /STACK sets the stack reserve size, ATL's pack list needs 3-4 MiB as of November 2022, provide 8 MiB
- set(CMAKE_EXE_LINKER_FLAGS "/MANIFEST:NO /STACK:8388608 ${CMAKE_EXE_LINKER_FLAGS}")
+ set(CMAKE_EXE_LINKER_FLAGS "/LTCG /MANIFEST:NO /STACK:8388608 ${CMAKE_EXE_LINKER_FLAGS}")
+
+ # /GL enables whole program optimizations
+ # /Gw helps reduce binary size
+ # /Gy allows the compiler to package individual functions
+ # /guard:cf enables control flow guard
+ foreach(lang C CXX)
+ set("CMAKE_${lang}_FLAGS_RELEASE" "/GL /Gw /Gy /guard:cf")
+ endforeach()
# See https://github.com/ccache/ccache/issues/1040
# Note, CMake 3.25 replaces this with CMAKE_MSVC_DEBUG_INFORMATION_FORMAT
@@ -199,9 +208,15 @@ set(Launcher_BUILD_TIMESTAMP "${TODAY}")
################################ 3rd Party Libs ################################
-if(NOT Launcher_FORCE_BUNDLED_LIBS)
+# Successive configurations of cmake without cleaning the build dir will cause zlib fallback to fail due to cached values
+# Record when fallback triggered and skip this find_package
+if(NOT Launcher_FORCE_BUNDLED_LIBS AND NOT FORCE_BUNDLED_ZLIB)
find_package(ZLIB QUIET)
endif()
+if(NOT ZLIB_FOUND)
+ set(FORCE_BUNDLED_ZLIB TRUE CACHE BOOL "")
+ mark_as_advanced(FORCE_BUNDLED_ZLIB)
+endif()
# Find the required Qt parts
include(QtVersionlessBackport)
@@ -257,8 +272,13 @@ if(NOT Launcher_FORCE_BUNDLED_LIBS)
# Find ghc_filesystem
find_package(ghc_filesystem QUIET)
+
+ # Find cmark
+ find_package(cmark QUIET)
endif()
+include(ECMQtDeclareLoggingCategory)
+
####################################### Program Info #######################################
set(Launcher_APP_BINARY_NAME "prismlauncher" CACHE STRING "Name of the Launcher binary")
@@ -363,16 +383,16 @@ option(NBT_BUILD_TESTS "Build NBT library tests" OFF) #FIXME: fix unit tests.
add_subdirectory(libraries/libnbtplusplus)
add_subdirectory(libraries/systeminfo) # system information library
-add_subdirectory(libraries/hoedown) # markdown parser
add_subdirectory(libraries/launcher) # java based launcher part for Minecraft
add_subdirectory(libraries/javacheck) # java compatibility checker
-if(NOT ZLIB_FOUND)
+if(FORCE_BUNDLED_ZLIB)
message(STATUS "Using bundled zlib")
+
set(CMAKE_POLICY_DEFAULT_CMP0069 NEW) # Suppress cmake warnings and allow INTERPROCEDURAL_OPTIMIZATION for zlib
set(SKIP_INSTALL_ALL ON)
add_subdirectory(libraries/zlib EXCLUDE_FROM_ALL)
- set(ZLIB_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/libraries/zlib" "${CMAKE_CURRENT_BINARY_DIR}/libraries/zlib" CACHE STRING "")
+ set(ZLIB_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/libraries/zlib" "${CMAKE_CURRENT_BINARY_DIR}/libraries/zlib" CACHE STRING "" FORCE)
set_target_properties(zlibstatic PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${ZLIB_INCLUDE_DIR}")
add_library(ZLIB::ZLIB ALIAS zlibstatic)
set(ZLIB_LIBRARY ZLIB::ZLIB CACHE STRING "zlib library name")
@@ -397,6 +417,16 @@ if(NOT tomlplusplus_FOUND)
else()
message(STATUS "Using system tomlplusplus")
endif()
+if(NOT cmark_FOUND)
+ message(STATUS "Using bundled cmark")
+ set(CMARK_STATIC ON CACHE BOOL "Build static libcmark library" FORCE)
+ set(CMARK_SHARED OFF CACHE BOOL "Build shared libcmark library" FORCE)
+ set(CMARK_TESTS OFF CACHE BOOL "Build cmark tests and enable testing" FORCE)
+ add_subdirectory(libraries/cmark EXCLUDE_FROM_ALL) # Markdown parser
+ add_library(cmark::cmark ALIAS cmark_static)
+else()
+ message(STATUS "Using system cmark")
+endif()
add_subdirectory(libraries/katabasis) # An OAuth2 library that tried to do too much
add_subdirectory(libraries/gamemode)
add_subdirectory(libraries/murmur2) # Hash for usage with the CurseForge API
diff --git a/COPYING.md b/COPYING.md
index 75a5c0eb..0221d1b0 100644
--- a/COPYING.md
+++ b/COPYING.md
@@ -1,7 +1,7 @@
## Prism Launcher
Prism Launcher - Minecraft Launcher
- Copyright (C) 2022 Prism Launcher Contributors
+ Copyright (C) 2022-2023 Prism Launcher Contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -156,23 +156,34 @@
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
-## Hoedown
+## cmark
- Copyright (c) 2008, Natacha Porté
- Copyright (c) 2011, Vicent Martí
- Copyright (c) 2014, Xavier Mendez, Devin Torres and the Hoedown authors
+ Copyright (c) 2014, John MacFarlane
- Permission to use, copy, modify, and distribute this software for any
- purpose with or without fee is hereby granted, provided that the above
- copyright notice and this permission notice appear in all copies.
+ All rights reserved.
- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
## Batch icon set
diff --git a/README.md b/README.md
index f02b5695..8765da93 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,15 @@
-<p align="left">
+<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="/program_info/org.prismlauncher.PrismLauncher.logo-darkmode.svg">
<source media="(prefers-color-scheme: light)" srcset="/program_info/org.prismlauncher.PrismLauncher.logo.svg">
- <img alt="Prism Launcher" src="/program_info/org.prismlauncher.PrismLauncher.logo.svg" width="50%">
+ <img alt="Prism Launcher" src="/program_info/org.prismlauncher.PrismLauncher.logo.svg" width="40%">
</picture>
</p>
-
-Prism Launcher is a custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once.
-
-This is a **fork** of the MultiMC Launcher and is not endorsed by MultiMC.
-
+<p align="center">
+ Prism Launcher is a custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once.<br />
+ <br />This is a <b>fork</b> of the MultiMC Launcher and is <b>not</b> endorsed by it.
+</p>
## Installation
@@ -18,7 +17,7 @@ This is a **fork** of the MultiMC Launcher and is not endorsed by MultiMC.
<img src="https://repology.org/badge/vertical-allrepos/prismlauncher.svg" alt="Packaging status" align="right">
</a>
-- All downloads and instructions for Prism Launcher can be found on our [Website](https://prismlauncher.org/download/).
+- All downloads and instructions for Prism Launcher can be found on our [Website](https://prismlauncher.org/download).
- Last build status can be found in the [GitHub Actions](https://github.com/PrismLauncher/PrismLauncher/actions).
### Development Builds
@@ -29,44 +28,33 @@ Prebuilt Development builds are provided for **Linux**, **Windows** and **macOS*
For **Arch**, **Debian**, **Fedora**, **OpenSUSE (Tumbleweed)** and **Gentoo**, respectively, you can use these packages for the latest development versions:
-[![prismlauncher-git](https://img.shields.io/badge/aur-prismlauncher--git-1793D1?style=flat-square&logo=archlinux&logoColor=white)](https://aur.archlinux.org/packages/prismlauncher-git/) [![prismlauncher-git](https://img.shields.io/badge/aur-prismlauncher--qt5--git-1793D1?style=flat-square&logo=archlinux&logoColor=white)](https://aur.archlinux.org/packages/prismlauncher-qt5-git/) [![prismlauncher-git](https://img.shields.io/badge/mpr-prismlauncher--git-A80030?style=flat-square&logo=debian&logoColor=white)](https://mpr.makedeb.org/packages/prismlauncher-git) [![prismlauncher-nightly](https://img.shields.io/badge/copr-prismlauncher--nightly-51A2DA?style=flat-square&logo=fedora&logoColor=white)](https://copr.fedorainfracloud.org/coprs/g3tchoo/prismlauncher/) [![prismlauncher-nightly](https://img.shields.io/badge/OBS-prismlauncher--nightly-3AB6A9?style=flat-square&logo=opensuse&logoColor=white)](https://build.opensuse.org/project/show/home:getchoo) [![prismlauncher-9999](https://img.shields.io/badge/gentoo-prismlauncher--9999-4D4270?style=flat-square&logo=gentoo&logoColor=white)](https://packages.gentoo.org/packages/games-action/prismlauncher)
+[![prismlauncher-git](https://img.shields.io/badge/aur-prismlauncher--git-1793D1?label=AUR&logo=archlinux&logoColor=white)](https://aur.archlinux.org/packages/prismlauncher-git) [![prismlauncher-git](https://img.shields.io/badge/aur-prismlauncher--qt5--git-1793D1?label=AUR&logo=archlinux&logoColor=white)](https://aur.archlinux.org/packages/prismlauncher-qt5-git) [![prismlauncher-git](https://img.shields.io/badge/mpr-prismlauncher--git-A80030?label=MPR&logo=debian&logoColor=white)](https://mpr.makedeb.org/packages/prismlauncher-git)<br />[![prismlauncher-nightly](https://img.shields.io/badge/copr-prismlauncher--nightly-51A2DA?label=CORP&logo=fedora&logoColor=white)](https://copr.fedorainfracloud.org/coprs/g3tchoo/prismlauncher/) [![prismlauncher-nightly](https://img.shields.io/badge/OBS-prismlauncher--nightly-3AB6A9?logo=opensuse&logoColor=white)](https://build.opensuse.org/project/show/home:getchoo) [![prismlauncher-9999](https://img.shields.io/badge/gentoo-prismlauncher--9999-4D4270?label=Gentoo&logo=gentoo&logoColor=white)](https://packages.gentoo.org/packages/games-action/prismlauncher)
+
+These packages are also availiable to all the distributions based on the ones mentioned above.
## Community & Support
-Feel free to create a GitHub issue if you find a bug or want to suggest a new feature. We have multiple community spaces where other community members can help you.
+Feel free to create a GitHub issue if you find a bug or want to suggest a new feature. We have multiple community spaces where other community members can help you:
-#### Join our Discord server:
-[![Prism Launcher Discord server](https://discordapp.com/api/guilds/1031648380885147709/widget.png?style=banner2)](https://discord.gg/prismlauncher)
+- **Our Discord server:**
-#### Join our Matrix space:
-[![PrismLauncher Space](https://img.shields.io/matrix/prismlauncher:matrix.org?style=for-the-badge&logo=matrix)](https://matrix.to/#/#prismlauncher:matrix.org)
+[![Prism Launcher Discord server](https://discordapp.com/api/guilds/1031648380885147709/widget.png?style=banner3)](https://discord.gg/prismlauncher)
-#### Join our Subreddit:
-[![r/PrismLauncher](https://img.shields.io/reddit/subreddit-subscribers/prismlauncher?style=for-the-badge&logo=reddit)](https://www.reddit.com/r/PrismLauncher/)
+- **Our Matrix space:**
-## Building
+[![PrismLauncher Space](https://img.shields.io/matrix/prismlauncher:matrix.org?style=for-the-badge&label=Matrix%20Space&logo=matrix&color=purple)](https://matrix.to/#/#prismlauncher:matrix.org)
-If you want to build Prism Launcher yourself, check the [Build Instructions](https://prismlauncher.org/wiki/development/build-instructions/).
+- **Our Subreddit:**
+
+[![r/PrismLauncher](https://img.shields.io/reddit/subreddit-subscribers/prismlauncher?style=for-the-badge&logo=reddit)](https://www.reddit.com/r/PrismLauncher/)
## Translations
The translation effort for PrismLauncher is hosted on [Weblate](https://hosted.weblate.org/projects/prismlauncher/launcher/) and information about translating Prism Launcher is available at <https://github.com/PrismLauncher/Translations>
-## Forking/Redistributing/Custom builds policy
-
-We don't care what you do with your fork/custom build as long as you follow the terms of the [license](LICENSE) (this is a legal responsibility), and if you made code changes rather than just packaging a custom build, please do the following as a basic courtesy:
-
-- Make it clear that your fork is not PrismLauncher and is not endorsed by or affiliated with the PrismLauncher project (<https://prismlauncher.org>).
-- Go through [CMakeLists.txt](CMakeLists.txt) and change PrismLauncher's API keys to your own or set them to empty strings (`""`) to disable them (this way the program will still compile but the functionality requiring those keys will be disabled).
-
-If you have any questions or want any clarification on the above conditions please make an issue and ask us.
-
-Be aware that if you build this software without removing the provided API keys in [CMakeLists.txt](CMakeLists.txt) you are accepting the following terms and conditions:
-
-- [Microsoft Identity Platform Terms of Use](https://docs.microsoft.com/en-us/legal/microsoft-identity-platform/terms-of-use)
-- [CurseForge 3rd Party API Terms and Conditions](https://support.curseforge.com/en/support/solutions/articles/9000207405-curse-forge-3rd-party-api-terms-and-conditions)
+## Building
-If you do not agree with these terms and conditions, then remove the associated API keys from the [CMakeLists.txt](CMakeLists.txt) file by setting them to an empty string (`""`).
+If you want to build Prism Launcher yourself, check the [Build Instructions](https://prismlauncher.org/wiki/development/build-instructions/).
## Sponsors & Partners
@@ -84,7 +72,7 @@ Thanks to Weblate for hosting our translation efforts.
<img src="https://hosted.weblate.org/widgets/prismlauncher/-/open-graph.png" alt="Translation status" width="300" />
</a>
-Thanks to Netlify for providing us their excellent web services, as part of their [Open Source program](https://www.netlify.com/open-source/)
+Thanks to Netlify for providing us their excellent web services, as part of their [Open Source program](https://www.netlify.com/open-source/).
<a href="https://www.netlify.com"> <img src="https://www.netlify.com/v3/img/components/netlify-color-accent.svg" alt="Deploys by Netlify" /> </a>
@@ -92,11 +80,24 @@ Thanks to the awesome people over at [MacStadium](https://www.macstadium.com/),
<a href="https://www.macstadium.com"><img src="https://uploads-ssl.webflow.com/5ac3c046c82724970fc60918/5c019d917bba312af7553b49_MacStadium-developerlogo.png" alt="Powered by MacStadium" width="300"></a>
+## Forking/Redistributing/Custom builds policy
-## License
+We don't care what you do with your fork/custom build as long as you follow the terms of the [license](LICENSE) (this is a legal responsibility), and if you made code changes rather than just packaging a custom build, please do the following as a basic courtesy:
-All launcher code is available under the GPL-3.0-only license.
+- Make it clear that your fork is not PrismLauncher and is not endorsed by or affiliated with the PrismLauncher project (<https://prismlauncher.org>).
+- Go through [CMakeLists.txt](CMakeLists.txt) and change PrismLauncher's API keys to your own or set them to empty strings (`""`) to disable them (this way the program will still compile but the functionality requiring those keys will be disabled).
-![https://github.com/PrismLauncher/PrismLauncher/blob/develop/LICENSE](https://img.shields.io/github/license/PrismLauncher/PrismLauncher?style=for-the-badge&logo=gnu&color=C4282D)
+If you have any questions or want any clarification on the above conditions please make an issue and ask us.
+
+Be aware that if you build this software without removing the provided API keys in [CMakeLists.txt](CMakeLists.txt) you are accepting the following terms and conditions:
+
+- [Microsoft Identity Platform Terms of Use](https://docs.microsoft.com/en-us/legal/microsoft-identity-platform/terms-of-use)
+- [CurseForge 3rd Party API Terms and Conditions](https://support.curseforge.com/en/support/solutions/articles/9000207405-curse-forge-3rd-party-api-terms-and-conditions)
+
+If you do not agree with these terms and conditions, then remove the associated API keys from the [CMakeLists.txt](CMakeLists.txt) file by setting them to an empty string (`""`).
+
+## License ![https://github.com/PrismLauncher/PrismLauncher/blob/develop/LICENSE](https://img.shields.io/github/license/PrismLauncher/PrismLauncher?label=License&logo=gnu&color=C4282D)
+
+All launcher code is available under the GPL-3.0-only license.
The logo and related assets are under the CC BY-SA 4.0 license.
diff --git a/buildconfig/BuildConfig.cpp.in b/buildconfig/BuildConfig.cpp.in
index 1262ce8e..35373189 100644
--- a/buildconfig/BuildConfig.cpp.in
+++ b/buildconfig/BuildConfig.cpp.in
@@ -76,7 +76,9 @@ Config::Config()
// Assume that builds outside of Git repos are "stable"
if (GIT_REFSPEC == QStringLiteral("GITDIR-NOTFOUND")
- || GIT_TAG == QStringLiteral("GITDIR-NOTFOUND"))
+ || GIT_TAG == QStringLiteral("GITDIR-NOTFOUND")
+ || GIT_REFSPEC == QStringLiteral("")
+ || GIT_TAG == QStringLiteral("GIT-NOTFOUND"))
{
GIT_REFSPEC = "refs/heads/stable";
GIT_TAG = versionString();
diff --git a/flake.lock b/flake.lock
index 7c0bb2f8..051e1664 100644
--- a/flake.lock
+++ b/flake.lock
@@ -3,11 +3,11 @@
"flake-compat": {
"flake": false,
"locked": {
- "lastModified": 1650374568,
- "narHash": "sha256-Z+s0J8/r907g149rllvwhb4pKi8Wam5ij0st8PwAh+E=",
+ "lastModified": 1668681692,
+ "narHash": "sha256-Ht91NGdewz8IQLtWZ9LCeNXMSXHUss+9COoqu6JLmXU=",
"owner": "edolstra",
"repo": "flake-compat",
- "rev": "b4a34015c698c7793d592d66adbab377907a2be8",
+ "rev": "009399224d5e398d03b22badca40a37ac85412a1",
"type": "github"
},
"original": {
@@ -34,11 +34,11 @@
},
"nixpkgs": {
"locked": {
- "lastModified": 1666057921,
- "narHash": "sha256-VpQqtXdj6G7cH//SvoprjR7XT3KS7p+tCVebGK1N6tE=",
+ "lastModified": 1671417167,
+ "narHash": "sha256-JkHam6WQOwZN1t2C2sbp1TqMv3TVRjzrdoejqfefwrM=",
"owner": "nixos",
"repo": "nixpkgs",
- "rev": "88eab1e431cabd0ed621428d8b40d425a07af39f",
+ "rev": "bb31220cca6d044baa6dc2715b07497a2a7c4bc7",
"type": "github"
},
"original": {
@@ -52,24 +52,7 @@
"inputs": {
"flake-compat": "flake-compat",
"libnbtplusplus": "libnbtplusplus",
- "nixpkgs": "nixpkgs",
- "tomlplusplus": "tomlplusplus"
- }
- },
- "tomlplusplus": {
- "flake": false,
- "locked": {
- "lastModified": 1666091090,
- "narHash": "sha256-djpMCFPvkJcfynV8WnsYdtwLq+J7jpV1iM4C6TojiyM=",
- "owner": "marzer",
- "repo": "tomlplusplus",
- "rev": "1e4a3833d013aee08f58c5b31c69f709afc69f73",
- "type": "github"
- },
- "original": {
- "owner": "marzer",
- "repo": "tomlplusplus",
- "type": "github"
+ "nixpkgs": "nixpkgs"
}
}
},
diff --git a/flake.nix b/flake.nix
index b1e07c91..5615a758 100644
--- a/flake.nix
+++ b/flake.nix
@@ -5,10 +5,9 @@
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
flake-compat = { url = "github:edolstra/flake-compat"; flake = false; };
libnbtplusplus = { url = "github:PrismLauncher/libnbtplusplus"; flake = false; };
- tomlplusplus = { url = "github:marzer/tomlplusplus"; flake = false; };
};
- outputs = { self, nixpkgs, libnbtplusplus, tomlplusplus, ... }:
+ outputs = { self, nixpkgs, libnbtplusplus, ... }:
let
# User-friendly version number.
version = builtins.substring 0 8 self.lastModifiedDate;
@@ -23,8 +22,8 @@
pkgs = forAllSystems (system: nixpkgs.legacyPackages.${system});
packagesFn = pkgs: rec {
- prismlauncher-qt5 = pkgs.libsForQt5.callPackage ./nix { inherit version self libnbtplusplus tomlplusplus; };
- prismlauncher = pkgs.qt6Packages.callPackage ./nix { inherit version self libnbtplusplus tomlplusplus; };
+ prismlauncher-qt5 = pkgs.libsForQt5.callPackage ./nix { inherit version self libnbtplusplus; };
+ prismlauncher = pkgs.qt6Packages.callPackage ./nix { inherit version self libnbtplusplus; };
};
in
{
diff --git a/flatpak/org.prismlauncher.PrismLauncher.yml b/flatpak/org.prismlauncher.PrismLauncher.yml
index fca306d7..071772c6 100644
--- a/flatpak/org.prismlauncher.PrismLauncher.yml
+++ b/flatpak/org.prismlauncher.PrismLauncher.yml
@@ -39,6 +39,7 @@ modules:
sources:
- type: dir
path: ../
+ builddir: true
- name: openjdk
buildsystem: simple
build-commands:
diff --git a/launcher/Application.cpp b/launcher/Application.cpp
index dc1d7f98..fd2a0378 100644
--- a/launcher/Application.cpp
+++ b/launcher/Application.cpp
@@ -62,15 +62,11 @@
#include "ui/pages/global/APIPage.h"
#include "ui/pages/global/CustomCommandsPage.h"
-#ifdef Q_OS_WIN
-#include "ui/WinDarkmode.h"
-#include <versionhelpers.h>
-#endif
-
#include "ui/setupwizard/SetupWizard.h"
#include "ui/setupwizard/LanguageWizardPage.h"
#include "ui/setupwizard/JavaWizardPage.h"
#include "ui/setupwizard/PasteWizardPage.h"
+#include "ui/setupwizard/ThemeWizardPage.h"
#include "ui/dialogs/CustomMessageBox.h"
@@ -150,19 +146,12 @@ static const QLatin1String liveCheckFile("live.check");
PixmapCache* PixmapCache::s_instance = nullptr;
namespace {
+
+/** This is used so that we can output to the log file in addition to the CLI. */
void appDebugOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg)
{
- const char *levels = "DWCFIS";
- const QString format("%1 %2 %3\n");
-
- qint64 msecstotal = APPLICATION->timeSinceStart();
- qint64 seconds = msecstotal / 1000;
- qint64 msecs = msecstotal % 1000;
- QString foo;
- char buf[1025] = {0};
- ::snprintf(buf, 1024, "%5lld.%03lld", seconds, msecs);
-
- QString out = format.arg(buf).arg(levels[type]).arg(msg);
+ QString out = qFormatLogMessage(type, context, msg);
+ out += QChar::LineFeed;
APPLICATION->logFile->write(out.toUtf8());
APPLICATION->logFile->flush();
@@ -231,9 +220,19 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
m_serverToJoin = parser.value("server");
m_profileToUse = parser.value("profile");
m_liveCheck = parser.isSet("alive");
- m_zipToImport = parser.value("import");
+
m_instanceIdToShowWindowOf = parser.value("show");
+ for (auto zip_path : parser.values("import")){
+ m_zipsToImport.append(QUrl::fromLocalFile(QFileInfo(zip_path).absoluteFilePath()));
+ }
+
+ // treat unspecified positional arguments as import urls
+ for (auto zip_path : parser.positionalArguments()) {
+ m_zipsToImport.append(QUrl::fromLocalFile(QFileInfo(zip_path).absoluteFilePath()));
+ }
+
+
// error if --launch is missing with --server or --profile
if((!m_serverToJoin.isEmpty() || !m_profileToUse.isEmpty()) && m_instanceIdToLaunch.isEmpty())
{
@@ -317,7 +316,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
}
/*
- * Establish the mechanism for communication with an already running PolyMC that uses the same data path.
+ * Establish the mechanism for communication with an already running PrismLauncher that uses the same data path.
* If there is one, tell it what the user actually wanted to do and exit.
* We want to initialize this before logging to avoid messing with the log of a potential already running copy.
*/
@@ -335,12 +334,14 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
activate.command = "activate";
m_peerInstance->sendMessage(activate.serialize(), timeout);
- if(!m_zipToImport.isEmpty())
+ if(!m_zipsToImport.isEmpty())
{
- ApplicationMessage import;
- import.command = "import";
- import.args.insert("path", m_zipToImport.toString());
- m_peerInstance->sendMessage(import.serialize(), timeout);
+ for (auto zip_url : m_zipsToImport) {
+ ApplicationMessage import;
+ import.command = "import";
+ import.args.insert("path", zip_url.toString());
+ m_peerInstance->sendMessage(import.serialize(), timeout);
+ }
}
}
else
@@ -396,6 +397,14 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
return;
}
qInstallMessageHandler(appDebugOutput);
+
+ qSetMessagePattern(
+ "%{time process}" " "
+ "%{if-debug}D%{endif}" "%{if-info}I%{endif}" "%{if-warning}W%{endif}" "%{if-critical}C%{endif}" "%{if-fatal}F%{endif}"
+ " " "|" " "
+ "%{if-category}[%{category}]: %{endif}"
+ "%{message}");
+
qDebug() << "<> Log initialized.";
}
@@ -462,7 +471,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
// Theming
m_settings->registerSetting("IconTheme", QString("pe_colored"));
- m_settings->registerSetting("ApplicationTheme", QString("system"));
+ m_settings->registerSetting("ApplicationTheme", QString());
m_settings->registerSetting("BackgroundCat", QString("kitteh"));
// Remembered state
@@ -811,10 +820,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
});
{
- setIconTheme(settings()->get("IconTheme").toString());
- qDebug() << "<> Icon theme set.";
- setApplicationTheme(settings()->get("ApplicationTheme").toString(), true);
- qDebug() << "<> Application theme set.";
+ applyCurrentlySelectedTheme();
}
updateCapabilities();
@@ -857,7 +863,8 @@ bool Application::createSetupWizard()
return false;
}();
bool pasteInterventionRequired = settings()->get("PastebinURL") != "";
- bool wizardRequired = javaRequired || languageRequired || pasteInterventionRequired;
+ bool themeInterventionRequired = settings()->get("ApplicationTheme") == "";
+ bool wizardRequired = javaRequired || languageRequired || pasteInterventionRequired || themeInterventionRequired;
if(wizardRequired)
{
@@ -876,6 +883,12 @@ bool Application::createSetupWizard()
{
m_setupWizard->addPage(new PasteWizardPage(m_setupWizard));
}
+
+ if (themeInterventionRequired)
+ {
+ settings()->set("ApplicationTheme", QString("system")); // set default theme after going into theme wizard
+ m_setupWizard->addPage(new ThemeWizardPage(m_setupWizard));
+ }
connect(m_setupWizard, &QDialog::finished, this, &Application::setupWizardFinished);
m_setupWizard->show();
return true;
@@ -898,7 +911,7 @@ bool Application::event(QEvent* event)
if (event->type() == QEvent::FileOpen) {
auto ev = static_cast<QFileOpenEvent*>(event);
- m_mainWindow->droppedURLs({ ev->url() });
+ m_mainWindow->processURLs({ ev->url() });
}
return QApplication::event(event);
@@ -958,10 +971,10 @@ void Application::performMainStartupAction()
showMainWindow(false);
qDebug() << "<> Main window shown.";
}
- if(!m_zipToImport.isEmpty())
+ if(!m_zipsToImport.isEmpty())
{
- qDebug() << "<> Importing instance from zip:" << m_zipToImport;
- m_mainWindow->droppedURLs({ m_zipToImport });
+ qDebug() << "<> Importing from zip:" << m_zipsToImport;
+ m_mainWindow->processURLs( m_zipsToImport );
}
}
@@ -1014,7 +1027,7 @@ void Application::messageReceived(const QByteArray& message)
qWarning() << "Received" << command << "message without a zip path/URL.";
return;
}
- m_mainWindow->droppedURLs({ QUrl(path) });
+ m_mainWindow->processURLs({ QUrl::fromLocalFile(QFileInfo(path).absoluteFilePath()) });
}
else if(command == "launch")
{
@@ -1083,9 +1096,14 @@ QList<ITheme*> Application::getValidApplicationThemes()
return m_themeManager->getValidApplicationThemes();
}
-void Application::setApplicationTheme(const QString& name, bool initial)
+void Application::applyCurrentlySelectedTheme()
+{
+ m_themeManager->applyCurrentlySelectedTheme();
+}
+
+void Application::setApplicationTheme(const QString& name)
{
- m_themeManager->setApplicationTheme(name, initial);
+ m_themeManager->setApplicationTheme(name);
}
void Application::setIconTheme(const QString& name)
@@ -1313,16 +1331,7 @@ MainWindow* Application::showMainWindow(bool minimized)
m_mainWindow = new MainWindow();
m_mainWindow->restoreState(QByteArray::fromBase64(APPLICATION->settings()->get("MainWindowState").toByteArray()));
m_mainWindow->restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("MainWindowGeometry").toByteArray()));
-#ifdef Q_OS_WIN
- if (IsWindows10OrGreater())
- {
- if (QString::compare(settings()->get("ApplicationTheme").toString(), "dark") == 0) {
- WinDarkmode::setDarkWinTitlebar(m_mainWindow->winId(), true);
- } else {
- WinDarkmode::setDarkWinTitlebar(m_mainWindow->winId(), false);
- }
- }
-#endif
+
if(minimized)
{
m_mainWindow->showMinimized();
diff --git a/launcher/Application.h b/launcher/Application.h
index 23c70e4c..1b3dc499 100644
--- a/launcher/Application.h
+++ b/launcher/Application.h
@@ -119,9 +119,11 @@ public:
void setIconTheme(const QString& name);
+ void applyCurrentlySelectedTheme();
+
QList<ITheme*> getValidApplicationThemes();
- void setApplicationTheme(const QString& name, bool initial);
+ void setApplicationTheme(const QString& name);
shared_qobject_ptr<ExternalUpdater> updater() {
return m_updater;
@@ -207,6 +209,7 @@ signals:
void updateAllowedChanged(bool status);
void globalSettingsAboutToOpen();
void globalSettingsClosed();
+ int currentCatChanged(int index);
#ifdef Q_OS_MACOS
void clickedOnDock();
@@ -304,7 +307,7 @@ public:
QString m_serverToJoin;
QString m_profileToUse;
bool m_liveCheck = false;
- QUrl m_zipToImport;
+ QList<QUrl> m_zipsToImport;
QString m_instanceIdToShowWindowOf;
std::unique_ptr<QFile> logFile;
};
diff --git a/launcher/ApplicationMessage.cpp b/launcher/ApplicationMessage.cpp
index ca276b89..700e43ce 100644
--- a/launcher/ApplicationMessage.cpp
+++ b/launcher/ApplicationMessage.cpp
@@ -47,8 +47,8 @@ void ApplicationMessage::parse(const QByteArray & input) {
args.clear();
auto parsedArgs = root.value("args").toObject();
- for(auto iter = parsedArgs.begin(); iter != parsedArgs.end(); iter++) {
- args[iter.key()] = iter.value().toString();
+ for(auto iter = parsedArgs.constBegin(); iter != parsedArgs.constEnd(); iter++) {
+ args.insert(iter.key(), iter.value().toString());
}
}
@@ -56,8 +56,8 @@ QByteArray ApplicationMessage::serialize() {
QJsonObject root;
root.insert("command", command);
QJsonObject outArgs;
- for (auto iter = args.begin(); iter != args.end(); iter++) {
- outArgs[iter.key()] = iter.value();
+ for (auto iter = args.constBegin(); iter != args.constEnd(); iter++) {
+ outArgs.insert(iter.key(), iter.value());
}
root.insert("args", outArgs);
diff --git a/launcher/ApplicationMessage.h b/launcher/ApplicationMessage.h
index 745bdead..d66456eb 100644
--- a/launcher/ApplicationMessage.h
+++ b/launcher/ApplicationMessage.h
@@ -1,12 +1,12 @@
#pragma once
#include <QString>
-#include <QMap>
+#include <QHash>
#include <QByteArray>
struct ApplicationMessage {
QString command;
- QMap<QString, QString> args;
+ QHash<QString, QString> args;
QByteArray serialize();
void parse(const QByteArray & input);
diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt
index 4945ac03..b435bf5e 100644
--- a/launcher/CMakeLists.txt
+++ b/launcher/CMakeLists.txt
@@ -38,9 +38,9 @@ set(CORE_SOURCES
InstanceImportTask.h
InstanceImportTask.cpp
- # Mod downloading task
- ModDownloadTask.h
- ModDownloadTask.cpp
+ # Resource downloading task
+ ResourceDownloadTask.h
+ ResourceDownloadTask.cpp
# Use tracking separate from memory management
Usable.h
@@ -325,12 +325,18 @@ set(MINECRAFT_SOURCES
minecraft/mod/Resource.cpp
minecraft/mod/ResourceFolderModel.h
minecraft/mod/ResourceFolderModel.cpp
+ minecraft/mod/DataPack.h
+ minecraft/mod/DataPack.cpp
minecraft/mod/ResourcePack.h
minecraft/mod/ResourcePack.cpp
minecraft/mod/ResourcePackFolderModel.h
minecraft/mod/ResourcePackFolderModel.cpp
minecraft/mod/TexturePack.h
minecraft/mod/TexturePack.cpp
+ minecraft/mod/ShaderPack.h
+ minecraft/mod/ShaderPack.cpp
+ minecraft/mod/WorldSave.h
+ minecraft/mod/WorldSave.cpp
minecraft/mod/TexturePackFolderModel.h
minecraft/mod/TexturePackFolderModel.cpp
minecraft/mod/ShaderPackFolderModel.h
@@ -341,10 +347,18 @@ set(MINECRAFT_SOURCES
minecraft/mod/tasks/LocalModParseTask.cpp
minecraft/mod/tasks/LocalModUpdateTask.h
minecraft/mod/tasks/LocalModUpdateTask.cpp
+ minecraft/mod/tasks/LocalDataPackParseTask.h
+ minecraft/mod/tasks/LocalDataPackParseTask.cpp
minecraft/mod/tasks/LocalResourcePackParseTask.h
minecraft/mod/tasks/LocalResourcePackParseTask.cpp
minecraft/mod/tasks/LocalTexturePackParseTask.h
minecraft/mod/tasks/LocalTexturePackParseTask.cpp
+ minecraft/mod/tasks/LocalShaderPackParseTask.h
+ minecraft/mod/tasks/LocalShaderPackParseTask.cpp
+ minecraft/mod/tasks/LocalWorldSaveParseTask.h
+ minecraft/mod/tasks/LocalWorldSaveParseTask.cpp
+ minecraft/mod/tasks/LocalResourceParse.h
+ minecraft/mod/tasks/LocalResourceParse.cpp
# Assets
minecraft/AssetsUtils.h
@@ -453,7 +467,7 @@ set(API_SOURCES
modplatform/ModIndex.h
modplatform/ModIndex.cpp
- modplatform/ModAPI.h
+ modplatform/ResourceAPI.h
modplatform/EnsureMetadataTask.h
modplatform/EnsureMetadataTask.cpp
@@ -464,8 +478,8 @@ set(API_SOURCES
modplatform/flame/FlameAPI.cpp
modplatform/modrinth/ModrinthAPI.h
modplatform/modrinth/ModrinthAPI.cpp
- modplatform/helpers/NetworkModAPI.h
- modplatform/helpers/NetworkModAPI.cpp
+ modplatform/helpers/NetworkResourceAPI.h
+ modplatform/helpers/NetworkResourceAPI.cpp
modplatform/helpers/HashUtils.h
modplatform/helpers/HashUtils.cpp
modplatform/helpers/OverrideUtils.h
@@ -545,6 +559,24 @@ set(ATLAUNCHER_SOURCES
modplatform/atlauncher/ATLShareCode.h
)
+######## Logging categories ########
+
+ecm_qt_declare_logging_category(CORE_SOURCES
+ HEADER Logging.h
+ IDENTIFIER authCredentials
+ CATEGORY_NAME "launcher.auth.credentials"
+ DEFAULT_SEVERITY Warning
+ DESCRIPTION "Secrets and credentials for debugging purposes"
+ EXPORT "${Launcher_Name}"
+)
+
+if(KDE_INSTALL_LOGGINGCATEGORIESDIR) # only install if there is a standard path for this
+ ecm_qt_install_logging_categories(
+ EXPORT "${Launcher_Name}"
+ DESTINATION "${KDE_INSTALL_LOGGINGCATEGORIESDIR}"
+ )
+endif()
+
################################ COMPILE ################################
set(LOGIC_SOURCES
@@ -591,7 +623,7 @@ SET(LAUNCHER_SOURCES
DesktopServices.cpp
VersionProxyModel.h
VersionProxyModel.cpp
- HoeDown.h
+ Markdown.h
# Super secret!
KonamiCode.h
@@ -643,6 +675,8 @@ SET(LAUNCHER_SOURCES
ui/setupwizard/LanguageWizardPage.h
ui/setupwizard/PasteWizardPage.cpp
ui/setupwizard/PasteWizardPage.h
+ ui/setupwizard/ThemeWizardPage.cpp
+ ui/setupwizard/ThemeWizardPage.h
# GUI - themes
ui/themes/FusionTheme.cpp
@@ -729,6 +763,11 @@ SET(LAUNCHER_SOURCES
ui/pages/modplatform/VanillaPage.cpp
ui/pages/modplatform/VanillaPage.h
+ ui/pages/modplatform/ResourcePage.cpp
+ ui/pages/modplatform/ResourcePage.h
+ ui/pages/modplatform/ResourceModel.cpp
+ ui/pages/modplatform/ResourceModel.h
+
ui/pages/modplatform/ModPage.cpp
ui/pages/modplatform/ModPage.h
ui/pages/modplatform/ModModel.cpp
@@ -761,10 +800,10 @@ SET(LAUNCHER_SOURCES
ui/pages/modplatform/flame/FlameModel.h
ui/pages/modplatform/flame/FlamePage.cpp
ui/pages/modplatform/flame/FlamePage.h
- ui/pages/modplatform/flame/FlameModModel.cpp
- ui/pages/modplatform/flame/FlameModModel.h
- ui/pages/modplatform/flame/FlameModPage.cpp
- ui/pages/modplatform/flame/FlameModPage.h
+ ui/pages/modplatform/flame/FlameResourceModels.cpp
+ ui/pages/modplatform/flame/FlameResourceModels.h
+ ui/pages/modplatform/flame/FlameResourcePages.cpp
+ ui/pages/modplatform/flame/FlameResourcePages.h
ui/pages/modplatform/modrinth/ModrinthPage.cpp
ui/pages/modplatform/modrinth/ModrinthPage.h
@@ -779,10 +818,10 @@ SET(LAUNCHER_SOURCES
ui/pages/modplatform/ImportPage.cpp
ui/pages/modplatform/ImportPage.h
- ui/pages/modplatform/modrinth/ModrinthModModel.cpp
- ui/pages/modplatform/modrinth/ModrinthModModel.h
- ui/pages/modplatform/modrinth/ModrinthModPage.cpp
- ui/pages/modplatform/modrinth/ModrinthModPage.h
+ ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp
+ ui/pages/modplatform/modrinth/ModrinthResourceModels.h
+ ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp
+ ui/pages/modplatform/modrinth/ModrinthResourcePages.h
# GUI - dialogs
ui/dialogs/AboutDialog.cpp
@@ -801,8 +840,8 @@ SET(LAUNCHER_SOURCES
ui/dialogs/ExportInstanceDialog.h
ui/dialogs/IconPickerDialog.cpp
ui/dialogs/IconPickerDialog.h
- ui/dialogs/ImportResourcePackDialog.cpp
- ui/dialogs/ImportResourcePackDialog.h
+ ui/dialogs/ImportResourceDialog.cpp
+ ui/dialogs/ImportResourceDialog.h
ui/dialogs/LoginDialog.cpp
ui/dialogs/LoginDialog.h
ui/dialogs/MSALoginDialog.cpp
@@ -825,8 +864,8 @@ SET(LAUNCHER_SOURCES
ui/dialogs/VersionSelectDialog.h
ui/dialogs/SkinUploadDialog.cpp
ui/dialogs/SkinUploadDialog.h
- ui/dialogs/ModDownloadDialog.cpp
- ui/dialogs/ModDownloadDialog.h
+ ui/dialogs/ResourceDownloadDialog.cpp
+ ui/dialogs/ResourceDownloadDialog.h
ui/dialogs/ScrollMessageBox.cpp
ui/dialogs/ScrollMessageBox.h
ui/dialogs/BlockedModsDialog.cpp
@@ -880,6 +919,8 @@ SET(LAUNCHER_SOURCES
ui/widgets/ProgressWidget.cpp
ui/widgets/WideBar.h
ui/widgets/WideBar.cpp
+ ui/widgets/ThemeCustomizationWidget.h
+ ui/widgets/ThemeCustomizationWidget.cpp
# GUI - instance group view
ui/instanceview/InstanceProxyModel.cpp
@@ -895,18 +936,9 @@ SET(LAUNCHER_SOURCES
ui/instanceview/VisualGroup.h
)
-if(WIN32)
- set(LAUNCHER_SOURCES
- ${LAUNCHER_SOURCES}
-
- # GUI - dark titlebar for Windows 10/11
- ui/WinDarkmode.h
- ui/WinDarkmode.cpp
- )
-endif()
-
qt_wrap_ui(LAUNCHER_UI
ui/setupwizard/PasteWizardPage.ui
+ ui/setupwizard/ThemeWizardPage.ui
ui/pages/global/AccountListPage.ui
ui/pages/global/JavaPage.ui
ui/pages/global/LauncherPage.ui
@@ -928,7 +960,7 @@ qt_wrap_ui(LAUNCHER_UI
ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui
ui/pages/modplatform/atlauncher/AtlPage.ui
ui/pages/modplatform/VanillaPage.ui
- ui/pages/modplatform/ModPage.ui
+ ui/pages/modplatform/ResourcePage.ui
ui/pages/modplatform/flame/FlamePage.ui
ui/pages/modplatform/legacy_ftb/Page.ui
ui/pages/modplatform/ImportPage.ui
@@ -939,6 +971,7 @@ qt_wrap_ui(LAUNCHER_UI
ui/widgets/CustomCommands.ui
ui/widgets/InfoFrame.ui
ui/widgets/ModFilterWidget.ui
+ ui/widgets/ThemeCustomizationWidget.ui
ui/dialogs/CopyInstanceDialog.ui
ui/dialogs/ProfileSetupDialog.ui
ui/dialogs/ProgressDialog.ui
@@ -949,7 +982,7 @@ qt_wrap_ui(LAUNCHER_UI
ui/dialogs/SkinUploadDialog.ui
ui/dialogs/ExportInstanceDialog.ui
ui/dialogs/IconPickerDialog.ui
- ui/dialogs/ImportResourcePackDialog.ui
+ ui/dialogs/ImportResourceDialog.ui
ui/dialogs/MSALoginDialog.ui
ui/dialogs/OfflineLoginDialog.ui
ui/dialogs/AboutDialog.ui
@@ -1014,7 +1047,7 @@ target_link_libraries(Launcher_logic
)
target_link_libraries(Launcher_logic
QuaZip::QuaZip
- hoedown
+ cmark::cmark
LocalPeer
Launcher_rainbow
)
@@ -1155,6 +1188,8 @@ if(INSTALL_BUNDLE STREQUAL "full")
CONFIGURATIONS Debug RelWithDebInfo ""
DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime
+ PATTERN "*qopensslbackend*" EXCLUDE
+ PATTERN "*qcertonlybackend*" EXCLUDE
)
install(
DIRECTORY "${QT_PLUGINS_DIR}/tls"
@@ -1164,6 +1199,8 @@ if(INSTALL_BUNDLE STREQUAL "full")
REGEX "dd\\." EXCLUDE
REGEX "_debug\\." EXCLUDE
REGEX "\\.dSYM" EXCLUDE
+ PATTERN "*qopensslbackend*" EXCLUDE
+ PATTERN "*qcertonlybackend*" EXCLUDE
)
endif()
configure_file(
diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp
index 3e8e10a5..aee5245d 100644
--- a/launcher/FileSystem.cpp
+++ b/launcher/FileSystem.cpp
@@ -1,7 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
- * PolyMC - Minecraft Launcher
+ * Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -56,6 +57,7 @@
#include <shlobj.h>
#include <shobjidl.h>
#include <sys/utime.h>
+#include <versionhelpers.h>
#include <windows.h>
#include <winnls.h>
#include <string>
@@ -213,6 +215,22 @@ bool copy::operator()(const QString& offset, bool dryRun)
return err.value() == 0;
}
+bool move(const QString& source, const QString& dest)
+{
+ std::error_code err;
+
+ ensureFilePathExists(dest);
+ fs::rename(StringUtils::toStdString(source), StringUtils::toStdString(dest), err);
+
+ if (err) {
+ qWarning() << "Failed to move file:" << QString::fromStdString(err.message());
+ qDebug() << "Source file:" << source;
+ qDebug() << "Destination file:" << dest;
+ }
+
+ return err.value() == 0;
+}
+
bool deletePath(QString path)
{
std::error_code err;
@@ -226,7 +244,7 @@ bool deletePath(QString path)
return err.value() == 0;
}
-bool trash(QString path, QString *pathInTrash = nullptr)
+bool trash(QString path, QString *pathInTrash)
{
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
return false;
@@ -234,6 +252,10 @@ bool trash(QString path, QString *pathInTrash = nullptr)
// FIXME: Figure out trash in Flatpak. Qt seemingly doesn't use the Trash portal
if (DesktopServices::isFlatpak())
return false;
+#if defined Q_OS_WIN32
+ if (IsWindowsServer())
+ return false;
+#endif
return QFile::moveToTrash(path, pathInTrash);
#endif
}
diff --git a/launcher/FileSystem.h b/launcher/FileSystem.h
index ac893725..f083f3c7 100644
--- a/launcher/FileSystem.h
+++ b/launcher/FileSystem.h
@@ -1,7 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
- * PolyMC - Minecraft Launcher
+ * Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -122,6 +123,14 @@ class copy : public QObject {
};
/**
+ * @brief moves a file by renaming it
+ * @param source source file path
+ * @param dest destination filepath
+ *
+ */
+bool move(const QString& source, const QString& dest);
+
+/**
* Delete a folder recursively
*/
bool deletePath(QString path);
@@ -129,7 +138,7 @@ bool deletePath(QString path);
/**
* Trash a folder / file
*/
-bool trash(QString path, QString *pathInTrash);
+bool trash(QString path, QString *pathInTrash = nullptr);
QString PathCombine(const QString& path1, const QString& path2);
QString PathCombine(const QString& path1, const QString& path2, const QString& path3);
diff --git a/launcher/HoeDown.h b/launcher/HoeDown.h
deleted file mode 100644
index cb62de6c..00000000
--- a/launcher/HoeDown.h
+++ /dev/null
@@ -1,76 +0,0 @@
-/* Copyright 2013-2021 MultiMC Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#pragma once
-#include <hoedown/html.h>
-#include <hoedown/document.h>
-#include <QString>
-#include <QByteArray>
-
-/**
- * hoedown wrapper, because dealing with resource lifetime in C is stupid
- */
-class HoeDown
-{
-public:
- class buffer
- {
- public:
- buffer(size_t unit = 4096)
- {
- buf = hoedown_buffer_new(unit);
- }
- ~buffer()
- {
- hoedown_buffer_free(buf);
- }
- const char * cstr()
- {
- return hoedown_buffer_cstr(buf);
- }
- void put(QByteArray input)
- {
- hoedown_buffer_put(buf, reinterpret_cast<uint8_t *>(input.data()), input.size());
- }
- const uint8_t * data() const
- {
- return buf->data;
- }
- size_t size() const
- {
- return buf->size;
- }
- hoedown_buffer * buf;
- } ib, ob;
- HoeDown()
- {
- renderer = hoedown_html_renderer_new((hoedown_html_flags) 0,0);
- document = hoedown_document_new(renderer, (hoedown_extensions) 0, 8);
- }
- ~HoeDown()
- {
- hoedown_document_free(document);
- hoedown_html_renderer_free(renderer);
- }
- QString process(QByteArray input)
- {
- ib.put(input);
- hoedown_document_render(document, ob.buf, ib.data(), ib.size());
- return ob.cstr();
- }
-private:
- hoedown_document * document;
- hoedown_renderer * renderer;
-};
diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp
index b97870da..6b3fd296 100644
--- a/launcher/InstanceImportTask.cpp
+++ b/launcher/InstanceImportTask.cpp
@@ -257,20 +257,26 @@ void InstanceImportTask::extractAborted()
void InstanceImportTask::processFlame()
{
- auto pack_id_it = m_extra_info.constFind("pack_id");
- Q_ASSERT(pack_id_it != m_extra_info.constEnd());
- auto pack_id = pack_id_it.value();
-
- auto pack_version_id_it = m_extra_info.constFind("pack_version_id");
- Q_ASSERT(pack_version_id_it != m_extra_info.constEnd());
- auto pack_version_id = pack_version_id_it.value();
-
- QString original_instance_id;
- auto original_instance_id_it = m_extra_info.constFind("original_instance_id");
- if (original_instance_id_it != m_extra_info.constEnd())
- original_instance_id = original_instance_id_it.value();
-
- auto* inst_creation_task = new FlameCreationTask(m_stagingPath, m_globalSettings, m_parent, pack_id, pack_version_id, original_instance_id);
+ FlameCreationTask* inst_creation_task = nullptr;
+ if (!m_extra_info.isEmpty()) {
+ auto pack_id_it = m_extra_info.constFind("pack_id");
+ Q_ASSERT(pack_id_it != m_extra_info.constEnd());
+ auto pack_id = pack_id_it.value();
+
+ auto pack_version_id_it = m_extra_info.constFind("pack_version_id");
+ Q_ASSERT(pack_version_id_it != m_extra_info.constEnd());
+ auto pack_version_id = pack_version_id_it.value();
+
+ QString original_instance_id;
+ auto original_instance_id_it = m_extra_info.constFind("original_instance_id");
+ if (original_instance_id_it != m_extra_info.constEnd())
+ original_instance_id = original_instance_id_it.value();
+
+ inst_creation_task = new FlameCreationTask(m_stagingPath, m_globalSettings, m_parent, pack_id, pack_version_id, original_instance_id);
+ } else {
+ // FIXME: Find a way to get IDs in directly imported ZIPs
+ inst_creation_task = new FlameCreationTask(m_stagingPath, m_globalSettings, m_parent, {}, {});
+ }
inst_creation_task->setName(*this);
inst_creation_task->setIcon(m_instIcon);
@@ -335,21 +341,33 @@ void InstanceImportTask::processMultiMC()
void InstanceImportTask::processModrinth()
{
- auto pack_id_it = m_extra_info.constFind("pack_id");
- Q_ASSERT(pack_id_it != m_extra_info.constEnd());
- auto pack_id = pack_id_it.value();
-
- QString pack_version_id;
- auto pack_version_id_it = m_extra_info.constFind("pack_version_id");
- if (pack_version_id_it != m_extra_info.constEnd())
- pack_version_id = pack_version_id_it.value();
-
- QString original_instance_id;
- auto original_instance_id_it = m_extra_info.constFind("original_instance_id");
- if (original_instance_id_it != m_extra_info.constEnd())
- original_instance_id = original_instance_id_it.value();
+ ModrinthCreationTask* inst_creation_task = nullptr;
+ if (!m_extra_info.isEmpty()) {
+ auto pack_id_it = m_extra_info.constFind("pack_id");
+ Q_ASSERT(pack_id_it != m_extra_info.constEnd());
+ auto pack_id = pack_id_it.value();
+
+ QString pack_version_id;
+ auto pack_version_id_it = m_extra_info.constFind("pack_version_id");
+ if (pack_version_id_it != m_extra_info.constEnd())
+ pack_version_id = pack_version_id_it.value();
+
+ QString original_instance_id;
+ auto original_instance_id_it = m_extra_info.constFind("original_instance_id");
+ if (original_instance_id_it != m_extra_info.constEnd())
+ original_instance_id = original_instance_id_it.value();
+
+ inst_creation_task = new ModrinthCreationTask(m_stagingPath, m_globalSettings, m_parent, pack_id, pack_version_id, original_instance_id);
+ } else {
+ QString pack_id;
+ if (!m_sourceUrl.isEmpty()) {
+ QRegularExpression regex(R"(data\/(.*)\/versions)");
+ pack_id = regex.match(m_sourceUrl.toString()).captured(1);
+ }
- auto* inst_creation_task = new ModrinthCreationTask(m_stagingPath, m_globalSettings, m_parent, pack_id, pack_version_id, original_instance_id);
+ // FIXME: Find a way to get the ID in directly imported ZIPs
+ inst_creation_task = new ModrinthCreationTask(m_stagingPath, m_globalSettings, m_parent, pack_id);
+ }
inst_creation_task->setName(*this);
inst_creation_task->setIcon(m_instIcon);
diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp
index 11e3de15..9741fd95 100644
--- a/launcher/LaunchController.cpp
+++ b/launcher/LaunchController.cpp
@@ -112,7 +112,15 @@ void LaunchController::decideAccount()
}
}
- m_accountToUse = accounts->defaultAccount();
+ // Select the account to use. If the instance has a specific account set, that will be used. Otherwise, the default account will be used
+ auto instanceAccountId = m_instance->settings()->get("InstanceAccountId").toString();
+ auto instanceAccountIndex = accounts->findAccountByProfileId(instanceAccountId);
+ if (instanceAccountIndex == -1) {
+ m_accountToUse = accounts->defaultAccount();
+ } else {
+ m_accountToUse = accounts->at(instanceAccountIndex);
+ }
+
if (!m_accountToUse)
{
// If no default account is set, ask the user which one to use.
diff --git a/launcher/Markdown.h b/launcher/Markdown.h
new file mode 100644
index 00000000..f115dd57
--- /dev/null
+++ b/launcher/Markdown.h
@@ -0,0 +1,34 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2023 Joshua Goins <josh@redstrate.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <QString>
+#include <cmark.h>
+
+static QString markdownToHTML(const QString& markdown)
+{
+ const QByteArray markdownData = markdown.toUtf8();
+ char* buffer = cmark_markdown_to_html(markdownData.constData(), markdownData.length(), CMARK_OPT_NOBREAKS | CMARK_OPT_UNSAFE);
+
+ QString htmlStr(buffer);
+
+ free(buffer);
+
+ return htmlStr;
+} \ No newline at end of file
diff --git a/launcher/ModDownloadTask.cpp b/launcher/ModDownloadTask.cpp
deleted file mode 100644
index 2b0343f4..00000000
--- a/launcher/ModDownloadTask.cpp
+++ /dev/null
@@ -1,72 +0,0 @@
-// SPDX-License-Identifier: GPL-3.0-only
-/*
-* PolyMC - Minecraft Launcher
-* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
-* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
-*
-* This program is free software: you can redistribute it and/or modify
-* it under the terms of the GNU General Public License as published by
-* the Free Software Foundation, version 3.
-*
-* This program is distributed in the hope that it will be useful,
-* but WITHOUT ANY WARRANTY; without even the implied warranty of
-* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-* GNU General Public License for more details.
-*
-* You should have received a copy of the GNU General Public License
-* along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-#include "ModDownloadTask.h"
-
-#include "Application.h"
-#include "minecraft/mod/ModFolderModel.h"
-
-ModDownloadTask::ModDownloadTask(ModPlatform::IndexedPack mod, ModPlatform::IndexedVersion version, const std::shared_ptr<ModFolderModel> mods, bool is_indexed)
- : m_mod(mod), m_mod_version(version), mods(mods)
-{
- if (is_indexed) {
- m_update_task.reset(new LocalModUpdateTask(mods->indexDir(), m_mod, m_mod_version));
- connect(m_update_task.get(), &LocalModUpdateTask::hasOldMod, this, &ModDownloadTask::hasOldMod);
-
- addTask(m_update_task);
- }
-
- m_filesNetJob.reset(new NetJob(tr("Mod download"), APPLICATION->network()));
- m_filesNetJob->setStatus(tr("Downloading mod:\n%1").arg(m_mod_version.downloadUrl));
-
- m_filesNetJob->addNetAction(Net::Download::makeFile(m_mod_version.downloadUrl, mods->dir().absoluteFilePath(getFilename())));
- connect(m_filesNetJob.get(), &NetJob::succeeded, this, &ModDownloadTask::downloadSucceeded);
- connect(m_filesNetJob.get(), &NetJob::progress, this, &ModDownloadTask::downloadProgressChanged);
- connect(m_filesNetJob.get(), &NetJob::failed, this, &ModDownloadTask::downloadFailed);
-
- addTask(m_filesNetJob);
-}
-
-void ModDownloadTask::downloadSucceeded()
-{
- m_filesNetJob.reset();
- auto name = std::get<0>(to_delete);
- auto filename = std::get<1>(to_delete);
- if (!name.isEmpty() && filename != m_mod_version.fileName) {
- mods->uninstallMod(filename, true);
- }
-}
-
-void ModDownloadTask::downloadFailed(QString reason)
-{
- emitFailed(reason);
- m_filesNetJob.reset();
-}
-
-void ModDownloadTask::downloadProgressChanged(qint64 current, qint64 total)
-{
- emit progress(current, total);
-}
-
-// This indirection is done so that we don't delete a mod before being sure it was
-// downloaded successfully!
-void ModDownloadTask::hasOldMod(QString name, QString filename)
-{
- to_delete = {name, filename};
-}
diff --git a/launcher/QObjectPtr.h b/launcher/QObjectPtr.h
index b1ef1c8d..ec466096 100644
--- a/launcher/QObjectPtr.h
+++ b/launcher/QObjectPtr.h
@@ -28,6 +28,10 @@ class shared_qobject_ptr : public QSharedPointer<T> {
constexpr shared_qobject_ptr(const shared_qobject_ptr<Derived>& other) : QSharedPointer<T>(other)
{}
+ template <typename Derived>
+ constexpr shared_qobject_ptr(const QSharedPointer<Derived>& other) : QSharedPointer<T>(other)
+ {}
+
void reset() { QSharedPointer<T>::reset(); }
void reset(const shared_qobject_ptr<T>& other)
{
diff --git a/launcher/ResourceDownloadTask.cpp b/launcher/ResourceDownloadTask.cpp
new file mode 100644
index 00000000..98bcf259
--- /dev/null
+++ b/launcher/ResourceDownloadTask.cpp
@@ -0,0 +1,90 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+* Prism Launcher - Minecraft Launcher
+* Copyright (c) 2022-2023 flowln <flowlnlnln@gmail.com>
+* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation, version 3.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+#include "ResourceDownloadTask.h"
+
+#include "Application.h"
+
+#include "minecraft/mod/ModFolderModel.h"
+#include "minecraft/mod/ResourceFolderModel.h"
+
+ResourceDownloadTask::ResourceDownloadTask(ModPlatform::IndexedPack pack,
+ ModPlatform::IndexedVersion version,
+ const std::shared_ptr<ResourceFolderModel> packs,
+ bool is_indexed)
+ : m_pack(std::move(pack)), m_pack_version(std::move(version)), m_pack_model(packs)
+{
+ if (auto model = dynamic_cast<ModFolderModel*>(m_pack_model.get()); model && is_indexed) {
+ m_update_task.reset(new LocalModUpdateTask(model->indexDir(), m_pack, m_pack_version));
+ connect(m_update_task.get(), &LocalModUpdateTask::hasOldMod, this, &ResourceDownloadTask::hasOldResource);
+
+ addTask(m_update_task);
+ }
+
+ m_filesNetJob.reset(new NetJob(tr("Resource download"), APPLICATION->network()));
+ m_filesNetJob->setStatus(tr("Downloading resource:\n%1").arg(m_pack_version.downloadUrl));
+
+ QDir dir { m_pack_model->dir() };
+ {
+ // FIXME: Make this more generic. May require adding additional info to IndexedVersion,
+ // or adquiring a reference to the base instance.
+ if (!m_pack_version.custom_target_folder.isEmpty()) {
+ dir.cdUp();
+ dir.cd(m_pack_version.custom_target_folder);
+ }
+ }
+
+ m_filesNetJob->addNetAction(Net::Download::makeFile(m_pack_version.downloadUrl, dir.absoluteFilePath(getFilename())));
+ connect(m_filesNetJob.get(), &NetJob::succeeded, this, &ResourceDownloadTask::downloadSucceeded);
+ connect(m_filesNetJob.get(), &NetJob::progress, this, &ResourceDownloadTask::downloadProgressChanged);
+ connect(m_filesNetJob.get(), &NetJob::failed, this, &ResourceDownloadTask::downloadFailed);
+
+ addTask(m_filesNetJob);
+}
+
+void ResourceDownloadTask::downloadSucceeded()
+{
+ m_filesNetJob.reset();
+ auto name = std::get<0>(to_delete);
+ auto filename = std::get<1>(to_delete);
+ if (!name.isEmpty() && filename != m_pack_version.fileName) {
+ if (auto model = dynamic_cast<ModFolderModel*>(m_pack_model.get()); model)
+ model->uninstallMod(filename, true);
+ else
+ m_pack_model->uninstallResource(filename);
+ }
+}
+
+void ResourceDownloadTask::downloadFailed(QString reason)
+{
+ emitFailed(reason);
+ m_filesNetJob.reset();
+}
+
+void ResourceDownloadTask::downloadProgressChanged(qint64 current, qint64 total)
+{
+ emit progress(current, total);
+}
+
+// This indirection is done so that we don't delete a mod before being sure it was
+// downloaded successfully!
+void ResourceDownloadTask::hasOldResource(QString name, QString filename)
+{
+ to_delete = { name, filename };
+}
diff --git a/launcher/ModDownloadTask.h b/launcher/ResourceDownloadTask.h
index 95020470..73ad2d07 100644
--- a/launcher/ModDownloadTask.h
+++ b/launcher/ResourceDownloadTask.h
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
-* PolyMC - Minecraft Launcher
-* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
+* Prism Launcher - Minecraft Launcher
+* Copyright (c) 2022-2023 flowln <flowlnlnln@gmail.com>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -25,32 +25,32 @@
#include "modplatform/ModIndex.h"
#include "minecraft/mod/tasks/LocalModUpdateTask.h"
-class ModFolderModel;
+class ResourceFolderModel;
-class ModDownloadTask : public SequentialTask {
+class ResourceDownloadTask : public SequentialTask {
Q_OBJECT
public:
- explicit ModDownloadTask(ModPlatform::IndexedPack mod, ModPlatform::IndexedVersion version, const std::shared_ptr<ModFolderModel> mods, bool is_indexed = true);
- const QString& getFilename() const { return m_mod_version.fileName; }
+ explicit ResourceDownloadTask(ModPlatform::IndexedPack pack, ModPlatform::IndexedVersion version, const std::shared_ptr<ResourceFolderModel> packs, bool is_indexed = true);
+ const QString& getFilename() const { return m_pack_version.fileName; }
+ const QString& getCustomPath() const { return m_pack_version.custom_target_folder; }
+ const QVariant& getVersionID() const { return m_pack_version.fileId; }
private:
- ModPlatform::IndexedPack m_mod;
- ModPlatform::IndexedVersion m_mod_version;
- const std::shared_ptr<ModFolderModel> mods;
+ ModPlatform::IndexedPack m_pack;
+ ModPlatform::IndexedVersion m_pack_version;
+ const std::shared_ptr<ResourceFolderModel> m_pack_model;
NetJob::Ptr m_filesNetJob;
LocalModUpdateTask::Ptr m_update_task;
void downloadProgressChanged(qint64 current, qint64 total);
-
void downloadFailed(QString reason);
-
void downloadSucceeded();
std::tuple<QString, QString> to_delete {"", ""};
private slots:
- void hasOldMod(QString name, QString filename);
+ void hasOldResource(QString name, QString filename);
};
diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp
index 1d37224a..d0a5ed31 100644
--- a/launcher/minecraft/MinecraftInstance.cpp
+++ b/launcher/minecraft/MinecraftInstance.cpp
@@ -192,6 +192,10 @@ void MinecraftInstance::loadSpecificSettings()
m_settings->registerSetting("JoinServerOnLaunch", false);
m_settings->registerSetting("JoinServerOnLaunchAddress", "");
+ // Use account for instance, this does not have a global override
+ m_settings->registerSetting("UseAccountForInstance", false);
+ m_settings->registerSetting("InstanceAccountId", "");
+
qDebug() << "Instance-type specific settings were loaded!";
setSpecificSettingsLoaded(true);
diff --git a/launcher/minecraft/PackProfile.cpp b/launcher/minecraft/PackProfile.cpp
index 43fa3f8d..42021b3c 100644
--- a/launcher/minecraft/PackProfile.cpp
+++ b/launcher/minecraft/PackProfile.cpp
@@ -55,12 +55,13 @@
#include "PackProfile_p.h"
#include "ComponentUpdateTask.h"
-#include "modplatform/ModAPI.h"
+#include "Application.h"
+#include "modplatform/ResourceAPI.h"
-static const QMap<QString, ModAPI::ModLoaderType> modloaderMapping{
- {"net.minecraftforge", ModAPI::Forge},
- {"net.fabricmc.fabric-loader", ModAPI::Fabric},
- {"org.quiltmc.quilt-loader", ModAPI::Quilt}
+static const QMap<QString, ResourceAPI::ModLoaderType> modloaderMapping{
+ {"net.minecraftforge", ResourceAPI::Forge},
+ {"net.fabricmc.fabric-loader", ResourceAPI::Fabric},
+ {"org.quiltmc.quilt-loader", ResourceAPI::Quilt}
};
PackProfile::PackProfile(MinecraftInstance * instance)
@@ -1066,19 +1067,22 @@ void PackProfile::disableInteraction(bool disable)
}
}
-ModAPI::ModLoaderTypes PackProfile::getModLoaders()
+std::optional<ResourceAPI::ModLoaderTypes> PackProfile::getModLoaders()
{
- ModAPI::ModLoaderTypes result = ModAPI::Unspecified;
+ ResourceAPI::ModLoaderTypes result;
+ bool has_any_loader = false;
- QMapIterator<QString, ModAPI::ModLoaderType> i(modloaderMapping);
+ QMapIterator<QString, ResourceAPI::ModLoaderType> i(modloaderMapping);
- while (i.hasNext())
- {
+ while (i.hasNext()) {
i.next();
- Component* c = getComponent(i.key());
- if (c != nullptr && c->isEnabled()) {
+ if (auto c = getComponent(i.key()); c != nullptr && c->isEnabled()) {
result |= i.value();
+ has_any_loader = true;
}
}
+
+ if (!has_any_loader)
+ return {};
return result;
}
diff --git a/launcher/minecraft/PackProfile.h b/launcher/minecraft/PackProfile.h
index 2330cca1..67b418f4 100644
--- a/launcher/minecraft/PackProfile.h
+++ b/launcher/minecraft/PackProfile.h
@@ -49,7 +49,7 @@
#include "BaseVersion.h"
#include "MojangDownloadInfo.h"
#include "net/Mode.h"
-#include "modplatform/ModAPI.h"
+#include "modplatform/ResourceAPI.h"
class MinecraftInstance;
struct PackProfileData;
@@ -145,7 +145,7 @@ public:
// todo(merged): is this the best approach
void appendComponent(ComponentPtr component);
- ModAPI::ModLoaderTypes getModLoaders();
+ std::optional<ResourceAPI::ModLoaderTypes> getModLoaders();
private:
void scheduleSave();
diff --git a/launcher/minecraft/World.cpp b/launcher/minecraft/World.cpp
index 90fcf337..d310f8b9 100644
--- a/launcher/minecraft/World.cpp
+++ b/launcher/minecraft/World.cpp
@@ -1,7 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
- * PolyMC - Minecraft Launcher
+ * Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -545,6 +546,10 @@ bool World::replace(World &with)
bool World::destroy()
{
if(!is_valid) return false;
+
+ if (FS::trash(m_containerFile.filePath()))
+ return true;
+
if (m_containerFile.isDir())
{
QDir d(m_containerFile.filePath());
diff --git a/launcher/minecraft/auth/Parsers.cpp b/launcher/minecraft/auth/Parsers.cpp
index 47473899..f3d9ad56 100644
--- a/launcher/minecraft/auth/Parsers.cpp
+++ b/launcher/minecraft/auth/Parsers.cpp
@@ -1,5 +1,6 @@
#include "Parsers.h"
#include "Json.h"
+#include "Logging.h"
#include <QJsonDocument>
#include <QJsonArray>
@@ -75,9 +76,7 @@ bool getBool(QJsonValue value, bool & out) {
bool parseXTokenResponse(QByteArray & data, Katabasis::Token &output, QString name) {
qDebug() << "Parsing" << name <<":";
-#ifndef NDEBUG
- qDebug() << data;
-#endif
+ qCDebug(authCredentials()) << data;
QJsonParseError jsonError;
QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
if(jsonError.error) {
@@ -137,9 +136,7 @@ bool parseXTokenResponse(QByteArray & data, Katabasis::Token &output, QString na
bool parseMinecraftProfile(QByteArray & data, MinecraftProfile &output) {
qDebug() << "Parsing Minecraft profile...";
-#ifndef NDEBUG
- qDebug() << data;
-#endif
+ qCDebug(authCredentials()) << data;
QJsonParseError jsonError;
QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
@@ -275,9 +272,7 @@ decoded base64 "value":
bool parseMinecraftProfileMojang(QByteArray & data, MinecraftProfile &output) {
qDebug() << "Parsing Minecraft profile...";
-#ifndef NDEBUG
- qDebug() << data;
-#endif
+ qCDebug(authCredentials()) << data;
QJsonParseError jsonError;
QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
@@ -389,9 +384,7 @@ bool parseMinecraftProfileMojang(QByteArray & data, MinecraftProfile &output) {
bool parseMinecraftEntitlements(QByteArray & data, MinecraftEntitlement &output) {
qDebug() << "Parsing Minecraft entitlements...";
-#ifndef NDEBUG
- qDebug() << data;
-#endif
+ qCDebug(authCredentials()) << data;
QJsonParseError jsonError;
QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
@@ -424,9 +417,7 @@ bool parseMinecraftEntitlements(QByteArray & data, MinecraftEntitlement &output)
bool parseRolloutResponse(QByteArray & data, bool& result) {
qDebug() << "Parsing Rollout response...";
-#ifndef NDEBUG
- qDebug() << data;
-#endif
+ qCDebug(authCredentials()) << data;
QJsonParseError jsonError;
QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
@@ -455,9 +446,7 @@ bool parseRolloutResponse(QByteArray & data, bool& result) {
bool parseMojangResponse(QByteArray & data, Katabasis::Token &output) {
QJsonParseError jsonError;
qDebug() << "Parsing Mojang response...";
-#ifndef NDEBUG
- qDebug() << data;
-#endif
+ qCDebug(authCredentials()) << data;
QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
if(jsonError.error) {
qWarning() << "Failed to parse response from api.minecraftservices.com/launcher/login as JSON: " << jsonError.errorString();
diff --git a/launcher/minecraft/auth/steps/EntitlementsStep.cpp b/launcher/minecraft/auth/steps/EntitlementsStep.cpp
index f726244f..bd604292 100644
--- a/launcher/minecraft/auth/steps/EntitlementsStep.cpp
+++ b/launcher/minecraft/auth/steps/EntitlementsStep.cpp
@@ -3,6 +3,7 @@
#include <QNetworkRequest>
#include <QUuid>
+#include "Logging.h"
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
@@ -41,9 +42,7 @@ void EntitlementsStep::onRequestDone(
auto requestor = qobject_cast<AuthRequest *>(QObject::sender());
requestor->deleteLater();
-#ifndef NDEBUG
- qDebug() << data;
-#endif
+ qCDebug(authCredentials()) << data;
// TODO: check presence of same entitlementsRequestId?
// TODO: validate JWTs?
diff --git a/launcher/minecraft/auth/steps/LauncherLoginStep.cpp b/launcher/minecraft/auth/steps/LauncherLoginStep.cpp
index 8c53f037..8a26cbe7 100644
--- a/launcher/minecraft/auth/steps/LauncherLoginStep.cpp
+++ b/launcher/minecraft/auth/steps/LauncherLoginStep.cpp
@@ -2,9 +2,10 @@
#include <QNetworkRequest>
+#include "Logging.h"
+#include "minecraft/auth/AccountTask.h"
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
-#include "minecraft/auth/AccountTask.h"
#include "net/NetUtils.h"
LauncherLoginStep::LauncherLoginStep(AccountData* data) : AuthStep(data) {
@@ -51,14 +52,10 @@ void LauncherLoginStep::onRequestDone(
auto requestor = qobject_cast<AuthRequest *>(QObject::sender());
requestor->deleteLater();
-#ifndef NDEBUG
- qDebug() << data;
-#endif
+ qCDebug(authCredentials()) << data;
if (error != QNetworkReply::NoError) {
qWarning() << "Reply error:" << error;
-#ifndef NDEBUG
- qDebug() << data;
-#endif
+ qCDebug(authCredentials()) << data;
if (Net::isApplicationError(error)) {
emit finished(
AccountTaskState::STATE_FAILED_SOFT,
@@ -76,9 +73,7 @@ void LauncherLoginStep::onRequestDone(
if(!Parsers::parseMojangResponse(data, m_data->yggdrasilToken)) {
qWarning() << "Could not parse login_with_xbox response...";
-#ifndef NDEBUG
- qDebug() << data;
-#endif
+ qCDebug(authCredentials()) << data;
emit finished(
AccountTaskState::STATE_FAILED_SOFT,
tr("Failed to parse the Minecraft access token response.")
diff --git a/launcher/minecraft/auth/steps/MSAStep.cpp b/launcher/minecraft/auth/steps/MSAStep.cpp
index 16afcb42..6fc8d468 100644
--- a/launcher/minecraft/auth/steps/MSAStep.cpp
+++ b/launcher/minecraft/auth/steps/MSAStep.cpp
@@ -42,6 +42,7 @@
#include "minecraft/auth/Parsers.h"
#include "Application.h"
+#include "Logging.h"
using OAuth2 = Katabasis::DeviceFlow;
using Activity = Katabasis::Activity;
@@ -117,14 +118,12 @@ void MSAStep::onOAuthActivityChanged(Katabasis::Activity activity) {
// Succeeded or did not invalidate tokens
emit hideVerificationUriAndCode();
QVariantMap extraTokens = m_oauth2->extraTokens();
-#ifndef NDEBUG
if (!extraTokens.isEmpty()) {
- qDebug() << "Extra tokens in response:";
+ qCDebug(authCredentials()) << "Extra tokens in response:";
foreach (QString key, extraTokens.keys()) {
- qDebug() << "\t" << key << ":" << extraTokens.value(key);
+ qCDebug(authCredentials()) << "\t" << key << ":" << extraTokens.value(key);
}
}
-#endif
emit finished(AccountTaskState::STATE_WORKING, tr("Got "));
return;
}
diff --git a/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp b/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp
index b39b9326..6cfa7c1c 100644
--- a/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp
+++ b/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp
@@ -2,6 +2,7 @@
#include <QNetworkRequest>
+#include "Logging.h"
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
#include "net/NetUtils.h"
@@ -40,9 +41,7 @@ void MinecraftProfileStep::onRequestDone(
auto requestor = qobject_cast<AuthRequest *>(QObject::sender());
requestor->deleteLater();
-#ifndef NDEBUG
- qDebug() << data;
-#endif
+ qCDebug(authCredentials()) << data;
if (error == QNetworkReply::ContentNotFoundError) {
// NOTE: Succeed even if we do not have a profile. This is a valid account state.
if(m_data->type == AccountType::Mojang) {
diff --git a/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp b/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp
index 6a1eb7a0..8c378588 100644
--- a/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp
+++ b/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp
@@ -2,6 +2,7 @@
#include <QNetworkRequest>
+#include "Logging.h"
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
#include "net/NetUtils.h"
@@ -43,9 +44,7 @@ void MinecraftProfileStepMojang::onRequestDone(
auto requestor = qobject_cast<AuthRequest *>(QObject::sender());
requestor->deleteLater();
-#ifndef NDEBUG
- qDebug() << data;
-#endif
+ qCDebug(authCredentials()) << data;
if (error == QNetworkReply::ContentNotFoundError) {
// NOTE: Succeed even if we do not have a profile. This is a valid account state.
if(m_data->type == AccountType::Mojang) {
diff --git a/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp b/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp
index 14bde47e..b397b734 100644
--- a/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp
+++ b/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp
@@ -4,6 +4,7 @@
#include <QJsonParseError>
#include <QJsonDocument>
+#include "Logging.h"
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
#include "net/NetUtils.h"
@@ -58,9 +59,7 @@ void XboxAuthorizationStep::onRequestDone(
auto requestor = qobject_cast<AuthRequest *>(QObject::sender());
requestor->deleteLater();
-#ifndef NDEBUG
- qDebug() << data;
-#endif
+ qCDebug(authCredentials()) << data;
if (error != QNetworkReply::NoError) {
qWarning() << "Reply error:" << error;
if (Net::isApplicationError(error)) {
diff --git a/launcher/minecraft/auth/steps/XboxProfileStep.cpp b/launcher/minecraft/auth/steps/XboxProfileStep.cpp
index 738fe1db..644c419b 100644
--- a/launcher/minecraft/auth/steps/XboxProfileStep.cpp
+++ b/launcher/minecraft/auth/steps/XboxProfileStep.cpp
@@ -3,7 +3,7 @@
#include <QNetworkRequest>
#include <QUrlQuery>
-
+#include "Logging.h"
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
#include "net/NetUtils.h"
@@ -56,9 +56,7 @@ void XboxProfileStep::onRequestDone(
if (error != QNetworkReply::NoError) {
qWarning() << "Reply error:" << error;
-#ifndef NDEBUG
- qDebug() << data;
-#endif
+ qCDebug(authCredentials()) << data;
if (Net::isApplicationError(error)) {
emit finished(
AccountTaskState::STATE_FAILED_SOFT,
@@ -74,9 +72,7 @@ void XboxProfileStep::onRequestDone(
return;
}
-#ifndef NDEBUG
- qDebug() << "XBox profile: " << data;
-#endif
+ qCDebug(authCredentials()) << "XBox profile: " << data;
emit finished(AccountTaskState::STATE_WORKING, tr("Got Xbox profile"));
}
diff --git a/launcher/minecraft/mod/DataPack.cpp b/launcher/minecraft/mod/DataPack.cpp
new file mode 100644
index 00000000..5c58f6b2
--- /dev/null
+++ b/launcher/minecraft/mod/DataPack.cpp
@@ -0,0 +1,108 @@
+// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "DataPack.h"
+
+#include <QDebug>
+#include <QMap>
+#include <QRegularExpression>
+
+#include "Version.h"
+
+// Values taken from:
+// https://minecraft.fandom.com/wiki/Tutorials/Creating_a_data_pack#%22pack_format%22
+static const QMap<int, std::pair<Version, Version>> s_pack_format_versions = {
+ { 4, { Version("1.13"), Version("1.14.4") } }, { 5, { Version("1.15"), Version("1.16.1") } },
+ { 6, { Version("1.16.2"), Version("1.16.5") } }, { 7, { Version("1.17"), Version("1.17.1") } },
+ { 8, { Version("1.18"), Version("1.18.1") } }, { 9, { Version("1.18.2"), Version("1.18.2") } },
+ { 10, { Version("1.19"), Version("1.19.3") } },
+};
+
+void DataPack::setPackFormat(int new_format_id)
+{
+ QMutexLocker locker(&m_data_lock);
+
+ if (!s_pack_format_versions.contains(new_format_id)) {
+ qWarning() << "Pack format '" << new_format_id << "' is not a recognized data pack id!";
+ }
+
+ m_pack_format = new_format_id;
+}
+
+void DataPack::setDescription(QString new_description)
+{
+ QMutexLocker locker(&m_data_lock);
+
+ m_description = new_description;
+}
+
+std::pair<Version, Version> DataPack::compatibleVersions() const
+{
+ if (!s_pack_format_versions.contains(m_pack_format)) {
+ return { {}, {} };
+ }
+
+ return s_pack_format_versions.constFind(m_pack_format).value();
+}
+
+std::pair<int, bool> DataPack::compare(const Resource& other, SortType type) const
+{
+ auto const& cast_other = static_cast<DataPack const&>(other);
+
+ switch (type) {
+ default: {
+ auto res = Resource::compare(other, type);
+ if (res.first != 0)
+ return res;
+ }
+ case SortType::PACK_FORMAT: {
+ auto this_ver = packFormat();
+ auto other_ver = cast_other.packFormat();
+
+ if (this_ver > other_ver)
+ return { 1, type == SortType::PACK_FORMAT };
+ if (this_ver < other_ver)
+ return { -1, type == SortType::PACK_FORMAT };
+ }
+ }
+ return { 0, false };
+}
+
+bool DataPack::applyFilter(QRegularExpression filter) const
+{
+ if (filter.match(description()).hasMatch())
+ return true;
+
+ if (filter.match(QString::number(packFormat())).hasMatch())
+ return true;
+
+ if (filter.match(compatibleVersions().first.toString()).hasMatch())
+ return true;
+ if (filter.match(compatibleVersions().second.toString()).hasMatch())
+ return true;
+
+ return Resource::applyFilter(filter);
+}
+
+bool DataPack::valid() const
+{
+ return m_pack_format != 0;
+}
diff --git a/launcher/minecraft/mod/DataPack.h b/launcher/minecraft/mod/DataPack.h
new file mode 100644
index 00000000..fc2703c7
--- /dev/null
+++ b/launcher/minecraft/mod/DataPack.h
@@ -0,0 +1,73 @@
+// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "Resource.h"
+
+#include <QMutex>
+
+class Version;
+
+/* TODO:
+ *
+ * Store localized descriptions
+ * */
+
+class DataPack : public Resource {
+ Q_OBJECT
+ public:
+ using Ptr = shared_qobject_ptr<Resource>;
+
+ DataPack(QObject* parent = nullptr) : Resource(parent) {}
+ DataPack(QFileInfo file_info) : Resource(file_info) {}
+
+ /** Gets the numerical ID of the pack format. */
+ [[nodiscard]] int packFormat() const { return m_pack_format; }
+ /** Gets, respectively, the lower and upper versions supported by the set pack format. */
+ [[nodiscard]] std::pair<Version, Version> compatibleVersions() const;
+
+ /** Gets the description of the data pack. */
+ [[nodiscard]] QString description() const { return m_description; }
+
+ /** Thread-safe. */
+ void setPackFormat(int new_format_id);
+
+ /** Thread-safe. */
+ void setDescription(QString new_description);
+
+ bool valid() const override;
+
+ [[nodiscard]] auto compare(Resource const& other, SortType type) const -> std::pair<int, bool> override;
+ [[nodiscard]] bool applyFilter(QRegularExpression filter) const override;
+
+ protected:
+ mutable QMutex m_data_lock;
+
+ /* The 'version' of a data pack, as defined in the pack.mcmeta file.
+ * See https://minecraft.fandom.com/wiki/Data_pack#pack.mcmeta
+ */
+ int m_pack_format = 0;
+
+ /** The data pack's description, as defined in the pack.mcmeta file.
+ */
+ QString m_description;
+};
diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp
index 39023f69..c495cd47 100644
--- a/launcher/minecraft/mod/Mod.cpp
+++ b/launcher/minecraft/mod/Mod.cpp
@@ -43,6 +43,9 @@
#include "MetadataHandler.h"
#include "Version.h"
+#include "minecraft/mod/ModDetails.h"
+
+static ModPlatform::ProviderCapabilities ProviderCaps;
Mod::Mod(const QFileInfo& file) : Resource(file), m_local_details()
{
@@ -68,6 +71,10 @@ void Mod::setMetadata(std::shared_ptr<Metadata::ModStruct>&& metadata)
m_local_details.metadata = metadata;
}
+void Mod::setDetails(const ModDetails& details) {
+ m_local_details = details;
+}
+
std::pair<int, bool> Mod::compare(const Resource& other, SortType type) const
{
auto cast_other = dynamic_cast<Mod const*>(&other);
@@ -91,6 +98,11 @@ std::pair<int, bool> Mod::compare(const Resource& other, SortType type) const
if (this_ver < other_ver)
return { -1, type == SortType::VERSION };
}
+ case SortType::PROVIDER: {
+ auto compare_result = QString::compare(provider().value_or("Unknown"), cast_other->provider().value_or("Unknown"), Qt::CaseInsensitive);
+ if (compare_result != 0)
+ return { compare_result, type == SortType::PROVIDER };
+ }
}
return { 0, false };
}
@@ -189,4 +201,16 @@ void Mod::finishResolvingWithDetails(ModDetails&& details)
m_local_details = std::move(details);
if (metadata)
setMetadata(std::move(metadata));
+};
+
+auto Mod::provider() const -> std::optional<QString>
+{
+ if (metadata())
+ return ProviderCaps.readableName(metadata()->provider);
+ return {};
+}
+
+bool Mod::valid() const
+{
+ return !m_local_details.mod_id.isEmpty();
}
diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h
index f336bec4..c4032538 100644
--- a/launcher/minecraft/mod/Mod.h
+++ b/launcher/minecraft/mod/Mod.h
@@ -39,6 +39,8 @@
#include <QFileInfo>
#include <QList>
+#include <optional>
+
#include "Resource.h"
#include "ModDetails.h"
@@ -61,6 +63,7 @@ public:
auto description() const -> QString;
auto authors() const -> QStringList;
auto status() const -> ModStatus;
+ auto provider() const -> std::optional<QString>;
auto metadata() -> std::shared_ptr<Metadata::ModStruct>;
auto metadata() const -> const std::shared_ptr<Metadata::ModStruct>;
@@ -68,6 +71,9 @@ public:
void setStatus(ModStatus status);
void setMetadata(std::shared_ptr<Metadata::ModStruct>&& metadata);
void setMetadata(const Metadata::ModStruct& metadata) { setMetadata(std::make_shared<Metadata::ModStruct>(metadata)); }
+ void setDetails(const ModDetails& details);
+
+ bool valid() const override;
[[nodiscard]] auto compare(Resource const& other, SortType type) const -> std::pair<int, bool> override;
[[nodiscard]] bool applyFilter(QRegularExpression filter) const override;
diff --git a/launcher/minecraft/mod/ModDetails.h b/launcher/minecraft/mod/ModDetails.h
index dd84b0a3..176e4fc1 100644
--- a/launcher/minecraft/mod/ModDetails.h
+++ b/launcher/minecraft/mod/ModDetails.h
@@ -81,7 +81,7 @@ struct ModDetails
ModDetails() = default;
/** Metadata should be handled manually to properly set the mod status. */
- ModDetails(ModDetails& other)
+ ModDetails(const ModDetails& other)
: mod_id(other.mod_id)
, name(other.name)
, version(other.version)
@@ -92,7 +92,7 @@ struct ModDetails
, status(other.status)
{}
- ModDetails& operator=(ModDetails& other)
+ ModDetails& operator=(const ModDetails& other)
{
this->mod_id = other.mod_id;
this->name = other.name;
@@ -106,7 +106,7 @@ struct ModDetails
return *this;
}
- ModDetails& operator=(ModDetails&& other)
+ ModDetails& operator=(const ModDetails&& other)
{
this->mod_id = other.mod_id;
this->name = other.name;
diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp
index 4ccc5d4d..f258ad69 100644
--- a/launcher/minecraft/mod/ModFolderModel.cpp
+++ b/launcher/minecraft/mod/ModFolderModel.cpp
@@ -48,10 +48,11 @@
#include "minecraft/mod/tasks/LocalModParseTask.h"
#include "minecraft/mod/tasks/ModFolderLoadTask.h"
+#include "modplatform/ModIndex.h"
ModFolderModel::ModFolderModel(const QString &dir, bool is_indexed) : ResourceFolderModel(QDir(dir)), m_is_indexed(is_indexed)
{
- m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::VERSION, SortType::DATE };
+ m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::VERSION, SortType::DATE, SortType::PROVIDER };
}
QVariant ModFolderModel::data(const QModelIndex &index, int role) const
@@ -82,7 +83,15 @@ QVariant ModFolderModel::data(const QModelIndex &index, int role) const
}
case DateColumn:
return m_resources[row]->dateTimeChanged();
+ case ProviderColumn: {
+ auto provider = at(row)->provider();
+ if (!provider.has_value()) {
+ //: Unknown mod provider (i.e. not Modrinth, CurseForge, etc...)
+ return tr("Unknown");
+ }
+ return provider.value();
+ }
default:
return QVariant();
}
@@ -118,6 +127,8 @@ QVariant ModFolderModel::headerData(int section, Qt::Orientation orientation, in
return tr("Version");
case DateColumn:
return tr("Last changed");
+ case ProviderColumn:
+ return tr("Provider");
default:
return QVariant();
}
@@ -133,6 +144,8 @@ QVariant ModFolderModel::headerData(int section, Qt::Orientation orientation, in
return tr("The version of the mod.");
case DateColumn:
return tr("The date and time this mod was last changed (or added).");
+ case ProviderColumn:
+ return tr("Where the mod was downloaded from.");
default:
return QVariant();
}
diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h
index 93980319..6898f6eb 100644
--- a/launcher/minecraft/mod/ModFolderModel.h
+++ b/launcher/minecraft/mod/ModFolderModel.h
@@ -67,6 +67,7 @@ public:
NameColumn,
VersionColumn,
DateColumn,
+ ProviderColumn,
NUM_COLUMNS
};
enum ModStatusAction {
diff --git a/launcher/minecraft/mod/Resource.cpp b/launcher/minecraft/mod/Resource.cpp
index 0fbcfd7c..7c572d92 100644
--- a/launcher/minecraft/mod/Resource.cpp
+++ b/launcher/minecraft/mod/Resource.cpp
@@ -143,5 +143,9 @@ bool Resource::enable(EnableAction action)
bool Resource::destroy()
{
m_type = ResourceType::UNKNOWN;
+
+ if (FS::trash(m_file_info.filePath()))
+ return true;
+
return FS::deletePath(m_file_info.filePath());
}
diff --git a/launcher/minecraft/mod/Resource.h b/launcher/minecraft/mod/Resource.h
index f9bd811e..0c37f3a3 100644
--- a/launcher/minecraft/mod/Resource.h
+++ b/launcher/minecraft/mod/Resource.h
@@ -20,7 +20,8 @@ enum class SortType {
DATE,
VERSION,
ENABLED,
- PACK_FORMAT
+ PACK_FORMAT,
+ PROVIDER
};
enum class EnableAction {
diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp
index 0310c8f6..a52c5db3 100644
--- a/launcher/minecraft/mod/ResourceFolderModel.cpp
+++ b/launcher/minecraft/mod/ResourceFolderModel.cpp
@@ -20,6 +20,7 @@ ResourceFolderModel::ResourceFolderModel(QDir dir, QObject* parent) : QAbstractL
m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware);
connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &ResourceFolderModel::directoryChanged);
+ connect(&m_helper_thread_task, &ConcurrentTask::finished, this, [this]{ m_helper_thread_task.clear(); });
}
ResourceFolderModel::~ResourceFolderModel()
@@ -275,7 +276,11 @@ void ResourceFolderModel::resolveResource(Resource* res)
connect(
task, &Task::finished, this, [=] { m_active_parse_tasks.remove(ticket); }, Qt::ConnectionType::QueuedConnection);
- QThreadPool::globalInstance()->start(task);
+ m_helper_thread_task.addTask(task);
+
+ if (!m_helper_thread_task.isRunning()) {
+ QThreadPool::globalInstance()->start(&m_helper_thread_task);
+ }
}
void ResourceFolderModel::onUpdateSucceeded()
diff --git a/launcher/minecraft/mod/ResourceFolderModel.h b/launcher/minecraft/mod/ResourceFolderModel.h
index fe283b04..f1bc2dd7 100644
--- a/launcher/minecraft/mod/ResourceFolderModel.h
+++ b/launcher/minecraft/mod/ResourceFolderModel.h
@@ -10,6 +10,7 @@
#include "Resource.h"
#include "tasks/Task.h"
+#include "tasks/ConcurrentTask.h"
class QSortFilterProxyModel;
@@ -197,6 +198,7 @@ class ResourceFolderModel : public QAbstractListModel {
// Represents the relationship between a resource's internal ID and it's row position on the model.
QMap<QString, int> m_resources_index;
+ ConcurrentTask m_helper_thread_task;
QMap<int, Task::Ptr> m_active_parse_tasks;
std::atomic<int> m_next_resolution_ticket = 0;
};
diff --git a/launcher/minecraft/mod/ResourcePack.cpp b/launcher/minecraft/mod/ResourcePack.cpp
index 3a2fd771..876d5c3e 100644
--- a/launcher/minecraft/mod/ResourcePack.cpp
+++ b/launcher/minecraft/mod/ResourcePack.cpp
@@ -13,11 +13,12 @@
// Values taken from:
// https://minecraft.fandom.com/wiki/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta
static const QMap<int, std::pair<Version, Version>> s_pack_format_versions = {
- { 1, { Version("1.6.1"), Version("1.8.9") } }, { 2, { Version("1.9"), Version("1.10.2") } },
- { 3, { Version("1.11"), Version("1.12.2") } }, { 4, { Version("1.13"), Version("1.14.4") } },
- { 5, { Version("1.15"), Version("1.16.1") } }, { 6, { Version("1.16.2"), Version("1.16.5") } },
- { 7, { Version("1.17"), Version("1.17.1") } }, { 8, { Version("1.18"), Version("1.18.2") } },
- { 9, { Version("1.19"), Version("1.19.2") } }, { 11, { Version("1.19.3"), Version("1.19.3") } },
+ { 1, { Version("1.6.1"), Version("1.8.9") } }, { 2, { Version("1.9"), Version("1.10.2") } },
+ { 3, { Version("1.11"), Version("1.12.2") } }, { 4, { Version("1.13"), Version("1.14.4") } },
+ { 5, { Version("1.15"), Version("1.16.1") } }, { 6, { Version("1.16.2"), Version("1.16.5") } },
+ { 7, { Version("1.17"), Version("1.17.1") } }, { 8, { Version("1.18"), Version("1.18.2") } },
+ { 9, { Version("1.19"), Version("1.19.2") } }, { 11, { Version("22w42a"), Version("22w44a") } },
+ { 12, { Version("1.19.3"), Version("1.19.3") } },
};
void ResourcePack::setPackFormat(int new_format_id)
@@ -25,7 +26,7 @@ void ResourcePack::setPackFormat(int new_format_id)
QMutexLocker locker(&m_data_lock);
if (!s_pack_format_versions.contains(new_format_id)) {
- qWarning() << "Pack format '%1' is not a recognized resource pack id!";
+ qWarning() << "Pack format '" << new_format_id << "' is not a recognized resource pack id!";
}
m_pack_format = new_format_id;
diff --git a/launcher/minecraft/mod/ShaderPack.cpp b/launcher/minecraft/mod/ShaderPack.cpp
new file mode 100644
index 00000000..6a9641de
--- /dev/null
+++ b/launcher/minecraft/mod/ShaderPack.cpp
@@ -0,0 +1,37 @@
+
+// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "ShaderPack.h"
+
+#include "minecraft/mod/tasks/LocalShaderPackParseTask.h"
+
+void ShaderPack::setPackFormat(ShaderPackFormat new_format)
+{
+ QMutexLocker locker(&m_data_lock);
+
+ m_pack_format = new_format;
+}
+
+bool ShaderPack::valid() const
+{
+ return m_pack_format != ShaderPackFormat::INVALID;
+}
diff --git a/launcher/minecraft/mod/ShaderPack.h b/launcher/minecraft/mod/ShaderPack.h
new file mode 100644
index 00000000..ec0f9404
--- /dev/null
+++ b/launcher/minecraft/mod/ShaderPack.h
@@ -0,0 +1,62 @@
+// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "Resource.h"
+
+/* Info:
+ * Currently For Optifine / Iris shader packs,
+ * could be expanded to support others should they exist?
+ *
+ * This class and enum are mostly here as placeholders for validating
+ * that a shaderpack exists and is in the right format,
+ * namely that they contain a folder named 'shaders'.
+ *
+ * In the technical sense it would be possible to parse files like `shaders/shaders.properties`
+ * to get information like the available profiles but this is not all that useful without more knowledge of the
+ * shader mod used to be able to change settings.
+ */
+
+#include <QMutex>
+
+enum class ShaderPackFormat { VALID, INVALID };
+
+class ShaderPack : public Resource {
+ Q_OBJECT
+ public:
+ using Ptr = shared_qobject_ptr<Resource>;
+
+ [[nodiscard]] ShaderPackFormat packFormat() const { return m_pack_format; }
+
+ ShaderPack(QObject* parent = nullptr) : Resource(parent) {}
+ ShaderPack(QFileInfo file_info) : Resource(file_info) {}
+
+ /** Thread-safe. */
+ void setPackFormat(ShaderPackFormat new_format);
+
+ bool valid() const override;
+
+ protected:
+ mutable QMutex m_data_lock;
+
+ ShaderPackFormat m_pack_format = ShaderPackFormat::INVALID;
+};
diff --git a/launcher/minecraft/mod/WorldSave.cpp b/launcher/minecraft/mod/WorldSave.cpp
new file mode 100644
index 00000000..7123f512
--- /dev/null
+++ b/launcher/minecraft/mod/WorldSave.cpp
@@ -0,0 +1,43 @@
+// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "WorldSave.h"
+
+#include "minecraft/mod/tasks/LocalWorldSaveParseTask.h"
+
+void WorldSave::setSaveFormat(WorldSaveFormat new_save_format)
+{
+ QMutexLocker locker(&m_data_lock);
+
+ m_save_format = new_save_format;
+}
+
+void WorldSave::setSaveDirName(QString dir_name)
+{
+ QMutexLocker locker(&m_data_lock);
+
+ m_save_dir_name = dir_name;
+}
+
+bool WorldSave::valid() const
+{
+ return m_save_format != WorldSaveFormat::INVALID;
+}
diff --git a/launcher/minecraft/mod/WorldSave.h b/launcher/minecraft/mod/WorldSave.h
new file mode 100644
index 00000000..5985fc8a
--- /dev/null
+++ b/launcher/minecraft/mod/WorldSave.h
@@ -0,0 +1,61 @@
+// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "Resource.h"
+
+#include <QMutex>
+
+class Version;
+
+enum class WorldSaveFormat { SINGLE, MULTI, INVALID };
+
+class WorldSave : public Resource {
+ Q_OBJECT
+ public:
+ using Ptr = shared_qobject_ptr<Resource>;
+
+ WorldSave(QObject* parent = nullptr) : Resource(parent) {}
+ WorldSave(QFileInfo file_info) : Resource(file_info) {}
+
+ /** Gets the format of the save. */
+ [[nodiscard]] WorldSaveFormat saveFormat() const { return m_save_format; }
+ /** Gets the name of the save dir (first found in multi mode). */
+ [[nodiscard]] QString saveDirName() const { return m_save_dir_name; }
+
+ /** Thread-safe. */
+ void setSaveFormat(WorldSaveFormat new_save_format);
+ /** Thread-safe. */
+ void setSaveDirName(QString dir_name);
+
+ bool valid() const override;
+
+ protected:
+ mutable QMutex m_data_lock;
+
+ /** The format in which the save file is in.
+ * Since saves can be distributed in various slightly different ways, this allows us to treat them separately.
+ */
+ WorldSaveFormat m_save_format = WorldSaveFormat::INVALID;
+
+ QString m_save_dir_name;
+};
diff --git a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp
new file mode 100644
index 00000000..5bb44877
--- /dev/null
+++ b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp
@@ -0,0 +1,177 @@
+// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "LocalDataPackParseTask.h"
+
+#include "FileSystem.h"
+#include "Json.h"
+
+#include <quazip/quazip.h>
+#include <quazip/quazipdir.h>
+#include <quazip/quazipfile.h>
+
+#include <QCryptographicHash>
+
+namespace DataPackUtils {
+
+bool process(DataPack& pack, ProcessingLevel level)
+{
+ switch (pack.type()) {
+ case ResourceType::FOLDER:
+ return DataPackUtils::processFolder(pack, level);
+ case ResourceType::ZIPFILE:
+ return DataPackUtils::processZIP(pack, level);
+ default:
+ qWarning() << "Invalid type for data pack parse task!";
+ return false;
+ }
+}
+
+bool processFolder(DataPack& pack, ProcessingLevel level)
+{
+ Q_ASSERT(pack.type() == ResourceType::FOLDER);
+
+ auto mcmeta_invalid = [&pack]() {
+ qWarning() << "Data pack at" << pack.fileinfo().filePath() << "does not have a valid pack.mcmeta";
+ return false; // the mcmeta is not optional
+ };
+
+ QFileInfo mcmeta_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.mcmeta"));
+ if (mcmeta_file_info.exists() && mcmeta_file_info.isFile()) {
+ QFile mcmeta_file(mcmeta_file_info.filePath());
+ if (!mcmeta_file.open(QIODevice::ReadOnly))
+ return mcmeta_invalid(); // can't open mcmeta file
+
+ auto data = mcmeta_file.readAll();
+
+ bool mcmeta_result = DataPackUtils::processMCMeta(pack, std::move(data));
+
+ mcmeta_file.close();
+ if (!mcmeta_result) {
+ return mcmeta_invalid(); // mcmeta invalid
+ }
+ } else {
+ return mcmeta_invalid(); // mcmeta file isn't a valid file
+ }
+
+ QFileInfo data_dir_info(FS::PathCombine(pack.fileinfo().filePath(), "data"));
+ if (!data_dir_info.exists() || !data_dir_info.isDir()) {
+ return false; // data dir does not exists or isn't valid
+ }
+
+ if (level == ProcessingLevel::BasicInfoOnly) {
+ return true; // only need basic info already checked
+ }
+
+ return true; // all tests passed
+}
+
+bool processZIP(DataPack& pack, ProcessingLevel level)
+{
+ Q_ASSERT(pack.type() == ResourceType::ZIPFILE);
+
+ QuaZip zip(pack.fileinfo().filePath());
+ if (!zip.open(QuaZip::mdUnzip))
+ return false; // can't open zip file
+
+ QuaZipFile file(&zip);
+
+ auto mcmeta_invalid = [&pack]() {
+ qWarning() << "Data pack at" << pack.fileinfo().filePath() << "does not have a valid pack.mcmeta";
+ return false; // the mcmeta is not optional
+ };
+
+ if (zip.setCurrentFile("pack.mcmeta")) {
+ if (!file.open(QIODevice::ReadOnly)) {
+ qCritical() << "Failed to open file in zip.";
+ zip.close();
+ return mcmeta_invalid();
+ }
+
+ auto data = file.readAll();
+
+ bool mcmeta_result = DataPackUtils::processMCMeta(pack, std::move(data));
+
+ file.close();
+ if (!mcmeta_result) {
+ return mcmeta_invalid(); // mcmeta invalid
+ }
+ } else {
+ return mcmeta_invalid(); // could not set pack.mcmeta as current file.
+ }
+
+ QuaZipDir zipDir(&zip);
+ if (!zipDir.exists("/data")) {
+ return false; // data dir does not exists at zip root
+ }
+
+ if (level == ProcessingLevel::BasicInfoOnly) {
+ zip.close();
+ return true; // only need basic info already checked
+ }
+
+ zip.close();
+
+ return true;
+}
+
+// https://minecraft.fandom.com/wiki/Data_pack#pack.mcmeta
+bool processMCMeta(DataPack& pack, QByteArray&& raw_data)
+{
+ try {
+ auto json_doc = QJsonDocument::fromJson(raw_data);
+ auto pack_obj = Json::requireObject(json_doc.object(), "pack", {});
+
+ pack.setPackFormat(Json::ensureInteger(pack_obj, "pack_format", 0));
+ pack.setDescription(Json::ensureString(pack_obj, "description", ""));
+ } catch (Json::JsonException& e) {
+ qWarning() << "JsonException: " << e.what() << e.cause();
+ return false;
+ }
+ return true;
+}
+
+bool validate(QFileInfo file)
+{
+ DataPack dp{ file };
+ return DataPackUtils::process(dp, ProcessingLevel::BasicInfoOnly) && dp.valid();
+}
+
+} // namespace DataPackUtils
+
+LocalDataPackParseTask::LocalDataPackParseTask(int token, DataPack& dp) : Task(nullptr, false), m_token(token), m_data_pack(dp) {}
+
+bool LocalDataPackParseTask::abort()
+{
+ m_aborted = true;
+ return true;
+}
+
+void LocalDataPackParseTask::executeTask()
+{
+ if (!DataPackUtils::process(m_data_pack))
+ return;
+
+ if (m_aborted)
+ emitAborted();
+ else
+ emitSucceeded();
+}
diff --git a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.h b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.h
new file mode 100644
index 00000000..12fd8c82
--- /dev/null
+++ b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.h
@@ -0,0 +1,65 @@
+// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <QDebug>
+#include <QObject>
+
+#include "minecraft/mod/DataPack.h"
+
+#include "tasks/Task.h"
+
+namespace DataPackUtils {
+
+enum class ProcessingLevel { Full, BasicInfoOnly };
+
+bool process(DataPack& pack, ProcessingLevel level = ProcessingLevel::Full);
+
+bool processZIP(DataPack& pack, ProcessingLevel level = ProcessingLevel::Full);
+bool processFolder(DataPack& pack, ProcessingLevel level = ProcessingLevel::Full);
+
+bool processMCMeta(DataPack& pack, QByteArray&& raw_data);
+
+/** Checks whether a file is valid as a data pack or not. */
+bool validate(QFileInfo file);
+
+} // namespace DataPackUtils
+
+class LocalDataPackParseTask : public Task {
+ Q_OBJECT
+ public:
+ LocalDataPackParseTask(int token, DataPack& dp);
+
+ [[nodiscard]] bool canAbort() const override { return true; }
+ bool abort() override;
+
+ void executeTask() override;
+
+ [[nodiscard]] int token() const { return m_token; }
+
+ private:
+ int m_token;
+
+ DataPack& m_data_pack;
+
+ bool m_aborted = false;
+};
diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp
index 774f6114..91cb747f 100644
--- a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp
+++ b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp
@@ -11,12 +11,13 @@
#include "FileSystem.h"
#include "Json.h"
+#include "minecraft/mod/ModDetails.h"
#include "settings/INIFile.h"
-namespace {
+namespace ModUtils {
// NEW format
-// https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/6f62b37cea040daf350dc253eae6326dd9c822c3
+// https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/c8d8f1929aff9979e322af79a59ce81f3e02db6a
// OLD format:
// https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/5bf6a2d05145ec79387acc0d45c958642fb049fc
@@ -73,10 +74,11 @@ ModDetails ReadMCModInfo(QByteArray contents)
version = Json::ensureString(val, "").toInt();
if (version != 2) {
- qCritical() << "BAD stuff happened to mod json:";
- qCritical() << contents;
- return {};
+ qWarning() << QString(R"(The value of 'modListVersion' is "%1" (expected "2")! The file may be corrupted.)").arg(version);
+ qWarning() << "The contents of 'mcmod.info' are as follows:";
+ qWarning() << contents;
}
+
auto arrVal = jsonDoc.object().value("modlist");
if (arrVal.isUndefined()) {
arrVal = jsonDoc.object().value("modList");
@@ -283,35 +285,46 @@ ModDetails ReadLiteModInfo(QByteArray contents)
return details;
}
-} // namespace
-
-LocalModParseTask::LocalModParseTask(int token, ResourceType type, const QFileInfo& modFile)
- : Task(nullptr, false), m_token(token), m_type(type), m_modFile(modFile), m_result(new Result())
-{}
+bool process(Mod& mod, ProcessingLevel level)
+{
+ switch (mod.type()) {
+ case ResourceType::FOLDER:
+ return processFolder(mod, level);
+ case ResourceType::ZIPFILE:
+ return processZIP(mod, level);
+ case ResourceType::LITEMOD:
+ return processLitemod(mod);
+ default:
+ qWarning() << "Invalid type for mod parse task!";
+ return false;
+ }
+}
-void LocalModParseTask::processAsZip()
+bool processZIP(Mod& mod, ProcessingLevel level)
{
- QuaZip zip(m_modFile.filePath());
+ ModDetails details;
+
+ QuaZip zip(mod.fileinfo().filePath());
if (!zip.open(QuaZip::mdUnzip))
- return;
+ return false;
QuaZipFile file(&zip);
if (zip.setCurrentFile("META-INF/mods.toml")) {
if (!file.open(QIODevice::ReadOnly)) {
zip.close();
- return;
+ return false;
}
- m_result->details = ReadMCModTOML(file.readAll());
+ details = ReadMCModTOML(file.readAll());
file.close();
// to replace ${file.jarVersion} with the actual version, as needed
- if (m_result->details.version == "${file.jarVersion}") {
+ if (details.version == "${file.jarVersion}") {
if (zip.setCurrentFile("META-INF/MANIFEST.MF")) {
if (!file.open(QIODevice::ReadOnly)) {
zip.close();
- return;
+ return false;
}
// quick and dirty line-by-line parser
@@ -330,93 +343,131 @@ void LocalModParseTask::processAsZip()
manifestVersion = "NONE";
}
- m_result->details.version = manifestVersion;
+ details.version = manifestVersion;
file.close();
}
}
zip.close();
- return;
+ mod.setDetails(details);
+
+ return true;
} else if (zip.setCurrentFile("mcmod.info")) {
if (!file.open(QIODevice::ReadOnly)) {
zip.close();
- return;
+ return false;
}
- m_result->details = ReadMCModInfo(file.readAll());
+ details = ReadMCModInfo(file.readAll());
file.close();
zip.close();
- return;
+
+ mod.setDetails(details);
+ return true;
} else if (zip.setCurrentFile("quilt.mod.json")) {
if (!file.open(QIODevice::ReadOnly)) {
zip.close();
- return;
+ return false;
}
- m_result->details = ReadQuiltModInfo(file.readAll());
+ details = ReadQuiltModInfo(file.readAll());
file.close();
zip.close();
- return;
+
+ mod.setDetails(details);
+ return true;
} else if (zip.setCurrentFile("fabric.mod.json")) {
if (!file.open(QIODevice::ReadOnly)) {
zip.close();
- return;
+ return false;
}
- m_result->details = ReadFabricModInfo(file.readAll());
+ details = ReadFabricModInfo(file.readAll());
file.close();
zip.close();
- return;
+
+ mod.setDetails(details);
+ return true;
} else if (zip.setCurrentFile("forgeversion.properties")) {
if (!file.open(QIODevice::ReadOnly)) {
zip.close();
- return;
+ return false;
}
- m_result->details = ReadForgeInfo(file.readAll());
+ details = ReadForgeInfo(file.readAll());
file.close();
zip.close();
- return;
+
+ mod.setDetails(details);
+ return true;
}
zip.close();
+ return false; // no valid mod found in archive
}
-void LocalModParseTask::processAsFolder()
+bool processFolder(Mod& mod, ProcessingLevel level)
{
- QFileInfo mcmod_info(FS::PathCombine(m_modFile.filePath(), "mcmod.info"));
- if (mcmod_info.isFile()) {
+ ModDetails details;
+
+ QFileInfo mcmod_info(FS::PathCombine(mod.fileinfo().filePath(), "mcmod.info"));
+ if (mcmod_info.exists() && mcmod_info.isFile()) {
QFile mcmod(mcmod_info.filePath());
if (!mcmod.open(QIODevice::ReadOnly))
- return;
+ return false;
auto data = mcmod.readAll();
if (data.isEmpty() || data.isNull())
- return;
- m_result->details = ReadMCModInfo(data);
+ return false;
+ details = ReadMCModInfo(data);
+
+ mod.setDetails(details);
+ return true;
}
+
+ return false; // no valid mcmod.info file found
}
-void LocalModParseTask::processAsLitemod()
+bool processLitemod(Mod& mod, ProcessingLevel level)
{
- QuaZip zip(m_modFile.filePath());
+ ModDetails details;
+
+ QuaZip zip(mod.fileinfo().filePath());
if (!zip.open(QuaZip::mdUnzip))
- return;
+ return false;
QuaZipFile file(&zip);
if (zip.setCurrentFile("litemod.json")) {
if (!file.open(QIODevice::ReadOnly)) {
zip.close();
- return;
+ return false;
}
- m_result->details = ReadLiteModInfo(file.readAll());
+ details = ReadLiteModInfo(file.readAll());
file.close();
+
+ mod.setDetails(details);
+ return true;
}
zip.close();
+
+ return false; // no valid litemod.json found in archive
}
+/** Checks whether a file is valid as a mod or not. */
+bool validate(QFileInfo file)
+{
+ Mod mod{ file };
+ return ModUtils::process(mod, ProcessingLevel::BasicInfoOnly) && mod.valid();
+}
+
+} // namespace ModUtils
+
+LocalModParseTask::LocalModParseTask(int token, ResourceType type, const QFileInfo& modFile)
+ : Task(nullptr, false), m_token(token), m_type(type), m_modFile(modFile), m_result(new Result())
+{}
+
bool LocalModParseTask::abort()
{
m_aborted.store(true);
@@ -425,19 +476,10 @@ bool LocalModParseTask::abort()
void LocalModParseTask::executeTask()
{
- switch (m_type) {
- case ResourceType::ZIPFILE:
- processAsZip();
- break;
- case ResourceType::FOLDER:
- processAsFolder();
- break;
- case ResourceType::LITEMOD:
- processAsLitemod();
- break;
- default:
- break;
- }
+ Mod mod{ m_modFile };
+ ModUtils::process(mod, ModUtils::ProcessingLevel::Full);
+
+ m_result->details = mod.details();
if (m_aborted)
emit finished();
diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.h b/launcher/minecraft/mod/tasks/LocalModParseTask.h
index 413eb2d1..38dae135 100644
--- a/launcher/minecraft/mod/tasks/LocalModParseTask.h
+++ b/launcher/minecraft/mod/tasks/LocalModParseTask.h
@@ -8,32 +8,48 @@
#include "tasks/Task.h"
-class LocalModParseTask : public Task
-{
+namespace ModUtils {
+
+ModDetails ReadFabricModInfo(QByteArray contents);
+ModDetails ReadQuiltModInfo(QByteArray contents);
+ModDetails ReadForgeInfo(QByteArray contents);
+ModDetails ReadLiteModInfo(QByteArray contents);
+
+enum class ProcessingLevel { Full, BasicInfoOnly };
+
+bool process(Mod& mod, ProcessingLevel level = ProcessingLevel::Full);
+
+bool processZIP(Mod& mod, ProcessingLevel level = ProcessingLevel::Full);
+bool processFolder(Mod& mod, ProcessingLevel level = ProcessingLevel::Full);
+bool processLitemod(Mod& mod, ProcessingLevel level = ProcessingLevel::Full);
+
+/** Checks whether a file is valid as a mod or not. */
+bool validate(QFileInfo file);
+} // namespace ModUtils
+
+class LocalModParseTask : public Task {
Q_OBJECT
-public:
+ public:
struct Result {
ModDetails details;
};
using ResultPtr = std::shared_ptr<Result>;
- ResultPtr result() const {
- return m_result;
- }
+ ResultPtr result() const { return m_result; }
[[nodiscard]] bool canAbort() const override { return true; }
bool abort() override;
- LocalModParseTask(int token, ResourceType type, const QFileInfo & modFile);
+ LocalModParseTask(int token, ResourceType type, const QFileInfo& modFile);
void executeTask() override;
[[nodiscard]] int token() const { return m_token; }
-private:
+ private:
void processAsZip();
void processAsFolder();
void processAsLitemod();
-private:
+ private:
int m_token;
ResourceType m_type;
QFileInfo m_modFile;
diff --git a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp
index 6fd4b024..4bf0b80d 100644
--- a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp
+++ b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp
@@ -22,6 +22,7 @@
#include "Json.h"
#include <quazip/quazip.h>
+#include <quazip/quazipdir.h>
#include <quazip/quazipfile.h>
#include <QCryptographicHash>
@@ -32,99 +33,152 @@ bool process(ResourcePack& pack, ProcessingLevel level)
{
switch (pack.type()) {
case ResourceType::FOLDER:
- ResourcePackUtils::processFolder(pack, level);
- return true;
+ return ResourcePackUtils::processFolder(pack, level);
case ResourceType::ZIPFILE:
- ResourcePackUtils::processZIP(pack, level);
- return true;
+ return ResourcePackUtils::processZIP(pack, level);
default:
qWarning() << "Invalid type for resource pack parse task!";
return false;
}
}
-void processFolder(ResourcePack& pack, ProcessingLevel level)
+bool processFolder(ResourcePack& pack, ProcessingLevel level)
{
Q_ASSERT(pack.type() == ResourceType::FOLDER);
+ auto mcmeta_invalid = [&pack]() {
+ qWarning() << "Resource pack at" << pack.fileinfo().filePath() << "does not have a valid pack.mcmeta";
+ return false; // the mcmeta is not optional
+ };
+
QFileInfo mcmeta_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.mcmeta"));
- if (mcmeta_file_info.isFile()) {
+ if (mcmeta_file_info.exists() && mcmeta_file_info.isFile()) {
QFile mcmeta_file(mcmeta_file_info.filePath());
if (!mcmeta_file.open(QIODevice::ReadOnly))
- return;
+ return mcmeta_invalid(); // can't open mcmeta file
auto data = mcmeta_file.readAll();
- ResourcePackUtils::processMCMeta(pack, std::move(data));
+ bool mcmeta_result = ResourcePackUtils::processMCMeta(pack, std::move(data));
mcmeta_file.close();
+ if (!mcmeta_result) {
+ return mcmeta_invalid(); // mcmeta invalid
+ }
+ } else {
+ return mcmeta_invalid(); // mcmeta file isn't a valid file
}
- if (level == ProcessingLevel::BasicInfoOnly)
- return;
+ QFileInfo assets_dir_info(FS::PathCombine(pack.fileinfo().filePath(), "assets"));
+ if (!assets_dir_info.exists() || !assets_dir_info.isDir()) {
+ return false; // assets dir does not exists or isn't valid
+ }
+
+ if (level == ProcessingLevel::BasicInfoOnly) {
+ return true; // only need basic info already checked
+ }
+
+ auto png_invalid = [&pack]() {
+ qWarning() << "Resource pack at" << pack.fileinfo().filePath() << "does not have a valid pack.png";
+ return true; // the png is optional
+ };
QFileInfo image_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.png"));
- if (image_file_info.isFile()) {
- QFile mcmeta_file(image_file_info.filePath());
- if (!mcmeta_file.open(QIODevice::ReadOnly))
- return;
+ if (image_file_info.exists() && image_file_info.isFile()) {
+ QFile pack_png_file(image_file_info.filePath());
+ if (!pack_png_file.open(QIODevice::ReadOnly))
+ return png_invalid(); // can't open pack.png file
- auto data = mcmeta_file.readAll();
+ auto data = pack_png_file.readAll();
- ResourcePackUtils::processPackPNG(pack, std::move(data));
+ bool pack_png_result = ResourcePackUtils::processPackPNG(pack, std::move(data));
- mcmeta_file.close();
+ pack_png_file.close();
+ if (!pack_png_result) {
+ return png_invalid(); // pack.png invalid
+ }
+ } else {
+ return png_invalid(); // pack.png does not exists or is not a valid file.
}
+
+ return true; // all tests passed
}
-void processZIP(ResourcePack& pack, ProcessingLevel level)
+bool processZIP(ResourcePack& pack, ProcessingLevel level)
{
Q_ASSERT(pack.type() == ResourceType::ZIPFILE);
QuaZip zip(pack.fileinfo().filePath());
if (!zip.open(QuaZip::mdUnzip))
- return;
+ return false; // can't open zip file
QuaZipFile file(&zip);
+ auto mcmeta_invalid = [&pack]() {
+ qWarning() << "Resource pack at" << pack.fileinfo().filePath() << "does not have a valid pack.mcmeta";
+ return false; // the mcmeta is not optional
+ };
+
if (zip.setCurrentFile("pack.mcmeta")) {
if (!file.open(QIODevice::ReadOnly)) {
qCritical() << "Failed to open file in zip.";
zip.close();
- return;
+ return mcmeta_invalid();
}
auto data = file.readAll();
- ResourcePackUtils::processMCMeta(pack, std::move(data));
+ bool mcmeta_result = ResourcePackUtils::processMCMeta(pack, std::move(data));
file.close();
+ if (!mcmeta_result) {
+ return mcmeta_invalid(); // mcmeta invalid
+ }
+ } else {
+ return mcmeta_invalid(); // could not set pack.mcmeta as current file.
+ }
+
+ QuaZipDir zipDir(&zip);
+ if (!zipDir.exists("/assets")) {
+ return false; // assets dir does not exists at zip root
}
if (level == ProcessingLevel::BasicInfoOnly) {
zip.close();
- return;
+ return true; // only need basic info already checked
}
+ auto png_invalid = [&pack]() {
+ qWarning() << "Resource pack at" << pack.fileinfo().filePath() << "does not have a valid pack.png";
+ return true; // the png is optional
+ };
+
if (zip.setCurrentFile("pack.png")) {
if (!file.open(QIODevice::ReadOnly)) {
qCritical() << "Failed to open file in zip.";
zip.close();
- return;
+ return png_invalid();
}
auto data = file.readAll();
- ResourcePackUtils::processPackPNG(pack, std::move(data));
+ bool pack_png_result = ResourcePackUtils::processPackPNG(pack, std::move(data));
file.close();
+ if (!pack_png_result) {
+ return png_invalid(); // pack.png invalid
+ }
+ } else {
+ return png_invalid(); // could not set pack.mcmeta as current file.
}
zip.close();
+
+ return true;
}
// https://minecraft.fandom.com/wiki/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta
-void processMCMeta(ResourcePack& pack, QByteArray&& raw_data)
+bool processMCMeta(ResourcePack& pack, QByteArray&& raw_data)
{
try {
auto json_doc = QJsonDocument::fromJson(raw_data);
@@ -134,17 +188,21 @@ void processMCMeta(ResourcePack& pack, QByteArray&& raw_data)
pack.setDescription(Json::ensureString(pack_obj, "description", ""));
} catch (Json::JsonException& e) {
qWarning() << "JsonException: " << e.what() << e.cause();
+ return false;
}
+ return true;
}
-void processPackPNG(ResourcePack& pack, QByteArray&& raw_data)
+bool processPackPNG(ResourcePack& pack, QByteArray&& raw_data)
{
auto img = QImage::fromData(raw_data);
if (!img.isNull()) {
pack.setImage(img);
} else {
qWarning() << "Failed to parse pack.png.";
+ return false;
}
+ return true;
}
bool validate(QFileInfo file)
diff --git a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h
index 69dbd6ad..d0c24c2b 100644
--- a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h
+++ b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h
@@ -31,11 +31,11 @@ enum class ProcessingLevel { Full, BasicInfoOnly };
bool process(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full);
-void processZIP(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full);
-void processFolder(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full);
+bool processZIP(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full);
+bool processFolder(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full);
-void processMCMeta(ResourcePack& pack, QByteArray&& raw_data);
-void processPackPNG(ResourcePack& pack, QByteArray&& raw_data);
+bool processMCMeta(ResourcePack& pack, QByteArray&& raw_data);
+bool processPackPNG(ResourcePack& pack, QByteArray&& raw_data);
/** Checks whether a file is valid as a resource pack or not. */
bool validate(QFileInfo file);
diff --git a/launcher/minecraft/mod/tasks/LocalResourceParse.cpp b/launcher/minecraft/mod/tasks/LocalResourceParse.cpp
new file mode 100644
index 00000000..4d760df2
--- /dev/null
+++ b/launcher/minecraft/mod/tasks/LocalResourceParse.cpp
@@ -0,0 +1,78 @@
+// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <QObject>
+
+#include "LocalResourceParse.h"
+
+#include "LocalDataPackParseTask.h"
+#include "LocalModParseTask.h"
+#include "LocalResourcePackParseTask.h"
+#include "LocalShaderPackParseTask.h"
+#include "LocalTexturePackParseTask.h"
+#include "LocalWorldSaveParseTask.h"
+
+
+static const QMap<PackedResourceType, QString> s_packed_type_names = {
+ {PackedResourceType::ResourcePack, QObject::tr("resource pack")},
+ {PackedResourceType::TexturePack, QObject::tr("texture pack")},
+ {PackedResourceType::DataPack, QObject::tr("data pack")},
+ {PackedResourceType::ShaderPack, QObject::tr("shader pack")},
+ {PackedResourceType::WorldSave, QObject::tr("world save")},
+ {PackedResourceType::Mod , QObject::tr("mod")},
+ {PackedResourceType::UNKNOWN, QObject::tr("unknown")}
+};
+
+namespace ResourceUtils {
+PackedResourceType identify(QFileInfo file){
+ if (file.exists() && file.isFile()) {
+ if (ResourcePackUtils::validate(file)) {
+ qDebug() << file.fileName() << "is a resource pack";
+ return PackedResourceType::ResourcePack;
+ } else if (TexturePackUtils::validate(file)) {
+ qDebug() << file.fileName() << "is a pre 1.6 texture pack";
+ return PackedResourceType::TexturePack;
+ } else if (DataPackUtils::validate(file)) {
+ qDebug() << file.fileName() << "is a data pack";
+ return PackedResourceType::DataPack;
+ } else if (ModUtils::validate(file)) {
+ qDebug() << file.fileName() << "is a mod";
+ return PackedResourceType::Mod;
+ } else if (WorldSaveUtils::validate(file)) {
+ qDebug() << file.fileName() << "is a world save";
+ return PackedResourceType::WorldSave;
+ } else if (ShaderPackUtils::validate(file)) {
+ qDebug() << file.fileName() << "is a shader pack";
+ return PackedResourceType::ShaderPack;
+ } else {
+ qDebug() << "Can't Identify" << file.fileName() ;
+ }
+ } else {
+ qDebug() << "Can't find" << file.absolutePath();
+ }
+ return PackedResourceType::UNKNOWN;
+}
+
+QString getPackedTypeName(PackedResourceType type) {
+ return s_packed_type_names.constFind(type).value();
+}
+
+}
diff --git a/launcher/minecraft/mod/tasks/LocalResourceParse.h b/launcher/minecraft/mod/tasks/LocalResourceParse.h
new file mode 100644
index 00000000..7385d24b
--- /dev/null
+++ b/launcher/minecraft/mod/tasks/LocalResourceParse.h
@@ -0,0 +1,37 @@
+// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <set>
+
+#include <QDebug>
+#include <QFileInfo>
+#include <QObject>
+
+enum class PackedResourceType { DataPack, ResourcePack, TexturePack, ShaderPack, WorldSave, Mod, UNKNOWN };
+namespace ResourceUtils {
+static const std::set<PackedResourceType> ValidResourceTypes = { PackedResourceType::DataPack, PackedResourceType::ResourcePack,
+ PackedResourceType::TexturePack, PackedResourceType::ShaderPack,
+ PackedResourceType::WorldSave, PackedResourceType::Mod };
+PackedResourceType identify(QFileInfo file);
+QString getPackedTypeName(PackedResourceType type);
+} // namespace ResourceUtils
diff --git a/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.cpp
new file mode 100644
index 00000000..a9949735
--- /dev/null
+++ b/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.cpp
@@ -0,0 +1,113 @@
+// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "LocalShaderPackParseTask.h"
+
+#include "FileSystem.h"
+
+#include <quazip/quazip.h>
+#include <quazip/quazipdir.h>
+#include <quazip/quazipfile.h>
+
+namespace ShaderPackUtils {
+
+bool process(ShaderPack& pack, ProcessingLevel level)
+{
+ switch (pack.type()) {
+ case ResourceType::FOLDER:
+ return ShaderPackUtils::processFolder(pack, level);
+ case ResourceType::ZIPFILE:
+ return ShaderPackUtils::processZIP(pack, level);
+ default:
+ qWarning() << "Invalid type for shader pack parse task!";
+ return false;
+ }
+}
+
+bool processFolder(ShaderPack& pack, ProcessingLevel level)
+{
+ Q_ASSERT(pack.type() == ResourceType::FOLDER);
+
+ QFileInfo shaders_dir_info(FS::PathCombine(pack.fileinfo().filePath(), "shaders"));
+ if (!shaders_dir_info.exists() || !shaders_dir_info.isDir()) {
+ return false; // assets dir does not exists or isn't valid
+ }
+ pack.setPackFormat(ShaderPackFormat::VALID);
+
+ if (level == ProcessingLevel::BasicInfoOnly) {
+ return true; // only need basic info already checked
+ }
+
+ return true; // all tests passed
+}
+
+bool processZIP(ShaderPack& pack, ProcessingLevel level)
+{
+ Q_ASSERT(pack.type() == ResourceType::ZIPFILE);
+
+ QuaZip zip(pack.fileinfo().filePath());
+ if (!zip.open(QuaZip::mdUnzip))
+ return false; // can't open zip file
+
+ QuaZipFile file(&zip);
+
+ QuaZipDir zipDir(&zip);
+ if (!zipDir.exists("/shaders")) {
+ return false; // assets dir does not exists at zip root
+ }
+ pack.setPackFormat(ShaderPackFormat::VALID);
+
+ if (level == ProcessingLevel::BasicInfoOnly) {
+ zip.close();
+ return true; // only need basic info already checked
+ }
+
+ zip.close();
+
+ return true;
+}
+
+bool validate(QFileInfo file)
+{
+ ShaderPack sp{ file };
+ return ShaderPackUtils::process(sp, ProcessingLevel::BasicInfoOnly) && sp.valid();
+}
+
+} // namespace ShaderPackUtils
+
+LocalShaderPackParseTask::LocalShaderPackParseTask(int token, ShaderPack& sp) : Task(nullptr, false), m_token(token), m_shader_pack(sp) {}
+
+bool LocalShaderPackParseTask::abort()
+{
+ m_aborted = true;
+ return true;
+}
+
+void LocalShaderPackParseTask::executeTask()
+{
+ if (!ShaderPackUtils::process(m_shader_pack))
+ return;
+
+ if (m_aborted)
+ emitAborted();
+ else
+ emitSucceeded();
+}
diff --git a/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.h b/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.h
new file mode 100644
index 00000000..6be2183c
--- /dev/null
+++ b/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.h
@@ -0,0 +1,62 @@
+// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <QDebug>
+#include <QObject>
+
+#include "minecraft/mod/ShaderPack.h"
+
+#include "tasks/Task.h"
+
+namespace ShaderPackUtils {
+
+enum class ProcessingLevel { Full, BasicInfoOnly };
+
+bool process(ShaderPack& pack, ProcessingLevel level = ProcessingLevel::Full);
+
+bool processZIP(ShaderPack& pack, ProcessingLevel level = ProcessingLevel::Full);
+bool processFolder(ShaderPack& pack, ProcessingLevel level = ProcessingLevel::Full);
+
+/** Checks whether a file is valid as a shader pack or not. */
+bool validate(QFileInfo file);
+} // namespace ShaderPackUtils
+
+class LocalShaderPackParseTask : public Task {
+ Q_OBJECT
+ public:
+ LocalShaderPackParseTask(int token, ShaderPack& sp);
+
+ [[nodiscard]] bool canAbort() const override { return true; }
+ bool abort() override;
+
+ void executeTask() override;
+
+ [[nodiscard]] int token() const { return m_token; }
+
+ private:
+ int m_token;
+
+ ShaderPack& m_shader_pack;
+
+ bool m_aborted = false;
+};
diff --git a/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp
index adb19aca..38f1d7c1 100644
--- a/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp
+++ b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp
@@ -32,18 +32,16 @@ bool process(TexturePack& pack, ProcessingLevel level)
{
switch (pack.type()) {
case ResourceType::FOLDER:
- TexturePackUtils::processFolder(pack, level);
- return true;
+ return TexturePackUtils::processFolder(pack, level);
case ResourceType::ZIPFILE:
- TexturePackUtils::processZIP(pack, level);
- return true;
+ return TexturePackUtils::processZIP(pack, level);
default:
qWarning() << "Invalid type for resource pack parse task!";
return false;
}
}
-void processFolder(TexturePack& pack, ProcessingLevel level)
+bool processFolder(TexturePack& pack, ProcessingLevel level)
{
Q_ASSERT(pack.type() == ResourceType::FOLDER);
@@ -51,39 +49,51 @@ void processFolder(TexturePack& pack, ProcessingLevel level)
if (mcmeta_file_info.isFile()) {
QFile mcmeta_file(mcmeta_file_info.filePath());
if (!mcmeta_file.open(QIODevice::ReadOnly))
- return;
+ return false;
auto data = mcmeta_file.readAll();
- TexturePackUtils::processPackTXT(pack, std::move(data));
+ bool packTXT_result = TexturePackUtils::processPackTXT(pack, std::move(data));
mcmeta_file.close();
+ if (!packTXT_result) {
+ return false;
+ }
+ } else {
+ return false;
}
if (level == ProcessingLevel::BasicInfoOnly)
- return;
+ return true;
QFileInfo image_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.png"));
if (image_file_info.isFile()) {
QFile mcmeta_file(image_file_info.filePath());
if (!mcmeta_file.open(QIODevice::ReadOnly))
- return;
+ return false;
auto data = mcmeta_file.readAll();
- TexturePackUtils::processPackPNG(pack, std::move(data));
+ bool packPNG_result = TexturePackUtils::processPackPNG(pack, std::move(data));
mcmeta_file.close();
+ if (!packPNG_result) {
+ return false;
+ }
+ } else {
+ return false;
}
+
+ return true;
}
-void processZIP(TexturePack& pack, ProcessingLevel level)
+bool processZIP(TexturePack& pack, ProcessingLevel level)
{
Q_ASSERT(pack.type() == ResourceType::ZIPFILE);
QuaZip zip(pack.fileinfo().filePath());
if (!zip.open(QuaZip::mdUnzip))
- return;
+ return false;
QuaZipFile file(&zip);
@@ -91,51 +101,62 @@ void processZIP(TexturePack& pack, ProcessingLevel level)
if (!file.open(QIODevice::ReadOnly)) {
qCritical() << "Failed to open file in zip.";
zip.close();
- return;
+ return false;
}
auto data = file.readAll();
- TexturePackUtils::processPackTXT(pack, std::move(data));
+ bool packTXT_result = TexturePackUtils::processPackTXT(pack, std::move(data));
file.close();
+ if (!packTXT_result) {
+ return false;
+ }
}
if (level == ProcessingLevel::BasicInfoOnly) {
zip.close();
- return;
+ return true;
}
if (zip.setCurrentFile("pack.png")) {
if (!file.open(QIODevice::ReadOnly)) {
qCritical() << "Failed to open file in zip.";
zip.close();
- return;
+ return false;
}
auto data = file.readAll();
- TexturePackUtils::processPackPNG(pack, std::move(data));
+ bool packPNG_result = TexturePackUtils::processPackPNG(pack, std::move(data));
file.close();
+ if (!packPNG_result) {
+ return false;
+ }
}
zip.close();
+
+ return true;
}
-void processPackTXT(TexturePack& pack, QByteArray&& raw_data)
+bool processPackTXT(TexturePack& pack, QByteArray&& raw_data)
{
pack.setDescription(QString(raw_data));
+ return true;
}
-void processPackPNG(TexturePack& pack, QByteArray&& raw_data)
+bool processPackPNG(TexturePack& pack, QByteArray&& raw_data)
{
auto img = QImage::fromData(raw_data);
if (!img.isNull()) {
pack.setImage(img);
} else {
qWarning() << "Failed to parse pack.png.";
+ return false;
}
+ return true;
}
bool validate(QFileInfo file)
diff --git a/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.h b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.h
index 9f7aab75..1589f8cb 100644
--- a/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.h
+++ b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.h
@@ -32,11 +32,11 @@ enum class ProcessingLevel { Full, BasicInfoOnly };
bool process(TexturePack& pack, ProcessingLevel level = ProcessingLevel::Full);
-void processZIP(TexturePack& pack, ProcessingLevel level = ProcessingLevel::Full);
-void processFolder(TexturePack& pack, ProcessingLevel level = ProcessingLevel::Full);
+bool processZIP(TexturePack& pack, ProcessingLevel level = ProcessingLevel::Full);
+bool processFolder(TexturePack& pack, ProcessingLevel level = ProcessingLevel::Full);
-void processPackTXT(TexturePack& pack, QByteArray&& raw_data);
-void processPackPNG(TexturePack& pack, QByteArray&& raw_data);
+bool processPackTXT(TexturePack& pack, QByteArray&& raw_data);
+bool processPackPNG(TexturePack& pack, QByteArray&& raw_data);
/** Checks whether a file is valid as a texture pack or not. */
bool validate(QFileInfo file);
diff --git a/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.cpp b/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.cpp
new file mode 100644
index 00000000..cbc8f8ce
--- /dev/null
+++ b/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.cpp
@@ -0,0 +1,190 @@
+
+// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "LocalWorldSaveParseTask.h"
+
+#include "FileSystem.h"
+
+#include <quazip/quazip.h>
+#include <quazip/quazipdir.h>
+#include <quazip/quazipfile.h>
+
+#include <QDir>
+#include <QFileInfo>
+
+namespace WorldSaveUtils {
+
+bool process(WorldSave& pack, ProcessingLevel level)
+{
+ switch (pack.type()) {
+ case ResourceType::FOLDER:
+ return WorldSaveUtils::processFolder(pack, level);
+ case ResourceType::ZIPFILE:
+ return WorldSaveUtils::processZIP(pack, level);
+ default:
+ qWarning() << "Invalid type for world save parse task!";
+ return false;
+ }
+}
+
+/// @brief checks a folder structure to see if it contains a level.dat
+/// @param dir the path to check
+/// @param saves used in recursive call if a "saves" dir was found
+/// @return std::tuple of (
+/// bool <found level.dat>,
+/// QString <name of folder containing level.dat>,
+/// bool <saves folder found>
+/// )
+static std::tuple<bool, QString, bool> contains_level_dat(QDir dir, bool saves = false)
+{
+ for (auto const& entry : dir.entryInfoList()) {
+ if (!entry.isDir()) {
+ continue;
+ }
+ if (!saves && entry.fileName() == "saves") {
+ return contains_level_dat(QDir(entry.filePath()), true);
+ }
+ QFileInfo level_dat(FS::PathCombine(entry.filePath(), "level.dat"));
+ if (level_dat.exists() && level_dat.isFile()) {
+ return std::make_tuple(true, entry.fileName(), saves);
+ }
+ }
+ return std::make_tuple(false, "", saves);
+}
+
+bool processFolder(WorldSave& save, ProcessingLevel level)
+{
+ Q_ASSERT(save.type() == ResourceType::FOLDER);
+
+ auto [found, save_dir_name, found_saves_dir] = contains_level_dat(QDir(save.fileinfo().filePath()));
+
+ if (!found) {
+ return false;
+ }
+
+ save.setSaveDirName(save_dir_name);
+
+ if (found_saves_dir) {
+ save.setSaveFormat(WorldSaveFormat::MULTI);
+ } else {
+ save.setSaveFormat(WorldSaveFormat::SINGLE);
+ }
+
+ if (level == ProcessingLevel::BasicInfoOnly) {
+ return true; // only need basic info already checked
+ }
+
+ // reserved for more intensive processing
+
+ return true; // all tests passed
+}
+
+/// @brief checks a folder structure to see if it contains a level.dat
+/// @param zip the zip file to check
+/// @return std::tuple of (
+/// bool <found level.dat>,
+/// QString <name of folder containing level.dat>,
+/// bool <saves folder found>
+/// )
+static std::tuple<bool, QString, bool> contains_level_dat(QuaZip& zip)
+{
+ bool saves = false;
+ QuaZipDir zipDir(&zip);
+ if (zipDir.exists("/saves")) {
+ saves = true;
+ zipDir.cd("/saves");
+ }
+
+ for (auto const& entry : zipDir.entryList()) {
+ zipDir.cd(entry);
+ if (zipDir.exists("level.dat")) {
+ return std::make_tuple(true, entry, saves);
+ }
+ zipDir.cd("..");
+ }
+ return std::make_tuple(false, "", saves);
+}
+
+bool processZIP(WorldSave& save, ProcessingLevel level)
+{
+ Q_ASSERT(save.type() == ResourceType::ZIPFILE);
+
+ QuaZip zip(save.fileinfo().filePath());
+ if (!zip.open(QuaZip::mdUnzip))
+ return false; // can't open zip file
+
+ auto [found, save_dir_name, found_saves_dir] = contains_level_dat(zip);
+
+ if (save_dir_name.endsWith("/")) {
+ save_dir_name.chop(1);
+ }
+
+ if (!found) {
+ return false;
+ }
+
+ save.setSaveDirName(save_dir_name);
+
+ if (found_saves_dir) {
+ save.setSaveFormat(WorldSaveFormat::MULTI);
+ } else {
+ save.setSaveFormat(WorldSaveFormat::SINGLE);
+ }
+
+ if (level == ProcessingLevel::BasicInfoOnly) {
+ zip.close();
+ return true; // only need basic info already checked
+ }
+
+ // reserved for more intensive processing
+
+ zip.close();
+
+ return true;
+}
+
+bool validate(QFileInfo file)
+{
+ WorldSave sp{ file };
+ return WorldSaveUtils::process(sp, ProcessingLevel::BasicInfoOnly) && sp.valid();
+}
+
+} // namespace WorldSaveUtils
+
+LocalWorldSaveParseTask::LocalWorldSaveParseTask(int token, WorldSave& save) : Task(nullptr, false), m_token(token), m_save(save) {}
+
+bool LocalWorldSaveParseTask::abort()
+{
+ m_aborted = true;
+ return true;
+}
+
+void LocalWorldSaveParseTask::executeTask()
+{
+ if (!WorldSaveUtils::process(m_save))
+ return;
+
+ if (m_aborted)
+ emitAborted();
+ else
+ emitSucceeded();
+}
diff --git a/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.h b/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.h
new file mode 100644
index 00000000..9dcdca2b
--- /dev/null
+++ b/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.h
@@ -0,0 +1,62 @@
+// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <QDebug>
+#include <QObject>
+
+#include "minecraft/mod/WorldSave.h"
+
+#include "tasks/Task.h"
+
+namespace WorldSaveUtils {
+
+enum class ProcessingLevel { Full, BasicInfoOnly };
+
+bool process(WorldSave& save, ProcessingLevel level = ProcessingLevel::Full);
+
+bool processZIP(WorldSave& pack, ProcessingLevel level = ProcessingLevel::Full);
+bool processFolder(WorldSave& pack, ProcessingLevel level = ProcessingLevel::Full);
+
+bool validate(QFileInfo file);
+
+} // namespace WorldSaveUtils
+
+class LocalWorldSaveParseTask : public Task {
+ Q_OBJECT
+ public:
+ LocalWorldSaveParseTask(int token, WorldSave& save);
+
+ [[nodiscard]] bool canAbort() const override { return true; }
+ bool abort() override;
+
+ void executeTask() override;
+
+ [[nodiscard]] int token() const { return m_token; }
+
+ private:
+ int m_token;
+
+ WorldSave& m_save;
+
+ bool m_aborted = false;
+};
diff --git a/launcher/modplatform/CheckUpdateTask.h b/launcher/modplatform/CheckUpdateTask.h
index 91922034..932a62d9 100644
--- a/launcher/modplatform/CheckUpdateTask.h
+++ b/launcher/modplatform/CheckUpdateTask.h
@@ -1,18 +1,18 @@
#pragma once
#include "minecraft/mod/Mod.h"
-#include "modplatform/ModAPI.h"
+#include "modplatform/ResourceAPI.h"
#include "modplatform/ModIndex.h"
#include "tasks/Task.h"
-class ModDownloadTask;
+class ResourceDownloadTask;
class ModFolderModel;
class CheckUpdateTask : public Task {
Q_OBJECT
public:
- CheckUpdateTask(QList<Mod*>& mods, std::list<Version>& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr<ModFolderModel> mods_folder)
+ CheckUpdateTask(QList<Mod*>& mods, std::list<Version>& mcVersions, std::optional<ResourceAPI::ModLoaderTypes> loaders, std::shared_ptr<ModFolderModel> mods_folder)
: Task(nullptr), m_mods(mods), m_game_versions(mcVersions), m_loaders(loaders), m_mods_folder(mods_folder) {};
struct UpdatableMod {
@@ -21,11 +21,11 @@ class CheckUpdateTask : public Task {
QString old_version;
QString new_version;
QString changelog;
- ModPlatform::Provider provider;
- ModDownloadTask* download;
+ ModPlatform::ResourceProvider provider;
+ ResourceDownloadTask* download;
public:
- UpdatableMod(QString name, QString old_h, QString old_v, QString new_v, QString changelog, ModPlatform::Provider p, ModDownloadTask* t)
+ UpdatableMod(QString name, QString old_h, QString old_v, QString new_v, QString changelog, ModPlatform::ResourceProvider p, ResourceDownloadTask* t)
: name(name), old_hash(old_h), old_version(old_v), new_version(new_v), changelog(changelog), provider(p), download(t)
{}
};
@@ -44,7 +44,7 @@ class CheckUpdateTask : public Task {
protected:
QList<Mod*>& m_mods;
std::list<Version>& m_game_versions;
- ModAPI::ModLoaderTypes m_loaders;
+ std::optional<ResourceAPI::ModLoaderTypes> m_loaders;
std::shared_ptr<ModFolderModel> m_mods_folder;
std::vector<UpdatableMod> m_updatable;
diff --git a/launcher/modplatform/EnsureMetadataTask.cpp b/launcher/modplatform/EnsureMetadataTask.cpp
index 234330a7..d9523052 100644
--- a/launcher/modplatform/EnsureMetadataTask.cpp
+++ b/launcher/modplatform/EnsureMetadataTask.cpp
@@ -13,14 +13,12 @@
#include "modplatform/modrinth/ModrinthAPI.h"
#include "modplatform/modrinth/ModrinthPackIndex.h"
-#include "net/NetJob.h"
-
static ModPlatform::ProviderCapabilities ProviderCaps;
static ModrinthAPI modrinth_api;
static FlameAPI flame_api;
-EnsureMetadataTask::EnsureMetadataTask(Mod* mod, QDir dir, ModPlatform::Provider prov)
+EnsureMetadataTask::EnsureMetadataTask(Mod* mod, QDir dir, ModPlatform::ResourceProvider prov)
: Task(nullptr), m_index_dir(dir), m_provider(prov), m_hashing_task(nullptr), m_current_task(nullptr)
{
auto hash_task = createNewHash(mod);
@@ -31,7 +29,7 @@ EnsureMetadataTask::EnsureMetadataTask(Mod* mod, QDir dir, ModPlatform::Provider
hash_task->start();
}
-EnsureMetadataTask::EnsureMetadataTask(QList<Mod*>& mods, QDir dir, ModPlatform::Provider prov)
+EnsureMetadataTask::EnsureMetadataTask(QList<Mod*>& mods, QDir dir, ModPlatform::ResourceProvider prov)
: Task(nullptr), m_index_dir(dir), m_provider(prov), m_current_task(nullptr)
{
m_hashing_task = new ConcurrentTask(this, "MakeHashesTask", 10);
@@ -107,13 +105,13 @@ void EnsureMetadataTask::executeTask()
}
}
- NetJob::Ptr version_task;
+ Task::Ptr version_task;
switch (m_provider) {
- case (ModPlatform::Provider::MODRINTH):
+ case (ModPlatform::ResourceProvider::MODRINTH):
version_task = modrinthVersionsTask();
break;
- case (ModPlatform::Provider::FLAME):
+ case (ModPlatform::ResourceProvider::FLAME):
version_task = flameVersionsTask();
break;
}
@@ -127,13 +125,13 @@ void EnsureMetadataTask::executeTask()
};
connect(version_task.get(), &Task::finished, this, [this, invalidade_leftover] {
- NetJob::Ptr project_task;
+ Task::Ptr project_task;
switch (m_provider) {
- case (ModPlatform::Provider::MODRINTH):
+ case (ModPlatform::ResourceProvider::MODRINTH):
project_task = modrinthProjectsTask();
break;
- case (ModPlatform::Provider::FLAME):
+ case (ModPlatform::ResourceProvider::FLAME):
project_task = flameProjectsTask();
break;
}
@@ -149,7 +147,7 @@ void EnsureMetadataTask::executeTask()
m_current_task = nullptr;
});
- m_current_task = project_task.get();
+ m_current_task = project_task;
project_task->start();
});
@@ -164,7 +162,7 @@ void EnsureMetadataTask::executeTask()
setStatus(tr("Requesting metadata information from %1 for '%2'...")
.arg(ProviderCaps.readableName(m_provider), m_mods.begin().value()->name()));
- m_current_task = version_task.get();
+ m_current_task = version_task;
version_task->start();
}
@@ -210,9 +208,9 @@ void EnsureMetadataTask::emitFail(Mod* m, QString key, RemoveFromList remove)
// Modrinth
-NetJob::Ptr EnsureMetadataTask::modrinthVersionsTask()
+Task::Ptr EnsureMetadataTask::modrinthVersionsTask()
{
- auto hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first();
+ auto hash_type = ProviderCaps.hashType(ModPlatform::ResourceProvider::MODRINTH).first();
auto* response = new QByteArray();
auto ver_task = modrinth_api.currentVersions(m_mods.keys(), hash_type, response);
@@ -221,7 +219,7 @@ NetJob::Ptr EnsureMetadataTask::modrinthVersionsTask()
if (!ver_task)
return {};
- connect(ver_task.get(), &NetJob::succeeded, this, [this, response] {
+ connect(ver_task.get(), &Task::succeeded, this, [this, response] {
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
@@ -260,14 +258,14 @@ NetJob::Ptr EnsureMetadataTask::modrinthVersionsTask()
return ver_task;
}
-NetJob::Ptr EnsureMetadataTask::modrinthProjectsTask()
+Task::Ptr EnsureMetadataTask::modrinthProjectsTask()
{
QHash<QString, QString> addonIds;
for (auto const& data : m_temp_versions)
addonIds.insert(data.addonId.toString(), data.hash);
auto response = new QByteArray();
- NetJob::Ptr proj_task;
+ Task::Ptr proj_task;
if (addonIds.isEmpty()) {
qWarning() << "No addonId found!";
@@ -281,7 +279,7 @@ NetJob::Ptr EnsureMetadataTask::modrinthProjectsTask()
if (!proj_task)
return {};
- connect(proj_task.get(), &NetJob::succeeded, this, [this, response, addonIds] {
+ connect(proj_task.get(), &Task::succeeded, this, [this, response, addonIds] {
QJsonParseError parse_error{};
auto doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
@@ -291,43 +289,53 @@ NetJob::Ptr EnsureMetadataTask::modrinthProjectsTask()
return;
}
+ QJsonArray entries;
+
try {
- QJsonArray entries;
if (addonIds.size() == 1)
entries = { doc.object() };
else
entries = Json::requireArray(doc);
+ } catch (Json::JsonException& e) {
+ qDebug() << e.cause();
+ qDebug() << doc;
+ }
- for (auto entry : entries) {
+ for (auto entry : entries) {
+ ModPlatform::IndexedPack pack;
+
+ try {
auto entry_obj = Json::requireObject(entry);
- ModPlatform::IndexedPack pack;
Modrinth::loadIndexedPack(pack, entry_obj);
+ } catch (Json::JsonException& e) {
+ qDebug() << e.cause();
+ qDebug() << doc;
- auto hash = addonIds.find(pack.addonId.toString()).value();
+ // Skip this entry, since it has problems
+ continue;
+ }
- auto mod_iter = m_mods.find(hash);
- if (mod_iter == m_mods.end()) {
- qWarning() << "Invalid project id from the API response.";
- continue;
- }
+ auto hash = addonIds.find(pack.addonId.toString()).value();
- auto* mod = mod_iter.value();
+ auto mod_iter = m_mods.find(hash);
+ if (mod_iter == m_mods.end()) {
+ qWarning() << "Invalid project id from the API response.";
+ continue;
+ }
- try {
- setStatus(tr("Parsing API response from Modrinth for '%1'...").arg(mod->name()));
+ auto* mod = mod_iter.value();
- modrinthCallback(pack, m_temp_versions.find(hash).value(), mod);
- } catch (Json::JsonException& e) {
- qDebug() << e.cause();
- qDebug() << entries;
+ try {
+ setStatus(tr("Parsing API response from Modrinth for '%1'...").arg(mod->name()));
- emitFail(mod);
- }
+ modrinthCallback(pack, m_temp_versions.find(hash).value(), mod);
+ } catch (Json::JsonException& e) {
+ qDebug() << e.cause();
+ qDebug() << entries;
+
+ emitFail(mod);
}
- } catch (Json::JsonException& e) {
- qDebug() << e.cause();
- qDebug() << doc;
}
});
@@ -335,7 +343,7 @@ NetJob::Ptr EnsureMetadataTask::modrinthProjectsTask()
}
// Flame
-NetJob::Ptr EnsureMetadataTask::flameVersionsTask()
+Task::Ptr EnsureMetadataTask::flameVersionsTask()
{
auto* response = new QByteArray();
@@ -400,7 +408,7 @@ NetJob::Ptr EnsureMetadataTask::flameVersionsTask()
return ver_task;
}
-NetJob::Ptr EnsureMetadataTask::flameProjectsTask()
+Task::Ptr EnsureMetadataTask::flameProjectsTask()
{
QHash<QString, QString> addonIds;
for (auto const& hash : m_mods.keys()) {
@@ -414,7 +422,7 @@ NetJob::Ptr EnsureMetadataTask::flameProjectsTask()
}
auto response = new QByteArray();
- NetJob::Ptr proj_task;
+ Task::Ptr proj_task;
if (addonIds.isEmpty()) {
qWarning() << "No addonId found!";
@@ -428,7 +436,7 @@ NetJob::Ptr EnsureMetadataTask::flameProjectsTask()
if (!proj_task)
return {};
- connect(proj_task.get(), &NetJob::succeeded, this, [this, response, addonIds] {
+ connect(proj_task.get(), &Task::succeeded, this, [this, response, addonIds] {
QJsonParseError parse_error{};
auto doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
diff --git a/launcher/modplatform/EnsureMetadataTask.h b/launcher/modplatform/EnsureMetadataTask.h
index a8b0851e..635f4a2b 100644
--- a/launcher/modplatform/EnsureMetadataTask.h
+++ b/launcher/modplatform/EnsureMetadataTask.h
@@ -14,8 +14,8 @@ class EnsureMetadataTask : public Task {
Q_OBJECT
public:
- EnsureMetadataTask(Mod*, QDir, ModPlatform::Provider = ModPlatform::Provider::MODRINTH);
- EnsureMetadataTask(QList<Mod*>&, QDir, ModPlatform::Provider = ModPlatform::Provider::MODRINTH);
+ EnsureMetadataTask(Mod*, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH);
+ EnsureMetadataTask(QList<Mod*>&, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH);
~EnsureMetadataTask() = default;
@@ -28,11 +28,11 @@ class EnsureMetadataTask : public Task {
private:
// FIXME: Move to their own namespace
- auto modrinthVersionsTask() -> NetJob::Ptr;
- auto modrinthProjectsTask() -> NetJob::Ptr;
+ auto modrinthVersionsTask() -> Task::Ptr;
+ auto modrinthProjectsTask() -> Task::Ptr;
- auto flameVersionsTask() -> NetJob::Ptr;
- auto flameProjectsTask() -> NetJob::Ptr;
+ auto flameVersionsTask() -> Task::Ptr;
+ auto flameProjectsTask() -> Task::Ptr;
// Helpers
enum class RemoveFromList {
@@ -57,9 +57,9 @@ class EnsureMetadataTask : public Task {
private:
QHash<QString, Mod*> m_mods;
QDir m_index_dir;
- ModPlatform::Provider m_provider;
+ ModPlatform::ResourceProvider m_provider;
QHash<QString, ModPlatform::IndexedVersion> m_temp_versions;
ConcurrentTask* m_hashing_task;
- NetJob* m_current_task;
+ Task::Ptr m_current_task;
};
diff --git a/launcher/modplatform/ModAPI.h b/launcher/modplatform/ModAPI.h
deleted file mode 100644
index 703de143..00000000
--- a/launcher/modplatform/ModAPI.h
+++ /dev/null
@@ -1,118 +0,0 @@
-// SPDX-License-Identifier: GPL-3.0-only
-/*
- * PolyMC - Minecraft Launcher
- * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- *
- * This file incorporates work covered by the following copyright and
- * permission notice:
- *
- * Copyright 2013-2021 MultiMC Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#pragma once
-
-#include <QString>
-#include <QList>
-#include <list>
-
-#include "../Version.h"
-#include "net/NetJob.h"
-
-namespace ModPlatform {
-class ListModel;
-struct IndexedPack;
-}
-
-class ModAPI {
- protected:
- using CallerType = ModPlatform::ListModel;
-
- public:
- virtual ~ModAPI() = default;
-
- enum ModLoaderType {
- Unspecified = 0,
- Forge = 1 << 0,
- Cauldron = 1 << 1,
- LiteLoader = 1 << 2,
- Fabric = 1 << 3,
- Quilt = 1 << 4
- };
- Q_DECLARE_FLAGS(ModLoaderTypes, ModLoaderType)
-
- struct SearchArgs {
- int offset;
- QString search;
- QString sorting;
- ModLoaderTypes loaders;
- std::list<Version> versions;
- };
-
- virtual void searchMods(CallerType* caller, SearchArgs&& args) const = 0;
- virtual void getModInfo(ModPlatform::IndexedPack& pack, std::function<void(QJsonDocument&, ModPlatform::IndexedPack&)> callback) = 0;
-
- virtual auto getProject(QString addonId, QByteArray* response) const -> NetJob* = 0;
- virtual auto getProjects(QStringList addonIds, QByteArray* response) const -> NetJob* = 0;
-
-
- struct VersionSearchArgs {
- QString addonId;
- std::list<Version> mcVersions;
- ModLoaderTypes loaders;
- };
-
- virtual void getVersions(VersionSearchArgs&& args, std::function<void(QJsonDocument&, QString)> callback) const = 0;
-
- static auto getModLoaderString(ModLoaderType type) -> const QString {
- switch (type) {
- case Unspecified:
- break;
- case Forge:
- return "forge";
- case Cauldron:
- return "cauldron";
- case LiteLoader:
- return "liteloader";
- case Fabric:
- return "fabric";
- case Quilt:
- return "quilt";
- }
- return "";
- }
-
- protected:
- inline auto getGameVersionsString(std::list<Version> mcVersions) const -> QString
- {
- QString s;
- for(auto& ver : mcVersions){
- s += QString("\"%1\",").arg(ver.toString());
- }
- s.remove(s.length() - 1, 1); //remove last comma
- return s;
- }
-};
diff --git a/launcher/modplatform/ModIndex.cpp b/launcher/modplatform/ModIndex.cpp
index 34fd9f30..6a507caf 100644
--- a/launcher/modplatform/ModIndex.cpp
+++ b/launcher/modplatform/ModIndex.cpp
@@ -24,47 +24,47 @@
namespace ModPlatform {
-auto ProviderCapabilities::name(Provider p) -> const char*
+auto ProviderCapabilities::name(ResourceProvider p) -> const char*
{
switch (p) {
- case Provider::MODRINTH:
+ case ResourceProvider::MODRINTH:
return "modrinth";
- case Provider::FLAME:
+ case ResourceProvider::FLAME:
return "curseforge";
}
return {};
}
-auto ProviderCapabilities::readableName(Provider p) -> QString
+auto ProviderCapabilities::readableName(ResourceProvider p) -> QString
{
switch (p) {
- case Provider::MODRINTH:
+ case ResourceProvider::MODRINTH:
return "Modrinth";
- case Provider::FLAME:
+ case ResourceProvider::FLAME:
return "CurseForge";
}
return {};
}
-auto ProviderCapabilities::hashType(Provider p) -> QStringList
+auto ProviderCapabilities::hashType(ResourceProvider p) -> QStringList
{
switch (p) {
- case Provider::MODRINTH:
+ case ResourceProvider::MODRINTH:
return { "sha512", "sha1" };
- case Provider::FLAME:
+ case ResourceProvider::FLAME:
// Try newer formats first, fall back to old format
return { "sha1", "md5", "murmur2" };
}
return {};
}
-auto ProviderCapabilities::hash(Provider p, QIODevice* device, QString type) -> QString
+auto ProviderCapabilities::hash(ResourceProvider p, QIODevice* device, QString type) -> QString
{
QCryptographicHash::Algorithm algo = QCryptographicHash::Sha1;
switch (p) {
- case Provider::MODRINTH: {
+ case ResourceProvider::MODRINTH: {
algo = (type == "sha1") ? QCryptographicHash::Sha1 : QCryptographicHash::Sha512;
break;
}
- case Provider::FLAME:
+ case ResourceProvider::FLAME:
algo = (type == "sha1") ? QCryptographicHash::Sha1 : QCryptographicHash::Md5;
break;
}
diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h
index 518fed7c..b1f8050d 100644
--- a/launcher/modplatform/ModIndex.h
+++ b/launcher/modplatform/ModIndex.h
@@ -28,17 +28,16 @@ class QIODevice;
namespace ModPlatform {
-enum class Provider {
- MODRINTH,
- FLAME
-};
+enum class ResourceProvider { MODRINTH, FLAME };
+
+enum class ResourceType { MOD, RESOURCE_PACK };
class ProviderCapabilities {
public:
- auto name(Provider) -> const char*;
- auto readableName(Provider) -> QString;
- auto hashType(Provider) -> QStringList;
- auto hash(Provider, QIODevice*, QString type = "") -> QString;
+ auto name(ResourceProvider) -> const char*;
+ auto readableName(ResourceProvider) -> QString;
+ auto hashType(ResourceProvider) -> QStringList;
+ auto hash(ResourceProvider, QIODevice*, QString type = "") -> QString;
};
struct ModpackAuthor {
@@ -66,6 +65,10 @@ struct IndexedVersion {
QString hash;
bool is_preferred = true;
QString changelog;
+
+ // For internal use, not provided by APIs
+ bool is_currently_selected = false;
+ QString custom_target_folder;
};
struct ExtraPackData {
@@ -81,7 +84,7 @@ struct ExtraPackData {
struct IndexedPack {
QVariant addonId;
- Provider provider;
+ ResourceProvider provider;
QString name;
QString slug;
QString description;
@@ -96,9 +99,26 @@ struct IndexedPack {
// Don't load by default, since some modplatform don't have that info
bool extraDataLoaded = true;
ExtraPackData extraData;
+
+ // For internal use, not provided by APIs
+ [[nodiscard]] bool isVersionSelected(size_t index) const
+ {
+ if (!versionsLoaded)
+ return false;
+
+ return versions.at(index).is_currently_selected;
+ }
+ [[nodiscard]] bool isAnyVersionSelected() const
+ {
+ if (!versionsLoaded)
+ return false;
+
+ return std::any_of(versions.constBegin(), versions.constEnd(),
+ [](auto const& v) { return v.is_currently_selected; });
+ }
};
} // namespace ModPlatform
Q_DECLARE_METATYPE(ModPlatform::IndexedPack)
-Q_DECLARE_METATYPE(ModPlatform::Provider)
+Q_DECLARE_METATYPE(ModPlatform::ResourceProvider)
diff --git a/launcher/modplatform/ResourceAPI.h b/launcher/modplatform/ResourceAPI.h
new file mode 100644
index 00000000..34f33779
--- /dev/null
+++ b/launcher/modplatform/ResourceAPI.h
@@ -0,0 +1,177 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
+/*
+ * PolyMC - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QDebug>
+#include <QList>
+#include <QString>
+
+#include <list>
+#include <optional>
+
+#include "../Version.h"
+
+#include "modplatform/ModIndex.h"
+#include "tasks/Task.h"
+
+/* Simple class with a common interface for interacting with APIs */
+class ResourceAPI {
+ public:
+ virtual ~ResourceAPI() = default;
+
+ enum ModLoaderType { Forge = 1 << 0, Cauldron = 1 << 1, LiteLoader = 1 << 2, Fabric = 1 << 3, Quilt = 1 << 4 };
+ Q_DECLARE_FLAGS(ModLoaderTypes, ModLoaderType)
+
+ struct SortingMethod {
+ // The index of the sorting method. Used to allow for arbitrary ordering in the list of methods.
+ // Used by Flame in the API request.
+ unsigned int index;
+ // The real name of the sorting, as used in the respective API specification.
+ // Used by Modrinth in the API request.
+ QString name;
+ // The human-readable name of the sorting, used for display in the UI.
+ QString readable_name;
+ };
+
+ struct SearchArgs {
+ ModPlatform::ResourceType type{};
+ int offset = 0;
+
+ std::optional<QString> search;
+ std::optional<SortingMethod> sorting;
+ std::optional<ModLoaderTypes> loaders;
+ std::optional<std::list<Version> > versions;
+ };
+ struct SearchCallbacks {
+ std::function<void(QJsonDocument&)> on_succeed;
+ std::function<void(QString const& reason, int network_error_code)> on_fail;
+ std::function<void()> on_abort;
+ };
+
+ struct VersionSearchArgs {
+ ModPlatform::IndexedPack pack;
+
+ std::optional<std::list<Version> > mcVersions;
+ std::optional<ModLoaderTypes> loaders;
+
+ VersionSearchArgs(VersionSearchArgs const&) = default;
+ void operator=(VersionSearchArgs other)
+ {
+ pack = other.pack;
+ mcVersions = other.mcVersions;
+ loaders = other.loaders;
+ }
+ };
+ struct VersionSearchCallbacks {
+ std::function<void(QJsonDocument&, ModPlatform::IndexedPack)> on_succeed;
+ };
+
+ struct ProjectInfoArgs {
+ ModPlatform::IndexedPack pack;
+
+ ProjectInfoArgs(ProjectInfoArgs const&) = default;
+ void operator=(ProjectInfoArgs other) { pack = other.pack; }
+ };
+ struct ProjectInfoCallbacks {
+ std::function<void(QJsonDocument&, ModPlatform::IndexedPack)> on_succeed;
+ };
+
+ public:
+ /** Gets a list of available sorting methods for this API. */
+ [[nodiscard]] virtual auto getSortingMethods() const -> QList<SortingMethod> = 0;
+
+ public slots:
+ [[nodiscard]] virtual Task::Ptr searchProjects(SearchArgs&&, SearchCallbacks&&) const
+ {
+ qWarning() << "TODO";
+ return nullptr;
+ }
+ [[nodiscard]] virtual Task::Ptr getProject(QString addonId, QByteArray* response) const
+ {
+ qWarning() << "TODO";
+ return nullptr;
+ }
+ [[nodiscard]] virtual Task::Ptr getProjects(QStringList addonIds, QByteArray* response) const
+ {
+ qWarning() << "TODO";
+ return nullptr;
+ }
+
+ [[nodiscard]] virtual Task::Ptr getProjectInfo(ProjectInfoArgs&&, ProjectInfoCallbacks&&) const
+ {
+ qWarning() << "TODO";
+ return nullptr;
+ }
+ [[nodiscard]] virtual Task::Ptr getProjectVersions(VersionSearchArgs&&, VersionSearchCallbacks&&) const
+ {
+ qWarning() << "TODO";
+ return nullptr;
+ }
+
+ static auto getModLoaderString(ModLoaderType type) -> const QString
+ {
+ switch (type) {
+ case Forge:
+ return "forge";
+ case Cauldron:
+ return "cauldron";
+ case LiteLoader:
+ return "liteloader";
+ case Fabric:
+ return "fabric";
+ case Quilt:
+ return "quilt";
+ default:
+ break;
+ }
+ return "";
+ }
+
+ protected:
+ [[nodiscard]] inline QString debugName() const { return "External resource API"; }
+
+ [[nodiscard]] inline auto getGameVersionsString(std::list<Version> mcVersions) const -> QString
+ {
+ QString s;
+ for (auto& ver : mcVersions) {
+ s += QString("\"%1\",").arg(ver.toString());
+ }
+ s.remove(s.length() - 1, 1); // remove last comma
+ return s;
+ }
+};
diff --git a/launcher/modplatform/flame/FlameAPI.cpp b/launcher/modplatform/flame/FlameAPI.cpp
index 4d71da21..4b926ec3 100644
--- a/launcher/modplatform/flame/FlameAPI.cpp
+++ b/launcher/modplatform/flame/FlameAPI.cpp
@@ -1,13 +1,17 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
#include "FlameAPI.h"
#include "FlameModIndex.h"
#include "Application.h"
#include "BuildConfig.h"
#include "Json.h"
-
+#include "net/NetJob.h"
#include "net/Upload.h"
-auto FlameAPI::matchFingerprints(const QList<uint>& fingerprints, QByteArray* response) -> NetJob::Ptr
+Task::Ptr FlameAPI::matchFingerprints(const QList<uint>& fingerprints, QByteArray* response)
{
auto* netJob = new NetJob(QString("Flame::MatchFingerprints"), APPLICATION->network());
@@ -106,13 +110,19 @@ auto FlameAPI::getModDescription(int modId) -> QString
auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion
{
+ auto versions_url_optional = getVersionsURL(args);
+ if (!versions_url_optional.has_value())
+ return {};
+
+ auto versions_url = versions_url_optional.value();
+
QEventLoop loop;
- auto netJob = new NetJob(QString("Flame::GetLatestVersion(%1)").arg(args.addonId), APPLICATION->network());
+ auto netJob = new NetJob(QString("Flame::GetLatestVersion(%1)").arg(args.pack.name), APPLICATION->network());
auto response = new QByteArray();
ModPlatform::IndexedVersion ver;
- netJob->addNetAction(Net::Download::makeByteArray(getVersionsURL(args), response));
+ netJob->addNetAction(Net::Download::makeByteArray(versions_url, response));
QObject::connect(netJob, &NetJob::succeeded, [response, args, &ver] {
QJsonParseError parse_error{};
@@ -161,7 +171,7 @@ auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::Indexe
return ver;
}
-auto FlameAPI::getProjects(QStringList addonIds, QByteArray* response) const -> NetJob*
+Task::Ptr FlameAPI::getProjects(QStringList addonIds, QByteArray* response) const
{
auto* netJob = new NetJob(QString("Flame::GetProjects"), APPLICATION->network());
@@ -178,13 +188,13 @@ auto FlameAPI::getProjects(QStringList addonIds, QByteArray* response) const ->
netJob->addNetAction(Net::Upload::makeByteArray(QString("https://api.curseforge.com/v1/mods"), response, body_raw));
- QObject::connect(netJob, &NetJob::finished, [response, netJob] { delete response; netJob->deleteLater(); });
+ QObject::connect(netJob, &NetJob::finished, [response] { delete response; });
QObject::connect(netJob, &NetJob::failed, [body_raw] { qDebug() << body_raw; });
return netJob;
}
-auto FlameAPI::getFiles(const QStringList& fileIds, QByteArray* response) const -> NetJob*
+Task::Ptr FlameAPI::getFiles(const QStringList& fileIds, QByteArray* response) const
{
auto* netJob = new NetJob(QString("Flame::GetFiles"), APPLICATION->network());
@@ -201,8 +211,23 @@ auto FlameAPI::getFiles(const QStringList& fileIds, QByteArray* response) const
netJob->addNetAction(Net::Upload::makeByteArray(QString("https://api.curseforge.com/v1/mods/files"), response, body_raw));
- QObject::connect(netJob, &NetJob::finished, [response, netJob] { delete response; netJob->deleteLater(); });
+ QObject::connect(netJob, &NetJob::finished, [response] { delete response; });
QObject::connect(netJob, &NetJob::failed, [body_raw] { qDebug() << body_raw; });
return netJob;
}
+
+// https://docs.curseforge.com/?python#tocS_ModsSearchSortField
+static QList<ResourceAPI::SortingMethod> s_sorts = { { 1, "Featured", QObject::tr("Sort by Featured") },
+ { 2, "Popularity", QObject::tr("Sort by Popularity") },
+ { 3, "LastUpdated", QObject::tr("Sort by Last Updated") },
+ { 4, "Name", QObject::tr("Sort by Name") },
+ { 5, "Author", QObject::tr("Sort by Author") },
+ { 6, "TotalDownloads", QObject::tr("Sort by Downloads") },
+ { 7, "Category", QObject::tr("Sort by Category") },
+ { 8, "GameVersion", QObject::tr("Sort by Game Version") } };
+
+QList<ResourceAPI::SortingMethod> FlameAPI::getSortingMethods() const
+{
+ return s_sorts;
+}
diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h
index 4c6ca64c..06d749e6 100644
--- a/launcher/modplatform/flame/FlameAPI.h
+++ b/launcher/modplatform/flame/FlameAPI.h
@@ -1,84 +1,84 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
#pragma once
#include "modplatform/ModIndex.h"
-#include "modplatform/helpers/NetworkModAPI.h"
+#include "modplatform/helpers/NetworkResourceAPI.h"
-class FlameAPI : public NetworkModAPI {
+class FlameAPI : public NetworkResourceAPI {
public:
- auto matchFingerprints(const QList<uint>& fingerprints, QByteArray* response) -> NetJob::Ptr;
auto getModFileChangelog(int modId, int fileId) -> QString;
auto getModDescription(int modId) -> QString;
auto getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion;
- auto getProjects(QStringList addonIds, QByteArray* response) const -> NetJob* override;
- auto getFiles(const QStringList& fileIds, QByteArray* response) const -> NetJob*;
+ Task::Ptr getProjects(QStringList addonIds, QByteArray* response) const override;
+ Task::Ptr matchFingerprints(const QList<uint>& fingerprints, QByteArray* response);
+ Task::Ptr getFiles(const QStringList& fileIds, QByteArray* response) const;
+
+ [[nodiscard]] auto getSortingMethods() const -> QList<ResourceAPI::SortingMethod> override;
private:
- inline auto getSortFieldInt(QString sortString) const -> int
+ static int getClassId(ModPlatform::ResourceType type)
{
- return sortString == "Featured" ? 1
- : sortString == "Popularity" ? 2
- : sortString == "LastUpdated" ? 3
- : sortString == "Name" ? 4
- : sortString == "Author" ? 5
- : sortString == "TotalDownloads" ? 6
- : sortString == "Category" ? 7
- : sortString == "GameVersion" ? 8
- : 1;
+ switch (type) {
+ default:
+ case ModPlatform::ResourceType::MOD:
+ return 6;
+ }
+ }
+
+ static int getMappedModLoader(ModLoaderTypes loaders)
+ {
+ // https://docs.curseforge.com/?http#tocS_ModLoaderType
+ if (loaders & Forge)
+ return 1;
+ if (loaders & Fabric)
+ return 4;
+ // TODO: remove this once Quilt drops official Fabric support
+ if (loaders & Quilt) // NOTE: Most if not all Fabric mods should work *currently*
+ return 4; // Quilt would probably be 5
+ return 0;
}
private:
- inline auto getModSearchURL(SearchArgs& args) const -> QString override
+ [[nodiscard]] std::optional<QString> getSearchURL(SearchArgs const& args) const override
{
- auto gameVersionStr = args.versions.size() != 0 ? QString("gameVersion=%1").arg(args.versions.front().toString()) : QString();
+ auto gameVersionStr = args.versions.has_value() ? QString("gameVersion=%1").arg(args.versions.value().front().toString()) : QString();
- return QString(
- "https://api.curseforge.com/v1/mods/search?"
- "gameId=432&"
- "classId=6&"
+ QStringList get_arguments;
+ get_arguments.append(QString("classId=%1").arg(getClassId(args.type)));
+ get_arguments.append(QString("index=%1").arg(args.offset));
+ get_arguments.append("pageSize=25");
+ if (args.search.has_value())
+ get_arguments.append(QString("searchFilter=%1").arg(args.search.value()));
+ if (args.sorting.has_value())
+ get_arguments.append(QString("sortField=%1").arg(args.sorting.value().index));
+ get_arguments.append("sortOrder=desc");
+ if (args.loaders.has_value())
+ get_arguments.append(QString("modLoaderType=%1").arg(getMappedModLoader(args.loaders.value())));
+ get_arguments.append(gameVersionStr);
- "index=%1&"
- "pageSize=25&"
- "searchFilter=%2&"
- "sortField=%3&"
- "sortOrder=desc&"
- "modLoaderType=%4&"
- "%5")
- .arg(args.offset)
- .arg(args.search)
- .arg(getSortFieldInt(args.sorting))
- .arg(getMappedModLoader(args.loaders))
- .arg(gameVersionStr);
+ return "https://api.curseforge.com/v1/mods/search?gameId=432&" + get_arguments.join('&');
};
- inline auto getModInfoURL(QString& id) const -> QString override
+ [[nodiscard]] std::optional<QString> getInfoURL(QString const& id) const override
{
return QString("https://api.curseforge.com/v1/mods/%1").arg(id);
};
- inline auto getVersionsURL(VersionSearchArgs& args) const -> QString override
+ [[nodiscard]] std::optional<QString> getVersionsURL(VersionSearchArgs const& args) const override
{
- QString gameVersionQuery = args.mcVersions.size() == 1 ? QString("gameVersion=%1&").arg(args.mcVersions.front().toString()) : "";
- QString modLoaderQuery = QString("modLoaderType=%1&").arg(getMappedModLoader(args.loaders));
+ QString url{QString("https://api.curseforge.com/v1/mods/%1/files?pageSize=10000&").arg(args.pack.addonId.toString())};
- return QString("https://api.curseforge.com/v1/mods/%1/files?pageSize=10000&%2%3")
- .arg(args.addonId)
- .arg(gameVersionQuery)
- .arg(modLoaderQuery);
- };
+ QStringList get_parameters;
+ if (args.mcVersions.has_value())
+ get_parameters.append(QString("gameVersion=%1").arg(args.mcVersions.value().front().toString()));
+ if (args.loaders.has_value())
+ get_parameters.append(QString("modLoaderType=%1").arg(getMappedModLoader(args.loaders.value())));
- public:
- static auto getMappedModLoader(const ModLoaderTypes loaders) -> int
- {
- // https://docs.curseforge.com/?http#tocS_ModLoaderType
- if (loaders & Forge)
- return 1;
- if (loaders & Fabric)
- return 4;
- // TODO: remove this once Quilt drops official Fabric support
- if (loaders & Quilt) // NOTE: Most if not all Fabric mods should work *currently*
- return 4; // Quilt would probably be 5
- return 0;
- }
+ return url + get_parameters.join('&');
+ };
};
diff --git a/launcher/modplatform/flame/FlameCheckUpdate.cpp b/launcher/modplatform/flame/FlameCheckUpdate.cpp
index 8dd3a846..7aee4f4c 100644
--- a/launcher/modplatform/flame/FlameCheckUpdate.cpp
+++ b/launcher/modplatform/flame/FlameCheckUpdate.cpp
@@ -7,7 +7,10 @@
#include "FileSystem.h"
#include "Json.h"
-#include "ModDownloadTask.h"
+#include "ResourceDownloadTask.h"
+
+#include "minecraft/mod/ModFolderModel.h"
+#include "minecraft/mod/ResourceFolderModel.h"
static FlameAPI api;
@@ -126,7 +129,8 @@ void FlameCheckUpdate::executeTask()
setStatus(tr("Getting API response from CurseForge for '%1'...").arg(mod->name()));
setProgress(i++, m_mods.size());
- auto latest_ver = api.getLatestVersion({ mod->metadata()->project_id.toString(), m_game_versions, m_loaders });
+ ModPlatform::IndexedPack pack{ mod->metadata()->project_id.toString() };
+ auto latest_ver = api.getLatestVersion({ pack, m_game_versions, m_loaders });
// Check if we were aborted while getting the latest version
if (m_was_aborted) {
@@ -160,7 +164,7 @@ void FlameCheckUpdate::executeTask()
for (auto& author : mod->authors())
pack.authors.append({ author });
pack.description = mod->description();
- pack.provider = ModPlatform::Provider::FLAME;
+ pack.provider = ModPlatform::ResourceProvider::FLAME;
auto old_version = mod->version();
if (old_version.isEmpty() && mod->status() != ModStatus::NotInstalled) {
@@ -168,10 +172,10 @@ void FlameCheckUpdate::executeTask()
old_version = current_ver.version;
}
- auto download_task = new ModDownloadTask(pack, latest_ver, m_mods_folder);
+ auto download_task = new ResourceDownloadTask(pack, latest_ver, m_mods_folder);
m_updatable.emplace_back(pack.name, mod->metadata()->hash, old_version, latest_ver.version,
api.getModFileChangelog(latest_ver.addonId.toInt(), latest_ver.fileId.toInt()),
- ModPlatform::Provider::FLAME, download_task);
+ ModPlatform::ResourceProvider::FLAME, download_task);
}
}
diff --git a/launcher/modplatform/flame/FlameCheckUpdate.h b/launcher/modplatform/flame/FlameCheckUpdate.h
index 163c706c..4a98d684 100644
--- a/launcher/modplatform/flame/FlameCheckUpdate.h
+++ b/launcher/modplatform/flame/FlameCheckUpdate.h
@@ -8,7 +8,7 @@ class FlameCheckUpdate : public CheckUpdateTask {
Q_OBJECT
public:
- FlameCheckUpdate(QList<Mod*>& mods, std::list<Version>& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr<ModFolderModel> mods_folder)
+ FlameCheckUpdate(QList<Mod*>& mods, std::list<Version>& mcVersions, std::optional<ResourceAPI::ModLoaderTypes> loaders, std::shared_ptr<ModFolderModel> mods_folder)
: CheckUpdateTask(mods, mcVersions, loaders, mods_folder)
{}
diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp
index 729268d7..890bff48 100644
--- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp
+++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp
@@ -53,6 +53,13 @@
#include "ui/dialogs/BlockedModsDialog.h"
#include "ui/dialogs/CustomMessageBox.h"
+#include <QDebug>
+#include <QFileInfo>
+
+#include "minecraft/World.h"
+#include "minecraft/mod/tasks/LocalResourceParse.h"
+
+
const static QMap<QString, QString> forgemap = { { "1.2.5", "3.4.9.171" },
{ "1.4.2", "6.0.1.355" },
{ "1.4.7", "6.6.2.534" },
@@ -176,7 +183,7 @@ bool FlameCreationTask::updateInstance()
QEventLoop loop;
- connect(job, &NetJob::succeeded, this, [this, raw_response, fileIds, old_inst_dir, &old_files, old_minecraft_dir] {
+ connect(job.get(), &Task::succeeded, this, [this, raw_response, fileIds, old_inst_dir, &old_files, old_minecraft_dir] {
// Parse the API response
QJsonParseError parse_error{};
auto doc = QJsonDocument::fromJson(*raw_response, &parse_error);
@@ -218,7 +225,7 @@ bool FlameCreationTask::updateInstance()
m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(relative_path));
}
});
- connect(job, &NetJob::finished, &loop, &QEventLoop::quit);
+ connect(job.get(), &Task::finished, &loop, &QEventLoop::quit);
m_process_update_file_info_job = job;
job->start();
@@ -361,7 +368,9 @@ bool FlameCreationTask::createInstance()
FS::deletePath(jarmodsPath);
}
- instance.setManagedPack("flame", m_managed_id, m_pack.name, m_managed_version_id, m_pack.version);
+ // Don't add managed info to packs without an ID (most likely imported from ZIP)
+ if (!m_managed_id.isEmpty())
+ instance.setManagedPack("flame", m_managed_id, m_pack.name, m_managed_version_id, m_pack.version);
instance.setName(name());
m_mod_id_resolver = new Flame::FileResolvingTask(APPLICATION->network(), m_pack);
@@ -399,6 +408,10 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop)
QList<BlockedMod> blocked_mods;
auto anyBlocked = false;
for (const auto& result : results.files.values()) {
+ if (result.fileName.endsWith(".zip")) {
+ m_ZIP_resources.append(std::make_pair(result.fileName, result.targetFolder));
+ }
+
if (!result.resolved || result.url.isEmpty()) {
BlockedMod blocked_mod;
blocked_mod.name = result.fileName;
@@ -437,38 +450,6 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop)
}
}
-/// @brief copy the matched blocked mods to the instance staging area
-/// @param blocked_mods list of the blocked mods and their matched paths
-void FlameCreationTask::copyBlockedMods(QList<BlockedMod> const& blocked_mods)
-{
- setStatus(tr("Copying Blocked Mods..."));
- setAbortable(false);
- int i = 0;
- int total = blocked_mods.length();
- setProgress(i, total);
- for (auto const& mod : blocked_mods) {
- if (!mod.matched) {
- qDebug() << mod.name << "was not matched to a local file, skipping copy";
- continue;
- }
-
- auto dest_path = FS::PathCombine(m_stagingPath, "minecraft", mod.targetFolder, mod.name);
-
- setStatus(tr("Copying Blocked Mods (%1 out of %2 are done)").arg(QString::number(i), QString::number(total)));
-
- qDebug() << "Will try to copy" << mod.localPath << "to" << dest_path;
-
- if (!FS::copy(mod.localPath, dest_path)()) {
- qDebug() << "Copy of" << mod.localPath << "to" << dest_path << "Failed";
- }
-
- i++;
- setProgress(i, total);
- }
-
- setAbortable(true);
-}
-
void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
{
m_files_job = new NetJob(tr("Mod download"), APPLICATION->network());
@@ -507,7 +488,10 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
}
m_mod_id_resolver.reset();
- connect(m_files_job.get(), &NetJob::succeeded, this, [&]() { m_files_job.reset(); });
+ connect(m_files_job.get(), &NetJob::succeeded, this, [&]() {
+ m_files_job.reset();
+ validateZIPResouces();
+ });
connect(m_files_job.get(), &NetJob::failed, [&](QString reason) {
m_files_job.reset();
setError(reason);
@@ -518,3 +502,103 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
setStatus(tr("Downloading mods..."));
m_files_job->start();
}
+
+/// @brief copy the matched blocked mods to the instance staging area
+/// @param blocked_mods list of the blocked mods and their matched paths
+void FlameCreationTask::copyBlockedMods(QList<BlockedMod> const& blocked_mods)
+{
+ setStatus(tr("Copying Blocked Mods..."));
+ setAbortable(false);
+ int i = 0;
+ int total = blocked_mods.length();
+ setProgress(i, total);
+ for (auto const& mod : blocked_mods) {
+ if (!mod.matched) {
+ qDebug() << mod.name << "was not matched to a local file, skipping copy";
+ continue;
+ }
+
+ auto destPath = FS::PathCombine(m_stagingPath, "minecraft", mod.targetFolder, mod.name);
+
+ setStatus(tr("Copying Blocked Mods (%1 out of %2 are done)").arg(QString::number(i), QString::number(total)));
+
+ qDebug() << "Will try to copy" << mod.localPath << "to" << destPath;
+
+ if (!FS::copy(mod.localPath, destPath)()) {
+ qDebug() << "Copy of" << mod.localPath << "to" << destPath << "Failed";
+ }
+
+ i++;
+ setProgress(i, total);
+ }
+
+ setAbortable(true);
+}
+
+
+void FlameCreationTask::validateZIPResouces()
+{
+ qDebug() << "Validating whether resources stored as .zip are in the right place";
+ for (auto [fileName, targetFolder] : m_ZIP_resources) {
+ qDebug() << "Checking" << fileName << "...";
+ auto localPath = FS::PathCombine(m_stagingPath, "minecraft", targetFolder, fileName);
+
+ /// @brief check the target and move the the file
+ /// @return path where file can now be found
+ auto validatePath = [&localPath, this](QString fileName, QString targetFolder, QString realTarget) {
+ if (targetFolder != realTarget) {
+ qDebug() << "Target folder of" << fileName << "is incorrect, it belongs in" << realTarget;
+ auto destPath = FS::PathCombine(m_stagingPath, "minecraft", realTarget, fileName);
+ qDebug() << "Moving" << localPath << "to" << destPath;
+ if (FS::move(localPath, destPath)) {
+ return destPath;
+ }
+ }
+ return localPath;
+ };
+
+ auto installWorld = [this](QString worldPath){
+ qDebug() << "Installing World from" << worldPath;
+ QFileInfo worldFileInfo(worldPath);
+ World w(worldFileInfo);
+ if (!w.isValid()) {
+ qDebug() << "World at" << worldPath << "is not valid, skipping install.";
+ } else {
+ w.install(FS::PathCombine(m_stagingPath, "minecraft", "saves"));
+ }
+ };
+
+ QFileInfo localFileInfo(localPath);
+ auto type = ResourceUtils::identify(localFileInfo);
+
+ QString worldPath;
+
+ switch (type) {
+ case PackedResourceType::ResourcePack :
+ validatePath(fileName, targetFolder, "resourcepacks");
+ break;
+ case PackedResourceType::TexturePack :
+ validatePath(fileName, targetFolder, "texturepacks");
+ break;
+ case PackedResourceType::DataPack :
+ validatePath(fileName, targetFolder, "datapacks");
+ break;
+ case PackedResourceType::Mod :
+ validatePath(fileName, targetFolder, "mods");
+ break;
+ case PackedResourceType::ShaderPack :
+ // in theroy flame API can't do this but who knows, that *may* change ?
+ // better to handle it if it *does* occure in the future
+ validatePath(fileName, targetFolder, "shaderpacks");
+ break;
+ case PackedResourceType::WorldSave :
+ worldPath = validatePath(fileName, targetFolder, "saves");
+ installWorld(worldPath);
+ break;
+ case PackedResourceType::UNKNOWN :
+ default :
+ qDebug() << "Can't Identify" << fileName << "at" << localPath << ", leaving it where it is.";
+ break;
+ }
+ }
+}
diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.h b/launcher/modplatform/flame/FlameInstanceCreationTask.h
index 3a1c729f..0ae4735b 100644
--- a/launcher/modplatform/flame/FlameInstanceCreationTask.h
+++ b/launcher/modplatform/flame/FlameInstanceCreationTask.h
@@ -77,6 +77,7 @@ class FlameCreationTask final : public InstanceCreationTask {
void idResolverSucceeded(QEventLoop&);
void setupDownloadJob(QEventLoop&);
void copyBlockedMods(QList<BlockedMod> const& blocked_mods);
+ void validateZIPResouces();
private:
QWidget* m_parent = nullptr;
@@ -85,10 +86,12 @@ class FlameCreationTask final : public InstanceCreationTask {
Flame::Manifest m_pack;
// Handle to allow aborting
- NetJob* m_process_update_file_info_job = nullptr;
+ Task::Ptr m_process_update_file_info_job = nullptr;
NetJob::Ptr m_files_job = nullptr;
QString m_managed_id, m_managed_version_id;
+ QList<std::pair<QString, QString>> m_ZIP_resources;
+
std::optional<InstancePtr> m_instance;
};
diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp
index 32aa4bdb..7498e830 100644
--- a/launcher/modplatform/flame/FlameModIndex.cpp
+++ b/launcher/modplatform/flame/FlameModIndex.cpp
@@ -11,7 +11,7 @@ static ModPlatform::ProviderCapabilities ProviderCaps;
void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
{
pack.addonId = Json::requireInteger(obj, "id");
- pack.provider = ModPlatform::Provider::FLAME;
+ pack.provider = ModPlatform::ResourceProvider::FLAME;
pack.name = Json::requireString(obj, "name");
pack.slug = Json::requireString(obj, "slug");
pack.websiteUrl = Json::ensureString(Json::ensureObject(obj, "links"), "websiteUrl", "");
@@ -76,10 +76,10 @@ static QString enumToString(int hash_algorithm)
void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
QJsonArray& arr,
const shared_qobject_ptr<QNetworkAccessManager>& network,
- BaseInstance* inst)
+ const BaseInstance* inst)
{
QVector<ModPlatform::IndexedVersion> unsortedVersions;
- auto profile = (dynamic_cast<MinecraftInstance*>(inst))->getPackProfile();
+ auto profile = (dynamic_cast<const MinecraftInstance*>(inst))->getPackProfile();
QString mcVersion = profile->getComponentVersion("net.minecraft");
for (auto versionIter : arr) {
@@ -127,7 +127,7 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) ->
auto hash_list = Json::ensureArray(obj, "hashes");
for (auto h : hash_list) {
auto hash_entry = Json::ensureObject(h);
- auto hash_types = ProviderCaps.hashType(ModPlatform::Provider::FLAME);
+ auto hash_types = ProviderCaps.hashType(ModPlatform::ResourceProvider::FLAME);
auto hash_algo = enumToString(Json::ensureInteger(hash_entry, "algo", 1, "algorithm"));
if (hash_types.contains(hash_algo)) {
file.hash = Json::requireString(hash_entry, "value");
diff --git a/launcher/modplatform/flame/FlameModIndex.h b/launcher/modplatform/flame/FlameModIndex.h
index db63cdbb..33c4a529 100644
--- a/launcher/modplatform/flame/FlameModIndex.h
+++ b/launcher/modplatform/flame/FlameModIndex.h
@@ -17,7 +17,7 @@ void loadBody(ModPlatform::IndexedPack& m, QJsonObject& obj);
void loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
QJsonArray& arr,
const shared_qobject_ptr<QNetworkAccessManager>& network,
- BaseInstance* inst);
+ const BaseInstance* inst);
auto loadIndexedPackVersion(QJsonObject& obj, bool load_changelog = false) -> ModPlatform::IndexedVersion;
} // namespace FlameMod
diff --git a/launcher/modplatform/helpers/HashUtils.cpp b/launcher/modplatform/helpers/HashUtils.cpp
index f1e4759e..af484be0 100644
--- a/launcher/modplatform/helpers/HashUtils.cpp
+++ b/launcher/modplatform/helpers/HashUtils.cpp
@@ -12,12 +12,12 @@ namespace Hashing {
static ModPlatform::ProviderCapabilities ProviderCaps;
-Hasher::Ptr createHasher(QString file_path, ModPlatform::Provider provider)
+Hasher::Ptr createHasher(QString file_path, ModPlatform::ResourceProvider provider)
{
switch (provider) {
- case ModPlatform::Provider::MODRINTH:
+ case ModPlatform::ResourceProvider::MODRINTH:
return createModrinthHasher(file_path);
- case ModPlatform::Provider::FLAME:
+ case ModPlatform::ResourceProvider::FLAME:
return createFlameHasher(file_path);
default:
qCritical() << "[Hashing]"
@@ -36,12 +36,12 @@ Hasher::Ptr createFlameHasher(QString file_path)
return new FlameHasher(file_path);
}
-Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::Provider provider)
+Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider)
{
return new BlockedModHasher(file_path, provider);
}
-Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::Provider provider, QString type)
+Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider, QString type)
{
auto hasher = new BlockedModHasher(file_path, provider);
hasher->useHashType(type);
@@ -62,8 +62,8 @@ void ModrinthHasher::executeTask()
return;
}
- auto hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first();
- m_hash = ProviderCaps.hash(ModPlatform::Provider::MODRINTH, &file, hash_type);
+ auto hash_type = ProviderCaps.hashType(ModPlatform::ResourceProvider::MODRINTH).first();
+ m_hash = ProviderCaps.hash(ModPlatform::ResourceProvider::MODRINTH, &file, hash_type);
file.close();
@@ -92,7 +92,7 @@ void FlameHasher::executeTask()
}
-BlockedModHasher::BlockedModHasher(QString file_path, ModPlatform::Provider provider)
+BlockedModHasher::BlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider)
: Hasher(file_path), provider(provider) {
setObjectName(QString("BlockedModHasher: %1").arg(file_path));
hash_type = ProviderCaps.hashType(provider).first();
diff --git a/launcher/modplatform/helpers/HashUtils.h b/launcher/modplatform/helpers/HashUtils.h
index fa3244f6..91146a52 100644
--- a/launcher/modplatform/helpers/HashUtils.h
+++ b/launcher/modplatform/helpers/HashUtils.h
@@ -42,21 +42,21 @@ class ModrinthHasher : public Hasher {
class BlockedModHasher : public Hasher {
public:
- BlockedModHasher(QString file_path, ModPlatform::Provider provider);
+ BlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider);
void executeTask() override;
QStringList getHashTypes();
bool useHashType(QString type);
private:
- ModPlatform::Provider provider;
+ ModPlatform::ResourceProvider provider;
QString hash_type;
};
-Hasher::Ptr createHasher(QString file_path, ModPlatform::Provider provider);
+Hasher::Ptr createHasher(QString file_path, ModPlatform::ResourceProvider provider);
Hasher::Ptr createFlameHasher(QString file_path);
Hasher::Ptr createModrinthHasher(QString file_path);
-Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::Provider provider);
-Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::Provider provider, QString type);
+Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider);
+Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider, QString type);
} // namespace Hashing
diff --git a/launcher/modplatform/helpers/NetworkModAPI.cpp b/launcher/modplatform/helpers/NetworkModAPI.cpp
deleted file mode 100644
index 7633030e..00000000
--- a/launcher/modplatform/helpers/NetworkModAPI.cpp
+++ /dev/null
@@ -1,97 +0,0 @@
-#include "NetworkModAPI.h"
-
-#include "ui/pages/modplatform/ModModel.h"
-
-#include "Application.h"
-#include "net/NetJob.h"
-
-void NetworkModAPI::searchMods(CallerType* caller, SearchArgs&& args) const
-{
- auto netJob = new NetJob(QString("%1::Search").arg(caller->debugName()), APPLICATION->network());
- auto searchUrl = getModSearchURL(args);
-
- auto response = new QByteArray();
- netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response));
-
- QObject::connect(netJob, &NetJob::started, caller, [caller, netJob] { caller->setActiveJob(netJob); });
- QObject::connect(netJob, &NetJob::failed, caller, &CallerType::searchRequestFailed);
- QObject::connect(netJob, &NetJob::aborted, caller, &CallerType::searchRequestAborted);
- QObject::connect(netJob, &NetJob::succeeded, caller, [caller, response] {
- QJsonParseError parse_error{};
- QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
- if (parse_error.error != QJsonParseError::NoError) {
- qWarning() << "Error while parsing JSON response from " << caller->debugName() << " at " << parse_error.offset
- << " reason: " << parse_error.errorString();
- qWarning() << *response;
- return;
- }
-
- caller->searchRequestFinished(doc);
- });
-
- netJob->start();
-}
-
-void NetworkModAPI::getModInfo(ModPlatform::IndexedPack& pack, std::function<void(QJsonDocument&, ModPlatform::IndexedPack&)> callback)
-{
- auto response = new QByteArray();
- auto job = getProject(pack.addonId.toString(), response);
-
- QObject::connect(job, &NetJob::succeeded, [callback, &pack, response] {
- QJsonParseError parse_error{};
- QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
- if (parse_error.error != QJsonParseError::NoError) {
- qWarning() << "Error while parsing JSON response for mod info at " << parse_error.offset
- << " reason: " << parse_error.errorString();
- qWarning() << *response;
- return;
- }
-
- callback(doc, pack);
- });
-
- job->start();
-}
-
-void NetworkModAPI::getVersions(VersionSearchArgs&& args, std::function<void(QJsonDocument&, QString)> callback) const
-{
- auto netJob = new NetJob(QString("ModVersions(%2)").arg(args.addonId), APPLICATION->network());
- auto response = new QByteArray();
-
- netJob->addNetAction(Net::Download::makeByteArray(getVersionsURL(args), response));
-
- QObject::connect(netJob, &NetJob::succeeded, [response, callback, args] {
- QJsonParseError parse_error{};
- QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
- if (parse_error.error != QJsonParseError::NoError) {
- qWarning() << "Error while parsing JSON response for getting versions at " << parse_error.offset
- << " reason: " << parse_error.errorString();
- qWarning() << *response;
- return;
- }
-
- callback(doc, args.addonId);
- });
-
- QObject::connect(netJob, &NetJob::finished, [response, netJob] {
- netJob->deleteLater();
- delete response;
- });
-
- netJob->start();
-}
-
-auto NetworkModAPI::getProject(QString addonId, QByteArray* response) const -> NetJob*
-{
- auto netJob = new NetJob(QString("%1::GetProject").arg(addonId), APPLICATION->network());
- auto searchUrl = getModInfoURL(addonId);
-
- netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response));
-
- QObject::connect(netJob, &NetJob::finished, [response, netJob] {
- netJob->deleteLater();
- delete response;
- });
-
- return netJob;
-}
diff --git a/launcher/modplatform/helpers/NetworkModAPI.h b/launcher/modplatform/helpers/NetworkModAPI.h
deleted file mode 100644
index b8af22c7..00000000
--- a/launcher/modplatform/helpers/NetworkModAPI.h
+++ /dev/null
@@ -1,17 +0,0 @@
-#pragma once
-
-#include "modplatform/ModAPI.h"
-
-class NetworkModAPI : public ModAPI {
- public:
- void searchMods(CallerType* caller, SearchArgs&& args) const override;
- void getModInfo(ModPlatform::IndexedPack& pack, std::function<void(QJsonDocument&, ModPlatform::IndexedPack&)> callback) override;
- void getVersions(VersionSearchArgs&& args, std::function<void(QJsonDocument&, QString)> callback) const override;
-
- auto getProject(QString addonId, QByteArray* response) const -> NetJob* override;
-
- protected:
- virtual auto getModSearchURL(SearchArgs& args) const -> QString = 0;
- virtual auto getModInfoURL(QString& id) const -> QString = 0;
- virtual auto getVersionsURL(VersionSearchArgs& args) const -> QString = 0;
-};
diff --git a/launcher/modplatform/helpers/NetworkResourceAPI.cpp b/launcher/modplatform/helpers/NetworkResourceAPI.cpp
new file mode 100644
index 00000000..ac994c31
--- /dev/null
+++ b/launcher/modplatform/helpers/NetworkResourceAPI.cpp
@@ -0,0 +1,128 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
+#include "NetworkResourceAPI.h"
+
+#include "Application.h"
+#include "net/NetJob.h"
+
+#include "modplatform/ModIndex.h"
+
+Task::Ptr NetworkResourceAPI::searchProjects(SearchArgs&& args, SearchCallbacks&& callbacks) const
+{
+ auto search_url_optional = getSearchURL(args);
+ if (!search_url_optional.has_value()) {
+ callbacks.on_fail("Failed to create search URL", -1);
+ return nullptr;
+ }
+
+ auto search_url = search_url_optional.value();
+
+ auto response = new QByteArray();
+ auto netJob = new NetJob(QString("%1::Search").arg(debugName()), APPLICATION->network());
+
+ netJob->addNetAction(Net::Download::makeByteArray(QUrl(search_url), response));
+
+ QObject::connect(netJob, &NetJob::succeeded, [=]{
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from " << debugName() << " at " << parse_error.offset
+ << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+
+ callbacks.on_fail(parse_error.errorString(), -1);
+
+ return;
+ }
+
+ callbacks.on_succeed(doc);
+ });
+
+ QObject::connect(netJob, &NetJob::failed, [=](QString reason){
+ int network_error_code = -1;
+ if (auto* failed_action = netJob->getFailedActions().at(0); failed_action && failed_action->m_reply)
+ network_error_code = failed_action->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ callbacks.on_fail(reason, network_error_code);
+ });
+ QObject::connect(netJob, &NetJob::aborted, [=]{
+ callbacks.on_abort();
+ });
+
+ return netJob;
+}
+
+Task::Ptr NetworkResourceAPI::getProjectInfo(ProjectInfoArgs&& args, ProjectInfoCallbacks&& callbacks) const
+{
+ auto response = new QByteArray();
+ auto job = getProject(args.pack.addonId.toString(), response);
+
+ QObject::connect(job.get(), &NetJob::succeeded, [response, callbacks, args] {
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response for mod info at " << parse_error.offset
+ << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+ return;
+ }
+
+ callbacks.on_succeed(doc, args.pack);
+ });
+
+ return job;
+}
+
+Task::Ptr NetworkResourceAPI::getProjectVersions(VersionSearchArgs&& args, VersionSearchCallbacks&& callbacks) const
+{
+ auto versions_url_optional = getVersionsURL(args);
+ if (!versions_url_optional.has_value())
+ return nullptr;
+
+ auto versions_url = versions_url_optional.value();
+
+ auto netJob = new NetJob(QString("%1::Versions").arg(args.pack.name), APPLICATION->network());
+ auto response = new QByteArray();
+
+ netJob->addNetAction(Net::Download::makeByteArray(versions_url, response));
+
+ QObject::connect(netJob, &NetJob::succeeded, [=] {
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response for getting versions at " << parse_error.offset
+ << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+ return;
+ }
+
+ callbacks.on_succeed(doc, args.pack);
+ });
+
+ QObject::connect(netJob, &NetJob::finished, [response] {
+ delete response;
+ });
+
+ return netJob;
+}
+
+Task::Ptr NetworkResourceAPI::getProject(QString addonId, QByteArray* response) const
+{
+ auto project_url_optional = getInfoURL(addonId);
+ if (!project_url_optional.has_value())
+ return nullptr;
+
+ auto project_url = project_url_optional.value();
+
+ auto netJob = new NetJob(QString("%1::GetProject").arg(addonId), APPLICATION->network());
+
+ netJob->addNetAction(Net::Download::makeByteArray(QUrl(project_url), response));
+
+ QObject::connect(netJob, &NetJob::finished, [response] {
+ delete response;
+ });
+
+ return netJob;
+}
diff --git a/launcher/modplatform/helpers/NetworkResourceAPI.h b/launcher/modplatform/helpers/NetworkResourceAPI.h
new file mode 100644
index 00000000..94813bec
--- /dev/null
+++ b/launcher/modplatform/helpers/NetworkResourceAPI.h
@@ -0,0 +1,22 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
+#pragma once
+
+#include "modplatform/ResourceAPI.h"
+
+class NetworkResourceAPI : public ResourceAPI {
+ public:
+ Task::Ptr searchProjects(SearchArgs&&, SearchCallbacks&&) const override;
+
+ Task::Ptr getProject(QString addonId, QByteArray* response) const override;
+
+ Task::Ptr getProjectInfo(ProjectInfoArgs&&, ProjectInfoCallbacks&&) const override;
+ Task::Ptr getProjectVersions(VersionSearchArgs&&, VersionSearchCallbacks&&) const override;
+
+ protected:
+ [[nodiscard]] virtual auto getSearchURL(SearchArgs const& args) const -> std::optional<QString> = 0;
+ [[nodiscard]] virtual auto getInfoURL(QString const& id) const -> std::optional<QString> = 0;
+ [[nodiscard]] virtual auto getVersionsURL(VersionSearchArgs const& args) const -> std::optional<QString> = 0;
+};
diff --git a/launcher/modplatform/modrinth/ModrinthAPI.cpp b/launcher/modplatform/modrinth/ModrinthAPI.cpp
index 747cf4c3..5a16113d 100644
--- a/launcher/modplatform/modrinth/ModrinthAPI.cpp
+++ b/launcher/modplatform/modrinth/ModrinthAPI.cpp
@@ -1,10 +1,15 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
#include "ModrinthAPI.h"
#include "Application.h"
#include "Json.h"
+#include "net/NetJob.h"
#include "net/Upload.h"
-auto ModrinthAPI::currentVersion(QString hash, QString hash_format, QByteArray* response) -> NetJob::Ptr
+Task::Ptr ModrinthAPI::currentVersion(QString hash, QString hash_format, QByteArray* response)
{
auto* netJob = new NetJob(QString("Modrinth::GetCurrentVersion"), APPLICATION->network());
@@ -16,7 +21,7 @@ auto ModrinthAPI::currentVersion(QString hash, QString hash_format, QByteArray*
return netJob;
}
-auto ModrinthAPI::currentVersions(const QStringList& hashes, QString hash_format, QByteArray* response) -> NetJob::Ptr
+Task::Ptr ModrinthAPI::currentVersions(const QStringList& hashes, QString hash_format, QByteArray* response)
{
auto* netJob = new NetJob(QString("Modrinth::GetCurrentVersions"), APPLICATION->network());
@@ -35,23 +40,26 @@ auto ModrinthAPI::currentVersions(const QStringList& hashes, QString hash_format
return netJob;
}
-auto ModrinthAPI::latestVersion(QString hash,
- QString hash_format,
- std::list<Version> mcVersions,
- ModLoaderTypes loaders,
- QByteArray* response) -> NetJob::Ptr
+Task::Ptr ModrinthAPI::latestVersion(QString hash,
+ QString hash_format,
+ std::optional<std::list<Version>> mcVersions,
+ std::optional<ModLoaderTypes> loaders,
+ QByteArray* response)
{
auto* netJob = new NetJob(QString("Modrinth::GetLatestVersion"), APPLICATION->network());
QJsonObject body_obj;
- Json::writeStringList(body_obj, "loaders", getModLoaderStrings(loaders));
+ if (loaders.has_value())
+ Json::writeStringList(body_obj, "loaders", getModLoaderStrings(loaders.value()));
- QStringList game_versions;
- for (auto& ver : mcVersions) {
- game_versions.append(ver.toString());
+ if (mcVersions.has_value()) {
+ QStringList game_versions;
+ for (auto& ver : mcVersions.value()) {
+ game_versions.append(ver.toString());
+ }
+ Json::writeStringList(body_obj, "game_versions", game_versions);
}
- Json::writeStringList(body_obj, "game_versions", game_versions);
QJsonDocument body(body_obj);
auto body_raw = body.toJson();
@@ -64,11 +72,11 @@ auto ModrinthAPI::latestVersion(QString hash,
return netJob;
}
-auto ModrinthAPI::latestVersions(const QStringList& hashes,
- QString hash_format,
- std::list<Version> mcVersions,
- ModLoaderTypes loaders,
- QByteArray* response) -> NetJob::Ptr
+Task::Ptr ModrinthAPI::latestVersions(const QStringList& hashes,
+ QString hash_format,
+ std::optional<std::list<Version>> mcVersions,
+ std::optional<ModLoaderTypes> loaders,
+ QByteArray* response)
{
auto* netJob = new NetJob(QString("Modrinth::GetLatestVersions"), APPLICATION->network());
@@ -77,13 +85,16 @@ auto ModrinthAPI::latestVersions(const QStringList& hashes,
Json::writeStringList(body_obj, "hashes", hashes);
Json::writeString(body_obj, "algorithm", hash_format);
- Json::writeStringList(body_obj, "loaders", getModLoaderStrings(loaders));
+ if (loaders.has_value())
+ Json::writeStringList(body_obj, "loaders", getModLoaderStrings(loaders.value()));
- QStringList game_versions;
- for (auto& ver : mcVersions) {
- game_versions.append(ver.toString());
+ if (mcVersions.has_value()) {
+ QStringList game_versions;
+ for (auto& ver : mcVersions.value()) {
+ game_versions.append(ver.toString());
+ }
+ Json::writeStringList(body_obj, "game_versions", game_versions);
}
- Json::writeStringList(body_obj, "game_versions", game_versions);
QJsonDocument body(body_obj);
auto body_raw = body.toJson();
@@ -95,14 +106,29 @@ auto ModrinthAPI::latestVersions(const QStringList& hashes,
return netJob;
}
-auto ModrinthAPI::getProjects(QStringList addonIds, QByteArray* response) const -> NetJob*
+Task::Ptr ModrinthAPI::getProjects(QStringList addonIds, QByteArray* response) const
{
auto netJob = new NetJob(QString("Modrinth::GetProjects"), APPLICATION->network());
auto searchUrl = getMultipleModInfoURL(addonIds);
netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response));
- QObject::connect(netJob, &NetJob::finished, [response, netJob] { delete response; netJob->deleteLater(); });
+ QObject::connect(netJob, &NetJob::finished, [response, netJob] {
+ delete response;
+ netJob->deleteLater();
+ });
return netJob;
}
+
+// https://docs.modrinth.com/api-spec/#tag/projects/operation/searchProjects
+static QList<ResourceAPI::SortingMethod> s_sorts = { { 1, "relevance", QObject::tr("Sort by Relevance") },
+ { 2, "downloads", QObject::tr("Sort by Downloads") },
+ { 3, "follows", QObject::tr("Sort by Follows") },
+ { 4, "newest", QObject::tr("Sort by Last Updated") },
+ { 5, "updated", QObject::tr("Sort by Newest") } };
+
+QList<ResourceAPI::SortingMethod> ModrinthAPI::getSortingMethods() const
+{
+ return s_sorts;
+}
diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h
index e1a18681..dda27303 100644
--- a/launcher/modplatform/modrinth/ModrinthAPI.h
+++ b/launcher/modplatform/modrinth/ModrinthAPI.h
@@ -1,69 +1,54 @@
+// SPDX-FileCopyrightText: 2022-2023 flowln <flowlnlnln@gmail.com>
+//
// SPDX-License-Identifier: GPL-3.0-only
-/*
- * PolyMC - Minecraft Launcher
- * Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
#pragma once
#include "BuildConfig.h"
-#include "modplatform/ModAPI.h"
#include "modplatform/ModIndex.h"
-#include "modplatform/helpers/NetworkModAPI.h"
+#include "modplatform/helpers/NetworkResourceAPI.h"
#include <QDebug>
-class ModrinthAPI : public NetworkModAPI {
+class ModrinthAPI : public NetworkResourceAPI {
public:
auto currentVersion(QString hash,
QString hash_format,
- QByteArray* response) -> NetJob::Ptr;
+ QByteArray* response) -> Task::Ptr;
auto currentVersions(const QStringList& hashes,
QString hash_format,
- QByteArray* response) -> NetJob::Ptr;
+ QByteArray* response) -> Task::Ptr;
auto latestVersion(QString hash,
QString hash_format,
- std::list<Version> mcVersions,
- ModLoaderTypes loaders,
- QByteArray* response) -> NetJob::Ptr;
+ std::optional<std::list<Version>> mcVersions,
+ std::optional<ModLoaderTypes> loaders,
+ QByteArray* response) -> Task::Ptr;
auto latestVersions(const QStringList& hashes,
QString hash_format,
- std::list<Version> mcVersions,
- ModLoaderTypes loaders,
- QByteArray* response) -> NetJob::Ptr;
+ std::optional<std::list<Version>> mcVersions,
+ std::optional<ModLoaderTypes> loaders,
+ QByteArray* response) -> Task::Ptr;
- auto getProjects(QStringList addonIds, QByteArray* response) const -> NetJob* override;
+ Task::Ptr getProjects(QStringList addonIds, QByteArray* response) const override;
public:
+ [[nodiscard]] auto getSortingMethods() const -> QList<ResourceAPI::SortingMethod> override;
+
inline auto getAuthorURL(const QString& name) const -> QString { return "https://modrinth.com/user/" + name; };
static auto getModLoaderStrings(const ModLoaderTypes types) -> const QStringList
{
QStringList l;
- for (auto loader : {Forge, Fabric, Quilt})
- {
- if ((types & loader) || types == Unspecified)
- {
- l << ModAPI::getModLoaderString(loader);
+ for (auto loader : {Forge, Fabric, Quilt}) {
+ if (types & loader) {
+ l << getModLoaderString(loader);
}
}
if ((types & Quilt) && (~types & Fabric)) // Add Fabric if Quilt is in use, if Fabric isn't already there
- l << ModAPI::getModLoaderString(Fabric);
+ l << getModLoaderString(Fabric);
return l;
}
@@ -78,28 +63,54 @@ class ModrinthAPI : public NetworkModAPI {
}
private:
- inline auto getModSearchURL(SearchArgs& args) const -> QString override
+ [[nodiscard]] static QString resourceTypeParameter(ModPlatform::ResourceType type)
+ {
+ switch (type) {
+ case ModPlatform::ResourceType::MOD:
+ return "mod";
+ default:
+ qWarning() << "Invalid resource type for Modrinth API!";
+ break;
+ }
+
+ return "";
+ }
+ [[nodiscard]] QString createFacets(SearchArgs const& args) const
+ {
+ QStringList facets_list;
+
+ if (args.loaders.has_value())
+ facets_list.append(QString("[%1]").arg(getModLoaderFilters(args.loaders.value())));
+ if (args.versions.has_value())
+ facets_list.append(QString("[%1]").arg(getGameVersionsArray(args.versions.value())));
+ facets_list.append(QString("[\"project_type:%1\"]").arg(resourceTypeParameter(args.type)));
+
+ return QString("[%1]").arg(facets_list.join(','));
+ }
+
+ public:
+ [[nodiscard]] inline auto getSearchURL(SearchArgs const& args) const -> std::optional<QString> override
{
- if (!validateModLoaders(args.loaders)) {
- qWarning() << "Modrinth only have Forge and Fabric-compatible mods!";
- return "";
+ if (args.loaders.has_value()) {
+ if (!validateModLoaders(args.loaders.value())) {
+ qWarning() << "Modrinth only have Forge and Fabric-compatible mods!";
+ return {};
+ }
}
- return QString(BuildConfig.MODRINTH_PROD_URL +
- "/search?"
- "offset=%1&"
- "limit=25&"
- "query=%2&"
- "index=%3&"
- "facets=[[%4],%5[\"project_type:mod\"]]")
- .arg(args.offset)
- .arg(args.search)
- .arg(args.sorting)
- .arg(getModLoaderFilters(args.loaders))
- .arg(getGameVersionsArray(args.versions));
+ QStringList get_arguments;
+ get_arguments.append(QString("offset=%1").arg(args.offset));
+ get_arguments.append(QString("limit=25"));
+ if (args.search.has_value())
+ get_arguments.append(QString("query=%1").arg(args.search.value()));
+ if (args.sorting.has_value())
+ get_arguments.append(QString("index=%1").arg(args.sorting.value().name));
+ get_arguments.append(QString("facets=%1").arg(createFacets(args)));
+
+ return BuildConfig.MODRINTH_PROD_URL + "/search?" + get_arguments.join('&');
};
- inline auto getModInfoURL(QString& id) const -> QString override
+ inline auto getInfoURL(QString const& id) const -> std::optional<QString> override
{
return BuildConfig.MODRINTH_PROD_URL + "/project/" + id;
};
@@ -109,15 +120,16 @@ class ModrinthAPI : public NetworkModAPI {
return BuildConfig.MODRINTH_PROD_URL + QString("/projects?ids=[\"%1\"]").arg(ids.join("\",\""));
};
- inline auto getVersionsURL(VersionSearchArgs& args) const -> QString override
+ inline auto getVersionsURL(VersionSearchArgs const& args) const -> std::optional<QString> override
{
- return QString(BuildConfig.MODRINTH_PROD_URL +
- "/project/%1/version?"
- "game_versions=[%2]&"
- "loaders=[\"%3\"]")
- .arg(args.addonId,
- getGameVersionsString(args.mcVersions),
- getModLoaderStrings(args.loaders).join("\",\""));
+ QStringList get_arguments;
+ if (args.mcVersions.has_value())
+ get_arguments.append(QString("game_versions=[%1]").arg(getGameVersionsString(args.mcVersions.value())));
+ if (args.loaders.has_value())
+ get_arguments.append(QString("loaders=[\"%1\"]").arg(getModLoaderStrings(args.loaders.value()).join("\",\"")));
+
+ return QString("%1/project/%2/version%3%4")
+ .arg(BuildConfig.MODRINTH_PROD_URL, args.pack.addonId.toString(), get_arguments.isEmpty() ? "" : "?", get_arguments.join('&'));
};
auto getGameVersionsArray(std::list<Version> mcVersions) const -> QString
@@ -127,12 +139,12 @@ class ModrinthAPI : public NetworkModAPI {
s += QString("\"versions:%1\",").arg(ver.toString());
}
s.remove(s.length() - 1, 1); //remove last comma
- return s.isEmpty() ? QString() : QString("[%1],").arg(s);
+ return s.isEmpty() ? QString() : s;
}
inline auto validateModLoaders(ModLoaderTypes loaders) const -> bool
{
- return (loaders == Unspecified) || (loaders & (Forge | Fabric | Quilt));
+ return loaders & (Forge | Fabric | Quilt);
}
};
diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp
index e2d27547..daca68d7 100644
--- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp
+++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp
@@ -4,12 +4,15 @@
#include "Json.h"
-#include "ModDownloadTask.h"
+#include "ResourceDownloadTask.h"
#include "modplatform/helpers/HashUtils.h"
#include "tasks/ConcurrentTask.h"
+#include "minecraft/mod/ModFolderModel.h"
+#include "minecraft/mod/ResourceFolderModel.h"
+
static ModrinthAPI api;
static ModPlatform::ProviderCapabilities ProviderCaps;
@@ -34,7 +37,7 @@ void ModrinthCheckUpdate::executeTask()
// Create all hashes
QStringList hashes;
- auto best_hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first();
+ auto best_hash_type = ProviderCaps.hashType(ModPlatform::ResourceProvider::MODRINTH).first();
ConcurrentTask hashing_task(this, "MakeModrinthHashesTask", 10);
for (auto* mod : m_mods) {
@@ -108,11 +111,13 @@ void ModrinthCheckUpdate::executeTask()
// Sometimes a version may have multiple files, one with "forge" and one with "fabric",
// so we may want to filter it
QString loader_filter;
- static auto flags = { ModAPI::ModLoaderType::Forge, ModAPI::ModLoaderType::Fabric, ModAPI::ModLoaderType::Quilt };
- for (auto flag : flags) {
- if (m_loaders.testFlag(flag)) {
- loader_filter = api.getModLoaderString(flag);
- break;
+ if (m_loaders.has_value()) {
+ static auto flags = { ResourceAPI::ModLoaderType::Forge, ResourceAPI::ModLoaderType::Fabric, ResourceAPI::ModLoaderType::Quilt };
+ for (auto flag : flags) {
+ if (m_loaders.value().testFlag(flag)) {
+ loader_filter = api.getModLoaderString(flag);
+ break;
+ }
}
}
@@ -152,12 +157,12 @@ void ModrinthCheckUpdate::executeTask()
for (auto& author : mod->authors())
pack.authors.append({ author });
pack.description = mod->description();
- pack.provider = ModPlatform::Provider::MODRINTH;
+ pack.provider = ModPlatform::ResourceProvider::MODRINTH;
- auto download_task = new ModDownloadTask(pack, project_ver, m_mods_folder);
+ auto download_task = new ResourceDownloadTask(pack, project_ver, m_mods_folder);
m_updatable.emplace_back(pack.name, hash, mod->version(), project_ver.version_number, project_ver.changelog,
- ModPlatform::Provider::MODRINTH, download_task);
+ ModPlatform::ResourceProvider::MODRINTH, download_task);
}
}
} catch (Json::JsonException& e) {
@@ -170,7 +175,7 @@ void ModrinthCheckUpdate::executeTask()
setStatus(tr("Waiting for the API response from Modrinth..."));
setProgress(1, 3);
- m_net_job = job.get();
+ m_net_job = qSharedPointerObjectCast<NetJob, Task>(job);
job->start();
lock.exec();
diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.h b/launcher/modplatform/modrinth/ModrinthCheckUpdate.h
index abf8ada1..88e1a675 100644
--- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.h
+++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.h
@@ -8,7 +8,7 @@ class ModrinthCheckUpdate : public CheckUpdateTask {
Q_OBJECT
public:
- ModrinthCheckUpdate(QList<Mod*>& mods, std::list<Version>& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr<ModFolderModel> mods_folder)
+ ModrinthCheckUpdate(QList<Mod*>& mods, std::list<Version>& mcVersions, std::optional<ResourceAPI::ModLoaderTypes> loaders, std::shared_ptr<ModFolderModel> mods_folder)
: CheckUpdateTask(mods, mcVersions, loaders, mods_folder)
{}
@@ -19,5 +19,5 @@ class ModrinthCheckUpdate : public CheckUpdateTask {
void executeTask() override;
private:
- NetJob* m_net_job = nullptr;
+ NetJob::Ptr m_net_job = nullptr;
};
diff --git a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp
index 1c0e8979..c5a27c9d 100644
--- a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp
+++ b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp
@@ -202,14 +202,14 @@ bool ModrinthCreationTask::createInstance()
auto components = instance.getPackProfile();
components->buildingFromScratch();
- components->setComponentVersion("net.minecraft", minecraftVersion, true);
+ components->setComponentVersion("net.minecraft", m_minecraft_version, true);
- if (!fabricVersion.isEmpty())
- components->setComponentVersion("net.fabricmc.fabric-loader", fabricVersion);
- if (!quiltVersion.isEmpty())
- components->setComponentVersion("org.quiltmc.quilt-loader", quiltVersion);
- if (!forgeVersion.isEmpty())
- components->setComponentVersion("net.minecraftforge", forgeVersion);
+ if (!m_fabric_version.isEmpty())
+ components->setComponentVersion("net.fabricmc.fabric-loader", m_fabric_version);
+ if (!m_quilt_version.isEmpty())
+ components->setComponentVersion("org.quiltmc.quilt-loader", m_quilt_version);
+ if (!m_forge_version.isEmpty())
+ components->setComponentVersion("net.minecraftforge", m_forge_version);
if (m_instIcon != "default") {
instance.setIconKey(m_instIcon);
@@ -217,7 +217,9 @@ bool ModrinthCreationTask::createInstance()
instance.setIconKey("modrinth");
}
- instance.setManagedPack("modrinth", m_managed_id, m_managed_name, m_managed_version_id, version());
+ // Don't add managed info to packs without an ID (most likely imported from ZIP)
+ if (!m_managed_id.isEmpty())
+ instance.setManagedPack("modrinth", m_managed_id, m_managed_name, m_managed_version_id, version());
instance.setName(name());
instance.saveNow();
@@ -277,7 +279,7 @@ bool ModrinthCreationTask::createInstance()
return ended_well;
}
-bool ModrinthCreationTask::parseManifest(const QString& index_path, std::vector<Modrinth::File>& files, bool set_managed_info, bool show_optional_dialog)
+bool ModrinthCreationTask::parseManifest(const QString& index_path, std::vector<Modrinth::File>& files, bool set_internal_data, bool show_optional_dialog)
{
try {
auto doc = Json::requireDocument(index_path);
@@ -289,7 +291,7 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path, std::vector<
throw JSONValidationError("Unknown game: " + game);
}
- if (set_managed_info) {
+ if (set_internal_data) {
if (m_managed_version_id.isEmpty())
m_managed_version_id = Json::ensureString(obj, "versionId", {}, "Managed ID");
m_managed_name = Json::ensureString(obj, "name", {}, "Managed Name");
@@ -365,19 +367,21 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path, std::vector<
files.push_back(file);
}
- auto dependencies = Json::requireObject(obj, "dependencies", "modrinth.index.json");
- for (auto it = dependencies.begin(), end = dependencies.end(); it != end; ++it) {
- QString name = it.key();
- if (name == "minecraft") {
- minecraftVersion = Json::requireString(*it, "Minecraft version");
- } else if (name == "fabric-loader") {
- fabricVersion = Json::requireString(*it, "Fabric Loader version");
- } else if (name == "quilt-loader") {
- quiltVersion = Json::requireString(*it, "Quilt Loader version");
- } else if (name == "forge") {
- forgeVersion = Json::requireString(*it, "Forge version");
- } else {
- throw JSONValidationError("Unknown dependency type: " + name);
+ if (set_internal_data) {
+ auto dependencies = Json::requireObject(obj, "dependencies", "modrinth.index.json");
+ for (auto it = dependencies.begin(), end = dependencies.end(); it != end; ++it) {
+ QString name = it.key();
+ if (name == "minecraft") {
+ m_minecraft_version = Json::requireString(*it, "Minecraft version");
+ } else if (name == "fabric-loader") {
+ m_fabric_version = Json::requireString(*it, "Fabric Loader version");
+ } else if (name == "quilt-loader") {
+ m_quilt_version = Json::requireString(*it, "Quilt Loader version");
+ } else if (name == "forge") {
+ m_forge_version = Json::requireString(*it, "Forge version");
+ } else {
+ throw JSONValidationError("Unknown dependency type: " + name);
+ }
}
}
} else {
diff --git a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h
index 122fc5ce..6de24fd4 100644
--- a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h
+++ b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h
@@ -37,12 +37,12 @@ class ModrinthCreationTask final : public InstanceCreationTask {
bool createInstance() override;
private:
- bool parseManifest(const QString&, std::vector<Modrinth::File>&, bool set_managed_info = true, bool show_optional_dialog = true);
+ bool parseManifest(const QString&, std::vector<Modrinth::File>&, bool set_internal_data = true, bool show_optional_dialog = true);
private:
QWidget* m_parent = nullptr;
- QString minecraftVersion, fabricVersion, quiltVersion, forgeVersion;
+ QString m_minecraft_version, m_fabric_version, m_quilt_version, m_forge_version;
QString m_managed_id, m_managed_version_id, m_managed_name;
std::vector<Modrinth::File> m_files;
diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp
index ae45e096..7ade131e 100644
--- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp
+++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp
@@ -27,13 +27,14 @@
static ModrinthAPI api;
static ModPlatform::ProviderCapabilities ProviderCaps;
+// https://docs.modrinth.com/api-spec/#tag/projects/operation/getProject
void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
{
pack.addonId = Json::ensureString(obj, "project_id");
if (pack.addonId.toString().isEmpty())
pack.addonId = Json::requireString(obj, "id");
- pack.provider = ModPlatform::Provider::MODRINTH;
+ pack.provider = ModPlatform::ResourceProvider::MODRINTH;
pack.name = Json::requireString(obj, "title");
pack.slug = Json::ensureString(obj, "slug", "");
@@ -44,7 +45,7 @@ void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
pack.description = Json::ensureString(obj, "description", "");
- pack.logoUrl = Json::requireString(obj, "icon_url");
+ pack.logoUrl = Json::ensureString(obj, "icon_url", "");
pack.logoName = pack.addonId.toString();
ModPlatform::ModpackAuthor modAuthor;
@@ -87,7 +88,7 @@ void Modrinth::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& ob
pack.extraData.donate.append(donate);
}
- pack.extraData.body = Json::ensureString(obj, "body");
+ pack.extraData.body = Json::ensureString(obj, "body").remove("<br>");
pack.extraDataLoaded = true;
}
@@ -95,10 +96,10 @@ void Modrinth::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& ob
void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
QJsonArray& arr,
const shared_qobject_ptr<QNetworkAccessManager>& network,
- BaseInstance* inst)
+ const BaseInstance* inst)
{
QVector<ModPlatform::IndexedVersion> unsortedVersions;
- QString mcVersion = (static_cast<MinecraftInstance*>(inst))->getPackProfile()->getComponentVersion("net.minecraft");
+ QString mcVersion = (static_cast<const MinecraftInstance*>(inst))->getPackProfile()->getComponentVersion("net.minecraft");
for (auto versionIter : arr) {
auto obj = versionIter.toObject();
@@ -179,7 +180,7 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_t
file.hash = Json::requireString(hash_list, preferred_hash_type);
file.hash_type = preferred_hash_type;
} else {
- auto hash_types = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH);
+ auto hash_types = ProviderCaps.hashType(ModPlatform::ResourceProvider::MODRINTH);
for (auto& hash_type : hash_types) {
if (hash_list.contains(hash_type)) {
file.hash = Json::requireString(hash_list, hash_type);
diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.h b/launcher/modplatform/modrinth/ModrinthPackIndex.h
index 31881414..e73e4b18 100644
--- a/launcher/modplatform/modrinth/ModrinthPackIndex.h
+++ b/launcher/modplatform/modrinth/ModrinthPackIndex.h
@@ -29,7 +29,7 @@ void loadExtraPackData(ModPlatform::IndexedPack& m, QJsonObject& obj);
void loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
QJsonArray& arr,
const shared_qobject_ptr<QNetworkAccessManager>& network,
- BaseInstance* inst);
+ const BaseInstance* inst);
auto loadIndexedPackVersion(QJsonObject& obj, QString hash_type = "sha512", QString filename_prefer = "") -> ModPlatform::IndexedVersion;
} // namespace Modrinth
diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp
index 0ed29311..510c7309 100644
--- a/launcher/modplatform/packwiz/Packwiz.cpp
+++ b/launcher/modplatform/packwiz/Packwiz.cpp
@@ -97,7 +97,7 @@ auto V1::createModFormat(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, Mo
mod.name = mod_pack.name;
mod.filename = mod_version.fileName;
- if (mod_pack.provider == ModPlatform::Provider::FLAME) {
+ if (mod_pack.provider == ModPlatform::ResourceProvider::FLAME) {
mod.mode = "metadata:curseforge";
} else {
mod.mode = "url";
@@ -176,11 +176,11 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod)
in_stream << QString("\n[update]\n");
in_stream << QString("[update.%1]\n").arg(ProviderCaps.name(mod.provider));
switch (mod.provider) {
- case (ModPlatform::Provider::FLAME):
+ case (ModPlatform::ResourceProvider::FLAME):
in_stream << QString("file-id = %1\n").arg(mod.file_id.toString());
in_stream << QString("project-id = %1\n").arg(mod.project_id.toString());
break;
- case (ModPlatform::Provider::MODRINTH):
+ case (ModPlatform::ResourceProvider::MODRINTH):
addToStream("mod-id", mod.mod_id().toString());
addToStream("version", mod.version().toString());
break;
@@ -273,7 +273,7 @@ auto V1::getIndexForMod(QDir& index_dir, QString slug) -> Mod
}
{ // [update] info
- using Provider = ModPlatform::Provider;
+ using Provider = ModPlatform::ResourceProvider;
auto update_table = table["update"];
if (!update_table || !update_table.is_table()) {
diff --git a/launcher/modplatform/packwiz/Packwiz.h b/launcher/modplatform/packwiz/Packwiz.h
index 9754e5c4..4b096eec 100644
--- a/launcher/modplatform/packwiz/Packwiz.h
+++ b/launcher/modplatform/packwiz/Packwiz.h
@@ -49,7 +49,7 @@ class V1 {
QString hash {};
// [update]
- ModPlatform::Provider provider {};
+ ModPlatform::ResourceProvider provider {};
QVariant file_id {};
QVariant project_id {};
diff --git a/launcher/modplatform/technic/TechnicPackProcessor.cpp b/launcher/modplatform/technic/TechnicPackProcessor.cpp
index 95feb4b2..df713a72 100644
--- a/launcher/modplatform/technic/TechnicPackProcessor.cpp
+++ b/launcher/modplatform/technic/TechnicPackProcessor.cpp
@@ -172,7 +172,7 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings, const
auto libraryObject = Json::ensureObject(library, {}, "");
auto libraryName = Json::ensureString(libraryObject, "name", "", "");
- if (libraryName.startsWith("net.minecraftforge:forge:") && libraryName.contains('-'))
+ if ((libraryName.startsWith("net.minecraftforge:forge:") || libraryName.startsWith("net.minecraftforge:fmlloader:")) && libraryName.contains('-'))
{
QString libraryVersion = libraryName.section(':', 2);
if (!libraryVersion.startsWith("1.7.10-"))
diff --git a/launcher/net/NetAction.h b/launcher/net/NetAction.h
index d9c4fadc..38fe058b 100644
--- a/launcher/net/NetAction.h
+++ b/launcher/net/NetAction.h
@@ -52,7 +52,6 @@ class NetAction : public Task {
virtual ~NetAction() = default;
QUrl url() { return m_url; }
- auto index() -> int { return m_index_within_job; }
void setNetwork(shared_qobject_ptr<QNetworkAccessManager> network) { m_network = network; }
@@ -75,9 +74,6 @@ class NetAction : public Task {
public:
shared_qobject_ptr<QNetworkAccessManager> m_network;
- /// index within the parent job, FIXME: nuke
- int m_index_within_job = 0;
-
/// the network reply
unique_qobject_ptr<QNetworkReply> m_reply;
diff --git a/launcher/net/NetJob.cpp b/launcher/net/NetJob.cpp
index 8ced1b7e..4bcd40b5 100644
--- a/launcher/net/NetJob.cpp
+++ b/launcher/net/NetJob.cpp
@@ -38,11 +38,10 @@
auto NetJob::addNetAction(NetAction::Ptr action) -> bool
{
- action->m_index_within_job = m_queue.size();
- m_queue.append(action);
-
action->setNetwork(m_network);
+ addTask(action);
+
return true;
}
@@ -123,7 +122,7 @@ auto NetJob::getFailedFiles() -> QList<QString>
void NetJob::updateState()
{
- emit progress(m_done.count(), m_total_size);
+ emit progress(m_done.count(), totalSize());
setStatus(tr("Executing %1 task(s) (%2 out of %3 are done)")
- .arg(QString::number(m_doing.count()), QString::number(m_done.count()), QString::number(m_total_size)));
+ .arg(QString::number(m_doing.count()), QString::number(m_done.count()), QString::number(totalSize())));
}
diff --git a/launcher/resources/backgrounds/backgrounds.qrc b/launcher/resources/backgrounds/backgrounds.qrc
index e55faf15..e63a25b5 100644
--- a/launcher/resources/backgrounds/backgrounds.qrc
+++ b/launcher/resources/backgrounds/backgrounds.qrc
@@ -13,5 +13,17 @@
<file alias="rory-flat-xmas">rory-flat-xmas.png</file>
<file alias="rory-flat-bday">rory-flat-bday.png</file>
<file alias="rory-flat-spooky">rory-flat-spooky.png</file>
+ <!-- teawie images -->
+ <!-- copyright (c) SympathyTea 2023 -->
+ <!-- these are licensed under the CC BY-SA 4.0 and have been unmodified aside from downscaling -->
+ <!-- the full license with appropriate notices is avalible at https://creativecommons.org/licenses/by-sa/4.0/ -->
+ <file alias="teawie">teawie.png</file>
+ <!-- https://commons.wikimedia.org/wiki/File:Teawie.png -->
+ <file alias="teawie-xmas">teawie-xmas.png</file>
+ <!-- https://commons.wikimedia.org/wiki/File:Teawie_Holiday.png -->
+ <file alias="teawie-bday">teawie-bday.png</file>
+ <!-- https://commons.wikimedia.org/wiki/File:Teawie_Party.png -->
+ <file alias="teawie-spooky">teawie-spooky.png</file>
+ <!-- https://commons.wikimedia.org/wiki/File:Teawie_Halloween.png -->
</qresource>
</RCC>
diff --git a/launcher/resources/backgrounds/teawie-bday.png b/launcher/resources/backgrounds/teawie-bday.png
new file mode 100644
index 00000000..f4ecf247
--- /dev/null
+++ b/launcher/resources/backgrounds/teawie-bday.png
Binary files differ
diff --git a/launcher/resources/backgrounds/teawie-spooky.png b/launcher/resources/backgrounds/teawie-spooky.png
new file mode 100644
index 00000000..cefc6c85
--- /dev/null
+++ b/launcher/resources/backgrounds/teawie-spooky.png
Binary files differ
diff --git a/launcher/resources/backgrounds/teawie-xmas.png b/launcher/resources/backgrounds/teawie-xmas.png
new file mode 100644
index 00000000..55fb7cfc
--- /dev/null
+++ b/launcher/resources/backgrounds/teawie-xmas.png
Binary files differ
diff --git a/launcher/resources/backgrounds/teawie.png b/launcher/resources/backgrounds/teawie.png
new file mode 100644
index 00000000..dc32c51f
--- /dev/null
+++ b/launcher/resources/backgrounds/teawie.png
Binary files differ
diff --git a/launcher/tasks/ConcurrentTask.cpp b/launcher/tasks/ConcurrentTask.cpp
index ce08a6a2..190d48d8 100644
--- a/launcher/tasks/ConcurrentTask.cpp
+++ b/launcher/tasks/ConcurrentTask.cpp
@@ -27,18 +27,13 @@ auto ConcurrentTask::getStepTotalProgress() const -> qint64
void ConcurrentTask::addTask(Task::Ptr task)
{
- if (!isRunning())
- m_queue.append(task);
- else
- qWarning() << "Tried to add a task to a running concurrent task!";
+ m_queue.append(task);
}
void ConcurrentTask::executeTask()
{
- m_total_size = m_queue.size();
-
// Start the least amount of tasks needed, but at least one
- int num_starts = std::max(1, std::min(m_total_max_size, m_total_size));
+ int num_starts = qMax(1, qMin(m_total_max_size, m_queue.size()));
for (int i = 0; i < num_starts; i++) {
QMetaObject::invokeMethod(this, &ConcurrentTask::startNext, Qt::QueuedConnection);
}
@@ -73,6 +68,20 @@ bool ConcurrentTask::abort()
return suceedeed;
}
+void ConcurrentTask::clear()
+{
+ Q_ASSERT(!isRunning());
+
+ m_done.clear();
+ m_failed.clear();
+ m_queue.clear();
+
+ m_aborted = false;
+
+ m_progress = 0;
+ m_stepProgress = 0;
+}
+
void ConcurrentTask::startNext()
{
if (m_aborted || m_doing.count() > m_total_max_size)
@@ -103,7 +112,12 @@ void ConcurrentTask::startNext()
QCoreApplication::processEvents();
- next->start();
+ QMetaObject::invokeMethod(next.get(), &Task::start, Qt::QueuedConnection);
+
+ // Allow going up the number of concurrent tasks in case of tasks being added in the middle of a running task.
+ int num_starts = m_total_max_size - m_doing.size();
+ for (int i = 0; i < num_starts; i++)
+ QMetaObject::invokeMethod(this, &ConcurrentTask::startNext, Qt::QueuedConnection);
}
void ConcurrentTask::subTaskSucceeded(Task::Ptr task)
@@ -145,7 +159,7 @@ void ConcurrentTask::subTaskProgress(qint64 current, qint64 total)
void ConcurrentTask::updateState()
{
- setProgress(m_done.count(), m_total_size);
+ setProgress(m_done.count(), totalSize());
setStatus(tr("Executing %1 task(s) (%2 out of %3 are done)")
- .arg(QString::number(m_doing.count()), QString::number(m_done.count()), QString::number(m_total_size)));
+ .arg(QString::number(m_doing.count()), QString::number(m_done.count()), QString::number(totalSize())));
}
diff --git a/launcher/tasks/ConcurrentTask.h b/launcher/tasks/ConcurrentTask.h
index f1279d32..b46919fb 100644
--- a/launcher/tasks/ConcurrentTask.h
+++ b/launcher/tasks/ConcurrentTask.h
@@ -24,6 +24,11 @@ public:
public slots:
bool abort() override;
+ /** Resets the internal state of the task.
+ * This allows the same task to be re-used.
+ */
+ void clear();
+
protected
slots:
void executeTask() override;
@@ -36,6 +41,9 @@ slots:
void subTaskProgress(qint64 current, qint64 total);
protected:
+ // NOTE: This is not thread-safe.
+ [[nodiscard]] unsigned int totalSize() const { return m_queue.size() + m_doing.size() + m_done.size(); }
+
void setStepStatus(QString status) { m_step_status = status; emit stepStatus(status); };
virtual void updateState();
@@ -51,7 +59,6 @@ protected:
QHash<Task*, Task::Ptr> m_failed;
int m_total_max_size;
- int m_total_size;
qint64 m_stepProgress = 0;
qint64 m_stepTotalProgress = 100;
diff --git a/launcher/tasks/MultipleOptionsTask.cpp b/launcher/tasks/MultipleOptionsTask.cpp
index 5ad6181f..034499df 100644
--- a/launcher/tasks/MultipleOptionsTask.cpp
+++ b/launcher/tasks/MultipleOptionsTask.cpp
@@ -22,6 +22,6 @@ void MultipleOptionsTask::startNext()
void MultipleOptionsTask::updateState()
{
- setProgress(m_done.count(), m_total_size);
- setStatus(tr("Attempting task %1 out of %2").arg(QString::number(m_doing.count() + m_done.count()), QString::number(m_total_size)));
+ setProgress(m_done.count(), totalSize());
+ setStatus(tr("Attempting task %1 out of %2").arg(QString::number(m_doing.count() + m_done.count()), QString::number(totalSize())));
}
diff --git a/launcher/tasks/SequentialTask.cpp b/launcher/tasks/SequentialTask.cpp
index a34137cb..b2f86328 100644
--- a/launcher/tasks/SequentialTask.cpp
+++ b/launcher/tasks/SequentialTask.cpp
@@ -17,6 +17,6 @@ void SequentialTask::startNext()
void SequentialTask::updateState()
{
- setProgress(m_done.count(), m_total_size);
- setStatus(tr("Executing task %1 out of %2").arg(QString::number(m_doing.count() + m_done.count()), QString::number(m_total_size)));
+ setProgress(m_done.count(), totalSize());
+ setStatus(tr("Executing task %1 out of %2").arg(QString::number(m_doing.count() + m_done.count()), QString::number(totalSize())));
}
diff --git a/launcher/ui/GuiUtil.cpp b/launcher/ui/GuiUtil.cpp
index b1ea5ee9..930e088a 100644
--- a/launcher/ui/GuiUtil.cpp
+++ b/launcher/ui/GuiUtil.cpp
@@ -1,8 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
- * PolyMC - Minecraft Launcher
+ * Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Lenny McLennington <lenny@sneed.church>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -50,11 +51,34 @@
#include <DesktopServices.h>
#include <BuildConfig.h>
-QString GuiUtil::uploadPaste(const QString &text, QWidget *parentWidget)
+std::optional<QString> GuiUtil::uploadPaste(const QString &name, const QString &text, QWidget *parentWidget)
{
ProgressDialog dialog(parentWidget);
auto pasteTypeSetting = static_cast<PasteUpload::PasteType>(APPLICATION->settings()->get("PastebinType").toInt());
auto pasteCustomAPIBaseSetting = APPLICATION->settings()->get("PastebinCustomAPIBase").toString();
+
+ {
+ QUrl baseUrl;
+ if (pasteCustomAPIBaseSetting.isEmpty())
+ baseUrl = PasteUpload::PasteTypes[pasteTypeSetting].defaultBase;
+ else
+ baseUrl = pasteCustomAPIBaseSetting;
+
+ if (baseUrl.isValid())
+ {
+ auto response = CustomMessageBox::selectable(parentWidget, QObject::tr("Confirm Upload"),
+ QObject::tr("You are about to upload \"%1\" to %2.\n"
+ "You should double-check for personal information.\n\n"
+ "Are you sure?")
+ .arg(name, baseUrl.host()),
+ QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No)
+ ->exec();
+
+ if (response != QMessageBox::Yes)
+ return {};
+ }
+ }
+
std::unique_ptr<PasteUpload> paste(new PasteUpload(parentWidget, text, pasteCustomAPIBaseSetting, pasteTypeSetting));
dialog.execWithTask(paste.get());
diff --git a/launcher/ui/GuiUtil.h b/launcher/ui/GuiUtil.h
index 5e109383..96ebd9a2 100644
--- a/launcher/ui/GuiUtil.h
+++ b/launcher/ui/GuiUtil.h
@@ -1,10 +1,11 @@
#pragma once
#include <QWidget>
+#include <optional>
namespace GuiUtil
{
-QString uploadPaste(const QString &text, QWidget *parentWidget);
+std::optional<QString> uploadPaste(const QString &name, const QString &text, QWidget *parentWidget);
void setClipboardText(const QString &text);
QStringList BrowseForFiles(QString context, QString caption, QString filter, QString defaultPath, QWidget *parentWidget);
QString BrowseForFile(QString context, QString caption, QString filter, QString defaultPath, QWidget *parentWidget);
diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp
index 6eaaac97..84bb1da4 100644
--- a/launcher/ui/MainWindow.cpp
+++ b/launcher/ui/MainWindow.cpp
@@ -1,7 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
- * PolyMC - Minecraft Launcher
+ * Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -105,13 +106,13 @@
#include "ui/dialogs/CopyInstanceDialog.h"
#include "ui/dialogs/EditAccountDialog.h"
#include "ui/dialogs/ExportInstanceDialog.h"
-#include "ui/dialogs/ImportResourcePackDialog.h"
+#include "ui/dialogs/ImportResourceDialog.h"
#include "ui/themes/ITheme.h"
+#include "ui/themes/ThemeManager.h"
-#include <minecraft/mod/ResourcePackFolderModel.h>
-#include <minecraft/mod/tasks/LocalResourcePackParseTask.h>
-#include <minecraft/mod/TexturePackFolderModel.h>
-#include <minecraft/mod/tasks/LocalTexturePackParseTask.h>
+#include "minecraft/mod/tasks/LocalResourceParse.h"
+#include "minecraft/mod/ModFolderModel.h"
+#include "minecraft/WorldList.h"
#include "KonamiCode.h"
@@ -486,7 +487,7 @@ public:
if (!BuildConfig.BUG_TRACKER_URL.isEmpty()) {
helpMenu->addAction(actionReportBug);
}
-
+
if(!BuildConfig.MATRIX_URL.isEmpty()) {
helpMenu->addAction(actionMATRIX);
}
@@ -949,7 +950,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow
view->installEventFilter(this);
view->setContextMenuPolicy(Qt::CustomContextMenu);
connect(view, &QWidget::customContextMenuRequested, this, &MainWindow::showInstanceContextMenu);
- connect(view, &InstanceView::droppedURLs, this, &MainWindow::droppedURLs, Qt::QueuedConnection);
+ connect(view, &InstanceView::droppedURLs, this, &MainWindow::processURLs, Qt::QueuedConnection);
proxymodel = new InstanceProxyModel(this);
proxymodel->setSourceModel(APPLICATION->instances().get());
@@ -969,6 +970,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow
ui->actionCAT->setChecked(cat_enable);
// NOTE: calling the operator like that is an ugly hack to appease ancient gcc...
connect(ui->actionCAT.operator->(), SIGNAL(toggled(bool)), SLOT(onCatToggled(bool)));
+ connect(APPLICATION, &Application::currentCatChanged, this, &MainWindow::onCatChanged);
setCatBackground(cat_enable);
}
@@ -1328,7 +1330,7 @@ void MainWindow::updateThemeMenu()
themeAction->setActionGroup(themesGroup);
connect(themeAction, &QAction::triggered, [theme]() {
- APPLICATION->setApplicationTheme(theme->id(),false);
+ APPLICATION->setApplicationTheme(theme->id());
APPLICATION->settings()->set("ApplicationTheme", theme->id());
});
}
@@ -1574,32 +1576,9 @@ void MainWindow::onCatToggled(bool state)
APPLICATION->settings()->set("TheCat", state);
}
-namespace {
-template <typename T>
-T non_stupid_abs(T in)
-{
- if (in < 0)
- return -in;
- return in;
-}
-}
-
void MainWindow::setCatBackground(bool enabled)
{
- if (enabled)
- {
- QDateTime now = QDateTime::currentDateTime();
- QDateTime birthday(QDate(now.date().year(), 11, 30), QTime(0, 0));
- QDateTime xmas(QDate(now.date().year(), 12, 25), QTime(0, 0));
- QDateTime halloween(QDate(now.date().year(), 10, 31), QTime(0, 0));
- QString cat = APPLICATION->settings()->get("BackgroundCat").toString();
- if (non_stupid_abs(now.daysTo(xmas)) <= 4) {
- cat += "-xmas";
- } else if (non_stupid_abs(now.daysTo(halloween)) <= 4) {
- cat += "-spooky";
- } else if (non_stupid_abs(now.daysTo(birthday)) <= 12) {
- cat += "-bday";
- }
+ if (enabled) {
view->setStyleSheet(QString(R"(
InstanceView
{
@@ -1610,10 +1589,8 @@ InstanceView
background-repeat: none;
background-color:palette(base);
})")
- .arg(cat));
- }
- else
- {
+ .arg(ThemeManager::getCatImage()));
+ } else {
view->setStyleSheet(QString());
}
}
@@ -1735,10 +1712,12 @@ void MainWindow::on_actionAddInstance_triggered()
addInstance();
}
-void MainWindow::droppedURLs(QList<QUrl> urls)
+void MainWindow::processURLs(QList<QUrl> urls)
{
// NOTE: This loop only processes one dropped file!
for (auto& url : urls) {
+ qDebug() << "Processing" << url;
+
// The isLocalFile() check below doesn't work as intended without an explicit scheme.
if (url.scheme().isEmpty())
url.setScheme("file");
@@ -1748,31 +1727,50 @@ void MainWindow::droppedURLs(QList<QUrl> urls)
break;
}
- auto localFileName = url.toLocalFile();
+ auto localFileName = QDir::toNativeSeparators(url.toLocalFile()) ;
QFileInfo localFileInfo(localFileName);
- bool isResourcePack = ResourcePackUtils::validate(localFileInfo);
- bool isTexturePack = TexturePackUtils::validate(localFileInfo);
+ auto type = ResourceUtils::identify(localFileInfo);
- if (!isResourcePack && !isTexturePack) { // probably instance/modpack
+ if (ResourceUtils::ValidResourceTypes.count(type) == 0) { // probably instance/modpack
addInstance(localFileName);
- break;
+ continue;
}
- ImportResourcePackDialog dlg(this);
+ ImportResourceDialog dlg(localFileName, type, this);
if (dlg.exec() != QDialog::Accepted)
- break;
+ continue;
- qDebug() << "Adding resource/texture pack" << localFileName << "to" << dlg.selectedInstanceKey;
+ qDebug() << "Adding resource" << localFileName << "to" << dlg.selectedInstanceKey;
auto inst = APPLICATION->instances()->getInstanceById(dlg.selectedInstanceKey);
auto minecraftInst = std::dynamic_pointer_cast<MinecraftInstance>(inst);
- if (isResourcePack)
- minecraftInst->resourcePackList()->installResource(localFileName);
- else if (isTexturePack)
- minecraftInst->texturePackList()->installResource(localFileName);
- break;
+
+ switch (type) {
+ case PackedResourceType::ResourcePack:
+ minecraftInst->resourcePackList()->installResource(localFileName);
+ break;
+ case PackedResourceType::TexturePack:
+ minecraftInst->texturePackList()->installResource(localFileName);
+ break;
+ case PackedResourceType::DataPack:
+ qWarning() << "Importing of Data Packs not supported at this time. Ignoring" << localFileName;
+ break;
+ case PackedResourceType::Mod:
+ minecraftInst->loaderModList()->installMod(localFileName);
+ break;
+ case PackedResourceType::ShaderPack:
+ minecraftInst->shaderPackList()->installResource(localFileName);
+ break;
+ case PackedResourceType::WorldSave:
+ minecraftInst->worldList()->installWorld(localFileInfo);
+ break;
+ case PackedResourceType::UNKNOWN:
+ default:
+ qDebug() << "Can't Identify" << localFileName << "Ignoring it.";
+ break;
+ }
}
}
@@ -2001,6 +1999,10 @@ void MainWindow::newsButtonClicked()
news_dialog.exec();
}
+void MainWindow::onCatChanged(int) {
+ setCatBackground(APPLICATION->settings()->get("TheCat").toBool());
+}
+
void MainWindow::on_actionAbout_triggered()
{
AboutDialog dialog(this);
@@ -2015,21 +2017,23 @@ void MainWindow::on_actionDeleteInstance_triggered()
auto id = m_selectedInstance->id();
- auto response =
- CustomMessageBox::selectable(this, tr("CAREFUL!"),
- tr("About to delete: %1\nThis may be permanent and will completely delete the instance.\n\nAre you sure?")
- .arg(m_selectedInstance->name()),
- QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No)
- ->exec();
+ auto response = CustomMessageBox::selectable(this, tr("Confirm Deletion"),
+ tr("You are about to delete \"%1\".\n"
+ "This may be permanent and will completely delete the instance.\n\n"
+ "Are you sure?")
+ .arg(m_selectedInstance->name()),
+ QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No)
+ ->exec();
- if (response == QMessageBox::Yes) {
- if (APPLICATION->instances()->trashInstance(id)) {
- ui->actionUndoTrashInstance->setEnabled(APPLICATION->instances()->trashedSomething());
- return;
- }
+ if (response != QMessageBox::Yes)
+ return;
- APPLICATION->instances()->deleteInstance(id);
+ if (APPLICATION->instances()->trashInstance(id)) {
+ ui->actionUndoTrashInstance->setEnabled(APPLICATION->instances()->trashedSomething());
+ return;
}
+
+ APPLICATION->instances()->deleteInstance(id);
}
void MainWindow::on_actionExportInstance_triggered()
diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h
index babc5e03..a36257a7 100644
--- a/launcher/ui/MainWindow.h
+++ b/launcher/ui/MainWindow.h
@@ -79,7 +79,7 @@ public:
void updatesAllowedChanged(bool allowed);
- void droppedURLs(QList<QUrl> urls);
+ void processURLs(QList<QUrl> urls);
signals:
void isClosing();
@@ -89,6 +89,8 @@ protected:
private slots:
void onCatToggled(bool);
+ void onCatChanged(int);
+
void on_actionAbout_triggered();
void on_actionAddInstance_triggered();
diff --git a/launcher/ui/WinDarkmode.cpp b/launcher/ui/WinDarkmode.cpp
deleted file mode 100644
index eac68e4f..00000000
--- a/launcher/ui/WinDarkmode.cpp
+++ /dev/null
@@ -1,32 +0,0 @@
-#include <QWidget>
-
-#include "WinDarkmode.h"
-
-namespace WinDarkmode {
-
-/* See https://github.com/statiolake/neovim-qt/commit/da8eaba7f0e38b6b51f3bacd02a8cc2d1f7a34d8 */
-void setDarkWinTitlebar(WId winid, bool darkmode)
-{
- HWND hwnd = reinterpret_cast<HWND>(winid);
- BOOL dark = (BOOL) darkmode;
-
- HMODULE hUxtheme = LoadLibraryExW(L"uxtheme.dll", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32);
- HMODULE hUser32 = GetModuleHandleW(L"user32.dll");
- fnAllowDarkModeForWindow AllowDarkModeForWindow
- = reinterpret_cast<fnAllowDarkModeForWindow>(GetProcAddress(hUxtheme, MAKEINTRESOURCEA(133)));
- fnSetPreferredAppMode SetPreferredAppMode
- = reinterpret_cast<fnSetPreferredAppMode>(GetProcAddress(hUxtheme, MAKEINTRESOURCEA(135)));
- fnSetWindowCompositionAttribute SetWindowCompositionAttribute
- = reinterpret_cast<fnSetWindowCompositionAttribute>(GetProcAddress(hUser32, "SetWindowCompositionAttribute"));
-
- SetPreferredAppMode(AllowDark);
- AllowDarkModeForWindow(hwnd, dark);
- WINDOWCOMPOSITIONATTRIBDATA data = {
- WCA_USEDARKMODECOLORS,
- &dark,
- sizeof(dark)
- };
- SetWindowCompositionAttribute(hwnd, &data);
-}
-
-}
diff --git a/launcher/ui/WinDarkmode.h b/launcher/ui/WinDarkmode.h
deleted file mode 100644
index 5b567c6b..00000000
--- a/launcher/ui/WinDarkmode.h
+++ /dev/null
@@ -1,60 +0,0 @@
-#pragma once
-
-#include <windows.h>
-#include <dwmapi.h>
-
-
-namespace WinDarkmode {
-
-void setDarkWinTitlebar(WId winid, bool darkmode);
-
-enum PreferredAppMode {
- Default,
- AllowDark,
- ForceDark,
- ForceLight,
- Max
-};
-
-enum WINDOWCOMPOSITIONATTRIB {
- WCA_UNDEFINED = 0,
- WCA_NCRENDERING_ENABLED = 1,
- WCA_NCRENDERING_POLICY = 2,
- WCA_TRANSITIONS_FORCEDISABLED = 3,
- WCA_ALLOW_NCPAINT = 4,
- WCA_CAPTION_BUTTON_BOUNDS = 5,
- WCA_NONCLIENT_RTL_LAYOUT = 6,
- WCA_FORCE_ICONIC_REPRESENTATION = 7,
- WCA_EXTENDED_FRAME_BOUNDS = 8,
- WCA_HAS_ICONIC_BITMAP = 9,
- WCA_THEME_ATTRIBUTES = 10,
- WCA_NCRENDERING_EXILED = 11,
- WCA_NCADORNMENTINFO = 12,
- WCA_EXCLUDED_FROM_LIVEPREVIEW = 13,
- WCA_VIDEO_OVERLAY_ACTIVE = 14,
- WCA_FORCE_ACTIVEWINDOW_APPEARANCE = 15,
- WCA_DISALLOW_PEEK = 16,
- WCA_CLOAK = 17,
- WCA_CLOAKED = 18,
- WCA_ACCENT_POLICY = 19,
- WCA_FREEZE_REPRESENTATION = 20,
- WCA_EVER_UNCLOAKED = 21,
- WCA_VISUAL_OWNER = 22,
- WCA_HOLOGRAPHIC = 23,
- WCA_EXCLUDED_FROM_DDA = 24,
- WCA_PASSIVEUPDATEMODE = 25,
- WCA_USEDARKMODECOLORS = 26,
- WCA_LAST = 27
-};
-
-struct WINDOWCOMPOSITIONATTRIBDATA {
- WINDOWCOMPOSITIONATTRIB Attrib;
- PVOID pvData;
- SIZE_T cbData;
-};
-
-using fnAllowDarkModeForWindow = BOOL (WINAPI *)(HWND hWnd, BOOL allow);
-using fnSetPreferredAppMode = PreferredAppMode (WINAPI *)(PreferredAppMode appMode);
-using fnSetWindowCompositionAttribute = BOOL (WINAPI *)(HWND hwnd, WINDOWCOMPOSITIONATTRIBDATA *);
-
-}
diff --git a/launcher/ui/dialogs/AboutDialog.cpp b/launcher/ui/dialogs/AboutDialog.cpp
index a36e4a3d..76e3d8ed 100644
--- a/launcher/ui/dialogs/AboutDialog.cpp
+++ b/launcher/ui/dialogs/AboutDialog.cpp
@@ -39,12 +39,11 @@
#include <QIcon>
#include "Application.h"
#include "BuildConfig.h"
+#include "Markdown.h"
#include <net/NetJob.h>
#include <qobject.h>
-#include "HoeDown.h"
-
namespace {
QString getLink(QString link, QString name) {
return QString("&lt;<a href='%1'>%2</a>&gt;").arg(link).arg(name);
@@ -114,10 +113,9 @@ QString getCreditsHtml()
QString getLicenseHtml()
{
- HoeDown hoedown;
QFile dataFile(":/documents/COPYING.md");
dataFile.open(QIODevice::ReadOnly);
- QString output = hoedown.process(dataFile.readAll());
+ QString output = markdownToHTML(dataFile.readAll());
return output;
}
diff --git a/launcher/ui/dialogs/BlockedModsDialog.cpp b/launcher/ui/dialogs/BlockedModsDialog.cpp
index 0480ced7..eb427953 100644
--- a/launcher/ui/dialogs/BlockedModsDialog.cpp
+++ b/launcher/ui/dialogs/BlockedModsDialog.cpp
@@ -234,7 +234,7 @@ void BlockedModsDialog::addHashTask(QString path)
/// @param path the path to the local file being hashed
void BlockedModsDialog::buildHashTask(QString path)
{
- auto hash_task = Hashing::createBlockedModHasher(path, ModPlatform::Provider::FLAME, "sha1");
+ auto hash_task = Hashing::createBlockedModHasher(path, ModPlatform::ResourceProvider::FLAME, "sha1");
qDebug() << "[Blocked Mods Dialog] Creating Hash task for path: " << path;
diff --git a/launcher/ui/dialogs/ChooseProviderDialog.cpp b/launcher/ui/dialogs/ChooseProviderDialog.cpp
index 89935d9a..83748e1e 100644
--- a/launcher/ui/dialogs/ChooseProviderDialog.cpp
+++ b/launcher/ui/dialogs/ChooseProviderDialog.cpp
@@ -67,9 +67,9 @@ void ChooseProviderDialog::confirmAll()
accept();
}
-auto ChooseProviderDialog::getSelectedProvider() const -> ModPlatform::Provider
+auto ChooseProviderDialog::getSelectedProvider() const -> ModPlatform::ResourceProvider
{
- return ModPlatform::Provider(m_providers.checkedId());
+ return ModPlatform::ResourceProvider(m_providers.checkedId());
}
void ChooseProviderDialog::addProviders()
@@ -77,7 +77,7 @@ void ChooseProviderDialog::addProviders()
int btn_index = 0;
QRadioButton* btn;
- for (auto& provider : { ModPlatform::Provider::MODRINTH, ModPlatform::Provider::FLAME }) {
+ for (auto& provider : { ModPlatform::ResourceProvider::MODRINTH, ModPlatform::ResourceProvider::FLAME }) {
btn = new QRadioButton(ProviderCaps.readableName(provider), this);
m_providers.addButton(btn, btn_index++);
ui->providersLayout->addWidget(btn);
diff --git a/launcher/ui/dialogs/ChooseProviderDialog.h b/launcher/ui/dialogs/ChooseProviderDialog.h
index 4a3b9f29..be9735b5 100644
--- a/launcher/ui/dialogs/ChooseProviderDialog.h
+++ b/launcher/ui/dialogs/ChooseProviderDialog.h
@@ -8,7 +8,7 @@ class ChooseProviderDialog;
}
namespace ModPlatform {
-enum class Provider;
+enum class ResourceProvider;
}
class Mod;
@@ -24,7 +24,7 @@ class ChooseProviderDialog : public QDialog {
bool try_others = false;
- ModPlatform::Provider chosen;
+ ModPlatform::ResourceProvider chosen;
};
public:
@@ -45,7 +45,7 @@ class ChooseProviderDialog : public QDialog {
void addProviders();
void disableInput();
- auto getSelectedProvider() const -> ModPlatform::Provider;
+ auto getSelectedProvider() const -> ModPlatform::ResourceProvider;
private:
Ui::ChooseProviderDialog* ui;
diff --git a/launcher/ui/dialogs/IconPickerDialog.cpp b/launcher/ui/dialogs/IconPickerDialog.cpp
index 0551a1ef..5131686a 100644
--- a/launcher/ui/dialogs/IconPickerDialog.cpp
+++ b/launcher/ui/dialogs/IconPickerDialog.cpp
@@ -47,7 +47,6 @@ IconPickerDialog::IconPickerDialog(QWidget *parent)
contentsWidget->setUniformItemSizes(true);
contentsWidget->setTextElideMode(Qt::ElideRight);
contentsWidget->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
- contentsWidget->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
contentsWidget->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
contentsWidget->setItemDelegate(new ListViewDelegate());
diff --git a/launcher/ui/dialogs/ImportResourcePackDialog.cpp b/launcher/ui/dialogs/ImportResourceDialog.cpp
index e807e926..84b69273 100644
--- a/launcher/ui/dialogs/ImportResourcePackDialog.cpp
+++ b/launcher/ui/dialogs/ImportResourceDialog.cpp
@@ -1,5 +1,5 @@
-#include "ImportResourcePackDialog.h"
-#include "ui_ImportResourcePackDialog.h"
+#include "ImportResourceDialog.h"
+#include "ui_ImportResourceDialog.h"
#include <QFileDialog>
#include <QPushButton>
@@ -8,10 +8,11 @@
#include "InstanceList.h"
#include <InstanceList.h>
-#include "ui/instanceview/InstanceProxyModel.h"
#include "ui/instanceview/InstanceDelegate.h"
+#include "ui/instanceview/InstanceProxyModel.h"
-ImportResourcePackDialog::ImportResourcePackDialog(QWidget* parent) : QDialog(parent), ui(new Ui::ImportResourcePackDialog)
+ImportResourceDialog::ImportResourceDialog(QString file_path, PackedResourceType type, QWidget* parent)
+ : QDialog(parent), ui(new Ui::ImportResourceDialog), m_resource_type(type), m_file_path(file_path)
{
ui->setupUi(this);
setWindowModality(Qt::WindowModal);
@@ -29,7 +30,6 @@ ImportResourcePackDialog::ImportResourcePackDialog(QWidget* parent) : QDialog(pa
// NOTE: We can't have uniform sizes because the text may wrap if it's too long. If we set this, it will cut off the wrapped text.
contentsWidget->setUniformItemSizes(false);
contentsWidget->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
- contentsWidget->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
contentsWidget->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
contentsWidget->setItemDelegate(new ListViewDelegate());
@@ -41,15 +41,19 @@ ImportResourcePackDialog::ImportResourcePackDialog(QWidget* parent) : QDialog(pa
connect(contentsWidget, SIGNAL(doubleClicked(QModelIndex)), SLOT(activated(QModelIndex)));
connect(contentsWidget->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)),
SLOT(selectionChanged(QItemSelection, QItemSelection)));
+
+ ui->label->setText(
+ tr("Choose the instance you would like to import this %1 to.").arg(ResourceUtils::getPackedTypeName(m_resource_type)));
+ ui->label_file_path->setText(tr("File: %1").arg(m_file_path));
}
-void ImportResourcePackDialog::activated(QModelIndex index)
+void ImportResourceDialog::activated(QModelIndex index)
{
selectedInstanceKey = index.data(InstanceList::InstanceIDRole).toString();
accept();
}
-void ImportResourcePackDialog::selectionChanged(QItemSelection selected, QItemSelection deselected)
+void ImportResourceDialog::selectionChanged(QItemSelection selected, QItemSelection deselected)
{
if (selected.empty())
return;
@@ -60,7 +64,7 @@ void ImportResourcePackDialog::selectionChanged(QItemSelection selected, QItemSe
}
}
-ImportResourcePackDialog::~ImportResourcePackDialog()
+ImportResourceDialog::~ImportResourceDialog()
{
delete ui;
}
diff --git a/launcher/ui/dialogs/ImportResourceDialog.h b/launcher/ui/dialogs/ImportResourceDialog.h
new file mode 100644
index 00000000..5f2f7a92
--- /dev/null
+++ b/launcher/ui/dialogs/ImportResourceDialog.h
@@ -0,0 +1,30 @@
+#pragma once
+
+#include <QDialog>
+#include <QItemSelection>
+
+#include "minecraft/mod/tasks/LocalResourceParse.h"
+#include "ui/instanceview/InstanceProxyModel.h"
+
+namespace Ui {
+class ImportResourceDialog;
+}
+
+class ImportResourceDialog : public QDialog {
+ Q_OBJECT
+
+ public:
+ explicit ImportResourceDialog(QString file_path, PackedResourceType type, QWidget* parent = nullptr);
+ ~ImportResourceDialog() override;
+ QString selectedInstanceKey;
+
+ private:
+ Ui::ImportResourceDialog* ui;
+ PackedResourceType m_resource_type;
+ QString m_file_path;
+ InstanceProxyModel* proxyModel;
+
+ private slots:
+ void selectionChanged(QItemSelection, QItemSelection);
+ void activated(QModelIndex);
+};
diff --git a/launcher/ui/dialogs/ImportResourcePackDialog.ui b/launcher/ui/dialogs/ImportResourceDialog.ui
index 20cb9177..cc3f4ec1 100644
--- a/launcher/ui/dialogs/ImportResourcePackDialog.ui
+++ b/launcher/ui/dialogs/ImportResourceDialog.ui
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
- <class>ImportResourcePackDialog</class>
- <widget class="QDialog" name="ImportResourcePackDialog">
+ <class>ImportResourceDialog</class>
+ <widget class="QDialog" name="ImportResourceDialog">
<property name="geometry">
<rect>
<x>0</x>
@@ -11,7 +11,7 @@
</rect>
</property>
<property name="windowTitle">
- <string>Choose instance to import</string>
+ <string>Choose instance to import to</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
@@ -22,6 +22,13 @@
</widget>
</item>
<item>
+ <widget class="QLabel" name="label_file_path">
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ <item>
<widget class="QListView" name="instanceView"/>
</item>
<item>
@@ -41,7 +48,7 @@
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
- <receiver>ImportResourcePackDialog</receiver>
+ <receiver>ImportResourceDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
@@ -57,7 +64,7 @@
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
- <receiver>ImportResourcePackDialog</receiver>
+ <receiver>ImportResourceDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
diff --git a/launcher/ui/dialogs/ImportResourcePackDialog.h b/launcher/ui/dialogs/ImportResourcePackDialog.h
deleted file mode 100644
index 8356f204..00000000
--- a/launcher/ui/dialogs/ImportResourcePackDialog.h
+++ /dev/null
@@ -1,27 +0,0 @@
-#pragma once
-
-#include <QDialog>
-#include <QItemSelection>
-
-#include "ui/instanceview/InstanceProxyModel.h"
-
-namespace Ui {
-class ImportResourcePackDialog;
-}
-
-class ImportResourcePackDialog : public QDialog {
- Q_OBJECT
-
- public:
- explicit ImportResourcePackDialog(QWidget* parent = 0);
- ~ImportResourcePackDialog();
- InstanceProxyModel* proxyModel;
- QString selectedInstanceKey;
-
- private:
- Ui::ImportResourcePackDialog* ui;
-
- private slots:
- void selectionChanged(QItemSelection, QItemSelection);
- void activated(QModelIndex);
-};
diff --git a/launcher/ui/dialogs/ModDownloadDialog.cpp b/launcher/ui/dialogs/ModDownloadDialog.cpp
deleted file mode 100644
index 24d23ba9..00000000
--- a/launcher/ui/dialogs/ModDownloadDialog.cpp
+++ /dev/null
@@ -1,202 +0,0 @@
-// SPDX-License-Identifier: GPL-3.0-only
-/*
- * Prism Launcher - Minecraft Launcher
- * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
- * Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
-
-#include "ModDownloadDialog.h"
-
-#include <BaseVersion.h>
-#include <InstanceList.h>
-#include <icons/IconList.h>
-
-#include "Application.h"
-#include "ReviewMessageBox.h"
-
-#include <QDialogButtonBox>
-#include <QLayout>
-#include <QPushButton>
-#include <QValidator>
-
-#include "ModDownloadTask.h"
-#include "ui/pages/modplatform/flame/FlameModPage.h"
-#include "ui/pages/modplatform/modrinth/ModrinthModPage.h"
-#include "ui/widgets/PageContainer.h"
-
-ModDownloadDialog::ModDownloadDialog(const std::shared_ptr<ModFolderModel>& mods, QWidget* parent, BaseInstance* instance)
- : QDialog(parent), mods(mods), m_verticalLayout(new QVBoxLayout(this)), m_instance(instance)
-{
- setObjectName(QStringLiteral("ModDownloadDialog"));
- m_verticalLayout->setObjectName(QStringLiteral("verticalLayout"));
-
- resize(std::max(0.5 * parent->width(), 400.0), std::max(0.75 * parent->height(), 400.0));
-
- setWindowIcon(APPLICATION->getThemedIcon("new"));
- // NOTE: m_buttons must be initialized before PageContainer, because it indirectly accesses m_buttons through setSuggestedPack! Do not
- // move this below.
- m_buttons = new QDialogButtonBox(QDialogButtonBox::Help | QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
-
- m_container = new PageContainer(this);
- m_container->setSizePolicy(QSizePolicy::Policy::Preferred, QSizePolicy::Policy::Expanding);
- m_container->layout()->setContentsMargins(0, 0, 0, 0);
- m_verticalLayout->addWidget(m_container);
-
- m_container->addButtons(m_buttons);
-
- connect(m_container, &PageContainer::selectedPageChanged, this, &ModDownloadDialog::selectedPageChanged);
-
- // Bonk Qt over its stupid head and make sure it understands which button is the default one...
- // See: https://stackoverflow.com/questions/24556831/qbuttonbox-set-default-button
- auto OkButton = m_buttons->button(QDialogButtonBox::Ok);
- OkButton->setEnabled(false);
- OkButton->setDefault(true);
- OkButton->setAutoDefault(true);
- OkButton->setText(tr("Review and confirm"));
- OkButton->setShortcut(tr("Ctrl+Return"));
- OkButton->setToolTip(tr("Opens a new popup to review your selected mods and confirm your selection. Shortcut: Ctrl+Return"));
- connect(OkButton, &QPushButton::clicked, this, &ModDownloadDialog::confirm);
-
- auto CancelButton = m_buttons->button(QDialogButtonBox::Cancel);
- CancelButton->setDefault(false);
- CancelButton->setAutoDefault(false);
- connect(CancelButton, &QPushButton::clicked, this, &ModDownloadDialog::reject);
-
- auto HelpButton = m_buttons->button(QDialogButtonBox::Help);
- HelpButton->setDefault(false);
- HelpButton->setAutoDefault(false);
- connect(HelpButton, &QPushButton::clicked, m_container, &PageContainer::help);
-
- QMetaObject::connectSlotsByName(this);
- setWindowModality(Qt::WindowModal);
- setWindowTitle(dialogTitle());
-
- restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("ModDownloadGeometry").toByteArray()));
-}
-
-QString ModDownloadDialog::dialogTitle()
-{
- return tr("Download mods");
-}
-
-void ModDownloadDialog::reject()
-{
- APPLICATION->settings()->set("ModDownloadGeometry", saveGeometry().toBase64());
- QDialog::reject();
-}
-
-void ModDownloadDialog::confirm()
-{
- auto keys = modTask.keys();
- keys.sort(Qt::CaseInsensitive);
-
- auto confirm_dialog = ReviewMessageBox::create(this, tr("Confirm mods to download"));
-
- for (auto& task : keys) {
- confirm_dialog->appendMod({ task, modTask.find(task).value()->getFilename() });
- }
-
- if (confirm_dialog->exec()) {
- auto deselected = confirm_dialog->deselectedMods();
- for (auto name : deselected) {
- modTask.remove(name);
- }
-
- this->accept();
- }
-}
-
-void ModDownloadDialog::accept()
-{
- APPLICATION->settings()->set("ModDownloadGeometry", saveGeometry().toBase64());
- QDialog::accept();
-}
-
-QList<BasePage*> ModDownloadDialog::getPages()
-{
- QList<BasePage*> pages;
-
- pages.append(ModrinthModPage::create(this, m_instance));
- if (APPLICATION->capabilities() & Application::SupportsFlame)
- pages.append(FlameModPage::create(this, m_instance));
-
- m_selectedPage = dynamic_cast<ModPage*>(pages[0]);
-
- return pages;
-}
-
-void ModDownloadDialog::addSelectedMod(QString name, ModDownloadTask* task)
-{
- removeSelectedMod(name);
- modTask.insert(name, task);
-
- m_buttons->button(QDialogButtonBox::Ok)->setEnabled(!modTask.isEmpty());
-}
-
-void ModDownloadDialog::removeSelectedMod(QString name)
-{
- if (modTask.contains(name))
- delete modTask.find(name).value();
- modTask.remove(name);
-
- m_buttons->button(QDialogButtonBox::Ok)->setEnabled(!modTask.isEmpty());
-}
-
-bool ModDownloadDialog::isModSelected(QString name, QString filename) const
-{
- // FIXME: Is there a way to check for versions without checking the filename
- // as a heuristic, other than adding such info to ModDownloadTask itself?
- auto iter = modTask.find(name);
- return iter != modTask.end() && (iter.value()->getFilename() == filename);
-}
-
-bool ModDownloadDialog::isModSelected(QString name) const
-{
- auto iter = modTask.find(name);
- return iter != modTask.end();
-}
-
-const QList<ModDownloadTask*> ModDownloadDialog::getTasks()
-{
- return modTask.values();
-}
-
-void ModDownloadDialog::selectedPageChanged(BasePage* previous, BasePage* selected)
-{
- auto* prev_page = dynamic_cast<ModPage*>(previous);
- if (!prev_page) {
- qCritical() << "Page '" << previous->displayName() << "' in ModDownloadDialog is not a ModPage!";
- return;
- }
-
- m_selectedPage = dynamic_cast<ModPage*>(selected);
- if (!m_selectedPage) {
- qCritical() << "Page '" << selected->displayName() << "' in ModDownloadDialog is not a ModPage!";
- return;
- }
-
- // Same effect as having a global search bar
- m_selectedPage->setSearchTerm(prev_page->getSearchTerm());
-}
-
-bool ModDownloadDialog::selectPage(QString pageId)
-{
- return m_container->selectPage(pageId);
-}
-
-ModPage* ModDownloadDialog::getSelectedPage()
-{
- return m_selectedPage;
-}
diff --git a/launcher/ui/dialogs/ModDownloadDialog.h b/launcher/ui/dialogs/ModDownloadDialog.h
deleted file mode 100644
index fcf6f4fc..00000000
--- a/launcher/ui/dialogs/ModDownloadDialog.h
+++ /dev/null
@@ -1,78 +0,0 @@
-// SPDX-License-Identifier: GPL-3.0-only
-/*
- * Prism Launcher - Minecraft Launcher
- * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
- * Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
-
-#pragma once
-
-#include <QDialog>
-#include <QVBoxLayout>
-
-#include "ModDownloadTask.h"
-#include "minecraft/mod/ModFolderModel.h"
-#include "ui/pages/BasePageProvider.h"
-
-namespace Ui
-{
-class ModDownloadDialog;
-}
-
-class PageContainer;
-class QDialogButtonBox;
-class ModPage;
-class ModrinthModPage;
-
-class ModDownloadDialog final : public QDialog, public BasePageProvider
-{
- Q_OBJECT
-
- public:
- explicit ModDownloadDialog(const std::shared_ptr<ModFolderModel>& mods, QWidget* parent, BaseInstance* instance);
- ~ModDownloadDialog() override = default;
-
- QString dialogTitle() override;
- QList<BasePage*> getPages() override;
-
- void addSelectedMod(QString name = QString(), ModDownloadTask* task = nullptr);
- void removeSelectedMod(QString name = QString());
- bool isModSelected(QString name, QString filename) const;
- bool isModSelected(QString name) const;
-
- const QList<ModDownloadTask*> getTasks();
- const std::shared_ptr<ModFolderModel>& mods;
-
- bool selectPage(QString pageId);
- ModPage* getSelectedPage();
-
- public slots:
- void confirm();
- void accept() override;
- void reject() override;
-
- private slots:
- void selectedPageChanged(BasePage* previous, BasePage* selected);
-
- private:
- Ui::ModDownloadDialog* ui = nullptr;
- PageContainer* m_container = nullptr;
- QDialogButtonBox* m_buttons = nullptr;
- QVBoxLayout* m_verticalLayout = nullptr;
- ModPage* m_selectedPage = nullptr;
-
- QHash<QString, ModDownloadTask*> modTask;
- BaseInstance* m_instance;
-};
diff --git a/launcher/ui/dialogs/ModUpdateDialog.cpp b/launcher/ui/dialogs/ModUpdateDialog.cpp
index cedd4a96..4ef42d6c 100644
--- a/launcher/ui/dialogs/ModUpdateDialog.cpp
+++ b/launcher/ui/dialogs/ModUpdateDialog.cpp
@@ -7,6 +7,7 @@
#include "FileSystem.h"
#include "Json.h"
+#include "Markdown.h"
#include "tasks/ConcurrentTask.h"
@@ -17,10 +18,11 @@
#include "modplatform/flame/FlameCheckUpdate.h"
#include "modplatform/modrinth/ModrinthCheckUpdate.h"
-#include <HoeDown.h>
#include <QTextBrowser>
#include <QTreeWidgetItem>
+#include <optional>
+
static ModPlatform::ProviderCapabilities ProviderCaps;
static std::list<Version> mcVersions(BaseInstance* inst)
@@ -28,7 +30,7 @@ static std::list<Version> mcVersions(BaseInstance* inst)
return { static_cast<MinecraftInstance*>(inst)->getPackProfile()->getComponent("net.minecraft")->getVersion() };
}
-static ModAPI::ModLoaderTypes mcLoaders(BaseInstance* inst)
+static std::optional<ResourceAPI::ModLoaderTypes> mcLoaders(BaseInstance* inst)
{
return { static_cast<MinecraftInstance*>(inst)->getPackProfile()->getModLoaders() };
}
@@ -212,14 +214,14 @@ auto ModUpdateDialog::ensureMetadata() -> bool
bool confirm_rest = false;
bool try_others_rest = false;
bool skip_rest = false;
- ModPlatform::Provider provider_rest = ModPlatform::Provider::MODRINTH;
+ ModPlatform::ResourceProvider provider_rest = ModPlatform::ResourceProvider::MODRINTH;
- auto addToTmp = [&](Mod* m, ModPlatform::Provider p) {
+ auto addToTmp = [&](Mod* m, ModPlatform::ResourceProvider p) {
switch (p) {
- case ModPlatform::Provider::MODRINTH:
+ case ModPlatform::ResourceProvider::MODRINTH:
modrinth_tmp.push_back(m);
break;
- case ModPlatform::Provider::FLAME:
+ case ModPlatform::ResourceProvider::FLAME:
flame_tmp.push_back(m);
break;
}
@@ -264,10 +266,10 @@ auto ModUpdateDialog::ensureMetadata() -> bool
}
if (!modrinth_tmp.empty()) {
- auto* modrinth_task = new EnsureMetadataTask(modrinth_tmp, index_dir, ModPlatform::Provider::MODRINTH);
+ auto* modrinth_task = new EnsureMetadataTask(modrinth_tmp, index_dir, ModPlatform::ResourceProvider::MODRINTH);
connect(modrinth_task, &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); });
connect(modrinth_task, &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Mod* candidate) {
- onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::Provider::MODRINTH);
+ onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::ResourceProvider::MODRINTH);
});
if (modrinth_task->getHashingTask())
@@ -277,10 +279,10 @@ auto ModUpdateDialog::ensureMetadata() -> bool
}
if (!flame_tmp.empty()) {
- auto* flame_task = new EnsureMetadataTask(flame_tmp, index_dir, ModPlatform::Provider::FLAME);
+ auto* flame_task = new EnsureMetadataTask(flame_tmp, index_dir, ModPlatform::ResourceProvider::FLAME);
connect(flame_task, &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); });
connect(flame_task, &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Mod* candidate) {
- onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::Provider::FLAME);
+ onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::ResourceProvider::FLAME);
});
if (flame_task->getHashingTask())
@@ -306,28 +308,28 @@ void ModUpdateDialog::onMetadataEnsured(Mod* mod)
return;
switch (mod->metadata()->provider) {
- case ModPlatform::Provider::MODRINTH:
+ case ModPlatform::ResourceProvider::MODRINTH:
m_modrinth_to_update.push_back(mod);
break;
- case ModPlatform::Provider::FLAME:
+ case ModPlatform::ResourceProvider::FLAME:
m_flame_to_update.push_back(mod);
break;
}
}
-ModPlatform::Provider next(ModPlatform::Provider p)
+ModPlatform::ResourceProvider next(ModPlatform::ResourceProvider p)
{
switch (p) {
- case ModPlatform::Provider::MODRINTH:
- return ModPlatform::Provider::FLAME;
- case ModPlatform::Provider::FLAME:
- return ModPlatform::Provider::MODRINTH;
+ case ModPlatform::ResourceProvider::MODRINTH:
+ return ModPlatform::ResourceProvider::FLAME;
+ case ModPlatform::ResourceProvider::FLAME:
+ return ModPlatform::ResourceProvider::MODRINTH;
}
- return ModPlatform::Provider::FLAME;
+ return ModPlatform::ResourceProvider::FLAME;
}
-void ModUpdateDialog::onMetadataFailed(Mod* mod, bool try_others, ModPlatform::Provider first_choice)
+void ModUpdateDialog::onMetadataFailed(Mod* mod, bool try_others, ModPlatform::ResourceProvider first_choice)
{
if (try_others) {
auto index_dir = indexDir();
@@ -368,15 +370,8 @@ void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info)
QString text = info.changelog;
switch (info.provider) {
- case ModPlatform::Provider::MODRINTH: {
- HoeDown h;
- // HoeDown bug?: \n aren't converted to <br>
- text = h.process(info.changelog.toUtf8());
-
- // Don't convert if there's an HTML tag right after (Qt rendering weirdness)
- text.remove(QRegularExpression("(\n+)(?=<)"));
- text.replace('\n', "<br>");
-
+ case ModPlatform::ResourceProvider::MODRINTH: {
+ text = markdownToHTML(info.changelog.toUtf8());
break;
}
default:
@@ -393,9 +388,9 @@ void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info)
ui->modTreeWidget->addTopLevelItem(item_top);
}
-auto ModUpdateDialog::getTasks() -> const QList<ModDownloadTask*>
+auto ModUpdateDialog::getTasks() -> const QList<ResourceDownloadTask*>
{
- QList<ModDownloadTask*> list;
+ QList<ResourceDownloadTask*> list;
auto* item = ui->modTreeWidget->topLevelItem(0);
diff --git a/launcher/ui/dialogs/ModUpdateDialog.h b/launcher/ui/dialogs/ModUpdateDialog.h
index bd486f0d..3e3dd90d 100644
--- a/launcher/ui/dialogs/ModUpdateDialog.h
+++ b/launcher/ui/dialogs/ModUpdateDialog.h
@@ -1,7 +1,7 @@
#pragma once
#include "BaseInstance.h"
-#include "ModDownloadTask.h"
+#include "ResourceDownloadTask.h"
#include "ReviewMessageBox.h"
#include "minecraft/mod/ModFolderModel.h"
@@ -25,7 +25,7 @@ class ModUpdateDialog final : public ReviewMessageBox {
void appendMod(const CheckUpdateTask::UpdatableMod& info);
- const QList<ModDownloadTask*> getTasks();
+ const QList<ResourceDownloadTask*> getTasks();
auto indexDir() const -> QDir { return m_mod_model->indexDir(); }
auto noUpdates() const -> bool { return m_no_updates; };
@@ -36,7 +36,7 @@ class ModUpdateDialog final : public ReviewMessageBox {
private slots:
void onMetadataEnsured(Mod*);
- void onMetadataFailed(Mod*, bool try_others = false, ModPlatform::Provider first_choice = ModPlatform::Provider::MODRINTH);
+ void onMetadataFailed(Mod*, bool try_others = false, ModPlatform::ResourceProvider first_choice = ModPlatform::ResourceProvider::MODRINTH);
private:
QWidget* m_parent;
@@ -54,7 +54,7 @@ class ModUpdateDialog final : public ReviewMessageBox {
QList<std::tuple<Mod*, QString>> m_failed_metadata;
QList<std::tuple<Mod*, QString, QUrl>> m_failed_check_update;
- QHash<QString, ModDownloadTask*> m_tasks;
+ QHash<QString, ResourceDownloadTask*> m_tasks;
BaseInstance* m_instance;
bool m_no_updates = false;
diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.cpp b/launcher/ui/dialogs/ResourceDownloadDialog.cpp
new file mode 100644
index 00000000..b9367c16
--- /dev/null
+++ b/launcher/ui/dialogs/ResourceDownloadDialog.cpp
@@ -0,0 +1,232 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "ResourceDownloadDialog.h"
+
+#include <QPushButton>
+
+#include "Application.h"
+#include "ResourceDownloadTask.h"
+
+#include "minecraft/mod/ModFolderModel.h"
+
+#include "ui/dialogs/ReviewMessageBox.h"
+
+#include "ui/pages/modplatform/ResourcePage.h"
+
+#include "ui/pages/modplatform/flame/FlameResourcePages.h"
+#include "ui/pages/modplatform/modrinth/ModrinthResourcePages.h"
+
+#include "ui/widgets/PageContainer.h"
+
+namespace ResourceDownload {
+
+ResourceDownloadDialog::ResourceDownloadDialog(QWidget* parent, const std::shared_ptr<ResourceFolderModel> base_model)
+ : QDialog(parent), m_base_model(base_model), m_buttons(QDialogButtonBox::Help | QDialogButtonBox::Ok | QDialogButtonBox::Cancel), m_vertical_layout(this)
+{
+ setObjectName(QStringLiteral("ResourceDownloadDialog"));
+
+ resize(std::max(0.5 * parent->width(), 400.0), std::max(0.75 * parent->height(), 400.0));
+
+ setWindowIcon(APPLICATION->getThemedIcon("new"));
+
+ // Bonk Qt over its stupid head and make sure it understands which button is the default one...
+ // See: https://stackoverflow.com/questions/24556831/qbuttonbox-set-default-button
+ auto OkButton = m_buttons.button(QDialogButtonBox::Ok);
+ OkButton->setEnabled(false);
+ OkButton->setDefault(true);
+ OkButton->setAutoDefault(true);
+ OkButton->setText(tr("Review and confirm"));
+ OkButton->setShortcut(tr("Ctrl+Return"));
+
+ auto CancelButton = m_buttons.button(QDialogButtonBox::Cancel);
+ CancelButton->setDefault(false);
+ CancelButton->setAutoDefault(false);
+
+ auto HelpButton = m_buttons.button(QDialogButtonBox::Help);
+ HelpButton->setDefault(false);
+ HelpButton->setAutoDefault(false);
+
+ setWindowModality(Qt::WindowModal);
+}
+
+void ResourceDownloadDialog::accept()
+{
+ if (!geometrySaveKey().isEmpty())
+ APPLICATION->settings()->set(geometrySaveKey(), saveGeometry().toBase64());
+
+ QDialog::accept();
+}
+
+void ResourceDownloadDialog::reject()
+{
+ if (!geometrySaveKey().isEmpty())
+ APPLICATION->settings()->set(geometrySaveKey(), saveGeometry().toBase64());
+
+ QDialog::reject();
+}
+
+// NOTE: We can't have this in the ctor because PageContainer calls a virtual function, and so
+// won't work with subclasses if we put it in this ctor.
+void ResourceDownloadDialog::initializeContainer()
+{
+ m_container = new PageContainer(this);
+ m_container->setSizePolicy(QSizePolicy::Policy::Preferred, QSizePolicy::Policy::Expanding);
+ m_container->layout()->setContentsMargins(0, 0, 0, 0);
+ m_vertical_layout.addWidget(m_container);
+
+ m_container->addButtons(&m_buttons);
+
+ connect(m_container, &PageContainer::selectedPageChanged, this, &ResourceDownloadDialog::selectedPageChanged);
+}
+
+void ResourceDownloadDialog::connectButtons()
+{
+ auto OkButton = m_buttons.button(QDialogButtonBox::Ok);
+ OkButton->setToolTip(tr("Opens a new popup to review your selected %1 and confirm your selection. Shortcut: Ctrl+Return").arg(resourcesString()));
+ connect(OkButton, &QPushButton::clicked, this, &ResourceDownloadDialog::confirm);
+
+ auto CancelButton = m_buttons.button(QDialogButtonBox::Cancel);
+ connect(CancelButton, &QPushButton::clicked, this, &ResourceDownloadDialog::reject);
+
+ auto HelpButton = m_buttons.button(QDialogButtonBox::Help);
+ connect(HelpButton, &QPushButton::clicked, m_container, &PageContainer::help);
+}
+
+void ResourceDownloadDialog::confirm()
+{
+ auto keys = m_selected.keys();
+ keys.sort(Qt::CaseInsensitive);
+
+ auto confirm_dialog = ReviewMessageBox::create(this, tr("Confirm %1 to download").arg(resourcesString()));
+ confirm_dialog->retranslateUi(resourcesString());
+
+ for (auto& task : keys) {
+ auto selected = m_selected.constFind(task).value();
+ confirm_dialog->appendResource({ task, selected->getFilename(), selected->getCustomPath() });
+ }
+
+ if (confirm_dialog->exec()) {
+ auto deselected = confirm_dialog->deselectedResources();
+ for (auto name : deselected) {
+ m_selected.remove(name);
+ }
+
+ this->accept();
+ }
+}
+
+bool ResourceDownloadDialog::selectPage(QString pageId)
+{
+ return m_container->selectPage(pageId);
+}
+
+ResourcePage* ResourceDownloadDialog::getSelectedPage()
+{
+ return m_selectedPage;
+}
+
+void ResourceDownloadDialog::addResource(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, bool is_indexed)
+{
+ removeResource(pack, ver);
+
+ ver.is_currently_selected = true;
+ m_selected.insert(pack.name, new ResourceDownloadTask(pack, ver, getBaseModel(), is_indexed));
+
+ m_buttons.button(QDialogButtonBox::Ok)->setEnabled(!m_selected.isEmpty());
+}
+
+static ModPlatform::IndexedVersion& getVersionWithID(ModPlatform::IndexedPack& pack, QVariant id)
+{
+ Q_ASSERT(pack.versionsLoaded);
+ auto it = std::find_if(pack.versions.begin(), pack.versions.end(), [id](auto const& v) { return v.fileId == id; });
+ Q_ASSERT(it != pack.versions.end());
+ return *it;
+}
+
+void ResourceDownloadDialog::removeResource(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver)
+{
+ if (auto selected_task_it = m_selected.find(pack.name); selected_task_it != m_selected.end()) {
+ auto selected_task = *selected_task_it;
+ auto old_version_id = selected_task->getVersionID();
+
+ // If the new and old version IDs don't match, search for the old one and deselect it.
+ if (ver.fileId != old_version_id)
+ getVersionWithID(pack, old_version_id).is_currently_selected = false;
+ }
+
+ // Deselect the new version too, since all versions of that pack got removed.
+ ver.is_currently_selected = false;
+
+ m_selected.remove(pack.name);
+
+ m_buttons.button(QDialogButtonBox::Ok)->setEnabled(!m_selected.isEmpty());
+}
+
+const QList<ResourceDownloadDialog::DownloadTaskPtr> ResourceDownloadDialog::getTasks()
+{
+ return m_selected.values();
+}
+
+void ResourceDownloadDialog::selectedPageChanged(BasePage* previous, BasePage* selected)
+{
+ auto* prev_page = dynamic_cast<ResourcePage*>(previous);
+ if (!prev_page) {
+ qCritical() << "Page '" << previous->displayName() << "' in ResourceDownloadDialog is not a ResourcePage!";
+ return;
+ }
+
+ m_selectedPage = dynamic_cast<ResourcePage*>(selected);
+ if (!m_selectedPage) {
+ qCritical() << "Page '" << selected->displayName() << "' in ResourceDownloadDialog is not a ResourcePage!";
+ return;
+ }
+
+ // Same effect as having a global search bar
+ m_selectedPage->setSearchTerm(prev_page->getSearchTerm());
+}
+
+
+
+ModDownloadDialog::ModDownloadDialog(QWidget* parent, const std::shared_ptr<ModFolderModel>& mods, BaseInstance* instance)
+ : ResourceDownloadDialog(parent, mods), m_instance(instance)
+{
+ setWindowTitle(dialogTitle());
+
+ initializeContainer();
+ connectButtons();
+
+ if (!geometrySaveKey().isEmpty())
+ restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get(geometrySaveKey()).toByteArray()));
+}
+
+QList<BasePage*> ModDownloadDialog::getPages()
+{
+ QList<BasePage*> pages;
+
+ pages.append(ModrinthModPage::create(this, *m_instance));
+ if (APPLICATION->capabilities() & Application::SupportsFlame)
+ pages.append(FlameModPage::create(this, *m_instance));
+
+ m_selectedPage = dynamic_cast<ModPage*>(pages[0]);
+
+ return pages;
+}
+
+} // namespace ResourceDownload
diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.h b/launcher/ui/dialogs/ResourceDownloadDialog.h
new file mode 100644
index 00000000..19843532
--- /dev/null
+++ b/launcher/ui/dialogs/ResourceDownloadDialog.h
@@ -0,0 +1,111 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <QDialog>
+#include <QDialogButtonBox>
+#include <QHash>
+#include <QLayout>
+
+#include "QObjectPtr.h"
+#include "modplatform/ModIndex.h"
+#include "ui/pages/BasePageProvider.h"
+
+class BaseInstance;
+class ModFolderModel;
+class PageContainer;
+class QVBoxLayout;
+class QDialogButtonBox;
+class ResourceDownloadTask;
+class ResourceFolderModel;
+
+namespace ResourceDownload {
+
+class ResourcePage;
+
+class ResourceDownloadDialog : public QDialog, public BasePageProvider {
+ Q_OBJECT
+
+ public:
+ using DownloadTaskPtr = shared_qobject_ptr<ResourceDownloadTask>;
+
+ ResourceDownloadDialog(QWidget* parent, const std::shared_ptr<ResourceFolderModel> base_model);
+
+ void initializeContainer();
+ void connectButtons();
+
+ //: String that gets appended to the download dialog title ("Download " + resourcesString())
+ [[nodiscard]] virtual QString resourcesString() const { return tr("resources"); }
+
+ QString dialogTitle() override { return tr("Download %1").arg(resourcesString()); };
+
+ bool selectPage(QString pageId);
+ ResourcePage* getSelectedPage();
+
+ void addResource(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&, bool is_indexed = false);
+ void removeResource(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&);
+
+ const QList<DownloadTaskPtr> getTasks();
+ [[nodiscard]] const std::shared_ptr<ResourceFolderModel> getBaseModel() const { return m_base_model; }
+
+ public slots:
+ void accept() override;
+ void reject() override;
+
+ protected slots:
+ void selectedPageChanged(BasePage* previous, BasePage* selected);
+
+ virtual void confirm();
+
+ protected:
+ [[nodiscard]] virtual QString geometrySaveKey() const { return ""; }
+
+ protected:
+ const std::shared_ptr<ResourceFolderModel> m_base_model;
+
+ PageContainer* m_container = nullptr;
+ ResourcePage* m_selectedPage = nullptr;
+
+ QDialogButtonBox m_buttons;
+ QVBoxLayout m_vertical_layout;
+
+ QHash<QString, DownloadTaskPtr> m_selected;
+};
+
+
+
+class ModDownloadDialog final : public ResourceDownloadDialog {
+ Q_OBJECT
+
+ public:
+ explicit ModDownloadDialog(QWidget* parent, const std::shared_ptr<ModFolderModel>& mods, BaseInstance* instance);
+ ~ModDownloadDialog() override = default;
+
+ //: String that gets appended to the mod download dialog title ("Download " + resourcesString())
+ [[nodiscard]] QString resourcesString() const override { return tr("mods"); }
+ [[nodiscard]] QString geometrySaveKey() const override { return "ModDownloadGeometry"; }
+
+ QList<BasePage*> getPages() override;
+
+ private:
+ BaseInstance* m_instance;
+};
+
+} // namespace ResourceDownload
diff --git a/launcher/ui/dialogs/ReviewMessageBox.cpp b/launcher/ui/dialogs/ReviewMessageBox.cpp
index 7c25c91c..7b2df278 100644
--- a/launcher/ui/dialogs/ReviewMessageBox.cpp
+++ b/launcher/ui/dialogs/ReviewMessageBox.cpp
@@ -1,6 +1,8 @@
#include "ReviewMessageBox.h"
#include "ui_ReviewMessageBox.h"
+#include "Application.h"
+
#include <QPushButton>
ReviewMessageBox::ReviewMessageBox(QWidget* parent, QString const& title, QString const& icon)
@@ -11,6 +13,10 @@ ReviewMessageBox::ReviewMessageBox(QWidget* parent, QString const& title, QStrin
auto back_button = ui->buttonBox->button(QDialogButtonBox::Cancel);
back_button->setText(tr("Back"));
+ ui->modTreeWidget->header()->setSectionResizeMode(0, QHeaderView::Stretch);
+ ui->modTreeWidget->header()->setStretchLastSection(false);
+ ui->modTreeWidget->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
+
connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &ReviewMessageBox::accept);
connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &ReviewMessageBox::reject);
}
@@ -25,7 +31,7 @@ auto ReviewMessageBox::create(QWidget* parent, QString&& title, QString&& icon)
return new ReviewMessageBox(parent, title, icon);
}
-void ReviewMessageBox::appendMod(ModInformation&& info)
+void ReviewMessageBox::appendResource(ResourceInformation&& info)
{
auto itemTop = new QTreeWidgetItem(ui->modTreeWidget);
itemTop->setCheckState(0, Qt::CheckState::Checked);
@@ -36,10 +42,20 @@ void ReviewMessageBox::appendMod(ModInformation&& info)
itemTop->insertChildren(0, { filenameItem });
+ if (!info.custom_file_path.isEmpty()) {
+ auto customPathItem = new QTreeWidgetItem(itemTop);
+ customPathItem->setText(0, tr("This download will be placed in: %1").arg(info.custom_file_path));
+
+ itemTop->insertChildren(1, { customPathItem });
+
+ itemTop->setIcon(1, QIcon(APPLICATION->getThemedIcon("status-yellow")));
+ itemTop->setToolTip(1, tr("This file will be downloaded to a folder location different from the default, possibly due to its loader requiring it."));
+ }
+
ui->modTreeWidget->addTopLevelItem(itemTop);
}
-auto ReviewMessageBox::deselectedMods() -> QStringList
+auto ReviewMessageBox::deselectedResources() -> QStringList
{
QStringList list;
@@ -55,3 +71,11 @@ auto ReviewMessageBox::deselectedMods() -> QStringList
return list;
}
+
+void ReviewMessageBox::retranslateUi(QString resources_name)
+{
+ setWindowTitle(tr("Confirm %1 selection").arg(resources_name));
+
+ ui->explainLabel->setText(tr("You're about to download the following %1:").arg(resources_name));
+ ui->onlyCheckedLabel->setText(tr("Only %1 with a check will be downloaded!").arg(resources_name));
+}
diff --git a/launcher/ui/dialogs/ReviewMessageBox.h b/launcher/ui/dialogs/ReviewMessageBox.h
index 9cfa679a..5ec2bc23 100644
--- a/launcher/ui/dialogs/ReviewMessageBox.h
+++ b/launcher/ui/dialogs/ReviewMessageBox.h
@@ -12,15 +12,18 @@ class ReviewMessageBox : public QDialog {
public:
static auto create(QWidget* parent, QString&& title, QString&& icon = "") -> ReviewMessageBox*;
- using ModInformation = struct {
+ using ResourceInformation = struct res_info {
QString name;
QString filename;
+ QString custom_file_path {};
};
- void appendMod(ModInformation&& info);
- auto deselectedMods() -> QStringList;
+ void appendResource(ResourceInformation&& info);
+ auto deselectedResources() -> QStringList;
- ~ReviewMessageBox();
+ void retranslateUi(QString resources_name);
+
+ ~ReviewMessageBox() override;
protected:
ReviewMessageBox(QWidget* parent, const QString& title, const QString& icon);
diff --git a/launcher/ui/dialogs/ReviewMessageBox.ui b/launcher/ui/dialogs/ReviewMessageBox.ui
index ab3bcc2f..bf53ae80 100644
--- a/launcher/ui/dialogs/ReviewMessageBox.ui
+++ b/launcher/ui/dialogs/ReviewMessageBox.ui
@@ -10,9 +10,6 @@
<height>350</height>
</rect>
</property>
- <property name="windowTitle">
- <string>Confirm mod selection</string>
- </property>
<property name="sizeGripEnabled">
<bool>true</bool>
</property>
@@ -39,22 +36,21 @@
<string/>
</property>
</column>
+ <column>
+ <property name="text">
+ <string/>
+ </property>
+ </column>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="explainLabel">
- <property name="text">
- <string>You're about to download the following mods:</string>
- </property>
</widget>
</item>
<item row="5" column="0" rowspan="2">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="onlyCheckedLabel">
- <property name="text">
- <string>Only mods with a check will be downloaded!</string>
- </property>
</widget>
</item>
<item>
diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp
index a4c2755c..a9f44c0b 100644
--- a/launcher/ui/pages/global/LauncherPage.cpp
+++ b/launcher/ui/pages/global/LauncherPage.cpp
@@ -83,6 +83,8 @@ LauncherPage::LauncherPage(QWidget *parent) : QWidget(parent), ui(new Ui::Launch
connect(ui->fontSizeBox, SIGNAL(valueChanged(int)), SLOT(refreshFontPreview()));
connect(ui->consoleFont, SIGNAL(currentFontChanged(QFont)), SLOT(refreshFontPreview()));
+
+ connect(ui->themeCustomizationWidget, &ThemeCustomizationWidget::currentCatChanged, APPLICATION, &Application::currentCatChanged);
}
LauncherPage::~LauncherPage()
@@ -185,73 +187,6 @@ void LauncherPage::applySettings()
APPLICATION->updater()->setAutomaticallyChecksForUpdates(ui->autoUpdateCheckBox->isChecked());
}
- auto original = s->get("IconTheme").toString();
- //FIXME: make generic
- switch (ui->themeComboBox->currentIndex())
- {
- case 0:
- s->set("IconTheme", "pe_colored");
- break;
- case 1:
- s->set("IconTheme", "pe_light");
- break;
- case 2:
- s->set("IconTheme", "pe_dark");
- break;
- case 3:
- s->set("IconTheme", "pe_blue");
- break;
- case 4:
- s->set("IconTheme", "breeze_light");
- break;
- case 5:
- s->set("IconTheme", "breeze_dark");
- break;
- case 6:
- s->set("IconTheme", "OSX");
- break;
- case 7:
- s->set("IconTheme", "iOS");
- break;
- case 8:
- s->set("IconTheme", "flat");
- break;
- case 9:
- s->set("IconTheme", "flat_white");
- break;
- case 10:
- s->set("IconTheme", "multimc");
- break;
- case 11:
- s->set("IconTheme", "custom");
- break;
- }
-
- if(original != s->get("IconTheme"))
- {
- APPLICATION->setIconTheme(s->get("IconTheme").toString());
- }
-
- auto originalAppTheme = s->get("ApplicationTheme").toString();
- auto newAppTheme = ui->themeComboBoxColors->currentData().toString();
- if(originalAppTheme != newAppTheme)
- {
- s->set("ApplicationTheme", newAppTheme);
- APPLICATION->setApplicationTheme(newAppTheme, false);
- }
-
- switch (ui->themeBackgroundCat->currentIndex()) {
- case 0: // original cat
- s->set("BackgroundCat", "kitteh");
- break;
- case 1: // rory the cat
- s->set("BackgroundCat", "rory");
- break;
- case 2: // rory the cat flat edition
- s->set("BackgroundCat", "rory-flat");
- break;
- }
-
s->set("MenuBarInsteadOfToolBar", ui->preferMenuBarCheckBox->isChecked());
// Console settings
@@ -294,45 +229,6 @@ void LauncherPage::loadSettings()
ui->autoUpdateCheckBox->setChecked(APPLICATION->updater()->getAutomaticallyChecksForUpdates());
}
- //FIXME: make generic
- auto theme = s->get("IconTheme").toString();
- QStringList iconThemeOptions{"pe_colored",
- "pe_light",
- "pe_dark",
- "pe_blue",
- "breeze_light",
- "breeze_dark",
- "OSX",
- "iOS",
- "flat",
- "flat_white",
- "multimc",
- "custom"};
- ui->themeComboBox->setCurrentIndex(iconThemeOptions.indexOf(theme));
-
- auto cat = s->get("BackgroundCat").toString();
- if (cat == "kitteh") {
- ui->themeBackgroundCat->setCurrentIndex(0);
- } else if (cat == "rory") {
- ui->themeBackgroundCat->setCurrentIndex(1);
- } else if (cat == "rory-flat") {
- ui->themeBackgroundCat->setCurrentIndex(2);
- }
-
- {
- auto currentTheme = s->get("ApplicationTheme").toString();
- auto themes = APPLICATION->getValidApplicationThemes();
- int idx = 0;
- for(auto &theme: themes)
- {
- ui->themeComboBoxColors->addItem(theme->name(), theme->id());
- if(currentTheme == theme->id())
- {
- ui->themeComboBoxColors->setCurrentIndex(idx);
- }
- idx++;
- }
- }
// Toolbar/menu bar settings (not applicable if native menu bar is present)
ui->toolsBox->setEnabled(!QMenuBar().isNativeMenuBar());
diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui
index fb36608d..f084d970 100644
--- a/launcher/ui/pages/global/LauncherPage.ui
+++ b/launcher/ui/pages/global/LauncherPage.ui
@@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
- <width>514</width>
+ <width>511</width>
<height>629</height>
</rect>
</property>
@@ -38,7 +38,7 @@
<enum>QTabWidget::Rounded</enum>
</property>
<property name="currentIndex">
- <number>0</number>
+ <number>1</number>
</property>
<widget class="QWidget" name="featuresTab">
<attribute name="title">
@@ -216,150 +216,9 @@
<property name="title">
<string>Theme</string>
</property>
- <layout class="QFormLayout" name="formLayout">
- <item row="0" column="0">
- <widget class="QLabel" name="label_3">
- <property name="text">
- <string>&amp;Icons</string>
- </property>
- <property name="buddy">
- <cstring>themeComboBox</cstring>
- </property>
- </widget>
- </item>
- <item row="0" column="1">
- <widget class="QComboBox" name="themeComboBox">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="focusPolicy">
- <enum>Qt::StrongFocus</enum>
- </property>
- <item>
- <property name="text">
- <string>Simple (Colored Icons)</string>
- </property>
- </item>
- <item>
- <property name="text">
- <string>Simple (Light Icons)</string>
- </property>
- </item>
- <item>
- <property name="text">
- <string>Simple (Dark Icons)</string>
- </property>
- </item>
- <item>
- <property name="text">
- <string>Simple (Blue Icons)</string>
- </property>
- </item>
- <item>
- <property name="text">
- <string>Breeze Light</string>
- </property>
- </item>
- <item>
- <property name="text">
- <string>Breeze Dark</string>
- </property>
- </item>
- <item>
- <property name="text">
- <string notr="true">OSX</string>
- </property>
- </item>
- <item>
- <property name="text">
- <string notr="true">iOS</string>
- </property>
- </item>
- <item>
- <property name="text">
- <string>Flat</string>
- </property>
- </item>
- <item>
- <property name="text">
- <string>Flat (White)</string>
- </property>
- </item>
- <item>
- <property name="text">
- <string>Legacy</string>
- </property>
- </item>
- <item>
- <property name="text">
- <string>Custom</string>
- </property>
- </item>
- </widget>
- </item>
- <item row="1" column="0">
- <widget class="QLabel" name="label_4">
- <property name="text">
- <string>&amp;Colors</string>
- </property>
- <property name="buddy">
- <cstring>themeComboBoxColors</cstring>
- </property>
- </widget>
- </item>
- <item row="1" column="1">
- <widget class="QComboBox" name="themeComboBoxColors">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="focusPolicy">
- <enum>Qt::StrongFocus</enum>
- </property>
- </widget>
- </item>
- <item row="2" column="0">
- <widget class="QLabel" name="label_5">
- <property name="text">
- <string>C&amp;at</string>
- </property>
- <property name="buddy">
- <cstring>themeBackgroundCat</cstring>
- </property>
- </widget>
- </item>
- <item row="2" column="1">
- <widget class="QComboBox" name="themeBackgroundCat">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="focusPolicy">
- <enum>Qt::StrongFocus</enum>
- </property>
- <item>
- <property name="text">
- <string>Background Cat (from MultiMC)</string>
- </property>
- </item>
- <item>
- <property name="text">
- <string>Rory ID 11 (drawn by Ashtaka)</string>
- </property>
- </item>
- <item>
- <property name="text">
- <string>Rory ID 11 (flat edition, drawn by Ashtaka)</string>
- </property>
- </item>
- </widget>
+ <layout class="QVBoxLayout" name="verticalLayout_5">
+ <item>
+ <widget class="ThemeCustomizationWidget" name="themeCustomizationWidget" native="true"/>
</item>
</layout>
</widget>
@@ -543,6 +402,14 @@
</item>
</layout>
</widget>
+ <customwidgets>
+ <customwidget>
+ <class>ThemeCustomizationWidget</class>
+ <extends>QWidget</extends>
+ <header>ui/widgets/ThemeCustomizationWidget.h</header>
+ <container>1</container>
+ </customwidget>
+ </customwidgets>
<tabstops>
<tabstop>tabWidget</tabstop>
<tabstop>autoUpdateCheckBox</tabstop>
@@ -554,8 +421,6 @@
<tabstop>iconsDirBrowseBtn</tabstop>
<tabstop>sortLastLaunchedBtn</tabstop>
<tabstop>sortByNameBtn</tabstop>
- <tabstop>themeComboBox</tabstop>
- <tabstop>themeComboBoxColors</tabstop>
<tabstop>showConsoleCheck</tabstop>
<tabstop>autoCloseConsoleCheck</tabstop>
<tabstop>showConsoleErrorCheck</tabstop>
diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.cpp b/launcher/ui/pages/instance/ExternalResourcesPage.cpp
index c66d1368..1115ddc3 100644
--- a/launcher/ui/pages/instance/ExternalResourcesPage.cpp
+++ b/launcher/ui/pages/instance/ExternalResourcesPage.cpp
@@ -1,4 +1,5 @@
#include "ExternalResourcesPage.h"
+#include "ui/dialogs/CustomMessageBox.h"
#include "ui_ExternalResourcesPage.h"
#include "DesktopServices.h"
@@ -128,7 +129,7 @@ bool ExternalResourcesPage::eventFilter(QObject* obj, QEvent* ev)
{
if (ev->type() != QEvent::KeyPress)
return QWidget::eventFilter(obj, ev);
-
+
QKeyEvent* keyEvent = static_cast<QKeyEvent*>(ev);
if (obj == ui->treeView)
return listFilter(keyEvent);
@@ -140,7 +141,6 @@ void ExternalResourcesPage::addItem()
{
if (!m_controlsEnabled)
return;
-
auto list = GuiUtil::BrowseForFiles(
helpPage(), tr("Select %1", "Select whatever type of files the page contains. Example: 'Loader Mods'").arg(displayName()),
@@ -157,8 +157,50 @@ void ExternalResourcesPage::removeItem()
{
if (!m_controlsEnabled)
return;
-
+
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection());
+
+ int count = 0;
+ bool folder = false;
+ for (auto& i : selection.indexes()) {
+ if (i.column() == 0) {
+ count++;
+
+ // if a folder is selected, show the confirmation dialog
+ if (m_model->at(i.row()).fileinfo().isDir())
+ folder = true;
+ }
+ }
+
+ QString text;
+ bool multiple = count > 1;
+
+ if (multiple) {
+ text = tr("You are about to remove %1 items.\n"
+ "This may be permanent and they will be gone from the folder.\n\n"
+ "Are you sure?")
+ .arg(count);
+ } else if (folder) {
+ text = tr("You are about to remove the folder \"%1\".\n"
+ "This may be permanent and it will be gone from the parent folder.\n\n"
+ "Are you sure?")
+ .arg(m_model->at(selection.indexes().at(0).row()).fileinfo().fileName());
+ }
+
+ if (!text.isEmpty()) {
+ auto response = CustomMessageBox::selectable(this, tr("Confirm Removal"), text, QMessageBox::Warning,
+ QMessageBox::Yes | QMessageBox::No, QMessageBox::No)
+ ->exec();
+
+ if (response != QMessageBox::Yes)
+ return;
+ }
+
+ removeItems(selection);
+}
+
+void ExternalResourcesPage::removeItems(const QItemSelection& selection)
+{
m_model->deleteResources(selection.indexes());
}
@@ -209,4 +251,3 @@ bool ExternalResourcesPage::onSelectionChanged(const QModelIndex& current, const
return true;
}
-
diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.h b/launcher/ui/pages/instance/ExternalResourcesPage.h
index 2d1a5b51..d17fbb7f 100644
--- a/launcher/ui/pages/instance/ExternalResourcesPage.h
+++ b/launcher/ui/pages/instance/ExternalResourcesPage.h
@@ -50,7 +50,8 @@ class ExternalResourcesPage : public QMainWindow, public BasePage {
void filterTextChanged(const QString& newContents);
virtual void addItem();
- virtual void removeItem();
+ void removeItem();
+ virtual void removeItems(const QItemSelection &selection);
virtual void enableItem();
virtual void disableItem();
diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.cpp b/launcher/ui/pages/instance/InstanceSettingsPage.cpp
index af2ba7c8..4b4c73dc 100644
--- a/launcher/ui/pages/instance/InstanceSettingsPage.cpp
+++ b/launcher/ui/pages/instance/InstanceSettingsPage.cpp
@@ -48,18 +48,23 @@
#include "JavaCommon.h"
#include "Application.h"
+#include "minecraft/auth/AccountList.h"
#include "java/JavaInstallList.h"
#include "java/JavaUtils.h"
#include "FileSystem.h"
-
InstanceSettingsPage::InstanceSettingsPage(BaseInstance *inst, QWidget *parent)
: QWidget(parent), ui(new Ui::InstanceSettingsPage), m_instance(inst)
{
m_settings = inst->settings();
ui->setupUi(this);
+ accountMenu = new QMenu(this);
+ // Use undocumented property... https://stackoverflow.com/questions/7121718/create-a-scrollbar-in-a-submenu-qt
+ accountMenu->setStyleSheet("QMenu { menu-scrollable: 1; }");
+ ui->instanceAccountSelector->setMenu(accountMenu);
+
connect(ui->openGlobalJavaSettingsButton, &QCommandLinkButton::clicked, this, &InstanceSettingsPage::globalSettingsButtonClicked);
connect(APPLICATION, &Application::globalSettingsAboutToOpen, this, &InstanceSettingsPage::applySettings);
connect(APPLICATION, &Application::globalSettingsClosed, this, &InstanceSettingsPage::loadSettings);
@@ -275,6 +280,13 @@ void InstanceSettingsPage::applySettings()
m_settings->reset("JoinServerOnLaunchAddress");
}
+ // Use an account for this instance
+ bool useAccountForInstance = ui->instanceAccountGroupBox->isChecked();
+ m_settings->set("UseAccountForInstance", useAccountForInstance);
+ if (!useAccountForInstance) {
+ m_settings->reset("InstanceAccountId");
+ }
+
// FIXME: This should probably be called by a signal instead
m_instance->updateRuntimeContext();
}
@@ -372,6 +384,9 @@ void InstanceSettingsPage::loadSettings()
ui->serverJoinGroupBox->setChecked(m_settings->get("JoinServerOnLaunch").toBool());
ui->serverJoinAddress->setText(m_settings->get("JoinServerOnLaunchAddress").toString());
+
+ ui->instanceAccountGroupBox->setChecked(m_settings->get("UseAccountForInstance").toBool());
+ updateAccountsMenu();
}
void InstanceSettingsPage::on_javaDetectBtn_clicked()
@@ -437,6 +452,65 @@ void InstanceSettingsPage::on_javaTestBtn_clicked()
checker->run();
}
+void InstanceSettingsPage::updateAccountsMenu()
+{
+ accountMenu->clear();
+
+ auto accounts = APPLICATION->accounts();
+ int accountIndex = accounts->findAccountByProfileId(m_settings->get("InstanceAccountId").toString());
+ MinecraftAccountPtr defaultAccount = accounts->defaultAccount();
+
+ if (accountIndex != -1 && accounts->at(accountIndex)) {
+ defaultAccount = accounts->at(accountIndex);
+ }
+
+ if (defaultAccount) {
+ ui->instanceAccountSelector->setText(defaultAccount->profileName());
+ ui->instanceAccountSelector->setIcon(getFaceForAccount(defaultAccount));
+ } else {
+ ui->instanceAccountSelector->setText(tr("No default account"));
+ ui->instanceAccountSelector->setIcon(APPLICATION->getThemedIcon("noaccount"));
+ }
+
+ for (int i = 0; i < accounts->count(); i++) {
+ MinecraftAccountPtr account = accounts->at(i);
+ QAction* action = new QAction(account->profileName(), this);
+ action->setData(i);
+ action->setCheckable(true);
+ if (accountIndex == i) {
+ action->setChecked(true);
+ }
+ action->setIcon(getFaceForAccount(account));
+ accountMenu->addAction(action);
+ connect(action, SIGNAL(triggered(bool)), this, SLOT(changeInstanceAccount()));
+ }
+}
+
+QIcon InstanceSettingsPage::getFaceForAccount(MinecraftAccountPtr account)
+{
+ if (auto face = account->getFace(); !face.isNull()) {
+ return face;
+ }
+
+ return APPLICATION->getThemedIcon("noaccount");
+}
+
+void InstanceSettingsPage::changeInstanceAccount()
+{
+ QAction* sAction = (QAction*)sender();
+
+ Q_ASSERT(sAction->data().type() == QVariant::Type::Int);
+
+ QVariant data = sAction->data();
+ int index = data.toInt();
+ auto accounts = APPLICATION->accounts();
+ auto account = accounts->at(index);
+ m_settings->set("InstanceAccountId", account->profileId());
+
+ ui->instanceAccountSelector->setText(account->profileName());
+ ui->instanceAccountSelector->setIcon(getFaceForAccount(account));
+}
+
void InstanceSettingsPage::on_maxMemSpinBox_valueChanged(int i)
{
updateThresholds();
diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.h b/launcher/ui/pages/instance/InstanceSettingsPage.h
index 7450188d..cb6fbae0 100644
--- a/launcher/ui/pages/instance/InstanceSettingsPage.h
+++ b/launcher/ui/pages/instance/InstanceSettingsPage.h
@@ -37,12 +37,13 @@
#include <QWidget>
-#include "java/JavaChecker.h"
-#include "BaseInstance.h"
#include <QObjectPtr.h>
-#include "ui/pages/BasePage.h"
-#include "JavaCommon.h"
+#include <QMenu>
#include "Application.h"
+#include "BaseInstance.h"
+#include "JavaCommon.h"
+#include "java/JavaChecker.h"
+#include "ui/pages/BasePage.h"
class JavaChecker;
namespace Ui
@@ -92,9 +93,14 @@ private slots:
void globalSettingsButtonClicked(bool checked);
+ void updateAccountsMenu();
+ QIcon getFaceForAccount(MinecraftAccountPtr account);
+ void changeInstanceAccount();
+
private:
Ui::InstanceSettingsPage *ui;
BaseInstance *m_instance;
SettingsObjectPtr m_settings;
unique_qobject_ptr<JavaCommon::TestCheck> checker;
+ QMenu *accountMenu = nullptr;
};
diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.ui b/launcher/ui/pages/instance/InstanceSettingsPage.ui
index b064367d..1b986184 100644
--- a/launcher/ui/pages/instance/InstanceSettingsPage.ui
+++ b/launcher/ui/pages/instance/InstanceSettingsPage.ui
@@ -609,6 +609,48 @@
</widget>
</item>
<item>
+ <widget class="QGroupBox" name="instanceAccountGroupBox">
+ <property name="title">
+ <string>Override default account</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_15">
+ <item>
+ <layout class="QGridLayout" name="instanceAccountLayout">
+ <item row="0" column="0">
+ <widget class="QLabel" name="instanceAccountNameLabel">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Account:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QToolButton" name="instanceAccountSelector">
+ <property name="popupMode">
+ <enum>QToolButton::InstantPopup</enum>
+ </property>
+ <property name="toolButtonStyle">
+ <enum>Qt::ToolButtonTextBesideIcon</enum>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
<spacer name="verticalSpacerMiscellaneous">
<property name="orientation">
<enum>Qt::Vertical</enum>
diff --git a/launcher/ui/pages/instance/LogPage.cpp b/launcher/ui/pages/instance/LogPage.cpp
index 9985f426..639cd711 100644
--- a/launcher/ui/pages/instance/LogPage.cpp
+++ b/launcher/ui/pages/instance/LogPage.cpp
@@ -1,8 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
- * PolyMC - Minecraft Launcher
+ * Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -277,28 +278,22 @@ void LogPage::on_btnPaste_clicked()
//FIXME: turn this into a proper task and move the upload logic out of GuiUtil!
m_model->append(
MessageLevel::Launcher,
- QString("%2: Log upload triggered at: %1").arg(
- QDateTime::currentDateTime().toString(Qt::RFC2822Date),
- BuildConfig.LAUNCHER_DISPLAYNAME
+ QString("Log upload triggered at: %1").arg(
+ QDateTime::currentDateTime().toString(Qt::RFC2822Date)
)
);
- auto url = GuiUtil::uploadPaste(m_model->toPlainText(), this);
- if(!url.isEmpty())
+ auto url = GuiUtil::uploadPaste(tr("Minecraft Log"), m_model->toPlainText(), this);
+ if(!url.has_value())
{
- m_model->append(
- MessageLevel::Launcher,
- QString("%2: Log uploaded to: %1").arg(
- url,
- BuildConfig.LAUNCHER_DISPLAYNAME
- )
- );
+ m_model->append(MessageLevel::Error, QString("Log upload canceled"));
+ }
+ else if (url->isNull())
+ {
+ m_model->append(MessageLevel::Error, QString("Log upload failed!"));
}
else
{
- m_model->append(
- MessageLevel::Error,
- QString("%1: Log upload failed!").arg(BuildConfig.LAUNCHER_DISPLAYNAME)
- );
+ m_model->append(MessageLevel::Launcher, QString("Log uploaded to: %1").arg(url.value()));
}
}
diff --git a/launcher/ui/pages/instance/ManagedPackPage.cpp b/launcher/ui/pages/instance/ManagedPackPage.cpp
index 4de80468..dc983d9a 100644
--- a/launcher/ui/pages/instance/ManagedPackPage.cpp
+++ b/launcher/ui/pages/instance/ManagedPackPage.cpp
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: 2022 flow <flowlnlnln@gmail.com>
+// SPDX-FileCopyrightText: 2022 flowln <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only
@@ -9,14 +9,13 @@
#include <QProxyStyle>
#include <QStyleFactory>
-#include <HoeDown.h>
-
#include "Application.h"
#include "BuildConfig.h"
#include "InstanceImportTask.h"
#include "InstanceList.h"
#include "InstanceTask.h"
#include "Json.h"
+#include "Markdown.h"
#include "modplatform/modrinth/ModrinthPackManifest.h"
@@ -263,8 +262,7 @@ void ModrinthManagedPackPage::suggestVersion()
auto index = ui->versionsComboBox->currentIndex();
auto version = m_pack.versions.at(index);
- HoeDown md_parser;
- ui->changelogTextBrowser->setHtml(md_parser.process(version.changelog.toUtf8()));
+ ui->changelogTextBrowser->setHtml(markdownToHTML(version.changelog.toUtf8()));
ManagedPackPage::suggestVersion();
}
diff --git a/launcher/ui/pages/instance/ManagedPackPage.h b/launcher/ui/pages/instance/ManagedPackPage.h
index d29a5e88..1ac6fc03 100644
--- a/launcher/ui/pages/instance/ManagedPackPage.h
+++ b/launcher/ui/pages/instance/ManagedPackPage.h
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: 2022 flow <flowlnlnln@gmail.com>
+// SPDX-FileCopyrightText: 2022 flowln <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only
@@ -12,6 +12,8 @@
#include "modplatform/flame/FlameAPI.h"
#include "modplatform/flame/FlamePackIndex.h"
+#include "net/NetJob.h"
+
#include "ui/pages/BasePage.h"
#include <QWidget>
diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp
index 0a2e6155..d9069915 100644
--- a/launcher/ui/pages/instance/ModFolderPage.cpp
+++ b/launcher/ui/pages/instance/ModFolderPage.cpp
@@ -1,8 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
- * PolyMC - Minecraft Launcher
+ * Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -48,8 +49,8 @@
#include "ui/GuiUtil.h"
#include "ui/dialogs/CustomMessageBox.h"
-#include "ui/dialogs/ModDownloadDialog.h"
#include "ui/dialogs/ModUpdateDialog.h"
+#include "ui/dialogs/ResourceDownloadDialog.h"
#include "DesktopServices.h"
@@ -58,7 +59,7 @@
#include "minecraft/mod/Mod.h"
#include "minecraft/mod/ModFolderModel.h"
-#include "modplatform/ModAPI.h"
+#include "modplatform/ResourceAPI.h"
#include "Version.h"
#include "tasks/ConcurrentTask.h"
@@ -139,13 +140,8 @@ bool ModFolderPage::onSelectionChanged(const QModelIndex& current, const QModelI
return true;
}
-void ModFolderPage::removeItem()
+void ModFolderPage::removeItems(const QItemSelection &selection)
{
-
- if (!m_controlsEnabled)
- return;
-
- auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection());
m_model->deleteMods(selection.indexes());
}
@@ -157,12 +153,12 @@ void ModFolderPage::installMods()
return; // this is a null instance or a legacy instance
auto profile = static_cast<MinecraftInstance*>(m_instance)->getPackProfile();
- if (profile->getModLoaders() == ModAPI::Unspecified) {
+ if (!profile->getModLoaders().has_value()) {
QMessageBox::critical(this, tr("Error"), tr("Please install a mod loader first!"));
return;
}
- ModDownloadDialog mdownload(m_model, this, m_instance);
+ ResourceDownload::ModDownloadDialog mdownload(this, m_model, m_instance);
if (mdownload.exec()) {
ConcurrentTask* tasks = new ConcurrentTask(this);
connect(tasks, &Task::failed, [this, tasks](QString reason) {
diff --git a/launcher/ui/pages/instance/ModFolderPage.h b/launcher/ui/pages/instance/ModFolderPage.h
index f20adf34..ff58b38a 100644
--- a/launcher/ui/pages/instance/ModFolderPage.h
+++ b/launcher/ui/pages/instance/ModFolderPage.h
@@ -1,8 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
- * PolyMC - Minecraft Launcher
+ * Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -59,7 +60,7 @@ class ModFolderPage : public ExternalResourcesPage {
private slots:
void runningStateChanged(bool running);
- void removeItem() override;
+ void removeItems(const QItemSelection &selection) override;
void installMods();
void updateMods();
diff --git a/launcher/ui/pages/instance/NotesPage.ui b/launcher/ui/pages/instance/NotesPage.ui
index 67cb261c..4b506da7 100644
--- a/launcher/ui/pages/instance/NotesPage.ui
+++ b/launcher/ui/pages/instance/NotesPage.ui
@@ -17,17 +17,11 @@
<property name="topMargin">
<number>0</number>
</property>
- <property name="rightMargin">
- <number>0</number>
- </property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QTextEdit" name="noteEditor">
- <property name="verticalScrollBarPolicy">
- <enum>Qt::ScrollBarAlwaysOn</enum>
- </property>
<property name="tabChangesFocus">
<bool>true</bool>
</property>
diff --git a/launcher/ui/pages/instance/OtherLogsPage.cpp b/launcher/ui/pages/instance/OtherLogsPage.cpp
index 0c1939c6..bbdd7324 100644
--- a/launcher/ui/pages/instance/OtherLogsPage.cpp
+++ b/launcher/ui/pages/instance/OtherLogsPage.cpp
@@ -1,7 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
- * PolyMC - Minecraft Launcher
+ * Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
+ * Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -204,7 +205,7 @@ void OtherLogsPage::on_btnReload_clicked()
void OtherLogsPage::on_btnPaste_clicked()
{
- GuiUtil::uploadPaste(ui->text->toPlainText(), this);
+ GuiUtil::uploadPaste(m_currentFile, ui->text->toPlainText(), this);
}
void OtherLogsPage::on_btnCopy_clicked()
@@ -219,13 +220,21 @@ void OtherLogsPage::on_btnDelete_clicked()
setControlsEnabled(false);
return;
}
- if (QMessageBox::question(this, tr("Delete"),
- tr("Do you really want to delete %1?").arg(m_currentFile),
- QMessageBox::Yes, QMessageBox::No) == QMessageBox::No)
- {
+ if (QMessageBox::question(this, tr("Confirm Deletion"),
+ tr("You are about to delete \"%1\".\n"
+ "This may be permanent and it will be gone from the logs folder.\n\n"
+ "Are you sure?")
+ .arg(m_currentFile),
+ QMessageBox::Yes, QMessageBox::No) == QMessageBox::No) {
return;
}
QFile file(FS::PathCombine(m_path, m_currentFile));
+
+ if (FS::trash(file.fileName()))
+ {
+ return;
+ }
+
if (!file.remove())
{
QMessageBox::critical(this, tr("Error"), tr("Unable to delete %1: %2")
@@ -243,15 +252,15 @@ void OtherLogsPage::on_btnClean_clicked()
return;
}
QMessageBox *messageBox = new QMessageBox(this);
- messageBox->setWindowTitle(tr("Clean up"));
+ messageBox->setWindowTitle(tr("Confirm Cleanup"));
if(toDelete.size() > 5)
{
- messageBox->setText(tr("Do you really want to delete all log files?"));
+ messageBox->setText(tr("Are you sure you want to delete all log files?"));
messageBox->setDetailedText(toDelete.join('\n'));
}
else
{
- messageBox->setText(tr("Do you really want to delete these files?\n%1").arg(toDelete.join('\n')));
+ messageBox->setText(tr("Are you sure you want to delete all these files?\n%1").arg(toDelete.join('\n')));
}
messageBox->setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel);
messageBox->setDefaultButton(QMessageBox::Ok);
@@ -267,6 +276,10 @@ void OtherLogsPage::on_btnClean_clicked()
for(auto item: toDelete)
{
QFile file(FS::PathCombine(m_path, item));
+ if (FS::trash(file.fileName()))
+ {
+ continue;
+ }
if (!file.remove())
{
failed.push_back(item);
diff --git a/launcher/ui/pages/instance/OtherLogsPage.ui b/launcher/ui/pages/instance/OtherLogsPage.ui
index 77f3e647..3fdb023f 100644
--- a/launcher/ui/pages/instance/OtherLogsPage.ui
+++ b/launcher/ui/pages/instance/OtherLogsPage.ui
@@ -48,9 +48,6 @@
<property name="enabled">
<bool>false</bool>
</property>
- <property name="verticalScrollBarPolicy">
- <enum>Qt::ScrollBarAlwaysOn</enum>
- </property>
<property name="readOnly">
<bool>true</bool>
</property>
diff --git a/launcher/ui/pages/instance/ResourcePackPage.h b/launcher/ui/pages/instance/ResourcePackPage.h
index 9633e3b4..db8af0c5 100644
--- a/launcher/ui/pages/instance/ResourcePackPage.h
+++ b/launcher/ui/pages/instance/ResourcePackPage.h
@@ -73,3 +73,4 @@ public:
return true;
}
};
+
diff --git a/launcher/ui/pages/instance/ScreenshotsPage.cpp b/launcher/ui/pages/instance/ScreenshotsPage.cpp
index 0092aef3..ca368d3b 100644
--- a/launcher/ui/pages/instance/ScreenshotsPage.cpp
+++ b/launcher/ui/pages/instance/ScreenshotsPage.cpp
@@ -1,8 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
- * PolyMC - Minecraft Launcher
+ * Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -379,6 +380,24 @@ void ScreenshotsPage::on_actionUpload_triggered()
if (selection.isEmpty())
return;
+
+ QString text;
+ if (selection.size() > 1)
+ text = tr("You are about to upload %1 screenshots.\n\n"
+ "Are you sure?")
+ .arg(selection.size());
+ else
+ text =
+ tr("You are about to upload the selected screenshot.\n\n"
+ "Are you sure?");
+
+ auto response = CustomMessageBox::selectable(this, "Confirm Upload", text, QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No,
+ QMessageBox::No)
+ ->exec();
+
+ if (response != QMessageBox::Yes)
+ return;
+
QList<ScreenShot::Ptr> uploaded;
auto job = NetJob::Ptr(new NetJob("Screenshot Upload", APPLICATION->network()));
if(selection.size() < 2)
@@ -491,17 +510,32 @@ void ScreenshotsPage::on_actionCopy_File_s_triggered()
void ScreenshotsPage::on_actionDelete_triggered()
{
- auto mbox = CustomMessageBox::selectable(
- this, tr("Are you sure?"), tr("This will delete all selected screenshots."),
- QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No);
- std::unique_ptr<QMessageBox> box(mbox);
+ auto selected = ui->listView->selectionModel()->selectedIndexes();
+
+ int count = ui->listView->selectionModel()->selectedRows().size();
+ QString text;
+ 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);
- if (box->exec() != QMessageBox::Yes)
+ auto response =
+ CustomMessageBox::selectable(this, tr("Confirm Deletion"), text, QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No)->exec();
+
+ if (response != QMessageBox::Yes)
return;
- auto selected = ui->listView->selectionModel()->selectedIndexes();
for (auto item : selected)
{
+ if (FS::trash(m_model->filePath(item)))
+ continue;
+
m_model->remove(item);
}
}
diff --git a/launcher/ui/pages/instance/ServersPage.cpp b/launcher/ui/pages/instance/ServersPage.cpp
index 08389aa3..4b1fa08a 100644
--- a/launcher/ui/pages/instance/ServersPage.cpp
+++ b/launcher/ui/pages/instance/ServersPage.cpp
@@ -1,8 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
- * PolyMC - Minecraft Launcher
+ * Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -35,6 +36,7 @@
*/
#include "ServersPage.h"
+#include "ui/dialogs/CustomMessageBox.h"
#include "ui_ServersPage.h"
#include <FileSystem.h>
@@ -800,6 +802,17 @@ void ServersPage::on_actionAdd_triggered()
void ServersPage::on_actionRemove_triggered()
{
+ auto response = CustomMessageBox::selectable(this, tr("Confirm Removal"),
+ tr("You are about to remove \"%1\".\n"
+ "This is permanent and the server will be gone from your list forever (A LONG TIME).\n\n"
+ "Are you sure?")
+ .arg(m_model->at(currentServer)->m_name),
+ QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No)
+ ->exec();
+
+ if (response != QMessageBox::Yes)
+ return;
+
m_model->removeRow(currentServer);
}
diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp
index c8a65f10..d200652a 100644
--- a/launcher/ui/pages/instance/VersionPage.cpp
+++ b/launcher/ui/pages/instance/VersionPage.cpp
@@ -318,13 +318,29 @@ void VersionPage::on_actionReload_triggered()
void VersionPage::on_actionRemove_triggered()
{
- if (ui->packageView->currentIndex().isValid())
+ if (!ui->packageView->currentIndex().isValid())
{
- // FIXME: use actual model, not reloading.
- if (!m_profile->remove(ui->packageView->currentIndex().row()))
- {
- QMessageBox::critical(this, tr("Error"), tr("Couldn't remove file"));
- }
+ return;
+ }
+ int index = ui->packageView->currentIndex().row();
+ auto component = m_profile->getComponent(index);
+ if (component->isCustom())
+ {
+ auto response = CustomMessageBox::selectable(this, tr("Confirm Removal"),
+ tr("You are about to remove \"%1\".\n"
+ "This is permanent and will completely remove the custom component.\n\n"
+ "Are you sure?")
+ .arg(component->getName()),
+ QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No)
+ ->exec();
+
+ if (response != QMessageBox::Yes)
+ return;
+ }
+ // FIXME: use actual model, not reloading.
+ if (!m_profile->remove(index))
+ {
+ QMessageBox::critical(this, tr("Error"), tr("Couldn't remove file"));
}
updateButtons();
reloadPackProfile();
@@ -707,6 +723,19 @@ void VersionPage::on_actionRevert_triggered()
{
return;
}
+ auto component = m_profile->getComponent(version);
+
+ auto response = CustomMessageBox::selectable(this, tr("Confirm Reversion"),
+ tr("You are about to revert \"%1\".\n"
+ "This is permanent and will completely revert your customizations.\n\n"
+ "Are you sure?")
+ .arg(component->getName()),
+ QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No)
+ ->exec();
+
+ if (response != QMessageBox::Yes)
+ return;
+
if(!m_profile->revertToBase(version))
{
// TODO: some error box here
diff --git a/launcher/ui/pages/instance/VersionPage.ui b/launcher/ui/pages/instance/VersionPage.ui
index 74b9568a..4cd50885 100644
--- a/launcher/ui/pages/instance/VersionPage.ui
+++ b/launcher/ui/pages/instance/VersionPage.ui
@@ -28,9 +28,6 @@
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="ModListView" name="packageView">
- <property name="verticalScrollBarPolicy">
- <enum>Qt::ScrollBarAlwaysOn</enum>
- </property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp
index b8b48ef9..d4a395d9 100644
--- a/launcher/ui/pages/instance/WorldListPage.cpp
+++ b/launcher/ui/pages/instance/WorldListPage.cpp
@@ -1,8 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
- * PolyMC - Minecraft Launcher
+ * Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -35,6 +36,7 @@
*/
#include "WorldListPage.h"
+#include "ui/dialogs/CustomMessageBox.h"
#include "ui_WorldListPage.h"
#include "minecraft/WorldList.h"
@@ -192,12 +194,14 @@ void WorldListPage::on_actionRemove_triggered()
if(!proxiedIndex.isValid())
return;
- auto result = QMessageBox::question(this,
- tr("Are you sure?"),
- tr("This will remove the selected world permenantly.\n"
- "The world will be gone forever (A LONG TIME).\n"
- "\n"
- "Do you want to continue?"));
+ auto result = CustomMessageBox::selectable(this, tr("Confirm Deletion"),
+ tr("You are about to delete \"%1\".\n"
+ "The world may be gone forever (A LONG TIME).\n\n"
+ "Are you sure?")
+ .arg(m_worlds->allWorlds().at(proxiedIndex.row()).name()),
+ QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No)
+ ->exec();
+
if(result != QMessageBox::Yes)
{
return;
diff --git a/launcher/ui/pages/instance/WorldListPage.ui b/launcher/ui/pages/instance/WorldListPage.ui
index 7c68bfae..d74dd079 100644
--- a/launcher/ui/pages/instance/WorldListPage.ui
+++ b/launcher/ui/pages/instance/WorldListPage.ui
@@ -109,7 +109,7 @@
</action>
<action name="actionRemove">
<property name="text">
- <string>Remove</string>
+ <string>Delete</string>
</property>
</action>
<action name="actionMCEdit">
diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp
index ed58eb32..3ffe6cb0 100644
--- a/launcher/ui/pages/modplatform/ModModel.cpp
+++ b/launcher/ui/pages/modplatform/ModModel.cpp
@@ -1,350 +1,70 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
#include "ModModel.h"
-#include "BuildConfig.h"
-#include "Json.h"
-#include "ModPage.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
-#include "ui/dialogs/ModDownloadDialog.h"
-
-#include "ui/widgets/ProjectItem.h"
#include <QMessageBox>
-namespace ModPlatform {
-
-// HACK: We need this to prevent callbacks from calling the ListModel after it has already been deleted.
-// This leaks a tiny bit of memory per time the user has opened the mod dialog. How to make this better?
-static QHash<ListModel*, bool> s_running;
-
-ListModel::ListModel(ModPage* parent) : QAbstractListModel(parent), m_parent(parent) { s_running.insert(this, true); }
-
-ListModel::~ListModel()
-{
- s_running.find(this).value() = false;
-}
+namespace ResourceDownload {
-auto ListModel::debugName() const -> QString
-{
- return m_parent->debugName();
-}
+ModModel::ModModel(BaseInstance const& base_inst, ResourceAPI* api) : ResourceModel(api), m_base_instance(base_inst) {}
/******** Make data requests ********/
-void ListModel::fetchMore(const QModelIndex& parent)
-{
- if (parent.isValid())
- return;
- if (nextSearchOffset == 0) {
- qWarning() << "fetchMore with 0 offset is wrong...";
- return;
- }
- performPaginatedSearch();
-}
-
-auto ListModel::data(const QModelIndex& index, int role) const -> QVariant
-{
- int pos = index.row();
- if (pos >= modpacks.size() || pos < 0 || !index.isValid()) {
- return QString("INVALID INDEX %1").arg(pos);
- }
-
- ModPlatform::IndexedPack pack = modpacks.at(pos);
- switch (role) {
- case Qt::ToolTipRole: {
- if (pack.description.length() > 100) {
- // some magic to prevent to long tooltips and replace html linebreaks
- QString edit = pack.description.left(97);
- edit = edit.left(edit.lastIndexOf("<br>")).left(edit.lastIndexOf(" ")).append("...");
- return edit;
- }
- return pack.description;
- }
- case Qt::DecorationRole: {
- if (m_logoMap.contains(pack.logoName)) {
- return m_logoMap.value(pack.logoName);
- }
- QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder");
- // un-const-ify this
- ((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl);
- return icon;
- }
- case Qt::SizeHintRole:
- return QSize(0, 58);
- case Qt::UserRole: {
- QVariant v;
- v.setValue(pack);
- return v;
- }
- // Custom data
- case UserDataTypes::TITLE:
- return pack.name;
- case UserDataTypes::DESCRIPTION:
- return pack.description;
- case UserDataTypes::SELECTED:
- return m_parent->getDialog()->isModSelected(pack.name);
- default:
- break;
- }
-
- return {};
-}
-
-bool ListModel::setData(const QModelIndex &index, const QVariant &value, int role)
-{
- int pos = index.row();
- if (pos >= modpacks.size() || pos < 0 || !index.isValid())
- return false;
-
- modpacks[pos] = value.value<ModPlatform::IndexedPack>();
-
- return true;
-}
-
-void ListModel::requestModVersions(ModPlatform::IndexedPack const& current, QModelIndex index)
-{
- auto profile = (dynamic_cast<MinecraftInstance*>((dynamic_cast<ModPage*>(parent()))->m_instance))->getPackProfile();
-
- m_parent->apiProvider()->getVersions({ current.addonId.toString(), getMineVersions(), profile->getModLoaders() },
- [this, current, index](QJsonDocument& doc, QString addonId) {
- if (!s_running.constFind(this).value())
- return;
- versionRequestSucceeded(doc, addonId, index);
- });
-}
-
-void ListModel::performPaginatedSearch()
+ResourceAPI::SearchArgs ModModel::createSearchArguments()
{
- auto profile = (dynamic_cast<MinecraftInstance*>((dynamic_cast<ModPage*>(parent()))->m_instance))->getPackProfile();
-
- m_parent->apiProvider()->searchMods(
- this, { nextSearchOffset, currentSearchTerm, getSorts()[currentSort], profile->getModLoaders(), getMineVersions() });
-}
+ auto profile = static_cast<MinecraftInstance const&>(m_base_instance).getPackProfile();
-void ListModel::requestModInfo(ModPlatform::IndexedPack& current, QModelIndex index)
-{
- m_parent->apiProvider()->getModInfo(current, [this, index](QJsonDocument& doc, ModPlatform::IndexedPack& pack) {
- if (!s_running.constFind(this).value())
- return;
- infoRequestFinished(doc, pack, index);
- });
-}
+ Q_ASSERT(profile);
+ Q_ASSERT(m_filter);
-void ListModel::refresh()
-{
- if (jobPtr) {
- jobPtr->abort();
- searchState = ResetRequested;
- return;
- } else {
- beginResetModel();
- modpacks.clear();
- endResetModel();
- searchState = None;
- }
- nextSearchOffset = 0;
- performPaginatedSearch();
-}
+ std::optional<std::list<Version>> versions{};
-void ListModel::searchWithTerm(const QString& term, const int sort, const bool filter_changed)
-{
- if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort && !filter_changed) {
- return;
+ { // Version filter
+ if (!m_filter->versions.empty())
+ versions = m_filter->versions;
}
- currentSearchTerm = term;
- currentSort = sort;
-
- refresh();
-}
+ auto sort = getCurrentSortingMethodByIndex();
-void ListModel::getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback)
-{
- if (m_logoMap.contains(logo)) {
- callback(APPLICATION->metacache()
- ->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo.section(".", 0, 0)))
- ->getFullPath());
- } else {
- requestLogo(logo, logoUrl);
- }
+ return { ModPlatform::ResourceType::MOD, m_next_search_offset, m_search_term, sort, profile->getModLoaders(), versions };
}
-void ListModel::requestLogo(QString logo, QString url)
+ResourceAPI::VersionSearchArgs ModModel::createVersionsArguments(QModelIndex& entry)
{
- if (m_loadingLogos.contains(logo) || m_failedLogos.contains(logo) || url.isEmpty()) {
- return;
- }
+ auto& pack = m_packs[entry.row()];
+ auto profile = static_cast<MinecraftInstance const&>(m_base_instance).getPackProfile();
- MetaEntryPtr entry =
- APPLICATION->metacache()->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo.section(".", 0, 0)));
- auto job = new NetJob(QString("%1 Icon Download %2").arg(m_parent->debugName()).arg(logo), APPLICATION->network());
- job->addNetAction(Net::Download::makeCached(QUrl(url), entry));
+ Q_ASSERT(profile);
+ Q_ASSERT(m_filter);
- auto fullPath = entry->getFullPath();
- QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] {
- job->deleteLater();
- emit logoLoaded(logo, QIcon(fullPath));
- if (waitingCallbacks.contains(logo)) {
- waitingCallbacks.value(logo)(fullPath);
- }
- });
+ std::optional<std::list<Version>> versions{};
+ if (!m_filter->versions.empty())
+ versions = m_filter->versions;
- QObject::connect(job, &NetJob::failed, this, [this, logo, job] {
- job->deleteLater();
- emit logoFailed(logo);
- });
-
- job->start();
- m_loadingLogos.append(logo);
-}
-
-/******** Request callbacks ********/
-
-void ListModel::logoLoaded(QString logo, QIcon out)
-{
- m_loadingLogos.removeAll(logo);
- m_logoMap.insert(logo, out);
- for (int i = 0; i < modpacks.size(); i++) {
- if (modpacks[i].logoName == logo) {
- emit dataChanged(createIndex(i, 0), createIndex(i, 0), { Qt::DecorationRole });
- }
- }
+ return { pack, versions, profile->getModLoaders() };
}
-void ListModel::logoFailed(QString logo)
+ResourceAPI::ProjectInfoArgs ModModel::createInfoArguments(QModelIndex& entry)
{
- m_failedLogos.append(logo);
- m_loadingLogos.removeAll(logo);
+ auto& pack = m_packs[entry.row()];
+ return { pack };
}
-void ListModel::searchRequestFinished(QJsonDocument& doc)
+void ModModel::searchWithTerm(const QString& term, unsigned int sort, bool filter_changed)
{
- jobPtr.reset();
-
- QList<ModPlatform::IndexedPack> newList;
- auto packs = documentToArray(doc);
-
- for (auto packRaw : packs) {
- auto packObj = packRaw.toObject();
-
- ModPlatform::IndexedPack pack;
- try {
- loadIndexedPack(pack, packObj);
- newList.append(pack);
- } catch (const JSONValidationError& e) {
- qWarning() << "Error while loading mod from " << m_parent->debugName() << ": " << e.cause();
- continue;
- }
- }
-
- if (packs.size() < 25) {
- searchState = Finished;
- } else {
- nextSearchOffset += 25;
- searchState = CanPossiblyFetchMore;
- }
-
- // When you have a Qt build with assertions turned on, proceeding here will abort the application
- if (newList.size() == 0)
+ if (m_search_term == term && m_search_term.isNull() == term.isNull() && m_current_sort_index == sort && !filter_changed) {
return;
-
- beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1);
- modpacks.append(newList);
- endInsertRows();
-}
-
-void ListModel::searchRequestFailed(QString reason)
-{
- auto failed_action = jobPtr->getFailedActions().at(0);
- if (!failed_action->m_reply) {
- // Network error
- QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load mods."));
- } else if (failed_action->m_reply && failed_action->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 409) {
- // 409 Gone, notify user to update
- QMessageBox::critical(nullptr, tr("Error"),
- //: %1 refers to the launcher itself
- QString("%1 %2")
- .arg(m_parent->displayName())
- .arg(tr("API version too old!\nPlease update %1!").arg(BuildConfig.LAUNCHER_DISPLAYNAME)));
}
- jobPtr.reset();
- searchState = Finished;
-}
-
-void ListModel::searchRequestAborted()
-{
- if (searchState != ResetRequested)
- qCritical() << "Search task in ModModel aborted by an unknown reason!";
-
- // Retry fetching
- jobPtr.reset();
-
- beginResetModel();
- modpacks.clear();
- endResetModel();
-
- nextSearchOffset = 0;
- performPaginatedSearch();
-}
+ setSearchTerm(term);
+ m_current_sort_index = sort;
-void ListModel::infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index)
-{
- qDebug() << "Loading mod info";
-
- try {
- auto obj = Json::requireObject(doc);
- loadExtraPackInfo(pack, obj);
- } catch (const JSONValidationError& e) {
- qDebug() << doc;
- qWarning() << "Error while reading " << debugName() << " mod info: " << e.cause();
- }
-
- // Check if the index is still valid for this mod or not
- if (pack.addonId == data(index, Qt::UserRole).value<ModPlatform::IndexedPack>().addonId) {
- // Cache info :^)
- QVariant new_pack;
- new_pack.setValue(pack);
- if (!setData(index, new_pack, Qt::UserRole)) {
- qWarning() << "Failed to cache mod info!";
- }
- }
-
- m_parent->updateUi();
-}
-
-void ListModel::versionRequestSucceeded(QJsonDocument doc, QString addonId, const QModelIndex& index)
-{
- auto& current = m_parent->getCurrent();
- if (addonId != current.addonId) {
- return;
- }
-
- auto arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array();
-
- try {
- loadIndexedPackVersions(current, arr);
- } catch (const JSONValidationError& e) {
- qDebug() << doc;
- qWarning() << "Error while reading " << debugName() << " mod version: " << e.cause();
- }
-
- // Cache info :^)
- QVariant new_pack;
- new_pack.setValue(current);
- if (!setData(index, new_pack, Qt::UserRole)) {
- qWarning() << "Failed to cache mod versions!";
- }
-
-
- m_parent->updateModVersions();
+ refresh();
}
-} // namespace ModPlatform
-
-/******** Helpers ********/
-
-auto ModPlatform::ListModel::getMineVersions() const -> std::list<Version>
-{
- return m_parent->getFilter()->versions;
-}
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/ModModel.h b/launcher/ui/pages/modplatform/ModModel.h
index 36840649..5d4a7785 100644
--- a/launcher/ui/pages/modplatform/ModModel.h
+++ b/launcher/ui/pages/modplatform/ModModel.h
@@ -1,92 +1,52 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
#pragma once
#include <QAbstractListModel>
+#include "BaseInstance.h"
+
#include "modplatform/ModIndex.h"
-#include "net/NetJob.h"
+#include "modplatform/ResourceAPI.h"
+
+#include "ui/pages/modplatform/ResourceModel.h"
+#include "ui/widgets/ModFilterWidget.h"
-class ModPage;
class Version;
-namespace ModPlatform {
+namespace ResourceDownload {
-using LogoMap = QMap<QString, QIcon>;
-using LogoCallback = std::function<void (QString)>;
+class ModPage;
-class ListModel : public QAbstractListModel {
+class ModModel : public ResourceModel {
Q_OBJECT
public:
- ListModel(ModPage* parent);
- ~ListModel() override;
-
- inline auto rowCount(const QModelIndex& parent) const -> int override { return parent.isValid() ? 0 : modpacks.size(); };
- inline auto columnCount(const QModelIndex& parent) const -> int override { return parent.isValid() ? 0 : 1; };
- inline auto flags(const QModelIndex& index) const -> Qt::ItemFlags override { return QAbstractListModel::flags(index); };
-
- auto debugName() const -> QString;
-
- /* Retrieve information from the model at a given index with the given role */
- auto data(const QModelIndex& index, int role) const -> QVariant override;
- bool setData(const QModelIndex &index, const QVariant &value, int role) override;
-
- inline void setActiveJob(NetJob::Ptr ptr) { jobPtr = ptr; }
- inline NetJob* activeJob() { return jobPtr.get(); }
+ ModModel(const BaseInstance&, ResourceAPI* api);
/* Ask the API for more information */
- void fetchMore(const QModelIndex& parent) override;
- void refresh();
- void searchWithTerm(const QString& term, const int sort, const bool filter_changed);
- void requestModInfo(ModPlatform::IndexedPack& current, QModelIndex index);
- void requestModVersions(const ModPlatform::IndexedPack& current, QModelIndex index);
+ void searchWithTerm(const QString& term, unsigned int sort, bool filter_changed);
- virtual void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) = 0;
- virtual void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) = 0;
- virtual void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) = 0;
+ void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override = 0;
+ void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override = 0;
+ void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override = 0;
- void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback);
-
- inline auto canFetchMore(const QModelIndex& parent) const -> bool override { return parent.isValid() ? false : searchState == CanPossiblyFetchMore; };
+ void setFilter(std::shared_ptr<ModFilterWidget::Filter> filter) { m_filter = filter; }
public slots:
- void searchRequestFinished(QJsonDocument& doc);
- void searchRequestFailed(QString reason);
- void searchRequestAborted();
-
- void infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index);
-
- void versionRequestSucceeded(QJsonDocument doc, QString addonId, const QModelIndex& index);
-
- protected slots:
-
- void logoFailed(QString logo);
- void logoLoaded(QString logo, QIcon out);
-
- void performPaginatedSearch();
+ ResourceAPI::SearchArgs createSearchArguments() override;
+ ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) override;
+ ResourceAPI::ProjectInfoArgs createInfoArguments(QModelIndex&) override;
protected:
- virtual auto documentToArray(QJsonDocument& obj) const -> QJsonArray = 0;
- virtual auto getSorts() const -> const char** = 0;
-
- void requestLogo(QString file, QString url);
-
- inline auto getMineVersions() const -> std::list<Version>;
+ auto documentToArray(QJsonDocument& obj) const -> QJsonArray override = 0;
protected:
- ModPage* m_parent;
-
- QList<ModPlatform::IndexedPack> modpacks;
-
- LogoMap m_logoMap;
- QMap<QString, LogoCallback> waitingCallbacks;
- QStringList m_failedLogos;
- QStringList m_loadingLogos;
+ const BaseInstance& m_base_instance;
- QString currentSearchTerm;
- int currentSort = 0;
- int nextSearchOffset = 0;
- enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } searchState = None;
-
- NetJob::Ptr jobPtr;
+ std::shared_ptr<ModFilterWidget::Filter> m_filter = nullptr;
};
-} // namespace ModPlatform
+
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp
index 677bc4d6..04be43ad 100644
--- a/launcher/ui/pages/modplatform/ModPage.cpp
+++ b/launcher/ui/pages/modplatform/ModPage.cpp
@@ -1,4 +1,6 @@
-// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
@@ -35,61 +37,30 @@
*/
#include "ModPage.h"
-#include "Application.h"
-#include "ui_ModPage.h"
+#include "ui_ResourcePage.h"
#include <QDesktopServices>
#include <QKeyEvent>
#include <QRegularExpression>
+
#include <memory>
-#include <HoeDown.h>
+#include "Application.h"
+#include "ResourceDownloadTask.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
-#include "ui/dialogs/ModDownloadDialog.h"
-#include "ui/widgets/ProjectItem.h"
-
-
-ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api)
- : QWidget(dialog)
- , m_instance(instance)
- , ui(new Ui::ModPage)
- , dialog(dialog)
- , m_fetch_progress(this, false)
- , api(api)
-{
- ui->setupUi(this);
-
- connect(ui->searchButton, &QPushButton::clicked, this, &ModPage::triggerSearch);
- connect(ui->modFilterButton, &QPushButton::clicked, this, &ModPage::filterMods);
- connect(ui->packView, &QListView::doubleClicked, this, &ModPage::onModSelected);
- m_search_timer.setTimerType(Qt::TimerType::CoarseTimer);
- m_search_timer.setSingleShot(true);
+#include "ui/dialogs/ResourceDownloadDialog.h"
- connect(&m_search_timer, &QTimer::timeout, this, &ModPage::triggerSearch);
-
- ui->searchEdit->installEventFilter(this);
-
- ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
- ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300);
-
- m_fetch_progress.hideIfInactive(true);
- m_fetch_progress.setFixedHeight(24);
- m_fetch_progress.progressFormat("");
-
- ui->gridLayout_3->addWidget(&m_fetch_progress, 0, 0, 1, ui->gridLayout_3->columnCount());
-
- ui->packView->setItemDelegate(new ProjectItemDelegate(this));
- ui->packView->installEventFilter(this);
-
- connect(ui->packDescription, &QTextBrowser::anchorClicked, this, &ModPage::openUrl);
-}
+namespace ResourceDownload {
-ModPage::~ModPage()
+ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance& instance)
+ : ResourcePage(dialog, instance)
{
- delete ui;
+ connect(m_ui->searchButton, &QPushButton::clicked, this, &ModPage::triggerSearch);
+ connect(m_ui->resourceFilterButton, &QPushButton::clicked, this, &ModPage::filterMods);
+ connect(m_ui->packView, &QListView::doubleClicked, this, &ModPage::onResourceSelected);
}
void ModPage::setFilterWidget(unique_qobject_ptr<ModFilterWidget>& widget)
@@ -99,59 +70,19 @@ void ModPage::setFilterWidget(unique_qobject_ptr<ModFilterWidget>& widget)
m_filter_widget.swap(widget);
- ui->gridLayout_3->addWidget(m_filter_widget.get(), 0, 0, 1, ui->gridLayout_3->columnCount());
+ m_ui->gridLayout_3->addWidget(m_filter_widget.get(), 0, 0, 1, m_ui->gridLayout_3->columnCount());
- m_filter_widget->setInstance(static_cast<MinecraftInstance*>(m_instance));
+ m_filter_widget->setInstance(&static_cast<MinecraftInstance&>(m_base_instance));
m_filter = m_filter_widget->getFilter();
connect(m_filter_widget.get(), &ModFilterWidget::filterChanged, this, [&]{
- ui->searchButton->setStyleSheet("text-decoration: underline");
+ m_ui->searchButton->setStyleSheet("text-decoration: underline");
});
connect(m_filter_widget.get(), &ModFilterWidget::filterUnchanged, this, [&]{
- ui->searchButton->setStyleSheet("text-decoration: none");
+ m_ui->searchButton->setStyleSheet("text-decoration: none");
});
}
-
-/******** Qt things ********/
-
-void ModPage::openedImpl()
-{
- updateSelectionButton();
- triggerSearch();
-}
-
-auto ModPage::eventFilter(QObject* watched, QEvent* event) -> bool
-{
- if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) {
- auto* keyEvent = dynamic_cast<QKeyEvent*>(event);
- if (keyEvent->key() == Qt::Key_Return) {
- triggerSearch();
- keyEvent->accept();
- return true;
- } else {
- if (m_search_timer.isActive())
- m_search_timer.stop();
-
- m_search_timer.start(350);
- }
- } else if (watched == ui->packView && event->type() == QEvent::KeyPress) {
- auto* keyEvent = dynamic_cast<QKeyEvent*>(event);
- if (keyEvent->key() == Qt::Key_Return) {
- onModSelected();
-
- // To have the 'select mod' button outlined instead of the 'review and confirm' one
- ui->modSelectionButton->setFocus(Qt::FocusReason::ShortcutFocusReason);
- ui->packView->setFocus(Qt::FocusReason::NoFocusReason);
-
- keyEvent->accept();
- return true;
- }
- }
- return QWidget::eventFilter(watched, event);
-}
-
-
/******** Callbacks to events in the UI (set up in the derived classes) ********/
void ModPage::filterMods()
@@ -165,176 +96,37 @@ void ModPage::triggerSearch()
m_filter = m_filter_widget->getFilter();
if (changed) {
- ui->packView->clearSelection();
- ui->packDescription->clear();
- ui->versionSelectionBox->clear();
- updateSelectionButton();
- }
-
- listModel->searchWithTerm(getSearchTerm(), ui->sortByBox->currentIndex(), changed);
- m_fetch_progress.watch(listModel->activeJob());
-}
-
-QString ModPage::getSearchTerm() const
-{
- return ui->searchEdit->text();
-}
-void ModPage::setSearchTerm(QString term)
-{
- ui->searchEdit->setText(term);
-}
-
-void ModPage::onSelectionChanged(QModelIndex curr, QModelIndex prev)
-{
- ui->versionSelectionBox->clear();
-
- if (!curr.isValid()) { return; }
-
- current = listModel->data(curr, Qt::UserRole).value<ModPlatform::IndexedPack>();
-
- if (!current.versionsLoaded) {
- qDebug() << QString("Loading %1 mod versions").arg(debugName());
-
- ui->modSelectionButton->setText(tr("Loading versions..."));
- ui->modSelectionButton->setEnabled(false);
-
- listModel->requestModVersions(current, curr);
- } else {
- for (int i = 0; i < current.versions.size(); i++) {
- ui->versionSelectionBox->addItem(current.versions[i].version, QVariant(i));
- }
- if (ui->versionSelectionBox->count() == 0) { ui->versionSelectionBox->addItem(tr("No valid version found."), QVariant(-1)); }
-
+ m_ui->packView->clearSelection();
+ m_ui->packDescription->clear();
+ m_ui->versionSelectionBox->clear();
updateSelectionButton();
}
- if(!current.extraDataLoaded){
- qDebug() << QString("Loading %1 mod info").arg(debugName());
-
- listModel->requestModInfo(current, curr);
- }
-
- updateUi();
-}
-
-void ModPage::onVersionSelectionChanged(QString data)
-{
- if (data.isNull() || data.isEmpty()) {
- selectedVersion = -1;
- return;
- }
- selectedVersion = ui->versionSelectionBox->currentData().toInt();
- updateSelectionButton();
-}
-
-void ModPage::onModSelected()
-{
- if (selectedVersion < 0)
- return;
-
- auto& version = current.versions[selectedVersion];
- if (dialog->isModSelected(current.name, version.fileName)) {
- dialog->removeSelectedMod(current.name);
- } else {
- bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool();
- dialog->addSelectedMod(current.name, new ModDownloadTask(current, version, dialog->mods, is_indexed));
- }
-
- updateSelectionButton();
-
- /* Force redraw on the mods list when the selection changes */
- ui->packView->adjustSize();
+ static_cast<ModModel*>(m_model)->searchWithTerm(getSearchTerm(), m_ui->sortByBox->currentData().toUInt(), changed);
+ m_fetch_progress.watch(m_model->activeSearchJob().get());
}
-static const QRegularExpression modrinth(QRegularExpression::anchoredPattern("(?:www\\.)?modrinth\\.com\\/mod\\/([^\\/]+)\\/?"));
-static const QRegularExpression curseForge(QRegularExpression::anchoredPattern("(?:www\\.)?curseforge\\.com\\/minecraft\\/mc-mods\\/([^\\/]+)\\/?"));
-static const QRegularExpression curseForgeOld(QRegularExpression::anchoredPattern("minecraft\\.curseforge\\.com\\/projects\\/([^\\/]+)\\/?"));
-
-void ModPage::openUrl(const QUrl& url)
+QMap<QString, QString> ModPage::urlHandlers() const
{
- // do not allow other url schemes for security reasons
- if (!(url.scheme() == "http" || url.scheme() == "https")) {
- qWarning() << "Unsupported scheme" << url.scheme();
- return;
- }
-
- // detect mod URLs and search instead
-
- const QString address = url.host() + url.path();
- QRegularExpressionMatch match;
- QString page;
-
- match = modrinth.match(address);
- if (match.hasMatch())
- page = "modrinth";
- else if (APPLICATION->capabilities() & Application::SupportsFlame) {
- match = curseForge.match(address);
- if (!match.hasMatch())
- match = curseForgeOld.match(address);
-
- if (match.hasMatch())
- page = "curseforge";
- }
-
- if (!page.isNull()) {
- const QString slug = match.captured(1);
-
- // ensure the user isn't opening the same mod
- if (slug != current.slug) {
- dialog->selectPage(page);
-
- ModPage* newPage = dialog->getSelectedPage();
-
- QLineEdit* searchEdit = newPage->ui->searchEdit;
- ModPlatform::ListModel* model = newPage->listModel;
- QListView* view = newPage->ui->packView;
-
- auto jump = [url, slug, model, view] {
- for (int row = 0; row < model->rowCount({}); row++) {
- const QModelIndex index = model->index(row);
- const auto pack = model->data(index, Qt::UserRole).value<ModPlatform::IndexedPack>();
-
- if (pack.slug == slug) {
- view->setCurrentIndex(index);
- return;
- }
- }
-
- // The final fallback.
- QDesktopServices::openUrl(url);
- };
-
- searchEdit->setText(slug);
- newPage->triggerSearch();
-
- if (model->activeJob())
- connect(model->activeJob(), &Task::finished, jump);
- else
- jump();
-
- return;
- }
- }
-
- // open in the user's web browser
- QDesktopServices::openUrl(url);
+ QMap<QString, QString> map;
+ map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?modrinth\\.com\\/mod\\/([^\\/]+)\\/?"), "modrinth");
+ map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?curseforge\\.com\\/minecraft\\/mc-mods\\/([^\\/]+)\\/?"), "curseforge");
+ map.insert(QRegularExpression::anchoredPattern("minecraft\\.curseforge\\.com\\/projects\\/([^\\/]+)\\/?"), "curseforge");
+ return map;
}
/******** Make changes to the UI ********/
-void ModPage::retranslate()
-{
- ui->retranslateUi(this);
-}
-
-void ModPage::updateModVersions(int prev_count)
+void ModPage::updateVersionList()
{
- auto packProfile = (dynamic_cast<MinecraftInstance*>(m_instance))->getPackProfile();
+ m_ui->versionSelectionBox->clear();
+ auto packProfile = (dynamic_cast<MinecraftInstance&>(m_base_instance)).getPackProfile();
QString mcVersion = packProfile->getComponentVersion("net.minecraft");
- for (int i = 0; i < current.versions.size(); i++) {
- auto version = current.versions[i];
+ auto current_pack = getCurrentPack();
+ for (int i = 0; i < current_pack.versions.size(); i++) {
+ auto version = current_pack.versions[i];
bool valid = false;
for(auto& mcVer : m_filter->versions){
//NOTE: Flame doesn't care about loader, so passing it changes nothing.
@@ -346,88 +138,20 @@ void ModPage::updateModVersions(int prev_count)
// Only add the version if it's valid or using the 'Any' filter, but never if the version is opted out
if ((valid || m_filter->versions.empty()) && !optedOut(version))
- ui->versionSelectionBox->addItem(version.version, QVariant(i));
+ m_ui->versionSelectionBox->addItem(version.version, QVariant(i));
}
- if (ui->versionSelectionBox->count() == 0 && prev_count != 0) {
- ui->versionSelectionBox->addItem(tr("No valid version found!"), QVariant(-1));
- ui->modSelectionButton->setText(tr("Cannot select invalid version :("));
+ if (m_ui->versionSelectionBox->count() == 0) {
+ m_ui->versionSelectionBox->addItem(tr("No valid version found!"), QVariant(-1));
+ m_ui->resourceSelectionButton->setText(tr("Cannot select invalid version :("));
}
updateSelectionButton();
}
-
-void ModPage::updateSelectionButton()
+void ModPage::addResourceToDialog(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& version)
{
- if (!isOpened || selectedVersion < 0) {
- ui->modSelectionButton->setEnabled(false);
- return;
- }
-
- ui->modSelectionButton->setEnabled(true);
- auto& version = current.versions[selectedVersion];
- if (!dialog->isModSelected(current.name, version.fileName)) {
- ui->modSelectionButton->setText(tr("Select mod for download"));
- } else {
- ui->modSelectionButton->setText(tr("Deselect mod for download"));
- }
+ bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool();
+ m_parent_dialog->addResource(pack, version, is_indexed);
}
-void ModPage::updateUi()
-{
- QString text = "";
- QString name = current.name;
-
- if (current.websiteUrl.isEmpty())
- text = name;
- else
- text = "<a href=\"" + current.websiteUrl + "\">" + name + "</a>";
-
- if (!current.authors.empty()) {
- auto authorToStr = [](ModPlatform::ModpackAuthor& author) -> QString {
- if (author.url.isEmpty()) { return author.name; }
- return QString("<a href=\"%1\">%2</a>").arg(author.url, author.name);
- };
- QStringList authorStrs;
- for (auto& author : current.authors) {
- authorStrs.push_back(authorToStr(author));
- }
- text += "<br>" + tr(" by ") + authorStrs.join(", ");
- }
-
- if (current.extraDataLoaded) {
- if (!current.extraData.donate.isEmpty()) {
- text += "<br><br>" + tr("Donate information: ");
- auto donateToStr = [](ModPlatform::DonationData& donate) -> QString {
- return QString("<a href=\"%1\">%2</a>").arg(donate.url, donate.platform);
- };
- QStringList donates;
- for (auto& donate : current.extraData.donate) {
- donates.append(donateToStr(donate));
- }
- text += donates.join(", ");
- }
-
- if (!current.extraData.issuesUrl.isEmpty()
- || !current.extraData.sourceUrl.isEmpty()
- || !current.extraData.wikiUrl.isEmpty()
- || !current.extraData.discordUrl.isEmpty()) {
- text += "<br><br>" + tr("External links:") + "<br>";
- }
-
- if (!current.extraData.issuesUrl.isEmpty())
- text += "- " + tr("Issues: <a href=%1>%1</a>").arg(current.extraData.issuesUrl) + "<br>";
- if (!current.extraData.wikiUrl.isEmpty())
- text += "- " + tr("Wiki: <a href=%1>%1</a>").arg(current.extraData.wikiUrl) + "<br>";
- if (!current.extraData.sourceUrl.isEmpty())
- text += "- " + tr("Source code: <a href=%1>%1</a>").arg(current.extraData.sourceUrl) + "<br>";
- if (!current.extraData.discordUrl.isEmpty())
- text += "- " + tr("Discord: <a href=%1>%1</a>").arg(current.extraData.discordUrl) + "<br>";
- }
-
- text += "<hr>";
-
- HoeDown h;
- ui->packDescription->setHtml(text + (current.extraData.body.isEmpty() ? current.description : h.process(current.extraData.body.toUtf8())));
- ui->packDescription->flush();
-}
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h
index c9ccbaf2..c3b58cd6 100644
--- a/launcher/ui/pages/modplatform/ModPage.h
+++ b/launcher/ui/pages/modplatform/ModPage.h
@@ -1,105 +1,76 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
#pragma once
#include <QWidget>
-#include "Application.h"
-#include "modplatform/ModAPI.h"
#include "modplatform/ModIndex.h"
-#include "ui/pages/BasePage.h"
+
+#include "ui/pages/modplatform/ResourcePage.h"
#include "ui/pages/modplatform/ModModel.h"
#include "ui/widgets/ModFilterWidget.h"
-#include "ui/widgets/ProgressWidget.h"
-
-class ModDownloadDialog;
namespace Ui {
-class ModPage;
+class ResourcePage;
}
+namespace ResourceDownload {
+
+class ModDownloadDialog;
+
/* This page handles most logic related to browsing and selecting mods to download. */
-class ModPage : public QWidget, public BasePage {
+class ModPage : public ResourcePage {
Q_OBJECT
public:
template<typename T>
- static T* create(ModDownloadDialog* dialog, BaseInstance* instance)
+ static T* create(ModDownloadDialog* dialog, BaseInstance& instance)
{
auto page = new T(dialog, instance);
+ auto model = static_cast<ModModel*>(page->getModel());
- auto filter_widget = ModFilterWidget::create(static_cast<MinecraftInstance*>(instance)->getPackProfile()->getComponentVersion("net.minecraft"), page);
+ auto filter_widget = ModFilterWidget::create(static_cast<MinecraftInstance&>(instance).getPackProfile()->getComponentVersion("net.minecraft"), page);
page->setFilterWidget(filter_widget);
+ model->setFilter(page->getFilter());
+
+ connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::updateVersionList);
+ connect(model, &ResourceModel::projectInfoUpdated, page, &ResourcePage::updateUi);
return page;
}
- ~ModPage() override;
-
- /* Affects what the user sees */
- auto displayName() const -> QString override = 0;
- auto icon() const -> QIcon override = 0;
- auto id() const -> QString override = 0;
- auto helpPage() const -> QString override = 0;
+ ~ModPage() override = default;
- /* Used internally */
- virtual auto metaEntryBase() const -> QString = 0;
- virtual auto debugName() const -> QString = 0;
+ //: The plural version of 'mod'
+ [[nodiscard]] inline QString resourcesString() const override { return tr("mods"); }
+ //: The singular version of 'mods'
+ [[nodiscard]] inline QString resourceString() const override { return tr("mod"); }
+ [[nodiscard]] QMap<QString, QString> urlHandlers() const override;
- void retranslate() override;
+ void addResourceToDialog(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&) override;
- void updateUi();
+ virtual auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, std::optional<ResourceAPI::ModLoaderTypes> loaders = {}) const -> bool = 0;
- auto shouldDisplay() const -> bool override = 0;
- virtual auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders = ModAPI::Unspecified) const -> bool = 0;
- virtual bool optedOut(ModPlatform::IndexedVersion& ver) const { return false; };
-
- auto apiProvider() -> ModAPI* { return api.get(); };
+ [[nodiscard]] bool supportsFiltering() const override { return true; };
auto getFilter() const -> const std::shared_ptr<ModFilterWidget::Filter> { return m_filter; }
- auto getDialog() const -> const ModDownloadDialog* { return dialog; }
-
- /** Get the current term in the search bar. */
- auto getSearchTerm() const -> QString;
- /** Programatically set the term in the search bar. */
- void setSearchTerm(QString);
-
void setFilterWidget(unique_qobject_ptr<ModFilterWidget>&);
- auto getCurrent() -> ModPlatform::IndexedPack& { return current; }
- void updateModVersions(int prev_count = -1);
-
- void openedImpl() override;
- auto eventFilter(QObject* watched, QEvent* event) -> bool override;
-
- BaseInstance* m_instance;
+ public slots:
+ void updateVersionList() override;
protected:
- ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api);
- void updateSelectionButton();
+ ModPage(ModDownloadDialog* dialog, BaseInstance& instance);
protected slots:
virtual void filterMods();
- void triggerSearch();
- void onSelectionChanged(QModelIndex first, QModelIndex second);
- void onVersionSelectionChanged(QString data);
- void onModSelected();
- virtual void openUrl(const QUrl& url);
+ void triggerSearch() override;
protected:
- Ui::ModPage* ui = nullptr;
- ModDownloadDialog* dialog = nullptr;
-
unique_qobject_ptr<ModFilterWidget> m_filter_widget;
std::shared_ptr<ModFilterWidget::Filter> m_filter;
-
- ProgressWidget m_fetch_progress;
-
- ModPlatform::ListModel* listModel = nullptr;
- ModPlatform::IndexedPack current;
-
- std::unique_ptr<ModAPI> api;
-
- int selectedVersion = -1;
-
- // Used to do instant searching with a delay to cache quick changes
- QTimer m_search_timer;
};
+
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/ResourceModel.cpp b/launcher/ui/pages/modplatform/ResourceModel.cpp
new file mode 100644
index 00000000..8af70104
--- /dev/null
+++ b/launcher/ui/pages/modplatform/ResourceModel.cpp
@@ -0,0 +1,444 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
+#include "ResourceModel.h"
+
+#include <QCryptographicHash>
+#include <QIcon>
+#include <QMessageBox>
+#include <QPixmapCache>
+#include <QUrl>
+
+#include "Application.h"
+#include "BuildConfig.h"
+#include "Json.h"
+
+#include "net/Download.h"
+#include "net/NetJob.h"
+
+#include "modplatform/ModIndex.h"
+
+#include "ui/widgets/ProjectItem.h"
+
+namespace ResourceDownload {
+
+QHash<ResourceModel*, bool> ResourceModel::s_running_models;
+
+ResourceModel::ResourceModel(ResourceAPI* api) : QAbstractListModel(), m_api(api)
+{
+ s_running_models.insert(this, true);
+}
+
+ResourceModel::~ResourceModel()
+{
+ s_running_models.find(this).value() = false;
+}
+
+auto ResourceModel::data(const QModelIndex& index, int role) const -> QVariant
+{
+ int pos = index.row();
+ if (pos >= m_packs.size() || pos < 0 || !index.isValid()) {
+ return QString("INVALID INDEX %1").arg(pos);
+ }
+
+ auto pack = m_packs.at(pos);
+ switch (role) {
+ case Qt::ToolTipRole: {
+ if (pack.description.length() > 100) {
+ // some magic to prevent to long tooltips and replace html linebreaks
+ QString edit = pack.description.left(97);
+ edit = edit.left(edit.lastIndexOf("<br>")).left(edit.lastIndexOf(" ")).append("...");
+ return edit;
+ }
+ return pack.description;
+ }
+ case Qt::DecorationRole: {
+ if (auto icon_or_none = const_cast<ResourceModel*>(this)->getIcon(const_cast<QModelIndex&>(index), pack.logoUrl);
+ icon_or_none.has_value())
+ return icon_or_none.value();
+
+ return APPLICATION->getThemedIcon("screenshot-placeholder");
+ }
+ case Qt::SizeHintRole:
+ return QSize(0, 58);
+ case Qt::UserRole: {
+ QVariant v;
+ v.setValue(pack);
+ return v;
+ }
+ // Custom data
+ case UserDataTypes::TITLE:
+ return pack.name;
+ case UserDataTypes::DESCRIPTION:
+ return pack.description;
+ case UserDataTypes::SELECTED:
+ return pack.isAnyVersionSelected();
+ default:
+ break;
+ }
+
+ return {};
+}
+
+QHash<int, QByteArray> ResourceModel::roleNames() const
+{
+ QHash<int, QByteArray> roles;
+
+ roles[Qt::ToolTipRole] = "toolTip";
+ roles[Qt::DecorationRole] = "decoration";
+ roles[Qt::SizeHintRole] = "sizeHint";
+ roles[Qt::UserRole] = "pack";
+ roles[UserDataTypes::TITLE] = "title";
+ roles[UserDataTypes::DESCRIPTION] = "description";
+ roles[UserDataTypes::SELECTED] = "selected";
+
+ return roles;
+}
+
+bool ResourceModel::setData(const QModelIndex& index, const QVariant& value, int role)
+{
+ int pos = index.row();
+ if (pos >= m_packs.size() || pos < 0 || !index.isValid())
+ return false;
+
+ m_packs[pos] = value.value<ModPlatform::IndexedPack>();
+ emit dataChanged(index, index);
+
+ return true;
+}
+
+QString ResourceModel::debugName() const
+{
+ return "ResourceDownload (Model)";
+}
+
+void ResourceModel::fetchMore(const QModelIndex& parent)
+{
+ if (parent.isValid() || m_search_state == SearchState::Finished)
+ return;
+
+ search();
+}
+
+void ResourceModel::search()
+{
+ if (hasActiveSearchJob())
+ return;
+
+ auto args{ createSearchArguments() };
+
+ auto callbacks{ createSearchCallbacks() };
+
+ // Use defaults if no callbacks are set
+ if (!callbacks.on_succeed)
+ callbacks.on_succeed = [this](auto& doc) {
+ if (!s_running_models.constFind(this).value())
+ return;
+ searchRequestSucceeded(doc);
+ };
+ if (!callbacks.on_fail)
+ callbacks.on_fail = [this](QString reason, int network_error_code) {
+ if (!s_running_models.constFind(this).value())
+ return;
+ searchRequestFailed(reason, network_error_code);
+ };
+ if (!callbacks.on_abort)
+ callbacks.on_abort = [this] {
+ if (!s_running_models.constFind(this).value())
+ return;
+ searchRequestAborted();
+ };
+
+ if (auto job = m_api->searchProjects(std::move(args), std::move(callbacks)); job)
+ runSearchJob(job);
+}
+
+void ResourceModel::loadEntry(QModelIndex& entry)
+{
+ auto const& pack = m_packs[entry.row()];
+
+ if (!hasActiveInfoJob())
+ m_current_info_job.clear();
+
+ if (!pack.versionsLoaded) {
+ auto args{ createVersionsArguments(entry) };
+ auto callbacks{ createVersionsCallbacks(entry) };
+
+ // Use default if no callbacks are set
+ if (!callbacks.on_succeed)
+ callbacks.on_succeed = [this, entry](auto& doc, auto pack) {
+ if (!s_running_models.constFind(this).value())
+ return;
+ versionRequestSucceeded(doc, pack, entry);
+ };
+
+ if (auto job = m_api->getProjectVersions(std::move(args), std::move(callbacks)); job)
+ runInfoJob(job);
+ }
+
+ if (!pack.extraDataLoaded) {
+ auto args{ createInfoArguments(entry) };
+ auto callbacks{ createInfoCallbacks(entry) };
+
+ // Use default if no callbacks are set
+ if (!callbacks.on_succeed)
+ callbacks.on_succeed = [this, entry](auto& doc, auto pack) {
+ if (!s_running_models.constFind(this).value())
+ return;
+ infoRequestSucceeded(doc, pack, entry);
+ };
+
+ if (auto job = m_api->getProjectInfo(std::move(args), std::move(callbacks)); job)
+ runInfoJob(job);
+ }
+}
+
+void ResourceModel::refresh()
+{
+ bool reset_requested = false;
+
+ if (hasActiveInfoJob()) {
+ m_current_info_job.abort();
+ reset_requested = true;
+ }
+
+ if (hasActiveSearchJob()) {
+ m_current_search_job->abort();
+ reset_requested = true;
+ }
+
+ if (reset_requested) {
+ m_search_state = SearchState::ResetRequested;
+ return;
+ }
+
+ clearData();
+ m_search_state = SearchState::None;
+
+ m_next_search_offset = 0;
+ search();
+}
+
+void ResourceModel::clearData()
+{
+ beginResetModel();
+ m_packs.clear();
+ endResetModel();
+}
+
+void ResourceModel::runSearchJob(Task::Ptr ptr)
+{
+ m_current_search_job = ptr;
+ m_current_search_job->start();
+}
+void ResourceModel::runInfoJob(Task::Ptr ptr)
+{
+ if (!m_current_info_job.isRunning())
+ m_current_info_job.clear();
+
+ m_current_info_job.addTask(ptr);
+
+ if (!m_current_info_job.isRunning())
+ m_current_info_job.run();
+}
+
+std::optional<ResourceAPI::SortingMethod> ResourceModel::getCurrentSortingMethodByIndex() const
+{
+ std::optional<ResourceAPI::SortingMethod> sort{};
+
+ { // Find sorting method by ID
+ auto sorting_methods = getSortingMethods();
+ auto method = std::find_if(sorting_methods.constBegin(), sorting_methods.constEnd(),
+ [this](auto const& e) { return m_current_sort_index == e.index; });
+ if (method != sorting_methods.constEnd())
+ sort = *method;
+ }
+
+ return sort;
+}
+
+std::optional<QIcon> ResourceModel::getIcon(QModelIndex& index, const QUrl& url)
+{
+ QPixmap pixmap;
+ if (QPixmapCache::find(url.toString(), &pixmap))
+ return { pixmap };
+
+ if (!m_current_icon_job)
+ m_current_icon_job = new NetJob("IconJob", APPLICATION->network());
+
+ if (m_currently_running_icon_actions.contains(url))
+ return {};
+ if (m_failed_icon_actions.contains(url))
+ return {};
+
+ auto cache_entry = APPLICATION->metacache()->resolveEntry(
+ metaEntryBase(),
+ QString("logos/%1").arg(QString(QCryptographicHash::hash(url.toEncoded(), QCryptographicHash::Algorithm::Sha1).toHex())));
+ auto icon_fetch_action = Net::Download::makeCached(url, cache_entry);
+
+ auto full_file_path = cache_entry->getFullPath();
+ connect(icon_fetch_action.get(), &NetAction::succeeded, this, [=] {
+ auto icon = QIcon(full_file_path);
+ QPixmapCache::insert(url.toString(), icon.pixmap(icon.actualSize({ 64, 64 })));
+
+ m_currently_running_icon_actions.remove(url);
+
+ emit dataChanged(index, index, { Qt::DecorationRole });
+ });
+ connect(icon_fetch_action.get(), &NetAction::failed, this, [=] {
+ m_currently_running_icon_actions.remove(url);
+ m_failed_icon_actions.insert(url);
+ });
+
+ m_currently_running_icon_actions.insert(url);
+
+ m_current_icon_job->addNetAction(icon_fetch_action);
+ if (!m_current_icon_job->isRunning())
+ QMetaObject::invokeMethod(m_current_icon_job.get(), &NetJob::start);
+
+ return {};
+}
+
+// No 'forgor to implement' shall pass here :blobfox_knife:
+#define NEED_FOR_CALLBACK_ASSERT(name) \
+ Q_ASSERT_X(0 != 0, #name, "You NEED to re-implement this if you intend on using the default callbacks.")
+
+QJsonArray ResourceModel::documentToArray(QJsonDocument& doc) const
+{
+ NEED_FOR_CALLBACK_ASSERT("documentToArray");
+ return {};
+}
+void ResourceModel::loadIndexedPack(ModPlatform::IndexedPack&, QJsonObject&)
+{
+ NEED_FOR_CALLBACK_ASSERT("loadIndexedPack");
+}
+void ResourceModel::loadExtraPackInfo(ModPlatform::IndexedPack&, QJsonObject&)
+{
+ NEED_FOR_CALLBACK_ASSERT("loadExtraPackInfo");
+}
+void ResourceModel::loadIndexedPackVersions(ModPlatform::IndexedPack&, QJsonArray&)
+{
+ NEED_FOR_CALLBACK_ASSERT("loadIndexedPackVersions");
+}
+
+/* Default callbacks */
+
+void ResourceModel::searchRequestSucceeded(QJsonDocument& doc)
+{
+ QList<ModPlatform::IndexedPack> newList;
+ auto packs = documentToArray(doc);
+
+ for (auto packRaw : packs) {
+ auto packObj = packRaw.toObject();
+
+ ModPlatform::IndexedPack pack;
+ try {
+ loadIndexedPack(pack, packObj);
+ newList.append(pack);
+ } catch (const JSONValidationError& e) {
+ qWarning() << "Error while loading resource from " << debugName() << ": " << e.cause();
+ continue;
+ }
+ }
+
+ if (packs.size() < 25) {
+ m_search_state = SearchState::Finished;
+ } else {
+ m_next_search_offset += 25;
+ m_search_state = SearchState::CanFetchMore;
+ }
+
+ // When you have a Qt build with assertions turned on, proceeding here will abort the application
+ if (newList.size() == 0)
+ return;
+
+ beginInsertRows(QModelIndex(), m_packs.size(), m_packs.size() + newList.size() - 1);
+ m_packs.append(newList);
+ endInsertRows();
+}
+
+void ResourceModel::searchRequestFailed(QString reason, int network_error_code)
+{
+ switch (network_error_code) {
+ default:
+ // Network error
+ QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load mods."));
+ break;
+ case 409:
+ // 409 Gone, notify user to update
+ QMessageBox::critical(nullptr, tr("Error"),
+ QString("%1").arg(tr("API version too old!\nPlease update %1!").arg(BuildConfig.LAUNCHER_DISPLAYNAME)));
+ break;
+ }
+
+ m_search_state = SearchState::Finished;
+}
+
+void ResourceModel::searchRequestAborted()
+{
+ if (m_search_state != SearchState::ResetRequested)
+ qCritical() << "Search task in" << debugName() << "aborted by an unknown reason!";
+
+ // Retry fetching
+ clearData();
+
+ m_next_search_offset = 0;
+ search();
+}
+
+void ResourceModel::versionRequestSucceeded(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index)
+{
+ auto current_pack = data(index, Qt::UserRole).value<ModPlatform::IndexedPack>();
+
+ // Check if the index is still valid for this resource or not
+ if (pack.addonId != current_pack.addonId)
+ return;
+
+ try {
+ auto arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array();
+ loadIndexedPackVersions(current_pack, arr);
+ } catch (const JSONValidationError& e) {
+ qDebug() << doc;
+ qWarning() << "Error while reading " << debugName() << " resource version: " << e.cause();
+ }
+
+ // Cache info :^)
+ QVariant new_pack;
+ new_pack.setValue(current_pack);
+ if (!setData(index, new_pack, Qt::UserRole)) {
+ qWarning() << "Failed to cache resource versions!";
+ return;
+ }
+
+ emit versionListUpdated();
+}
+
+void ResourceModel::infoRequestSucceeded(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index)
+{
+ auto current_pack = data(index, Qt::UserRole).value<ModPlatform::IndexedPack>();
+
+ // Check if the index is still valid for this resource or not
+ if (pack.addonId != current_pack.addonId)
+ return;
+
+ try {
+ auto obj = Json::requireObject(doc);
+ loadExtraPackInfo(current_pack, obj);
+ } catch (const JSONValidationError& e) {
+ qDebug() << doc;
+ qWarning() << "Error while reading " << debugName() << " resource info: " << e.cause();
+ }
+
+ // Cache info :^)
+ QVariant new_pack;
+ new_pack.setValue(current_pack);
+ if (!setData(index, new_pack, Qt::UserRole)) {
+ qWarning() << "Failed to cache resource info!";
+ return;
+ }
+
+ emit projectInfoUpdated();
+}
+
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/ResourceModel.h b/launcher/ui/pages/modplatform/ResourceModel.h
new file mode 100644
index 00000000..610b631c
--- /dev/null
+++ b/launcher/ui/pages/modplatform/ResourceModel.h
@@ -0,0 +1,147 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
+#pragma once
+
+#include <optional>
+
+#include <QAbstractListModel>
+
+#include "QObjectPtr.h"
+
+#include "modplatform/ResourceAPI.h"
+
+#include "tasks/ConcurrentTask.h"
+
+class NetJob;
+class ResourceAPI;
+
+namespace ModPlatform {
+struct IndexedPack;
+}
+
+namespace ResourceDownload {
+
+class ResourceModel : public QAbstractListModel {
+ Q_OBJECT
+
+ Q_PROPERTY(QString search_term MEMBER m_search_term WRITE setSearchTerm)
+
+ public:
+ ResourceModel(ResourceAPI* api);
+ ~ResourceModel() override;
+
+ [[nodiscard]] auto data(const QModelIndex&, int role) const -> QVariant override;
+ [[nodiscard]] auto roleNames() const -> QHash<int, QByteArray> override;
+ bool setData(const QModelIndex& index, const QVariant& value, int role) override;
+
+ [[nodiscard]] virtual auto debugName() const -> QString;
+ [[nodiscard]] virtual auto metaEntryBase() const -> QString = 0;
+
+ [[nodiscard]] inline int rowCount(const QModelIndex& parent) const override { return parent.isValid() ? 0 : m_packs.size(); }
+ [[nodiscard]] inline int columnCount(const QModelIndex& parent) const override { return parent.isValid() ? 0 : 1; }
+ [[nodiscard]] inline auto flags(const QModelIndex& index) const -> Qt::ItemFlags override { return QAbstractListModel::flags(index); }
+
+ [[nodiscard]] bool hasActiveSearchJob() const { return m_current_search_job && m_current_search_job->isRunning(); }
+ [[nodiscard]] bool hasActiveInfoJob() const { return m_current_info_job.isRunning(); }
+ [[nodiscard]] Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? m_current_search_job : nullptr; }
+
+ [[nodiscard]] auto getSortingMethods() const { return m_api->getSortingMethods(); }
+
+ public slots:
+ void fetchMore(const QModelIndex& parent) override;
+ // NOTE: Can't use [[nodiscard]] here because of https://bugreports.qt.io/browse/QTBUG-58628 on Qt 5.12
+ inline bool canFetchMore(const QModelIndex& parent) const override
+ {
+ return parent.isValid() ? false : m_search_state == SearchState::CanFetchMore;
+ }
+
+ void setSearchTerm(QString term) { m_search_term = term; }
+
+ virtual ResourceAPI::SearchArgs createSearchArguments() = 0;
+ virtual ResourceAPI::SearchCallbacks createSearchCallbacks() { return {}; }
+
+ virtual ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) = 0;
+ virtual ResourceAPI::VersionSearchCallbacks createVersionsCallbacks(QModelIndex&) { return {}; }
+
+ virtual ResourceAPI::ProjectInfoArgs createInfoArguments(QModelIndex&) = 0;
+ virtual ResourceAPI::ProjectInfoCallbacks createInfoCallbacks(QModelIndex&) { return {}; }
+
+ /** Requests the API for more entries. */
+ virtual void search();
+
+ /** Applies any processing / extra requests needed to fully load the specified entry's information. */
+ virtual void loadEntry(QModelIndex&);
+
+ /** Schedule a refresh, clearing the current state. */
+ void refresh();
+
+ /** Gets the icon at the URL for the given index. If it's not fetched yet, fetch it and update when fisinhed. */
+ std::optional<QIcon> getIcon(QModelIndex&, const QUrl&);
+
+ protected:
+ /** Resets the model's data. */
+ void clearData();
+
+ void runSearchJob(Task::Ptr);
+ void runInfoJob(Task::Ptr);
+
+ [[nodiscard]] auto getCurrentSortingMethodByIndex() const -> std::optional<ResourceAPI::SortingMethod>;
+
+ /** Converts a JSON document to a common array format.
+ *
+ * This is needed so that different providers, with different JSON structures, can be parsed
+ * uniformally. You NEED to re-implement this if you intend on using the default callbacks.
+ */
+ [[nodiscard]] virtual auto documentToArray(QJsonDocument&) const -> QJsonArray;
+
+ /** Functions to load data into a pack.
+ *
+ * Those are needed for the same reason as ddocumentToArray, and NEED to be re-implemented in the same way.
+ */
+
+ virtual void loadIndexedPack(ModPlatform::IndexedPack&, QJsonObject&);
+ virtual void loadExtraPackInfo(ModPlatform::IndexedPack&, QJsonObject&);
+ virtual void loadIndexedPackVersions(ModPlatform::IndexedPack&, QJsonArray&);
+
+ protected:
+ /* Basic search parameters */
+ enum class SearchState { None, CanFetchMore, ResetRequested, Finished } m_search_state = SearchState::None;
+ int m_next_search_offset = 0;
+ QString m_search_term;
+ unsigned int m_current_sort_index = 0;
+
+ std::unique_ptr<ResourceAPI> m_api;
+
+ // Job for searching for new entries
+ shared_qobject_ptr<Task> m_current_search_job;
+ // Job for fetching versions and extra info on existing entries
+ ConcurrentTask m_current_info_job;
+
+ shared_qobject_ptr<NetJob> m_current_icon_job;
+ QSet<QUrl> m_currently_running_icon_actions;
+ QSet<QUrl> m_failed_icon_actions;
+
+ QList<ModPlatform::IndexedPack> m_packs;
+
+ // HACK: We need this to prevent callbacks from calling the model after it has already been deleted.
+ // This leaks a tiny bit of memory per time the user has opened a resource dialog. How to make this better?
+ static QHash<ResourceModel*, bool> s_running_models;
+
+ private:
+ /* Default search request callbacks */
+ void searchRequestSucceeded(QJsonDocument&);
+ void searchRequestFailed(QString reason, int network_error_code);
+ void searchRequestAborted();
+
+ void versionRequestSucceeded(QJsonDocument&, ModPlatform::IndexedPack&, const QModelIndex&);
+
+ void infoRequestSucceeded(QJsonDocument&, ModPlatform::IndexedPack&, const QModelIndex&);
+
+ signals:
+ void versionListUpdated();
+ void projectInfoUpdated();
+};
+
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/ResourcePage.cpp b/launcher/ui/pages/modplatform/ResourcePage.cpp
new file mode 100644
index 00000000..bbd465bc
--- /dev/null
+++ b/launcher/ui/pages/modplatform/ResourcePage.cpp
@@ -0,0 +1,411 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "ResourcePage.h"
+#include "ui_ResourcePage.h"
+
+#include <QDesktopServices>
+#include <QKeyEvent>
+
+#include "Markdown.h"
+#include "ResourceDownloadTask.h"
+
+#include "minecraft/MinecraftInstance.h"
+
+#include "ui/dialogs/ResourceDownloadDialog.h"
+#include "ui/pages/modplatform/ResourceModel.h"
+#include "ui/widgets/ProjectItem.h"
+
+namespace ResourceDownload {
+
+ResourcePage::ResourcePage(ResourceDownloadDialog* parent, BaseInstance& base_instance)
+ : QWidget(parent), m_base_instance(base_instance), m_ui(new Ui::ResourcePage), m_parent_dialog(parent), m_fetch_progress(this, false)
+{
+ m_ui->setupUi(this);
+
+ m_ui->searchEdit->installEventFilter(this);
+
+ m_ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
+ m_ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300);
+
+ m_search_timer.setTimerType(Qt::TimerType::CoarseTimer);
+ m_search_timer.setSingleShot(true);
+
+ connect(&m_search_timer, &QTimer::timeout, this, &ResourcePage::triggerSearch);
+
+ m_fetch_progress.hideIfInactive(true);
+ m_fetch_progress.setFixedHeight(24);
+ m_fetch_progress.progressFormat("");
+
+ m_ui->gridLayout_3->addWidget(&m_fetch_progress, 0, 0, 1, m_ui->gridLayout_3->columnCount());
+
+ m_ui->packView->setItemDelegate(new ProjectItemDelegate(this));
+ m_ui->packView->installEventFilter(this);
+
+ connect(m_ui->packDescription, &QTextBrowser::anchorClicked, this, &ResourcePage::openUrl);
+}
+
+ResourcePage::~ResourcePage()
+{
+ delete m_ui;
+}
+
+void ResourcePage::retranslate()
+{
+ m_ui->retranslateUi(this);
+}
+
+void ResourcePage::openedImpl()
+{
+ if (!supportsFiltering())
+ m_ui->resourceFilterButton->setVisible(false);
+
+ //: String in the search bar of the mod downloading dialog
+ m_ui->searchEdit->setPlaceholderText(tr("Search for %1...").arg(resourcesString()));
+ m_ui->resourceSelectionButton->setText(tr("Select %1 for download").arg(resourceString()));
+
+ updateSelectionButton();
+ triggerSearch();
+}
+
+auto ResourcePage::eventFilter(QObject* watched, QEvent* event) -> bool
+{
+ if (event->type() == QEvent::KeyPress) {
+ auto* keyEvent = static_cast<QKeyEvent*>(event);
+ if (watched == m_ui->searchEdit) {
+ if (keyEvent->key() == Qt::Key_Return) {
+ triggerSearch();
+ keyEvent->accept();
+ return true;
+ } else {
+ if (m_search_timer.isActive())
+ m_search_timer.stop();
+
+ m_search_timer.start(350);
+ }
+ } else if (watched == m_ui->packView) {
+ if (keyEvent->key() == Qt::Key_Return) {
+ onResourceSelected();
+
+ // To have the 'select mod' button outlined instead of the 'review and confirm' one
+ m_ui->resourceSelectionButton->setFocus(Qt::FocusReason::ShortcutFocusReason);
+ m_ui->packView->setFocus(Qt::FocusReason::NoFocusReason);
+
+ keyEvent->accept();
+ return true;
+ }
+ }
+ }
+
+ return QWidget::eventFilter(watched, event);
+}
+
+QString ResourcePage::getSearchTerm() const
+{
+ return m_ui->searchEdit->text();
+}
+
+void ResourcePage::setSearchTerm(QString term)
+{
+ m_ui->searchEdit->setText(term);
+}
+
+void ResourcePage::addSortings()
+{
+ Q_ASSERT(m_model);
+
+ auto sorts = m_model->getSortingMethods();
+ std::sort(sorts.begin(), sorts.end(), [](auto const& l, auto const& r) { return l.index < r.index; });
+
+ for (auto&& sorting : sorts)
+ m_ui->sortByBox->addItem(sorting.readable_name, QVariant(sorting.index));
+}
+
+bool ResourcePage::setCurrentPack(ModPlatform::IndexedPack pack)
+{
+ QVariant v;
+ v.setValue(pack);
+ return m_model->setData(m_ui->packView->currentIndex(), v, Qt::UserRole);
+}
+
+ModPlatform::IndexedPack ResourcePage::getCurrentPack() const
+{
+ return m_model->data(m_ui->packView->currentIndex(), Qt::UserRole).value<ModPlatform::IndexedPack>();
+}
+
+void ResourcePage::updateUi()
+{
+ auto current_pack = getCurrentPack();
+
+ QString text = "";
+ QString name = current_pack.name;
+
+ if (current_pack.websiteUrl.isEmpty())
+ text = name;
+ else
+ text = "<a href=\"" + current_pack.websiteUrl + "\">" + name + "</a>";
+
+ if (!current_pack.authors.empty()) {
+ auto authorToStr = [](ModPlatform::ModpackAuthor& author) -> QString {
+ if (author.url.isEmpty()) {
+ return author.name;
+ }
+ return QString("<a href=\"%1\">%2</a>").arg(author.url, author.name);
+ };
+ QStringList authorStrs;
+ for (auto& author : current_pack.authors) {
+ authorStrs.push_back(authorToStr(author));
+ }
+ text += "<br>" + tr(" by ") + authorStrs.join(", ");
+ }
+
+ if (current_pack.extraDataLoaded) {
+ if (!current_pack.extraData.donate.isEmpty()) {
+ text += "<br><br>" + tr("Donate information: ");
+ auto donateToStr = [](ModPlatform::DonationData& donate) -> QString {
+ return QString("<a href=\"%1\">%2</a>").arg(donate.url, donate.platform);
+ };
+ QStringList donates;
+ for (auto& donate : current_pack.extraData.donate) {
+ donates.append(donateToStr(donate));
+ }
+ text += donates.join(", ");
+ }
+
+ if (!current_pack.extraData.issuesUrl.isEmpty() || !current_pack.extraData.sourceUrl.isEmpty() ||
+ !current_pack.extraData.wikiUrl.isEmpty() || !current_pack.extraData.discordUrl.isEmpty()) {
+ text += "<br><br>" + tr("External links:") + "<br>";
+ }
+
+ if (!current_pack.extraData.issuesUrl.isEmpty())
+ text += "- " + tr("Issues: <a href=%1>%1</a>").arg(current_pack.extraData.issuesUrl) + "<br>";
+ if (!current_pack.extraData.wikiUrl.isEmpty())
+ text += "- " + tr("Wiki: <a href=%1>%1</a>").arg(current_pack.extraData.wikiUrl) + "<br>";
+ if (!current_pack.extraData.sourceUrl.isEmpty())
+ text += "- " + tr("Source code: <a href=%1>%1</a>").arg(current_pack.extraData.sourceUrl) + "<br>";
+ if (!current_pack.extraData.discordUrl.isEmpty())
+ text += "- " + tr("Discord: <a href=%1>%1</a>").arg(current_pack.extraData.discordUrl) + "<br>";
+ }
+
+ text += "<hr>";
+
+ m_ui->packDescription->setHtml(
+ text + (current_pack.extraData.body.isEmpty() ? current_pack.description : markdownToHTML(current_pack.extraData.body)));
+ m_ui->packDescription->flush();
+}
+
+void ResourcePage::updateSelectionButton()
+{
+ if (!isOpened || m_selected_version_index < 0) {
+ m_ui->resourceSelectionButton->setEnabled(false);
+ return;
+ }
+
+ m_ui->resourceSelectionButton->setEnabled(true);
+ if (!getCurrentPack().isVersionSelected(m_selected_version_index)) {
+ m_ui->resourceSelectionButton->setText(tr("Select %1 for download").arg(resourceString()));
+ } else {
+ m_ui->resourceSelectionButton->setText(tr("Deselect %1 for download").arg(resourceString()));
+ }
+}
+
+void ResourcePage::updateVersionList()
+{
+ auto current_pack = getCurrentPack();
+
+ m_ui->versionSelectionBox->blockSignals(true);
+ m_ui->versionSelectionBox->clear();
+ m_ui->versionSelectionBox->blockSignals(false);
+
+ for (int i = 0; i < current_pack.versions.size(); i++) {
+ auto& version = current_pack.versions[i];
+ if (optedOut(version))
+ continue;
+
+ m_ui->versionSelectionBox->addItem(current_pack.versions[i].version, QVariant(i));
+ }
+
+ if (m_ui->versionSelectionBox->count() == 0) {
+ m_ui->versionSelectionBox->addItem(tr("No valid version found."), QVariant(-1));
+ m_ui->resourceSelectionButton->setText(tr("Cannot select invalid version :("));
+ }
+
+ updateSelectionButton();
+}
+
+void ResourcePage::onSelectionChanged(QModelIndex curr, QModelIndex prev)
+{
+ if (!curr.isValid()) {
+ return;
+ }
+
+ auto current_pack = getCurrentPack();
+
+ bool request_load = false;
+ if (!current_pack.versionsLoaded) {
+ m_ui->resourceSelectionButton->setText(tr("Loading versions..."));
+ m_ui->resourceSelectionButton->setEnabled(false);
+
+ request_load = true;
+ } else {
+ updateVersionList();
+ }
+
+ if (!current_pack.extraDataLoaded)
+ request_load = true;
+
+ if (request_load)
+ m_model->loadEntry(curr);
+
+ updateUi();
+}
+
+void ResourcePage::onVersionSelectionChanged(QString data)
+{
+ if (data.isNull() || data.isEmpty()) {
+ m_selected_version_index = -1;
+ return;
+ }
+
+ m_selected_version_index = m_ui->versionSelectionBox->currentData().toInt();
+ updateSelectionButton();
+}
+
+void ResourcePage::addResourceToDialog(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& version)
+{
+ m_parent_dialog->addResource(pack, version);
+}
+
+void ResourcePage::removeResourceFromDialog(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& version)
+{
+ m_parent_dialog->removeResource(pack, version);
+}
+
+void ResourcePage::onResourceSelected()
+{
+ if (m_selected_version_index < 0)
+ return;
+
+ auto current_pack = getCurrentPack();
+ if (!current_pack.versionsLoaded)
+ return;
+
+ auto& version = current_pack.versions[m_selected_version_index];
+ if (version.is_currently_selected)
+ removeResourceFromDialog(current_pack, version);
+ else
+ addResourceToDialog(current_pack, version);
+
+ // Save the modified pack (and prevent warning in release build)
+ [[maybe_unused]] bool set = setCurrentPack(current_pack);
+ Q_ASSERT(set);
+
+ updateSelectionButton();
+
+ /* Force redraw on the resource list when the selection changes */
+ m_ui->packView->adjustSize();
+}
+
+void ResourcePage::openUrl(const QUrl& url)
+{
+ // do not allow other url schemes for security reasons
+ if (!(url.scheme() == "http" || url.scheme() == "https")) {
+ qWarning() << "Unsupported scheme" << url.scheme();
+ return;
+ }
+
+ // detect URLs and search instead
+
+ const QString address = url.host() + url.path();
+ QRegularExpressionMatch match;
+ QString page;
+
+ auto handlers = urlHandlers();
+ for (auto it = handlers.constKeyValueBegin(); it != handlers.constKeyValueEnd(); it++) {
+ auto&& [regex, candidate] = *it;
+ if (match = QRegularExpression(regex).match(address); match.hasMatch()) {
+ page = candidate;
+ break;
+ }
+ }
+
+ if (!page.isNull()) {
+ const QString slug = match.captured(1);
+
+ // ensure the user isn't opening the same mod
+ if (slug != getCurrentPack().slug) {
+ m_parent_dialog->selectPage(page);
+
+ auto newPage = m_parent_dialog->getSelectedPage();
+
+ QLineEdit* searchEdit = newPage->m_ui->searchEdit;
+ auto model = newPage->m_model;
+ QListView* view = newPage->m_ui->packView;
+
+ auto jump = [url, slug, model, view] {
+ for (int row = 0; row < model->rowCount({}); row++) {
+ const QModelIndex index = model->index(row);
+ const auto pack = model->data(index, Qt::UserRole).value<ModPlatform::IndexedPack>();
+
+ if (pack.slug == slug) {
+ view->setCurrentIndex(index);
+ return;
+ }
+ }
+
+ // The final fallback.
+ QDesktopServices::openUrl(url);
+ };
+
+ searchEdit->setText(slug);
+ newPage->triggerSearch();
+
+ if (model->hasActiveSearchJob())
+ connect(model->activeSearchJob().get(), &Task::finished, jump);
+ else
+ jump();
+
+ return;
+ }
+ }
+
+ // open in the user's web browser
+ QDesktopServices::openUrl(url);
+}
+
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/ResourcePage.h b/launcher/ui/pages/modplatform/ResourcePage.h
new file mode 100644
index 00000000..1896d53e
--- /dev/null
+++ b/launcher/ui/pages/modplatform/ResourcePage.h
@@ -0,0 +1,111 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
+#pragma once
+
+#include <QTimer>
+#include <QWidget>
+
+#include "modplatform/ModIndex.h"
+#include "modplatform/ResourceAPI.h"
+
+#include "ui/pages/BasePage.h"
+#include "ui/widgets/ProgressWidget.h"
+
+namespace Ui {
+class ResourcePage;
+}
+
+class BaseInstance;
+
+namespace ResourceDownload {
+
+class ResourceDownloadDialog;
+class ResourceModel;
+
+class ResourcePage : public QWidget, public BasePage {
+ Q_OBJECT
+ public:
+ ~ResourcePage() override;
+
+ /* Affects what the user sees */
+ [[nodiscard]] auto displayName() const -> QString override = 0;
+ [[nodiscard]] auto icon() const -> QIcon override = 0;
+ [[nodiscard]] auto id() const -> QString override = 0;
+ [[nodiscard]] auto helpPage() const -> QString override = 0;
+ [[nodiscard]] bool shouldDisplay() const override = 0;
+
+ /* Used internally */
+ [[nodiscard]] virtual auto metaEntryBase() const -> QString = 0;
+ [[nodiscard]] virtual auto debugName() const -> QString = 0;
+
+ //: The plural version of 'resource'
+ [[nodiscard]] virtual inline QString resourcesString() const { return tr("resources"); }
+ //: The singular version of 'resources'
+ [[nodiscard]] virtual inline QString resourceString() const { return tr("resource"); }
+
+ /* Features this resource's page supports */
+ [[nodiscard]] virtual bool supportsFiltering() const = 0;
+
+ void retranslate() override;
+ void openedImpl() override;
+ auto eventFilter(QObject* watched, QEvent* event) -> bool override;
+
+ /** Get the current term in the search bar. */
+ [[nodiscard]] auto getSearchTerm() const -> QString;
+ /** Programatically set the term in the search bar. */
+ void setSearchTerm(QString);
+
+ [[nodiscard]] bool setCurrentPack(ModPlatform::IndexedPack);
+ [[nodiscard]] auto getCurrentPack() const -> ModPlatform::IndexedPack;
+ [[nodiscard]] auto getDialog() const -> const ResourceDownloadDialog* { return m_parent_dialog; }
+ [[nodiscard]] auto getModel() const -> ResourceModel* { return m_model; }
+
+ protected:
+ ResourcePage(ResourceDownloadDialog* parent, BaseInstance&);
+
+ void addSortings();
+
+ public slots:
+ virtual void updateUi();
+ virtual void updateSelectionButton();
+ virtual void updateVersionList();
+
+ virtual void addResourceToDialog(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&);
+ virtual void removeResourceFromDialog(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&);
+
+ protected slots:
+ virtual void triggerSearch() {}
+
+ void onSelectionChanged(QModelIndex first, QModelIndex second);
+ void onVersionSelectionChanged(QString data);
+ void onResourceSelected();
+
+ // NOTE: Can't use [[nodiscard]] here because of https://bugreports.qt.io/browse/QTBUG-58628 on Qt 5.12
+
+ /** Associates regex expressions to pages in the order they're given in the map. */
+ virtual QMap<QString, QString> urlHandlers() const = 0;
+ virtual void openUrl(const QUrl&);
+
+ /** Whether the version is opted out or not. Currently only makes sense in CF. */
+ virtual bool optedOut(ModPlatform::IndexedVersion& ver) const { return false; };
+
+ public:
+ BaseInstance& m_base_instance;
+
+ protected:
+ Ui::ResourcePage* m_ui;
+
+ ResourceDownloadDialog* m_parent_dialog = nullptr;
+ ResourceModel* m_model = nullptr;
+
+ int m_selected_version_index = -1;
+
+ ProgressWidget m_fetch_progress;
+
+ // Used to do instant searching with a delay to cache quick changes
+ QTimer m_search_timer;
+};
+
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/ModPage.ui b/launcher/ui/pages/modplatform/ResourcePage.ui
index 94365aa5..73a9d3b1 100644
--- a/launcher/ui/pages/modplatform/ModPage.ui
+++ b/launcher/ui/pages/modplatform/ResourcePage.ui
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
- <class>ModPage</class>
- <widget class="QWidget" name="ModPage">
+ <class>ResourcePage</class>
+ <widget class="QWidget" name="ResourcePage">
<property name="geometry">
<rect>
<x>0</x>
@@ -49,11 +49,7 @@
</widget>
</item>
<item row="0" column="0">
- <widget class="QLineEdit" name="searchEdit">
- <property name="placeholderText">
- <string>Search for mods...</string>
- </property>
- </widget>
+ <widget class="QLineEdit" name="searchEdit"/>
</item>
<item row="2" column="0" colspan="4">
<layout class="QGridLayout" name="gridLayout_4" columnstretch="0,0,0">
@@ -74,16 +70,12 @@
<widget class="QComboBox" name="sortByBox"/>
</item>
<item row="1" column="2">
- <widget class="QPushButton" name="modSelectionButton">
- <property name="text">
- <string>Select mod for download</string>
- </property>
- </widget>
+ <widget class="QPushButton" name="resourceSelectionButton"/>
</item>
</layout>
</item>
<item row="0" column="1">
- <widget class="QPushButton" name="modFilterButton">
+ <widget class="QPushButton" name="resourceFilterButton">
<property name="text">
<string>Filter options</string>
</property>
diff --git a/launcher/ui/pages/modplatform/flame/FlameModModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModModel.cpp
deleted file mode 100644
index bc2c686c..00000000
--- a/launcher/ui/pages/modplatform/flame/FlameModModel.cpp
+++ /dev/null
@@ -1,31 +0,0 @@
-#include "FlameModModel.h"
-#include "Json.h"
-#include "modplatform/flame/FlameModIndex.h"
-
-namespace FlameMod {
-
-// NOLINTNEXTLINE(modernize-avoid-c-arrays)
-const char* ListModel::sorts[6]{ "Featured", "Popularity", "LastUpdated", "Name", "Author", "TotalDownloads" };
-
-void ListModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
-{
- FlameMod::loadIndexedPack(m, obj);
-}
-
-// We already deal with the URLs when initializing the pack, due to the API response's structure
-void ListModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj)
-{
- FlameMod::loadBody(m, obj);
-}
-
-void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
-{
- FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), m_parent->m_instance);
-}
-
-auto ListModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
-{
- return Json::ensureArray(obj.object(), "data");
-}
-
-} // namespace FlameMod
diff --git a/launcher/ui/pages/modplatform/flame/FlameModModel.h b/launcher/ui/pages/modplatform/flame/FlameModModel.h
deleted file mode 100644
index 6a6aef2e..00000000
--- a/launcher/ui/pages/modplatform/flame/FlameModModel.h
+++ /dev/null
@@ -1,26 +0,0 @@
-#pragma once
-
-#include "FlameModPage.h"
-
-namespace FlameMod {
-
-class ListModel : public ModPlatform::ListModel {
- Q_OBJECT
-
- public:
- ListModel(FlameModPage* parent) : ModPlatform::ListModel(parent) {}
- ~ListModel() override = default;
-
- private:
- void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
- void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
- void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override;
-
- auto documentToArray(QJsonDocument& obj) const -> QJsonArray override;
-
- // NOLINTNEXTLINE(modernize-avoid-c-arrays)
- static const char* sorts[6];
- inline auto getSorts() const -> const char** override { return sorts; };
-};
-
-} // namespace FlameMod
diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp
new file mode 100644
index 00000000..de1f2122
--- /dev/null
+++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp
@@ -0,0 +1,37 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
+#include "FlameResourceModels.h"
+
+#include "Json.h"
+
+#include "modplatform/flame/FlameAPI.h"
+#include "modplatform/flame/FlameModIndex.h"
+
+namespace ResourceDownload {
+
+FlameModModel::FlameModModel(BaseInstance const& base) : ModModel(base, new FlameAPI) {}
+
+void FlameModModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
+{
+ FlameMod::loadIndexedPack(m, obj);
+}
+
+// We already deal with the URLs when initializing the pack, due to the API response's structure
+void FlameModModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj)
+{
+ FlameMod::loadBody(m, obj);
+}
+
+void FlameModModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
+{
+ FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance);
+}
+
+auto FlameModModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
+{
+ return Json::ensureArray(obj.object(), "data");
+}
+
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.h b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h
new file mode 100644
index 00000000..625a2a7d
--- /dev/null
+++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h
@@ -0,0 +1,30 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
+#pragma once
+
+#include "ui/pages/modplatform/ModModel.h"
+#include "ui/pages/modplatform/flame/FlameResourcePages.h"
+
+namespace ResourceDownload {
+
+class FlameModModel : public ModModel {
+ Q_OBJECT
+
+ public:
+ FlameModModel(const BaseInstance&);
+ ~FlameModModel() override = default;
+
+ private:
+ [[nodiscard]] QString debugName() const override { return Flame::debugName() + " (Model)"; }
+ [[nodiscard]] QString metaEntryBase() const override { return Flame::metaEntryBase(); }
+
+ void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
+ void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
+ void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override;
+
+ auto documentToArray(QJsonDocument& obj) const -> QJsonArray override;
+};
+
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp
index bad78c97..485431a7 100644
--- a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp
+++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp
@@ -1,4 +1,6 @@
-// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
@@ -34,37 +36,33 @@
* limitations under the License.
*/
-#include "FlameModPage.h"
-#include "ui_ModPage.h"
+#include "FlameResourcePages.h"
+#include "ui_ResourcePage.h"
-#include "FlameModModel.h"
-#include "ui/dialogs/ModDownloadDialog.h"
+#include "FlameResourceModels.h"
+#include "ui/dialogs/ResourceDownloadDialog.h"
-FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance)
- : ModPage(dialog, instance, new FlameAPI())
+namespace ResourceDownload {
+
+FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance& instance)
+ : ModPage(dialog, instance)
{
- listModel = new FlameMod::ListModel(this);
- ui->packView->setModel(listModel);
+ m_model = new FlameModModel(instance);
+ m_ui->packView->setModel(m_model);
- // index is used to set the sorting with the flame api
- ui->sortByBox->addItem(tr("Sort by Featured"));
- ui->sortByBox->addItem(tr("Sort by Popularity"));
- ui->sortByBox->addItem(tr("Sort by Last Updated"));
- ui->sortByBox->addItem(tr("Sort by Name"));
- ui->sortByBox->addItem(tr("Sort by Author"));
- ui->sortByBox->addItem(tr("Sort by Downloads"));
+ addSortings();
// sometimes Qt just ignores virtual slots and doesn't work as intended it seems,
// so it's best not to connect them in the parent's contructor...
- connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch()));
- connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameModPage::onSelectionChanged);
- connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FlameModPage::onVersionSelectionChanged);
- connect(ui->modSelectionButton, &QPushButton::clicked, this, &FlameModPage::onModSelected);
+ connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch()));
+ connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameModPage::onSelectionChanged);
+ connect(m_ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FlameModPage::onVersionSelectionChanged);
+ connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &FlameModPage::onResourceSelected);
- ui->packDescription->setMetaEntry(metaEntryBase());
+ m_ui->packDescription->setMetaEntry(metaEntryBase());
}
-auto FlameModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders) const -> bool
+auto FlameModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, std::optional<ResourceAPI::ModLoaderTypes> loaders) const -> bool
{
Q_UNUSED(loaders);
return ver.mcVersion.contains(mineVer) && !ver.downloadUrl.isEmpty();
@@ -95,3 +93,5 @@ void FlameModPage::openUrl(const QUrl& url)
ModPage::openUrl(url);
}
+
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.h b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h
index 58479ab9..b21a53ad 100644
--- a/launcher/ui/pages/modplatform/flame/FlameModPage.h
+++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h
@@ -1,4 +1,6 @@
-// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
@@ -36,35 +38,48 @@
#pragma once
-#include "modplatform/ModAPI.h"
+#include "Application.h"
+
+#include "modplatform/ResourceAPI.h"
+
#include "ui/pages/modplatform/ModPage.h"
-#include "modplatform/flame/FlameAPI.h"
+namespace ResourceDownload {
+
+namespace Flame {
+static inline QString displayName() { return "CurseForge"; }
+static inline QIcon icon() { return APPLICATION->getThemedIcon("flame"); }
+static inline QString id() { return "curseforge"; }
+static inline QString debugName() { return "Flame"; }
+static inline QString metaEntryBase() { return "FlameMods"; }
+}
class FlameModPage : public ModPage {
Q_OBJECT
public:
- static FlameModPage* create(ModDownloadDialog* dialog, BaseInstance* instance)
+ static FlameModPage* create(ModDownloadDialog* dialog, BaseInstance& instance)
{
return ModPage::create<FlameModPage>(dialog, instance);
}
- FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance);
+ FlameModPage(ModDownloadDialog* dialog, BaseInstance& instance);
~FlameModPage() override = default;
- inline auto displayName() const -> QString override { return "CurseForge"; }
- inline auto icon() const -> QIcon override { return APPLICATION->getThemedIcon("flame"); }
- inline auto id() const -> QString override { return "curseforge"; }
- inline auto helpPage() const -> QString override { return "Mod-platform"; }
+ [[nodiscard]] bool shouldDisplay() const override;
- inline auto debugName() const -> QString override { return "Flame"; }
- inline auto metaEntryBase() const -> QString override { return "FlameMods"; };
+ [[nodiscard]] inline auto displayName() const -> QString override { return Flame::displayName(); }
+ [[nodiscard]] inline auto icon() const -> QIcon override { return Flame::icon(); }
+ [[nodiscard]] inline auto id() const -> QString override { return Flame::id(); }
+ [[nodiscard]] inline auto debugName() const -> QString override { return Flame::debugName(); }
+ [[nodiscard]] inline auto metaEntryBase() const -> QString override { return Flame::metaEntryBase(); }
- auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders = ModAPI::Unspecified) const -> bool override;
- bool optedOut(ModPlatform::IndexedVersion& ver) const override;
+ [[nodiscard]] inline auto helpPage() const -> QString override { return "Mod-platform"; }
- auto shouldDisplay() const -> bool override;
+ bool validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, std::optional<ResourceAPI::ModLoaderTypes> loaders = {}) const override;
+ bool optedOut(ModPlatform::IndexedVersion& ver) const override;
void openUrl(const QUrl& url) override;
};
+
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/ftb/FtbPage.cpp b/launcher/ui/pages/modplatform/ftb/FtbPage.cpp
index b08f3bc4..7d59a6ae 100644
--- a/launcher/ui/pages/modplatform/ftb/FtbPage.cpp
+++ b/launcher/ui/pages/modplatform/ftb/FtbPage.cpp
@@ -43,7 +43,7 @@
#include "ui/dialogs/NewInstanceDialog.h"
#include "modplatform/modpacksch/FTBPackInstallTask.h"
-#include "HoeDown.h"
+#include "Markdown.h"
FtbPage::FtbPage(NewInstanceDialog* dialog, QWidget *parent)
: QWidget(parent), ui(new Ui::FtbPage), dialog(dialog)
@@ -175,8 +175,7 @@ void FtbPage::onSelectionChanged(QModelIndex first, QModelIndex second)
selected = filterModel->data(first, Qt::UserRole).value<ModpacksCH::Modpack>();
- HoeDown hoedown;
- QString output = hoedown.process(selected.description.toUtf8());
+ QString output = markdownToHTML(selected.description.toUtf8());
ui->packDescription->setHtml(output);
// reverse foreach, so that the newest versions are first
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp
index e6704eef..80850b4c 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp
@@ -40,7 +40,6 @@
#include "Json.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
-#include "ui/dialogs/ModDownloadDialog.h"
#include "ui/widgets/ProjectItem.h"
#include <QMessageBox>
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp
index 8ab2ad1d..0bb11d83 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp
@@ -42,11 +42,10 @@
#include "BuildConfig.h"
#include "InstanceImportTask.h"
#include "Json.h"
+#include "Markdown.h"
#include "ui/widgets/ProjectItem.h"
-#include <HoeDown.h>
-
#include <QComboBox>
#include <QKeyEvent>
#include <QPushButton>
@@ -280,8 +279,7 @@ void ModrinthPage::updateUI()
text += "<hr>";
- HoeDown h;
- text += h.process(current.extra.body.toUtf8());
+ text += markdownToHTML(current.extra.body.toUtf8());
ui->packDescription->setHtml(text + current.description);
ui->packDescription->flush();
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp
index af92e63e..73d55133 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.cpp
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp
@@ -1,3 +1,5 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
@@ -16,33 +18,33 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-#include "ModrinthModModel.h"
+#include "ModrinthResourceModels.h"
+#include "modplatform/modrinth/ModrinthAPI.h"
#include "modplatform/modrinth/ModrinthPackIndex.h"
-namespace Modrinth {
+namespace ResourceDownload {
-// NOLINTNEXTLINE(modernize-avoid-c-arrays)
-const char* ListModel::sorts[5]{ "relevance", "downloads", "follows", "updated", "newest" };
+ModrinthModModel::ModrinthModModel(BaseInstance const& base) : ModModel(base, new ModrinthAPI) {}
-void ListModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
+void ModrinthModModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
- Modrinth::loadIndexedPack(m, obj);
+ ::Modrinth::loadIndexedPack(m, obj);
}
-void ListModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj)
+void ModrinthModModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
- Modrinth::loadExtraPackData(m, obj);
+ ::Modrinth::loadExtraPackData(m, obj);
}
-void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
+void ModrinthModModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
{
- Modrinth::loadIndexedPackVersions(m, arr, APPLICATION->network(), m_parent->m_instance);
+ ::Modrinth::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance);
}
-auto ListModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
+auto ModrinthModModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
{
return obj.object().value("hits").toArray();
}
-} // namespace Modrinth
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h
index 386897fd..56cab146 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h
@@ -1,3 +1,5 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
@@ -18,27 +20,29 @@
#pragma once
-#include "ModrinthModPage.h"
+#include "ui/pages/modplatform/ModModel.h"
+#include "ui/pages/modplatform/modrinth/ModrinthResourcePages.h"
-namespace Modrinth {
+namespace ResourceDownload {
-class ListModel : public ModPlatform::ListModel {
+class ModrinthModPage;
+
+class ModrinthModModel : public ModModel {
Q_OBJECT
public:
- ListModel(ModrinthModPage* parent) : ModPlatform::ListModel(parent){};
- ~ListModel() override = default;
+ ModrinthModModel(const BaseInstance&);
+ ~ModrinthModModel() override = default;
private:
+ [[nodiscard]] QString debugName() const override { return Modrinth::debugName() + " (Model)"; }
+ [[nodiscard]] QString metaEntryBase() const override { return Modrinth::metaEntryBase(); }
+
void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override;
-
- auto documentToArray(QJsonDocument& obj) const -> QJsonArray override;
- // NOLINTNEXTLINE(modernize-avoid-c-arrays)
- static const char* sorts[5];
- inline auto getSorts() const -> const char** override { return sorts; };
+ auto documentToArray(QJsonDocument& obj) const -> QJsonArray override;
};
-} // namespace Modrinth
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp
index c531ea90..b82f800e 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp
@@ -1,3 +1,5 @@
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
@@ -33,48 +35,50 @@
* limitations under the License.
*/
-#include "ModrinthModPage.h"
+#include "ModrinthResourcePages.h"
+#include "ui_ResourcePage.h"
+
#include "modplatform/modrinth/ModrinthAPI.h"
-#include "ui_ModPage.h"
-#include "ModrinthModModel.h"
-#include "ui/dialogs/ModDownloadDialog.h"
+#include "ui/dialogs/ResourceDownloadDialog.h"
+
+#include "ui/pages/modplatform/modrinth/ModrinthResourceModels.h"
+
+namespace ResourceDownload {
-ModrinthModPage::ModrinthModPage(ModDownloadDialog* dialog, BaseInstance* instance)
- : ModPage(dialog, instance, new ModrinthAPI())
+ModrinthModPage::ModrinthModPage(ModDownloadDialog* dialog, BaseInstance& instance)
+ : ModPage(dialog, instance)
{
- listModel = new Modrinth::ListModel(this);
- ui->packView->setModel(listModel);
+ m_model = new ModrinthModModel(instance);
+ m_ui->packView->setModel(m_model);
- // index is used to set the sorting with the modrinth api
- ui->sortByBox->addItem(tr("Sort by Relevance"));
- ui->sortByBox->addItem(tr("Sort by Downloads"));
- ui->sortByBox->addItem(tr("Sort by Follows"));
- ui->sortByBox->addItem(tr("Sort by Last Updated"));
- ui->sortByBox->addItem(tr("Sort by Newest"));
+ addSortings();
// sometimes Qt just ignores virtual slots and doesn't work as intended it seems,
// so it's best not to connect them in the parent's constructor...
- connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch()));
- connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthModPage::onSelectionChanged);
- connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthModPage::onVersionSelectionChanged);
- connect(ui->modSelectionButton, &QPushButton::clicked, this, &ModrinthModPage::onModSelected);
+ connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch()));
+ connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthModPage::onSelectionChanged);
+ connect(m_ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthModPage::onVersionSelectionChanged);
+ connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &ModrinthModPage::onResourceSelected);
- ui->packDescription->setMetaEntry(metaEntryBase());
+ m_ui->packDescription->setMetaEntry(metaEntryBase());
}
-auto ModrinthModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders) const -> bool
+auto ModrinthModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, std::optional<ResourceAPI::ModLoaderTypes> loaders) const -> bool
{
- auto loaderStrings = ModrinthAPI::getModLoaderStrings(loaders);
+ auto loaderCompatible = !loaders.has_value();
- auto loaderCompatible = false;
- for (auto remoteLoader : ver.loaders)
- {
- if (loaderStrings.contains(remoteLoader)) {
- loaderCompatible = true;
- break;
+ if (!loaderCompatible) {
+ auto loaderStrings = ModrinthAPI::getModLoaderStrings(loaders.value());
+ for (auto remoteLoader : ver.loaders)
+ {
+ if (loaderStrings.contains(remoteLoader)) {
+ loaderCompatible = true;
+ break;
+ }
}
}
+
return ver.mcVersion.contains(mineVer) && loaderCompatible;
}
@@ -82,3 +86,5 @@ auto ModrinthModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString
// other mod providers start loading before being selected, at least with
// my Qt, so we need to implement this in every derived class...
auto ModrinthModPage::shouldDisplay() const -> bool { return true; }
+
+} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h
index 40d82e6f..be38eff1 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h
@@ -1,4 +1,6 @@
-// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
/*
* PolyMC - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
@@ -35,32 +37,45 @@
#pragma once
-#include "modplatform/ModAPI.h"
+#include "Application.h"
+
+#include "modplatform/ResourceAPI.h"
+
#include "ui/pages/modplatform/ModPage.h"
-#include "modplatform/modrinth/ModrinthAPI.h"
+namespace ResourceDownload {
+
+namespace Modrinth {
+static inline QString displayName() { return "Modrinth"; }
+static inline QIcon icon() { return APPLICATION->getThemedIcon("modrinth"); }
+static inline QString id() { return "modrinth"; }
+static inline QString debugName() { return "Modrinth"; }
+static inline QString metaEntryBase() { return "ModrinthPacks"; }
+}
class ModrinthModPage : public ModPage {
Q_OBJECT
public:
- static ModrinthModPage* create(ModDownloadDialog* dialog, BaseInstance* instance)
+ static ModrinthModPage* create(ModDownloadDialog* dialog, BaseInstance& instance)
{
return ModPage::create<ModrinthModPage>(dialog, instance);
}
- ModrinthModPage(ModDownloadDialog* dialog, BaseInstance* instance);
+ ModrinthModPage(ModDownloadDialog* dialog, BaseInstance& instance);
~ModrinthModPage() override = default;
- inline auto displayName() const -> QString override { return "Modrinth"; }
- inline auto icon() const -> QIcon override { return APPLICATION->getThemedIcon("modrinth"); }
- inline auto id() const -> QString override { return "modrinth"; }
- inline auto helpPage() const -> QString override { return "Mod-platform"; }
+ [[nodiscard]] bool shouldDisplay() const override;
- inline auto debugName() const -> QString override { return "Modrinth"; }
- inline auto metaEntryBase() const -> QString override { return "ModrinthPacks"; };
+ [[nodiscard]] inline auto displayName() const -> QString override { return Modrinth::displayName(); }
+ [[nodiscard]] inline auto icon() const -> QIcon override { return Modrinth::icon(); }
+ [[nodiscard]] inline auto id() const -> QString override { return Modrinth::id(); }
+ [[nodiscard]] inline auto debugName() const -> QString override { return Modrinth::debugName(); }
+ [[nodiscard]] inline auto metaEntryBase() const -> QString override { return Modrinth::metaEntryBase(); }
- auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders = ModAPI::Unspecified) const -> bool override;
+ [[nodiscard]] inline auto helpPage() const -> QString override { return "Mod-platform"; }
- auto shouldDisplay() const -> bool override;
+ auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, std::optional<ResourceAPI::ModLoaderTypes> loaders = {}) const -> bool override;
};
+
+} // namespace ResourceDownload
diff --git a/launcher/ui/setupwizard/SetupWizard.cpp b/launcher/ui/setupwizard/SetupWizard.cpp
index 3c8b5d39..3fd9bb23 100644
--- a/launcher/ui/setupwizard/SetupWizard.cpp
+++ b/launcher/ui/setupwizard/SetupWizard.cpp
@@ -13,7 +13,8 @@
SetupWizard::SetupWizard(QWidget *parent) : QWizard(parent)
{
setObjectName(QStringLiteral("SetupWizard"));
- resize(615, 659);
+ resize(620, 660);
+ setMinimumSize(300, 400);
// make it ugly everywhere to avoid variability in theming
setWizardStyle(QWizard::ClassicStyle);
setOptions(QWizard::NoCancelButton | QWizard::IndependentPages | QWizard::HaveCustomButton1);
diff --git a/launcher/ui/setupwizard/ThemeWizardPage.cpp b/launcher/ui/setupwizard/ThemeWizardPage.cpp
new file mode 100644
index 00000000..42826aba
--- /dev/null
+++ b/launcher/ui/setupwizard/ThemeWizardPage.cpp
@@ -0,0 +1,70 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Tayou <tayou@gmx.net>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+#include "ThemeWizardPage.h"
+#include "ui_ThemeWizardPage.h"
+
+#include "Application.h"
+#include "ui/themes/ITheme.h"
+#include "ui/themes/ThemeManager.h"
+#include "ui/widgets/ThemeCustomizationWidget.h"
+#include "ui_ThemeCustomizationWidget.h"
+
+ThemeWizardPage::ThemeWizardPage(QWidget* parent) : BaseWizardPage(parent), ui(new Ui::ThemeWizardPage)
+{
+ ui->setupUi(this);
+
+ connect(ui->themeCustomizationWidget, &ThemeCustomizationWidget::currentIconThemeChanged, this, &ThemeWizardPage::updateIcons);
+ connect(ui->themeCustomizationWidget, &ThemeCustomizationWidget::currentCatChanged, this, &ThemeWizardPage::updateCat);
+
+ updateIcons();
+ updateCat();
+}
+
+ThemeWizardPage::~ThemeWizardPage()
+{
+ delete ui;
+}
+
+void ThemeWizardPage::updateIcons()
+{
+ qDebug() << "Setting Icons";
+ ui->previewIconButton0->setIcon(APPLICATION->getThemedIcon("new"));
+ ui->previewIconButton1->setIcon(APPLICATION->getThemedIcon("centralmods"));
+ ui->previewIconButton2->setIcon(APPLICATION->getThemedIcon("viewfolder"));
+ ui->previewIconButton3->setIcon(APPLICATION->getThemedIcon("launch"));
+ ui->previewIconButton4->setIcon(APPLICATION->getThemedIcon("copy"));
+ ui->previewIconButton5->setIcon(APPLICATION->getThemedIcon("export"));
+ ui->previewIconButton6->setIcon(APPLICATION->getThemedIcon("delete"));
+ ui->previewIconButton7->setIcon(APPLICATION->getThemedIcon("about"));
+ ui->previewIconButton8->setIcon(APPLICATION->getThemedIcon("settings"));
+ ui->previewIconButton9->setIcon(APPLICATION->getThemedIcon("cat"));
+ update();
+ repaint();
+ parentWidget()->update();
+}
+
+void ThemeWizardPage::updateCat()
+{
+ qDebug() << "Setting Cat";
+ ui->catImagePreviewButton->setIcon(QIcon(QString(R"(:/backgrounds/%1)").arg(ThemeManager::getCatImage())));
+}
+
+void ThemeWizardPage::retranslate()
+{
+ ui->retranslateUi(this);
+}
diff --git a/launcher/ui/setupwizard/ThemeWizardPage.h b/launcher/ui/setupwizard/ThemeWizardPage.h
new file mode 100644
index 00000000..61a3d0c0
--- /dev/null
+++ b/launcher/ui/setupwizard/ThemeWizardPage.h
@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Tayou <tayou@gmx.net>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+#pragma once
+
+#include <QWidget>
+#include "BaseWizardPage.h"
+
+namespace Ui {
+class ThemeWizardPage;
+}
+
+class ThemeWizardPage : public BaseWizardPage {
+ Q_OBJECT
+
+ public:
+ explicit ThemeWizardPage(QWidget* parent = nullptr);
+ ~ThemeWizardPage();
+
+ bool validatePage() override { return true; };
+ void retranslate() override;
+
+ private slots:
+ void updateIcons();
+ void updateCat();
+
+ private:
+ Ui::ThemeWizardPage* ui;
+};
diff --git a/launcher/ui/setupwizard/ThemeWizardPage.ui b/launcher/ui/setupwizard/ThemeWizardPage.ui
new file mode 100644
index 00000000..01394ea4
--- /dev/null
+++ b/launcher/ui/setupwizard/ThemeWizardPage.ui
@@ -0,0 +1,371 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ThemeWizardPage</class>
+ <widget class="QWizardPage" name="ThemeWizardPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>510</width>
+ <height>552</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>WizardPage</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>Select the Theme you wish to use</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="ThemeCustomizationWidget" name="themeCustomizationWidget" native="true">
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>100</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_2">
+ <property name="text">
+ <string>Hint: The cat appears in the background and is not shown by default. It is only made visible when pressing the Cat button in the Toolbar.</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="Line" name="line">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_4">
+ <property name="text">
+ <string> Preview:</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QGridLayout" name="iconPreview">
+ <item row="0" column="2">
+ <widget class="QPushButton" name="previewIconButton2">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Maximum" vsizetype="Maximum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>30</width>
+ <height>30</height>
+ </size>
+ </property>
+ <property name="icon">
+ <iconset theme="applications-engineering">
+ <normaloff>.</normaloff>.</iconset>
+ </property>
+ <property name="checkable">
+ <bool>false</bool>
+ </property>
+ <property name="flat">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="5">
+ <widget class="QPushButton" name="previewIconButton5">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Maximum" vsizetype="Maximum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>30</width>
+ <height>30</height>
+ </size>
+ </property>
+ <property name="icon">
+ <iconset theme="applications-engineering">
+ <normaloff>.</normaloff>.</iconset>
+ </property>
+ <property name="checkable">
+ <bool>false</bool>
+ </property>
+ <property name="flat">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="7">
+ <widget class="QPushButton" name="previewIconButton7">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Maximum" vsizetype="Maximum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>30</width>
+ <height>30</height>
+ </size>
+ </property>
+ <property name="icon">
+ <iconset theme="applications-engineering">
+ <normaloff>.</normaloff>.</iconset>
+ </property>
+ <property name="checkable">
+ <bool>false</bool>
+ </property>
+ <property name="flat">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="4">
+ <widget class="QPushButton" name="previewIconButton4">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Maximum" vsizetype="Maximum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>30</width>
+ <height>30</height>
+ </size>
+ </property>
+ <property name="icon">
+ <iconset theme="applications-engineering">
+ <normaloff>.</normaloff>.</iconset>
+ </property>
+ <property name="checkable">
+ <bool>false</bool>
+ </property>
+ <property name="flat">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QPushButton" name="previewIconButton1">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Maximum" vsizetype="Maximum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>30</width>
+ <height>30</height>
+ </size>
+ </property>
+ <property name="icon">
+ <iconset theme="centralmods">
+ <normaloff>.</normaloff>.</iconset>
+ </property>
+ <property name="checkable">
+ <bool>false</bool>
+ </property>
+ <property name="flat">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QPushButton" name="previewIconButton0">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Maximum" vsizetype="Maximum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>30</width>
+ <height>30</height>
+ </size>
+ </property>
+ <property name="icon">
+ <iconset theme="applications-engineering">
+ <normaloff>.</normaloff>.</iconset>
+ </property>
+ <property name="checkable">
+ <bool>false</bool>
+ </property>
+ <property name="flat">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="9">
+ <widget class="QPushButton" name="previewIconButton9">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Maximum" vsizetype="Maximum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>30</width>
+ <height>30</height>
+ </size>
+ </property>
+ <property name="icon">
+ <iconset theme="viewfolder">
+ <normaloff>.</normaloff>.</iconset>
+ </property>
+ <property name="checkable">
+ <bool>false</bool>
+ </property>
+ <property name="flat">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="6">
+ <widget class="QPushButton" name="previewIconButton6">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Maximum" vsizetype="Maximum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>30</width>
+ <height>30</height>
+ </size>
+ </property>
+ <property name="icon">
+ <iconset theme="new">
+ <normaloff>.</normaloff>.</iconset>
+ </property>
+ <property name="checkable">
+ <bool>false</bool>
+ </property>
+ <property name="flat">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="3">
+ <widget class="QPushButton" name="previewIconButton3">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Maximum" vsizetype="Maximum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>30</width>
+ <height>30</height>
+ </size>
+ </property>
+ <property name="icon">
+ <iconset theme="applications-engineering">
+ <normaloff>.</normaloff>.</iconset>
+ </property>
+ <property name="checkable">
+ <bool>false</bool>
+ </property>
+ <property name="flat">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="8">
+ <widget class="QPushButton" name="previewIconButton8">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Maximum" vsizetype="Maximum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>30</width>
+ <height>30</height>
+ </size>
+ </property>
+ <property name="icon">
+ <iconset theme="applications-engineering">
+ <normaloff>.</normaloff>.</iconset>
+ </property>
+ <property name="checkable">
+ <bool>false</bool>
+ </property>
+ <property name="flat">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QPushButton" name="catImagePreviewButton">
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>256</height>
+ </size>
+ </property>
+ <property name="toolTip">
+ <string>The cat appears in the background and does not serve a purpose, it is purely visual.</string>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>256</width>
+ <height>256</height>
+ </size>
+ </property>
+ <property name="flat">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>193</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>ThemeCustomizationWidget</class>
+ <extends>QWidget</extends>
+ <header>ui/widgets/ThemeCustomizationWidget.h</header>
+ </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/ui/themes/CustomTheme.cpp b/launcher/ui/themes/CustomTheme.cpp
index 3ad61668..198e76ba 100644
--- a/launcher/ui/themes/CustomTheme.cpp
+++ b/launcher/ui/themes/CustomTheme.cpp
@@ -167,8 +167,6 @@ CustomTheme::CustomTheme(ITheme* baseTheme, QFileInfo& fileInfo, bool isManifest
if (!FS::ensureFolderPathExists(path) || !FS::ensureFolderPathExists(pathResources)) {
themeWarningLog() << "couldn't create folder for theme!";
- m_palette = baseTheme->colorScheme();
- m_styleSheet = baseTheme->appStyleSheet();
return;
}
@@ -177,18 +175,15 @@ CustomTheme::CustomTheme(ITheme* baseTheme, QFileInfo& fileInfo, bool isManifest
bool jsonDataIncomplete = false;
m_palette = baseTheme->colorScheme();
- if (!readThemeJson(themeFilePath, m_palette, m_fadeAmount, m_fadeColor, m_name, m_widgets, m_qssFilePath, jsonDataIncomplete)) {
- themeDebugLog() << "Did not read theme json file correctly, writing new one to: " << themeFilePath;
- m_name = "Custom";
- m_palette = baseTheme->colorScheme();
- m_fadeColor = baseTheme->fadeColor();
- m_fadeAmount = baseTheme->fadeAmount();
- m_widgets = baseTheme->qtTheme();
- m_qssFilePath = "themeStyle.css";
- } else {
+ if (readThemeJson(themeFilePath, m_palette, m_fadeAmount, m_fadeColor, m_name, m_widgets, m_qssFilePath, jsonDataIncomplete)) {
+ // If theme data was found, fade "Disabled" color of each role according to FadeAmount
m_palette = fadeInactive(m_palette, m_fadeAmount, m_fadeColor);
+ } else {
+ themeDebugLog() << "Did not read theme json file correctly, not changing theme, keeping previous.";
+ return;
}
+ // FIXME: This is kinda jank, it only actually checks if the qss file path is not present. It should actually check for any relevant missing data (e.g. name, colors)
if (jsonDataIncomplete) {
writeThemeJson(fileInfo.absoluteFilePath(), m_palette, m_fadeAmount, m_fadeColor, m_name, m_widgets, m_qssFilePath);
}
@@ -197,20 +192,14 @@ CustomTheme::CustomTheme(ITheme* baseTheme, QFileInfo& fileInfo, bool isManifest
QFileInfo info(qssFilePath);
if (info.isFile()) {
try {
- // TODO: validate css?
+ // TODO: validate qss?
m_styleSheet = QString::fromUtf8(FS::read(qssFilePath));
} catch (const Exception& e) {
- themeWarningLog() << "Couldn't load css:" << e.cause() << "from" << qssFilePath;
- m_styleSheet = baseTheme->appStyleSheet();
+ themeWarningLog() << "Couldn't load qss:" << e.cause() << "from" << qssFilePath;
+ return;
}
} else {
- themeDebugLog() << "No theme css present.";
- m_styleSheet = baseTheme->appStyleSheet();
- try {
- FS::write(qssFilePath, m_styleSheet.toUtf8());
- } catch (const Exception& e) {
- themeWarningLog() << "Couldn't write css:" << e.cause() << "to" << qssFilePath;
- }
+ themeDebugLog() << "No theme qss present.";
}
} else {
m_id = fileInfo.fileName();
diff --git a/launcher/ui/themes/ITheme.cpp b/launcher/ui/themes/ITheme.cpp
index 8bfc466d..22043e44 100644
--- a/launcher/ui/themes/ITheme.cpp
+++ b/launcher/ui/themes/ITheme.cpp
@@ -1,19 +1,51 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Tayou <tayou@gmx.net>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
#include "ITheme.h"
#include "rainbow.h"
#include <QStyleFactory>
#include <QDir>
#include "Application.h"
-void ITheme::apply(bool)
+void ITheme::apply()
{
APPLICATION->setStyleSheet(QString());
QApplication::setStyle(QStyleFactory::create(qtTheme()));
if (hasColorScheme()) {
QApplication::setPalette(colorScheme());
}
- if (hasStyleSheet())
- APPLICATION->setStyleSheet(appStyleSheet());
-
+ APPLICATION->setStyleSheet(appStyleSheet());
QDir::setSearchPaths("theme", searchPaths());
}
diff --git a/launcher/ui/themes/ITheme.h b/launcher/ui/themes/ITheme.h
index c2347cf6..2e5b7f25 100644
--- a/launcher/ui/themes/ITheme.h
+++ b/launcher/ui/themes/ITheme.h
@@ -1,14 +1,47 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Tayou <tayou@gmx.net>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
#pragma once
-#include <QString>
#include <QPalette>
+#include <QString>
class QStyle;
-class ITheme
-{
-public:
+class ITheme {
+ public:
virtual ~ITheme() {}
- virtual void apply(bool initial);
+ virtual void apply();
virtual QString id() = 0;
virtual QString name() = 0;
virtual bool hasStyleSheet() = 0;
@@ -18,10 +51,7 @@ public:
virtual QPalette colorScheme() = 0;
virtual QColor fadeColor() = 0;
virtual double fadeAmount() = 0;
- virtual QStringList searchPaths()
- {
- return {};
- }
+ virtual QStringList searchPaths() { return {}; }
static QPalette fadeInactive(QPalette in, qreal bias, QColor color);
};
diff --git a/launcher/ui/themes/SystemTheme.cpp b/launcher/ui/themes/SystemTheme.cpp
index a63d1741..24875e33 100644
--- a/launcher/ui/themes/SystemTheme.cpp
+++ b/launcher/ui/themes/SystemTheme.cpp
@@ -34,24 +34,22 @@
*/
#include "SystemTheme.h"
#include <QApplication>
+#include <QDebug>
#include <QStyle>
#include <QStyleFactory>
-#include <QDebug>
#include "ThemeManager.h"
SystemTheme::SystemTheme()
{
themeDebugLog() << "Determining System Theme...";
- const auto & style = QApplication::style();
+ const auto& style = QApplication::style();
systemPalette = style->standardPalette();
QString lowerThemeName = style->objectName();
themeDebugLog() << "System theme seems to be:" << lowerThemeName;
QStringList styles = QStyleFactory::keys();
- for(auto &st: styles)
- {
+ for (auto& st : styles) {
themeDebugLog() << "Considering theme from theme factory:" << st.toLower();
- if(st.toLower() == lowerThemeName)
- {
+ if (st.toLower() == lowerThemeName) {
systemTheme = st;
themeDebugLog() << "System theme has been determined to be:" << systemTheme;
return;
@@ -62,14 +60,9 @@ SystemTheme::SystemTheme()
themeDebugLog() << "System theme not found, defaulted to Fusion";
}
-void SystemTheme::apply(bool initial)
+void SystemTheme::apply()
{
- // if we are applying the system theme as the first theme, just don't touch anything. it's for the better...
- if(initial)
- {
- return;
- }
- ITheme::apply(initial);
+ ITheme::apply();
}
QString SystemTheme::id()
@@ -104,7 +97,7 @@ double SystemTheme::fadeAmount()
QColor SystemTheme::fadeColor()
{
- return QColor(128,128,128);
+ return QColor(128, 128, 128);
}
bool SystemTheme::hasStyleSheet()
diff --git a/launcher/ui/themes/SystemTheme.h b/launcher/ui/themes/SystemTheme.h
index fe450600..b5c03def 100644
--- a/launcher/ui/themes/SystemTheme.h
+++ b/launcher/ui/themes/SystemTheme.h
@@ -1,13 +1,46 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Tayou <tayou@gmx.net>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
#pragma once
#include "ITheme.h"
-class SystemTheme: public ITheme
-{
-public:
+class SystemTheme : public ITheme {
+ public:
SystemTheme();
virtual ~SystemTheme() {}
- void apply(bool initial) override;
+ void apply() override;
QString id() override;
QString name() override;
@@ -18,7 +51,8 @@ public:
QPalette colorScheme() override;
double fadeAmount() override;
QColor fadeColor() override;
-private:
+
+ private:
QPalette systemPalette;
QString systemTheme;
};
diff --git a/launcher/ui/themes/ThemeManager.cpp b/launcher/ui/themes/ThemeManager.cpp
index 01a38a86..13406485 100644
--- a/launcher/ui/themes/ThemeManager.cpp
+++ b/launcher/ui/themes/ThemeManager.cpp
@@ -1,155 +1,155 @@
-// SPDX-License-Identifier: GPL-3.0-only
-/*
- * Prism Launcher - Minecraft Launcher
- * Copyright (C) 2022 Tayou <tayou@gmx.net>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
-#include "ThemeManager.h"
-
-#include <QApplication>
-#include <QDir>
-#include <QDirIterator>
-#include <QIcon>
-#include "ui/themes/BrightTheme.h"
-#include "ui/themes/CustomTheme.h"
-#include "ui/themes/DarkTheme.h"
-#include "ui/themes/SystemTheme.h"
-
-#include "Application.h"
-
-#ifdef Q_OS_WIN
-#include <windows.h>
-// this is needed for versionhelpers.h, it is also included in WinDarkmode, but we can't rely on that.
-// Ultimately this should be included in versionhelpers, but that is outside of the project.
-#include "ui/WinDarkmode.h"
-#include <versionhelpers.h>
-#endif
-
-ThemeManager::ThemeManager(MainWindow* mainWindow)
-{
- m_mainWindow = mainWindow;
- InitializeThemes();
-}
-
-/// @brief Adds the Theme to the list of themes
-/// @param theme The Theme to add
-/// @return Theme ID
-QString ThemeManager::AddTheme(std::unique_ptr<ITheme> theme)
-{
- QString id = theme->id();
- m_themes.emplace(id, std::move(theme));
- return id;
-}
-
-/// @brief Gets the Theme from the List via ID
-/// @param themeId Theme ID of theme to fetch
-/// @return Theme at themeId
-ITheme* ThemeManager::GetTheme(QString themeId)
-{
- return m_themes[themeId].get();
-}
-
-void ThemeManager::InitializeThemes()
-{
- // Icon themes
- {
- // TODO: icon themes and instance icons do not mesh well together. Rearrange and fix discrepancies!
- // set icon theme search path!
- auto searchPaths = QIcon::themeSearchPaths();
- searchPaths.append("iconthemes");
- QIcon::setThemeSearchPaths(searchPaths);
- themeDebugLog() << "<> Icon themes initialized.";
- }
-
- // Initialize widget themes
- {
- themeDebugLog() << "<> Initializing Widget Themes";
- themeDebugLog() << "Loading Built-in Theme:" << AddTheme(std::make_unique<SystemTheme>());
- auto darkThemeId = AddTheme(std::make_unique<DarkTheme>());
- themeDebugLog() << "Loading Built-in Theme:" << darkThemeId;
- themeDebugLog() << "Loading Built-in Theme:" << AddTheme(std::make_unique<BrightTheme>());
-
- // TODO: need some way to differentiate same name themes in different subdirectories (maybe smaller grey text next to theme name in
- // dropdown?)
- QString themeFolder = QDir("./themes/").absoluteFilePath("");
- themeDebugLog() << "Theme Folder Path: " << themeFolder;
-
- QDirIterator directoryIterator(themeFolder, QDir::Dirs | QDir::NoDotAndDotDot, QDirIterator::Subdirectories);
- while (directoryIterator.hasNext()) {
- QDir dir(directoryIterator.next());
- QFileInfo themeJson(dir.absoluteFilePath("theme.json"));
- if (themeJson.exists()) {
- // Load "theme.json" based themes
- themeDebugLog() << "Loading JSON Theme from:" << themeJson.absoluteFilePath();
- AddTheme(std::make_unique<CustomTheme>(GetTheme(darkThemeId), themeJson, true));
- } else {
- // Load pure QSS Themes
- QDirIterator stylesheetFileIterator(dir.absoluteFilePath(""), { "*.qss", "*.css" }, QDir::Files);
- while (stylesheetFileIterator.hasNext()) {
- QFile customThemeFile(stylesheetFileIterator.next());
- QFileInfo customThemeFileInfo(customThemeFile);
- themeDebugLog() << "Loading QSS Theme from:" << customThemeFileInfo.absoluteFilePath();
- AddTheme(std::make_unique<CustomTheme>(GetTheme(darkThemeId), customThemeFileInfo, false));
- }
- }
- }
-
- themeDebugLog() << "<> Widget themes initialized.";
- }
-}
-
-QList<ITheme*> ThemeManager::getValidApplicationThemes()
-{
- QList<ITheme*> ret;
- ret.reserve(m_themes.size());
- for (auto&& [id, theme] : m_themes) {
- ret.append(theme.get());
- }
- return ret;
-}
-
-void ThemeManager::setIconTheme(const QString& name)
-{
- QIcon::setThemeName(name);
-}
-
-void ThemeManager::applyCurrentlySelectedTheme()
-{
- setIconTheme(APPLICATION->settings()->get("IconTheme").toString());
- themeDebugLog() << "<> Icon theme set.";
- setApplicationTheme(APPLICATION->settings()->get("ApplicationTheme").toString(), true);
- themeDebugLog() << "<> Application theme set.";
-}
-
-void ThemeManager::setApplicationTheme(const QString& name, bool initial)
-{
- auto systemPalette = qApp->palette();
- auto themeIter = m_themes.find(name);
- if (themeIter != m_themes.end()) {
- auto& theme = themeIter->second;
- themeDebugLog() << "applying theme" << theme->name();
- theme->apply(initial);
-#ifdef Q_OS_WIN
- if (m_mainWindow && IsWindows10OrGreater()) {
- if (QString::compare(theme->id(), "dark") == 0) {
- WinDarkmode::setDarkWinTitlebar(m_mainWindow->winId(), true);
- } else {
- WinDarkmode::setDarkWinTitlebar(m_mainWindow->winId(), false);
- }
- }
-#endif
- } else {
- themeWarningLog() << "Tried to set invalid theme:" << name;
- }
-}
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Tayou <tayou@gmx.net>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+#include "ThemeManager.h"
+
+#include <QApplication>
+#include <QDir>
+#include <QDirIterator>
+#include <QIcon>
+#include "ui/themes/BrightTheme.h"
+#include "ui/themes/CustomTheme.h"
+#include "ui/themes/DarkTheme.h"
+#include "ui/themes/SystemTheme.h"
+
+#include "Application.h"
+
+ThemeManager::ThemeManager(MainWindow* mainWindow)
+{
+ m_mainWindow = mainWindow;
+ initializeThemes();
+}
+
+/// @brief Adds the Theme to the list of themes
+/// @param theme The Theme to add
+/// @return Theme ID
+QString ThemeManager::addTheme(std::unique_ptr<ITheme> theme)
+{
+ QString id = theme->id();
+ m_themes.emplace(id, std::move(theme));
+ return id;
+}
+
+/// @brief Gets the Theme from the List via ID
+/// @param themeId Theme ID of theme to fetch
+/// @return Theme at themeId
+ITheme* ThemeManager::getTheme(QString themeId)
+{
+ return m_themes[themeId].get();
+}
+
+void ThemeManager::initializeThemes()
+{
+ // Icon themes
+ {
+ // TODO: icon themes and instance icons do not mesh well together. Rearrange and fix discrepancies!
+ // set icon theme search path!
+ auto searchPaths = QIcon::themeSearchPaths();
+ searchPaths.append("iconthemes");
+ QIcon::setThemeSearchPaths(searchPaths);
+ themeDebugLog() << "<> Icon themes initialized.";
+ }
+
+ // Initialize widget themes
+ {
+ themeDebugLog() << "<> Initializing Widget Themes";
+ themeDebugLog() << "Loading Built-in Theme:" << addTheme(std::make_unique<SystemTheme>());
+ auto darkThemeId = addTheme(std::make_unique<DarkTheme>());
+ themeDebugLog() << "Loading Built-in Theme:" << darkThemeId;
+ themeDebugLog() << "Loading Built-in Theme:" << addTheme(std::make_unique<BrightTheme>());
+
+ // TODO: need some way to differentiate same name themes in different subdirectories (maybe smaller grey text next to theme name in
+ // dropdown?)
+ QString themeFolder = QDir("./themes/").absoluteFilePath("");
+ themeDebugLog() << "Theme Folder Path: " << themeFolder;
+
+ QDirIterator directoryIterator(themeFolder, QDir::Dirs | QDir::NoDotAndDotDot, QDirIterator::Subdirectories);
+ while (directoryIterator.hasNext()) {
+ QDir dir(directoryIterator.next());
+ QFileInfo themeJson(dir.absoluteFilePath("theme.json"));
+ if (themeJson.exists()) {
+ // Load "theme.json" based themes
+ themeDebugLog() << "Loading JSON Theme from:" << themeJson.absoluteFilePath();
+ addTheme(std::make_unique<CustomTheme>(getTheme(darkThemeId), themeJson, true));
+ } else {
+ // Load pure QSS Themes
+ QDirIterator stylesheetFileIterator(dir.absoluteFilePath(""), { "*.qss", "*.css" }, QDir::Files);
+ while (stylesheetFileIterator.hasNext()) {
+ QFile customThemeFile(stylesheetFileIterator.next());
+ QFileInfo customThemeFileInfo(customThemeFile);
+ themeDebugLog() << "Loading QSS Theme from:" << customThemeFileInfo.absoluteFilePath();
+ addTheme(std::make_unique<CustomTheme>(getTheme(darkThemeId), customThemeFileInfo, false));
+ }
+ }
+ }
+
+ themeDebugLog() << "<> Widget themes initialized.";
+ }
+}
+
+QList<ITheme*> ThemeManager::getValidApplicationThemes()
+{
+ QList<ITheme*> ret;
+ ret.reserve(m_themes.size());
+ for (auto&& [id, theme] : m_themes) {
+ ret.append(theme.get());
+ }
+ return ret;
+}
+
+void ThemeManager::setIconTheme(const QString& name)
+{
+ QIcon::setThemeName(name);
+}
+
+void ThemeManager::applyCurrentlySelectedTheme()
+{
+ setIconTheme(APPLICATION->settings()->get("IconTheme").toString());
+ themeDebugLog() << "<> Icon theme set.";
+ setApplicationTheme(APPLICATION->settings()->get("ApplicationTheme").toString());
+ themeDebugLog() << "<> Application theme set.";
+}
+
+void ThemeManager::setApplicationTheme(const QString& name)
+{
+ auto systemPalette = qApp->palette();
+ auto themeIter = m_themes.find(name);
+ if (themeIter != m_themes.end()) {
+ auto& theme = themeIter->second;
+ themeDebugLog() << "applying theme" << theme->name();
+ theme->apply();
+ } else {
+ themeWarningLog() << "Tried to set invalid theme:" << name;
+ }
+}
+
+QString ThemeManager::getCatImage(QString catName)
+{
+ QDateTime now = QDateTime::currentDateTime();
+ QDateTime birthday(QDate(now.date().year(), 11, 30), QTime(0, 0));
+ QDateTime xmas(QDate(now.date().year(), 12, 25), QTime(0, 0));
+ QDateTime halloween(QDate(now.date().year(), 10, 31), QTime(0, 0));
+ QString cat = !catName.isEmpty() ? catName : APPLICATION->settings()->get("BackgroundCat").toString();
+ if (std::abs(now.daysTo(xmas)) <= 4) {
+ cat += "-xmas";
+ } else if (std::abs(now.daysTo(halloween)) <= 4) {
+ cat += "-spooky";
+ } else if (std::abs(now.daysTo(birthday)) <= 12) {
+ cat += "-bday";
+ }
+ return cat;
+}
diff --git a/launcher/ui/themes/ThemeManager.h b/launcher/ui/themes/ThemeManager.h
index b85cb742..9af44b5a 100644
--- a/launcher/ui/themes/ThemeManager.h
+++ b/launcher/ui/themes/ThemeManager.h
@@ -1,52 +1,57 @@
-// SPDX-License-Identifier: GPL-3.0-only
-/*
- * Prism Launcher - Minecraft Launcher
- * Copyright (C) 2022 Tayou <tayou@gmx.net>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
-#pragma once
-
-#include <QString>
-
-#include "ui/MainWindow.h"
-#include "ui/themes/ITheme.h"
-
-inline auto themeDebugLog()
-{
- return qDebug() << "[Theme]";
-}
-inline auto themeWarningLog()
-{
- return qWarning() << "[Theme]";
-}
-
-class ThemeManager {
- public:
- ThemeManager(MainWindow* mainWindow);
-
- // maybe make private? Or put in ctor?
- void InitializeThemes();
-
- QList<ITheme*> getValidApplicationThemes();
- void setIconTheme(const QString& name);
- void applyCurrentlySelectedTheme();
- void setApplicationTheme(const QString& name, bool initial);
-
- private:
- std::map<QString, std::unique_ptr<ITheme>> m_themes;
- MainWindow* m_mainWindow;
-
- QString AddTheme(std::unique_ptr<ITheme> theme);
- ITheme* GetTheme(QString themeId);
-};
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Tayou <tayou@gmx.net>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+#pragma once
+
+#include <QString>
+
+#include "ui/MainWindow.h"
+#include "ui/themes/ITheme.h"
+
+inline auto themeDebugLog()
+{
+ return qDebug() << "[Theme]";
+}
+inline auto themeWarningLog()
+{
+ return qWarning() << "[Theme]";
+}
+
+class ThemeManager {
+ public:
+ ThemeManager(MainWindow* mainWindow);
+
+ QList<ITheme*> getValidApplicationThemes();
+ void setIconTheme(const QString& name);
+ void applyCurrentlySelectedTheme();
+ void setApplicationTheme(const QString& name);
+
+ /// <summary>
+ /// Returns the cat based on selected cat and with events (Birthday, XMas, etc.)
+ /// </summary>
+ /// <param name="catName">Optional, if you need a specific cat.</param>
+ /// <returns></returns>
+ static QString getCatImage(QString catName = "");
+
+ private:
+ std::map<QString, std::unique_ptr<ITheme>> m_themes;
+ MainWindow* m_mainWindow;
+
+ void initializeThemes();
+ QString addTheme(std::unique_ptr<ITheme> theme);
+ ITheme* getTheme(QString themeId);
+};
diff --git a/launcher/ui/widgets/ModListView.cpp b/launcher/ui/widgets/ModListView.cpp
index c8ccd292..09b03a76 100644
--- a/launcher/ui/widgets/ModListView.cpp
+++ b/launcher/ui/widgets/ModListView.cpp
@@ -14,6 +14,9 @@
*/
#include "ModListView.h"
+
+#include "minecraft/mod/ModFolderModel.h"
+
#include <QHeaderView>
#include <QMouseEvent>
#include <QPainter>
@@ -31,7 +34,6 @@ ModListView::ModListView ( QWidget* parent )
setSelectionMode ( QAbstractItemView::ExtendedSelection );
setHeaderHidden ( false );
setSelectionBehavior(QAbstractItemView::SelectRows);
- setVerticalScrollBarPolicy ( Qt::ScrollBarAlwaysOn );
setHorizontalScrollBarPolicy ( Qt::ScrollBarAsNeeded );
setDropIndicatorShown(true);
setDragEnabled(true);
@@ -63,4 +65,17 @@ void ModListView::setModel ( QAbstractItemModel* model )
for(int i = 1; i < head->count(); i++)
head->setSectionResizeMode(i, QHeaderView::ResizeToContents);
}
+
+ auto real_model = model;
+ if (auto proxy_model = dynamic_cast<QSortFilterProxyModel*>(model); proxy_model)
+ real_model = proxy_model->sourceModel();
+
+ if (auto mod_model = dynamic_cast<ModFolderModel*>(real_model); mod_model) {
+ connect(mod_model, &ModFolderModel::updateFinished, this, [this, mod_model]{
+ auto mods = mod_model->allMods();
+ // Hide the 'Provider' column if no mod has a defined provider!
+ setColumnHidden(ModFolderModel::Columns::ProviderColumn,
+ std::none_of(mods.constBegin(), mods.constEnd(), [](auto const mod){ return mod->provider().has_value(); }));
+ });
+ }
}
diff --git a/launcher/ui/widgets/ProgressWidget.cpp b/launcher/ui/widgets/ProgressWidget.cpp
index b60d9a7a..f736af08 100644
--- a/launcher/ui/widgets/ProgressWidget.cpp
+++ b/launcher/ui/widgets/ProgressWidget.cpp
@@ -39,7 +39,7 @@ void ProgressWidget::progressFormat(QString format)
m_bar->setFormat(format);
}
-void ProgressWidget::watch(Task* task)
+void ProgressWidget::watch(const Task* task)
{
if (!task)
return;
@@ -54,14 +54,17 @@ void ProgressWidget::watch(Task* task)
connect(m_task, &Task::progress, this, &ProgressWidget::handleTaskProgress);
connect(m_task, &Task::destroyed, this, &ProgressWidget::taskDestroyed);
- show();
+ if (m_task->isRunning())
+ show();
+ else
+ connect(m_task, &Task::started, this, &ProgressWidget::show);
}
-void ProgressWidget::start(Task* task)
+void ProgressWidget::start(const Task* task)
{
watch(task);
if (!m_task->isRunning())
- QMetaObject::invokeMethod(m_task, "start", Qt::QueuedConnection);
+ QMetaObject::invokeMethod(const_cast<Task*>(m_task), "start", Qt::QueuedConnection);
}
bool ProgressWidget::exec(std::shared_ptr<Task> task)
diff --git a/launcher/ui/widgets/ProgressWidget.h b/launcher/ui/widgets/ProgressWidget.h
index 4d9097b8..b0458f33 100644
--- a/launcher/ui/widgets/ProgressWidget.h
+++ b/launcher/ui/widgets/ProgressWidget.h
@@ -27,10 +27,10 @@ class ProgressWidget : public QWidget {
public slots:
/** Watch the progress of a task. */
- void watch(Task* task);
+ void watch(const Task* task);
/** Watch the progress of a task, and start it if needed */
- void start(Task* task);
+ void start(const Task* task);
/** Blocking way of waiting for a task to finish. */
bool exec(std::shared_ptr<Task> task);
@@ -50,7 +50,7 @@ class ProgressWidget : public QWidget {
private:
QLabel* m_label = nullptr;
QProgressBar* m_bar = nullptr;
- Task* m_task = nullptr;
+ const Task* m_task = nullptr;
bool m_hide_if_inactive = false;
};
diff --git a/launcher/ui/widgets/ThemeCustomizationWidget.cpp b/launcher/ui/widgets/ThemeCustomizationWidget.cpp
new file mode 100644
index 00000000..dcf13303
--- /dev/null
+++ b/launcher/ui/widgets/ThemeCustomizationWidget.cpp
@@ -0,0 +1,150 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Tayou <tayou@gmx.net>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+#include "ThemeCustomizationWidget.h"
+#include "ui_ThemeCustomizationWidget.h"
+
+#include "Application.h"
+#include "ui/themes/ITheme.h"
+#include "ui/themes/ThemeManager.h"
+
+ThemeCustomizationWidget::ThemeCustomizationWidget(QWidget *parent) : QWidget(parent), ui(new Ui::ThemeCustomizationWidget)
+{
+ ui->setupUi(this);
+ loadSettings();
+
+ connect(ui->iconsComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ThemeCustomizationWidget::applyIconTheme);
+ connect(ui->widgetStyleComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ThemeCustomizationWidget::applyWidgetTheme);
+ connect(ui->backgroundCatComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ThemeCustomizationWidget::applyCatTheme);
+}
+
+ThemeCustomizationWidget::~ThemeCustomizationWidget()
+{
+ delete ui;
+}
+
+/// <summary>
+/// The layout was not quite right, so currently this just disables the UI elements, which should be hidden instead
+/// TODO FIXME
+///
+/// Original Method One:
+/// ui->iconsComboBox->setVisible(features& ThemeFields::ICONS);
+/// ui->iconsLabel->setVisible(features& ThemeFields::ICONS);
+/// ui->widgetStyleComboBox->setVisible(features& ThemeFields::WIDGETS);
+/// ui->widgetThemeLabel->setVisible(features& ThemeFields::WIDGETS);
+/// ui->backgroundCatComboBox->setVisible(features& ThemeFields::CAT);
+/// ui->backgroundCatLabel->setVisible(features& ThemeFields::CAT);
+///
+/// original Method Two:
+/// if (!(features & ThemeFields::ICONS)) {
+/// ui->formLayout->setRowVisible(0, false);
+/// }
+/// if (!(features & ThemeFields::WIDGETS)) {
+/// ui->formLayout->setRowVisible(1, false);
+/// }
+/// if (!(features & ThemeFields::CAT)) {
+/// ui->formLayout->setRowVisible(2, false);
+/// }
+/// </summary>
+/// <param name="features"></param>
+void ThemeCustomizationWidget::showFeatures(ThemeFields features) {
+ ui->iconsComboBox->setEnabled(features & ThemeFields::ICONS);
+ ui->iconsLabel->setEnabled(features & ThemeFields::ICONS);
+ ui->widgetStyleComboBox->setEnabled(features & ThemeFields::WIDGETS);
+ ui->widgetThemeLabel->setEnabled(features & ThemeFields::WIDGETS);
+ ui->backgroundCatComboBox->setEnabled(features & ThemeFields::CAT);
+ ui->backgroundCatLabel->setEnabled(features & ThemeFields::CAT);
+}
+
+void ThemeCustomizationWidget::applyIconTheme(int index) {
+ auto settings = APPLICATION->settings();
+ auto originalIconTheme = settings->get("IconTheme").toString();
+ auto& newIconTheme = m_iconThemeOptions[index].first;
+ settings->set("IconTheme", newIconTheme);
+
+ if (originalIconTheme != newIconTheme) {
+ APPLICATION->applyCurrentlySelectedTheme();
+ }
+
+ emit currentIconThemeChanged(index);
+}
+
+void ThemeCustomizationWidget::applyWidgetTheme(int index) {
+ auto settings = APPLICATION->settings();
+ auto originalAppTheme = settings->get("ApplicationTheme").toString();
+ auto newAppTheme = ui->widgetStyleComboBox->currentData().toString();
+ if (originalAppTheme != newAppTheme) {
+ settings->set("ApplicationTheme", newAppTheme);
+ APPLICATION->applyCurrentlySelectedTheme();
+ }
+
+ emit currentWidgetThemeChanged(index);
+}
+
+void ThemeCustomizationWidget::applyCatTheme(int index) {
+ auto settings = APPLICATION->settings();
+ settings->set("BackgroundCat", m_catOptions[index].first);
+
+ emit currentCatChanged(index);
+}
+
+void ThemeCustomizationWidget::applySettings()
+{
+ applyIconTheme(ui->iconsComboBox->currentIndex());
+ applyWidgetTheme(ui->widgetStyleComboBox->currentIndex());
+ applyCatTheme(ui->backgroundCatComboBox->currentIndex());
+}
+void ThemeCustomizationWidget::loadSettings()
+{
+ auto settings = APPLICATION->settings();
+
+ auto iconTheme = settings->get("IconTheme").toString();
+ for (auto& iconThemeFromList : m_iconThemeOptions) {
+ QIcon iconForComboBox = QIcon(QString(":/icons/%1/scalable/settings").arg(iconThemeFromList.first));
+ ui->iconsComboBox->addItem(iconForComboBox, iconThemeFromList.second);
+ if (iconTheme == iconThemeFromList.first) {
+ ui->iconsComboBox->setCurrentIndex(ui->iconsComboBox->count() - 1);
+ }
+ }
+
+ {
+ auto currentTheme = settings->get("ApplicationTheme").toString();
+ auto themes = APPLICATION->getValidApplicationThemes();
+ int idx = 0;
+ for (auto& theme : themes) {
+ ui->widgetStyleComboBox->addItem(theme->name(), theme->id());
+ if (currentTheme == theme->id()) {
+ ui->widgetStyleComboBox->setCurrentIndex(idx);
+ }
+ idx++;
+ }
+ }
+
+ auto cat = settings->get("BackgroundCat").toString();
+ for (auto& catFromList : m_catOptions) {
+ QIcon catIcon = QIcon(QString(":/backgrounds/%1").arg(ThemeManager::getCatImage(catFromList.first)));
+ ui->backgroundCatComboBox->addItem(catIcon, catFromList.second);
+ if (cat == catFromList.first) {
+ ui->backgroundCatComboBox->setCurrentIndex(ui->backgroundCatComboBox->count() - 1);
+ }
+ }
+}
+
+void ThemeCustomizationWidget::retranslate()
+{
+ ui->retranslateUi(this);
+}
diff --git a/launcher/ui/widgets/ThemeCustomizationWidget.h b/launcher/ui/widgets/ThemeCustomizationWidget.h
new file mode 100644
index 00000000..d955a266
--- /dev/null
+++ b/launcher/ui/widgets/ThemeCustomizationWidget.h
@@ -0,0 +1,77 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Tayou <tayou@gmx.net>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+#pragma once
+
+#include <QWidget>
+#include "translations/TranslationsModel.h"
+
+enum ThemeFields { NONE = 0b0000, ICONS = 0b0001, WIDGETS = 0b0010, CAT = 0b0100 };
+
+namespace Ui {
+class ThemeCustomizationWidget;
+}
+
+class ThemeCustomizationWidget : public QWidget {
+ Q_OBJECT
+
+ public:
+ explicit ThemeCustomizationWidget(QWidget* parent = nullptr);
+ ~ThemeCustomizationWidget();
+
+ void showFeatures(ThemeFields features);
+
+ void applySettings();
+
+ void loadSettings();
+ void retranslate();
+
+ private slots:
+ void applyIconTheme(int index);
+ void applyWidgetTheme(int index);
+ void applyCatTheme(int index);
+
+ signals:
+ int currentIconThemeChanged(int index);
+ int currentWidgetThemeChanged(int index);
+ int currentCatChanged(int index);
+
+ private:
+ Ui::ThemeCustomizationWidget* ui;
+
+ //TODO finish implementing
+ QList<std::pair<QString, QString>> m_iconThemeOptions{
+ { "pe_colored", QObject::tr("Simple (Colored Icons)") },
+ { "pe_light", QObject::tr("Simple (Light Icons)") },
+ { "pe_dark", QObject::tr("Simple (Dark Icons)") },
+ { "pe_blue", QObject::tr("Simple (Blue Icons)") },
+ { "breeze_light", QObject::tr("Breeze Light") },
+ { "breeze_dark", QObject::tr("Breeze Dark") },
+ { "OSX", QObject::tr("OSX") },
+ { "iOS", QObject::tr("iOS") },
+ { "flat", QObject::tr("Flat") },
+ { "flat_white", QObject::tr("Flat (White)") },
+ { "multimc", QObject::tr("Legacy") },
+ { "custom", QObject::tr("Custom") }
+ };
+ QList<std::pair<QString, QString>> m_catOptions{
+ { "kitteh", QObject::tr("Background Cat (from MultiMC)") },
+ { "rory", QObject::tr("Rory ID 11 (drawn by Ashtaka)") },
+ { "rory-flat", QObject::tr("Rory ID 11 (flat edition, drawn by Ashtaka)") },
+ { "teawie", QObject::tr("Teawie (drawn by SympathyTea)") }
+ };
+};
diff --git a/launcher/ui/widgets/ThemeCustomizationWidget.ui b/launcher/ui/widgets/ThemeCustomizationWidget.ui
new file mode 100644
index 00000000..f216a610
--- /dev/null
+++ b/launcher/ui/widgets/ThemeCustomizationWidget.ui
@@ -0,0 +1,132 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ThemeCustomizationWidget</class>
+ <widget class="QWidget" name="ThemeCustomizationWidget">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>400</width>
+ <height>191</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string notr="true">Form</string>
+ </property>
+ <layout class="QFormLayout" name="formLayout">
+ <property name="sizeConstraint">
+ <enum>QLayout::SetMinimumSize</enum>
+ </property>
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item row="0" column="0">
+ <widget class="QLabel" name="iconsLabel">
+ <property name="text">
+ <string>&amp;Icons</string>
+ </property>
+ <property name="buddy">
+ <cstring>iconsComboBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QComboBox" name="iconsComboBox">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="focusPolicy">
+ <enum>Qt::StrongFocus</enum>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="widgetThemeLabel">
+ <property name="text">
+ <string>&amp;Colors</string>
+ </property>
+ <property name="buddy">
+ <cstring>widgetStyleComboBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QComboBox" name="widgetStyleComboBox">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="focusPolicy">
+ <enum>Qt::StrongFocus</enum>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="backgroundCatLabel">
+ <property name="toolTip">
+ <string>The cat appears in the background and is not shown by default. It is only made visible when pressing the Cat button in the Toolbar.</string>
+ </property>
+ <property name="text">
+ <string>C&amp;at</string>
+ </property>
+ <property name="buddy">
+ <cstring>backgroundCatComboBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QComboBox" name="backgroundCatComboBox">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="focusPolicy">
+ <enum>Qt::StrongFocus</enum>
+ </property>
+ <property name="toolTip">
+ <string>The cat appears in the background and is not shown by default. It is only made visible when pressing the Cat button in the Toolbar.</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="catInfoLabel">
+ <property name="toolTip">
+ <string>The cat appears in the background and is not shown by default. It is only made visible when pressing the Cat button in the Toolbar.</string>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="icon">
+ <iconset theme="about">
+ <normaloff>.</normaloff>.</iconset>
+ </property>
+ <property name="flat">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/ui/widgets/VariableSizedImageObject.cpp b/launcher/ui/widgets/VariableSizedImageObject.cpp
index e57f7e95..991b4a04 100644
--- a/launcher/ui/widgets/VariableSizedImageObject.cpp
+++ b/launcher/ui/widgets/VariableSizedImageObject.cpp
@@ -101,7 +101,7 @@ void VariableSizedImageObject::loadImage(QTextDocument* doc, const QUrl& source,
auto full_entry_path = entry->getFullPath();
auto source_url = source;
- connect(job, &NetJob::succeeded, [this, doc, full_entry_path, source_url, posInDocument] {
+ connect(job, &NetJob::succeeded, this, [this, doc, full_entry_path, source_url, posInDocument] {
qDebug() << "Loaded resource at" << full_entry_path;
// If we flushed, don't proceed.
diff --git a/libraries/README.md b/libraries/README.md
index ac5a3618..95be8740 100644
--- a/libraries/README.md
+++ b/libraries/README.md
@@ -18,11 +18,13 @@ See [github repo](https://github.com/FeralInteractive/gamemode).
BSD-3-Clause licensed
-## hoedown
+## cmark
-Hoedown is a revived fork of Sundown, the Markdown parser based on the original code of the Upskirt library by Natacha Porté.
+The C reference implementation of CommonMark, a standardized Markdown spec.
-See [github repo](https://github.com/hoedown/hoedown).
+See [github_repo](https://github.com/commonmark/cmark).
+
+BSD2 licensed.
## javacheck
diff --git a/libraries/cmark b/libraries/cmark
new file mode 160000
+Subproject a8da5a2f252b96eca60ae8bada1a9ba059a3840
diff --git a/libraries/hoedown/CMakeLists.txt b/libraries/hoedown/CMakeLists.txt
deleted file mode 100644
index 7902e734..00000000
--- a/libraries/hoedown/CMakeLists.txt
+++ /dev/null
@@ -1,26 +0,0 @@
-# hoedown 3.0.2 - https://github.com/hoedown/hoedown/archive/3.0.2.tar.gz
-project(hoedown LANGUAGES C VERSION 3.0.2)
-
-set(HOEDOWN_SOURCES
-include/hoedown/autolink.h
-include/hoedown/buffer.h
-include/hoedown/document.h
-include/hoedown/escape.h
-include/hoedown/html.h
-include/hoedown/stack.h
-include/hoedown/version.h
-src/autolink.c
-src/buffer.c
-src/document.c
-src/escape.c
-src/html.c
-src/html_blocks.c
-src/html_smartypants.c
-src/stack.c
-src/version.c
-)
-
-# Include self.
-add_library(hoedown STATIC ${HOEDOWN_SOURCES})
-
-target_include_directories(hoedown PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
diff --git a/libraries/hoedown/LICENSE b/libraries/hoedown/LICENSE
deleted file mode 100644
index 4e75de4d..00000000
--- a/libraries/hoedown/LICENSE
+++ /dev/null
@@ -1,15 +0,0 @@
-Copyright (c) 2008, Natacha Porté
-Copyright (c) 2011, Vicent Martí
-Copyright (c) 2014, Xavier Mendez, Devin Torres and the Hoedown authors
-
-Permission to use, copy, modify, and distribute this software for any
-purpose with or without fee is hereby granted, provided that the above
-copyright notice and this permission notice appear in all copies.
-
-THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/libraries/hoedown/README.md b/libraries/hoedown/README.md
deleted file mode 100644
index abe2b6ca..00000000
--- a/libraries/hoedown/README.md
+++ /dev/null
@@ -1,9 +0,0 @@
-Hoedown
-=======
-
-This is Hoedown 3.0.2, taken from [the hoedown github repo](https://github.com/hoedown/hoedown).
-
-`Hoedown` is a revived fork of [Sundown](https://github.com/vmg/sundown),
-the Markdown parser based on the original code of the
-[Upskirt library](http://fossil.instinctive.eu/libupskirt/index)
-by Natacha Porté.
diff --git a/libraries/hoedown/include/hoedown/autolink.h b/libraries/hoedown/include/hoedown/autolink.h
deleted file mode 100644
index 953e7807..00000000
--- a/libraries/hoedown/include/hoedown/autolink.h
+++ /dev/null
@@ -1,46 +0,0 @@
-/* autolink.h - versatile autolinker */
-
-#ifndef HOEDOWN_AUTOLINK_H
-#define HOEDOWN_AUTOLINK_H
-
-#include "buffer.h"
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-
-/*************
- * CONSTANTS *
- *************/
-
-typedef enum hoedown_autolink_flags {
- HOEDOWN_AUTOLINK_SHORT_DOMAINS = (1 << 0)
-} hoedown_autolink_flags;
-
-
-/*************
- * FUNCTIONS *
- *************/
-
-/* hoedown_autolink_is_safe: verify that a URL has a safe protocol */
-int hoedown_autolink_is_safe(const uint8_t *data, size_t size);
-
-/* hoedown_autolink__www: search for the next www link in data */
-size_t hoedown_autolink__www(size_t *rewind_p, hoedown_buffer *link,
- uint8_t *data, size_t offset, size_t size, hoedown_autolink_flags flags);
-
-/* hoedown_autolink__email: search for the next email in data */
-size_t hoedown_autolink__email(size_t *rewind_p, hoedown_buffer *link,
- uint8_t *data, size_t offset, size_t size, hoedown_autolink_flags flags);
-
-/* hoedown_autolink__url: search for the next URL in data */
-size_t hoedown_autolink__url(size_t *rewind_p, hoedown_buffer *link,
- uint8_t *data, size_t offset, size_t size, hoedown_autolink_flags flags);
-
-
-#ifdef __cplusplus
-}
-#endif
-
-#endif /** HOEDOWN_AUTOLINK_H **/
diff --git a/libraries/hoedown/include/hoedown/buffer.h b/libraries/hoedown/include/hoedown/buffer.h
deleted file mode 100644
index 062d86ce..00000000
--- a/libraries/hoedown/include/hoedown/buffer.h
+++ /dev/null
@@ -1,134 +0,0 @@
-/* buffer.h - simple, fast buffers */
-
-#ifndef HOEDOWN_BUFFER_H
-#define HOEDOWN_BUFFER_H
-
-#include <stdio.h>
-#include <stddef.h>
-#include <stdarg.h>
-#include <stdint.h>
-#include <stdlib.h>
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-#if defined(_MSC_VER)
-#define __attribute__(x)
-#define inline __inline
-#define __builtin_expect(x,n) x
-#endif
-
-
-/*********
- * TYPES *
- *********/
-
-typedef void *(*hoedown_realloc_callback)(void *, size_t);
-typedef void (*hoedown_free_callback)(void *);
-
-struct hoedown_buffer {
- uint8_t *data; /* actual character data */
- size_t size; /* size of the string */
- size_t asize; /* allocated size (0 = volatile buffer) */
- size_t unit; /* reallocation unit size (0 = read-only buffer) */
-
- hoedown_realloc_callback data_realloc;
- hoedown_free_callback data_free;
- hoedown_free_callback buffer_free;
-};
-
-typedef struct hoedown_buffer hoedown_buffer;
-
-
-/*************
- * FUNCTIONS *
- *************/
-
-/* allocation wrappers */
-void *hoedown_malloc(size_t size) __attribute__ ((malloc));
-void *hoedown_calloc(size_t nmemb, size_t size) __attribute__ ((malloc));
-void *hoedown_realloc(void *ptr, size_t size) __attribute__ ((malloc));
-
-/* hoedown_buffer_init: initialize a buffer with custom allocators */
-void hoedown_buffer_init(
- hoedown_buffer *buffer,
- size_t unit,
- hoedown_realloc_callback data_realloc,
- hoedown_free_callback data_free,
- hoedown_free_callback buffer_free
-);
-
-/* hoedown_buffer_uninit: uninitialize an existing buffer */
-void hoedown_buffer_uninit(hoedown_buffer *buf);
-
-/* hoedown_buffer_new: allocate a new buffer */
-hoedown_buffer *hoedown_buffer_new(size_t unit) __attribute__ ((malloc));
-
-/* hoedown_buffer_reset: free internal data of the buffer */
-void hoedown_buffer_reset(hoedown_buffer *buf);
-
-/* hoedown_buffer_grow: increase the allocated size to the given value */
-void hoedown_buffer_grow(hoedown_buffer *buf, size_t neosz);
-
-/* hoedown_buffer_put: append raw data to a buffer */
-void hoedown_buffer_put(hoedown_buffer *buf, const uint8_t *data, size_t size);
-
-/* hoedown_buffer_puts: append a NUL-terminated string to a buffer */
-void hoedown_buffer_puts(hoedown_buffer *buf, const char *str);
-
-/* hoedown_buffer_putc: append a single char to a buffer */
-void hoedown_buffer_putc(hoedown_buffer *buf, uint8_t c);
-
-/* hoedown_buffer_putf: read from a file and append to a buffer, until EOF or error */
-int hoedown_buffer_putf(hoedown_buffer *buf, FILE* file);
-
-/* hoedown_buffer_set: replace the buffer's contents with raw data */
-void hoedown_buffer_set(hoedown_buffer *buf, const uint8_t *data, size_t size);
-
-/* hoedown_buffer_sets: replace the buffer's contents with a NUL-terminated string */
-void hoedown_buffer_sets(hoedown_buffer *buf, const char *str);
-
-/* hoedown_buffer_eq: compare a buffer's data with other data for equality */
-int hoedown_buffer_eq(const hoedown_buffer *buf, const uint8_t *data, size_t size);
-
-/* hoedown_buffer_eq: compare a buffer's data with NUL-terminated string for equality */
-int hoedown_buffer_eqs(const hoedown_buffer *buf, const char *str);
-
-/* hoedown_buffer_prefix: compare the beginning of a buffer with a string */
-int hoedown_buffer_prefix(const hoedown_buffer *buf, const char *prefix);
-
-/* hoedown_buffer_slurp: remove a given number of bytes from the head of the buffer */
-void hoedown_buffer_slurp(hoedown_buffer *buf, size_t size);
-
-/* hoedown_buffer_cstr: NUL-termination of the string array (making a C-string) */
-const char *hoedown_buffer_cstr(hoedown_buffer *buf);
-
-/* hoedown_buffer_printf: formatted printing to a buffer */
-void hoedown_buffer_printf(hoedown_buffer *buf, const char *fmt, ...) __attribute__ ((format (printf, 2, 3)));
-
-/* hoedown_buffer_put_utf8: put a Unicode character encoded as UTF-8 */
-void hoedown_buffer_put_utf8(hoedown_buffer *buf, unsigned int codepoint);
-
-/* hoedown_buffer_free: free the buffer */
-void hoedown_buffer_free(hoedown_buffer *buf);
-
-
-/* HOEDOWN_BUFPUTSL: optimized hoedown_buffer_puts of a string literal */
-#define HOEDOWN_BUFPUTSL(output, literal) \
- hoedown_buffer_put(output, (const uint8_t *)literal, sizeof(literal) - 1)
-
-/* HOEDOWN_BUFSETSL: optimized hoedown_buffer_sets of a string literal */
-#define HOEDOWN_BUFSETSL(output, literal) \
- hoedown_buffer_set(output, (const uint8_t *)literal, sizeof(literal) - 1)
-
-/* HOEDOWN_BUFEQSL: optimized hoedown_buffer_eqs of a string literal */
-#define HOEDOWN_BUFEQSL(output, literal) \
- hoedown_buffer_eq(output, (const uint8_t *)literal, sizeof(literal) - 1)
-
-
-#ifdef __cplusplus
-}
-#endif
-
-#endif /** HOEDOWN_BUFFER_H **/
diff --git a/libraries/hoedown/include/hoedown/document.h b/libraries/hoedown/include/hoedown/document.h
deleted file mode 100644
index 210c565e..00000000
--- a/libraries/hoedown/include/hoedown/document.h
+++ /dev/null
@@ -1,172 +0,0 @@
-/* document.h - generic markdown parser */
-
-#ifndef HOEDOWN_DOCUMENT_H
-#define HOEDOWN_DOCUMENT_H
-
-#include "buffer.h"
-#include "autolink.h"
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-
-/*************
- * CONSTANTS *
- *************/
-
-typedef enum hoedown_extensions {
- /* block-level extensions */
- HOEDOWN_EXT_TABLES = (1 << 0),
- HOEDOWN_EXT_FENCED_CODE = (1 << 1),
- HOEDOWN_EXT_FOOTNOTES = (1 << 2),
-
- /* span-level extensions */
- HOEDOWN_EXT_AUTOLINK = (1 << 3),
- HOEDOWN_EXT_STRIKETHROUGH = (1 << 4),
- HOEDOWN_EXT_UNDERLINE = (1 << 5),
- HOEDOWN_EXT_HIGHLIGHT = (1 << 6),
- HOEDOWN_EXT_QUOTE = (1 << 7),
- HOEDOWN_EXT_SUPERSCRIPT = (1 << 8),
- HOEDOWN_EXT_MATH = (1 << 9),
-
- /* other flags */
- HOEDOWN_EXT_NO_INTRA_EMPHASIS = (1 << 11),
- HOEDOWN_EXT_SPACE_HEADERS = (1 << 12),
- HOEDOWN_EXT_MATH_EXPLICIT = (1 << 13),
-
- /* negative flags */
- HOEDOWN_EXT_DISABLE_INDENTED_CODE = (1 << 14)
-} hoedown_extensions;
-
-#define HOEDOWN_EXT_BLOCK (\
- HOEDOWN_EXT_TABLES |\
- HOEDOWN_EXT_FENCED_CODE |\
- HOEDOWN_EXT_FOOTNOTES )
-
-#define HOEDOWN_EXT_SPAN (\
- HOEDOWN_EXT_AUTOLINK |\
- HOEDOWN_EXT_STRIKETHROUGH |\
- HOEDOWN_EXT_UNDERLINE |\
- HOEDOWN_EXT_HIGHLIGHT |\
- HOEDOWN_EXT_QUOTE |\
- HOEDOWN_EXT_SUPERSCRIPT |\
- HOEDOWN_EXT_MATH )
-
-#define HOEDOWN_EXT_FLAGS (\
- HOEDOWN_EXT_NO_INTRA_EMPHASIS |\
- HOEDOWN_EXT_SPACE_HEADERS |\
- HOEDOWN_EXT_MATH_EXPLICIT )
-
-#define HOEDOWN_EXT_NEGATIVE (\
- HOEDOWN_EXT_DISABLE_INDENTED_CODE )
-
-typedef enum hoedown_list_flags {
- HOEDOWN_LIST_ORDERED = (1 << 0),
- HOEDOWN_LI_BLOCK = (1 << 1) /* <li> containing block data */
-} hoedown_list_flags;
-
-typedef enum hoedown_table_flags {
- HOEDOWN_TABLE_ALIGN_LEFT = 1,
- HOEDOWN_TABLE_ALIGN_RIGHT = 2,
- HOEDOWN_TABLE_ALIGN_CENTER = 3,
- HOEDOWN_TABLE_ALIGNMASK = 3,
- HOEDOWN_TABLE_HEADER = 4
-} hoedown_table_flags;
-
-typedef enum hoedown_autolink_type {
- HOEDOWN_AUTOLINK_NONE, /* used internally when it is not an autolink*/
- HOEDOWN_AUTOLINK_NORMAL, /* normal http/http/ftp/mailto/etc link */
- HOEDOWN_AUTOLINK_EMAIL /* e-mail link without explit mailto: */
-} hoedown_autolink_type;
-
-
-/*********
- * TYPES *
- *********/
-
-struct hoedown_document;
-typedef struct hoedown_document hoedown_document;
-
-struct hoedown_renderer_data {
- void *opaque;
-};
-typedef struct hoedown_renderer_data hoedown_renderer_data;
-
-/* hoedown_renderer - functions for rendering parsed data */
-struct hoedown_renderer {
- /* state object */
- void *opaque;
-
- /* block level callbacks - NULL skips the block */
- void (*blockcode)(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_buffer *lang, const hoedown_renderer_data *data);
- void (*blockquote)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data);
- void (*header)(hoedown_buffer *ob, const hoedown_buffer *content, int level, const hoedown_renderer_data *data);
- void (*hrule)(hoedown_buffer *ob, const hoedown_renderer_data *data);
- void (*list)(hoedown_buffer *ob, const hoedown_buffer *content, hoedown_list_flags flags, const hoedown_renderer_data *data);
- void (*listitem)(hoedown_buffer *ob, const hoedown_buffer *content, hoedown_list_flags flags, const hoedown_renderer_data *data);
- void (*paragraph)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data);
- void (*table)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data);
- void (*table_header)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data);
- void (*table_body)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data);
- void (*table_row)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data);
- void (*table_cell)(hoedown_buffer *ob, const hoedown_buffer *content, hoedown_table_flags flags, const hoedown_renderer_data *data);
- void (*footnotes)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data);
- void (*footnote_def)(hoedown_buffer *ob, const hoedown_buffer *content, unsigned int num, const hoedown_renderer_data *data);
- void (*blockhtml)(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data);
-
- /* span level callbacks - NULL or return 0 prints the span verbatim */
- int (*autolink)(hoedown_buffer *ob, const hoedown_buffer *link, hoedown_autolink_type type, const hoedown_renderer_data *data);
- int (*codespan)(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data);
- int (*double_emphasis)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data);
- int (*emphasis)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data);
- int (*underline)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data);
- int (*highlight)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data);
- int (*quote)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data);
- int (*image)(hoedown_buffer *ob, const hoedown_buffer *link, const hoedown_buffer *title, const hoedown_buffer *alt, const hoedown_renderer_data *data);
- int (*linebreak)(hoedown_buffer *ob, const hoedown_renderer_data *data);
- int (*link)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_buffer *link, const hoedown_buffer *title, const hoedown_renderer_data *data);
- int (*triple_emphasis)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data);
- int (*strikethrough)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data);
- int (*superscript)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data);
- int (*footnote_ref)(hoedown_buffer *ob, unsigned int num, const hoedown_renderer_data *data);
- int (*math)(hoedown_buffer *ob, const hoedown_buffer *text, int displaymode, const hoedown_renderer_data *data);
- int (*raw_html)(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data);
-
- /* low level callbacks - NULL copies input directly into the output */
- void (*entity)(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data);
- void (*normal_text)(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data);
-
- /* miscellaneous callbacks */
- void (*doc_header)(hoedown_buffer *ob, int inline_render, const hoedown_renderer_data *data);
- void (*doc_footer)(hoedown_buffer *ob, int inline_render, const hoedown_renderer_data *data);
-};
-typedef struct hoedown_renderer hoedown_renderer;
-
-
-/*************
- * FUNCTIONS *
- *************/
-
-/* hoedown_document_new: allocate a new document processor instance */
-hoedown_document *hoedown_document_new(
- const hoedown_renderer *renderer,
- hoedown_extensions extensions,
- size_t max_nesting
-) __attribute__ ((malloc));
-
-/* hoedown_document_render: render regular Markdown using the document processor */
-void hoedown_document_render(hoedown_document *doc, hoedown_buffer *ob, const uint8_t *data, size_t size);
-
-/* hoedown_document_render_inline: render inline Markdown using the document processor */
-void hoedown_document_render_inline(hoedown_document *doc, hoedown_buffer *ob, const uint8_t *data, size_t size);
-
-/* hoedown_document_free: deallocate a document processor instance */
-void hoedown_document_free(hoedown_document *doc);
-
-
-#ifdef __cplusplus
-}
-#endif
-
-#endif /** HOEDOWN_DOCUMENT_H **/
diff --git a/libraries/hoedown/include/hoedown/escape.h b/libraries/hoedown/include/hoedown/escape.h
deleted file mode 100644
index d7659c27..00000000
--- a/libraries/hoedown/include/hoedown/escape.h
+++ /dev/null
@@ -1,28 +0,0 @@
-/* escape.h - escape utilities */
-
-#ifndef HOEDOWN_ESCAPE_H
-#define HOEDOWN_ESCAPE_H
-
-#include "buffer.h"
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-
-/*************
- * FUNCTIONS *
- *************/
-
-/* hoedown_escape_href: escape (part of) a URL inside HTML */
-void hoedown_escape_href(hoedown_buffer *ob, const uint8_t *data, size_t size);
-
-/* hoedown_escape_html: escape HTML */
-void hoedown_escape_html(hoedown_buffer *ob, const uint8_t *data, size_t size, int secure);
-
-
-#ifdef __cplusplus
-}
-#endif
-
-#endif /** HOEDOWN_ESCAPE_H **/
diff --git a/libraries/hoedown/include/hoedown/html.h b/libraries/hoedown/include/hoedown/html.h
deleted file mode 100644
index 7c68809a..00000000
--- a/libraries/hoedown/include/hoedown/html.h
+++ /dev/null
@@ -1,84 +0,0 @@
-/* html.h - HTML renderer and utilities */
-
-#ifndef HOEDOWN_HTML_H
-#define HOEDOWN_HTML_H
-
-#include "document.h"
-#include "buffer.h"
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-
-/*************
- * CONSTANTS *
- *************/
-
-typedef enum hoedown_html_flags {
- HOEDOWN_HTML_SKIP_HTML = (1 << 0),
- HOEDOWN_HTML_ESCAPE = (1 << 1),
- HOEDOWN_HTML_HARD_WRAP = (1 << 2),
- HOEDOWN_HTML_USE_XHTML = (1 << 3)
-} hoedown_html_flags;
-
-typedef enum hoedown_html_tag {
- HOEDOWN_HTML_TAG_NONE = 0,
- HOEDOWN_HTML_TAG_OPEN,
- HOEDOWN_HTML_TAG_CLOSE
-} hoedown_html_tag;
-
-
-/*********
- * TYPES *
- *********/
-
-struct hoedown_html_renderer_state {
- void *opaque;
-
- struct {
- int header_count;
- int current_level;
- int level_offset;
- int nesting_level;
- } toc_data;
-
- hoedown_html_flags flags;
-
- /* extra callbacks */
- void (*link_attributes)(hoedown_buffer *ob, const hoedown_buffer *url, const hoedown_renderer_data *data);
-};
-typedef struct hoedown_html_renderer_state hoedown_html_renderer_state;
-
-
-/*************
- * FUNCTIONS *
- *************/
-
-/* hoedown_html_smartypants: process an HTML snippet using SmartyPants for smart punctuation */
-void hoedown_html_smartypants(hoedown_buffer *ob, const uint8_t *data, size_t size);
-
-/* hoedown_html_is_tag: checks if data starts with a specific tag, returns the tag type or NONE */
-hoedown_html_tag hoedown_html_is_tag(const uint8_t *data, size_t size, const char *tagname);
-
-
-/* hoedown_html_renderer_new: allocates a regular HTML renderer */
-hoedown_renderer *hoedown_html_renderer_new(
- hoedown_html_flags render_flags,
- int nesting_level
-) __attribute__ ((malloc));
-
-/* hoedown_html_toc_renderer_new: like hoedown_html_renderer_new, but the returned renderer produces the Table of Contents */
-hoedown_renderer *hoedown_html_toc_renderer_new(
- int nesting_level
-) __attribute__ ((malloc));
-
-/* hoedown_html_renderer_free: deallocate an HTML renderer */
-void hoedown_html_renderer_free(hoedown_renderer *renderer);
-
-
-#ifdef __cplusplus
-}
-#endif
-
-#endif /** HOEDOWN_HTML_H **/
diff --git a/libraries/hoedown/include/hoedown/stack.h b/libraries/hoedown/include/hoedown/stack.h
deleted file mode 100644
index d1855f4f..00000000
--- a/libraries/hoedown/include/hoedown/stack.h
+++ /dev/null
@@ -1,52 +0,0 @@
-/* stack.h - simple stacking */
-
-#ifndef HOEDOWN_STACK_H
-#define HOEDOWN_STACK_H
-
-#include <stddef.h>
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-
-/*********
- * TYPES *
- *********/
-
-struct hoedown_stack {
- void **item;
- size_t size;
- size_t asize;
-};
-typedef struct hoedown_stack hoedown_stack;
-
-
-/*************
- * FUNCTIONS *
- *************/
-
-/* hoedown_stack_init: initialize a stack */
-void hoedown_stack_init(hoedown_stack *st, size_t initial_size);
-
-/* hoedown_stack_uninit: free internal data of the stack */
-void hoedown_stack_uninit(hoedown_stack *st);
-
-/* hoedown_stack_grow: increase the allocated size to the given value */
-void hoedown_stack_grow(hoedown_stack *st, size_t neosz);
-
-/* hoedown_stack_push: push an item to the top of the stack */
-void hoedown_stack_push(hoedown_stack *st, void *item);
-
-/* hoedown_stack_pop: retrieve and remove the item at the top of the stack */
-void *hoedown_stack_pop(hoedown_stack *st);
-
-/* hoedown_stack_top: retrieve the item at the top of the stack */
-void *hoedown_stack_top(const hoedown_stack *st);
-
-
-#ifdef __cplusplus
-}
-#endif
-
-#endif /** HOEDOWN_STACK_H **/
diff --git a/libraries/hoedown/include/hoedown/version.h b/libraries/hoedown/include/hoedown/version.h
deleted file mode 100644
index 4938cae5..00000000
--- a/libraries/hoedown/include/hoedown/version.h
+++ /dev/null
@@ -1,33 +0,0 @@
-/* version.h - holds Hoedown's version */
-
-#ifndef HOEDOWN_VERSION_H
-#define HOEDOWN_VERSION_H
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-
-/*************
- * CONSTANTS *
- *************/
-
-#define HOEDOWN_VERSION "3.0.2"
-#define HOEDOWN_VERSION_MAJOR 3
-#define HOEDOWN_VERSION_MINOR 0
-#define HOEDOWN_VERSION_REVISION 2
-
-
-/*************
- * FUNCTIONS *
- *************/
-
-/* hoedown_version: retrieve Hoedown's version numbers */
-void hoedown_version(int *major, int *minor, int *revision);
-
-
-#ifdef __cplusplus
-}
-#endif
-
-#endif /** HOEDOWN_VERSION_H **/
diff --git a/libraries/hoedown/src/autolink.c b/libraries/hoedown/src/autolink.c
deleted file mode 100644
index 3592b8e3..00000000
--- a/libraries/hoedown/src/autolink.c
+++ /dev/null
@@ -1,281 +0,0 @@
-#include "hoedown/autolink.h"
-
-#include <string.h>
-#include <stdlib.h>
-#include <stdio.h>
-#include <ctype.h>
-
-#ifndef _MSC_VER
-#include <strings.h>
-#else
-#define strncasecmp _strnicmp
-#endif
-
-int
-hoedown_autolink_is_safe(const uint8_t *data, size_t size)
-{
- static const size_t valid_uris_count = 6;
- static const char *valid_uris[] = {
- "http://", "https://", "/", "#", "ftp://", "mailto:"
- };
- static const size_t valid_uris_size[] = { 7, 8, 1, 1, 6, 7 };
- size_t i;
-
- for (i = 0; i < valid_uris_count; ++i) {
- size_t len = valid_uris_size[i];
-
- if (size > len &&
- strncasecmp((char *)data, valid_uris[i], len) == 0 &&
- isalnum(data[len]))
- return 1;
- }
-
- return 0;
-}
-
-static size_t
-autolink_delim(uint8_t *data, size_t link_end, size_t max_rewind, size_t size)
-{
- uint8_t cclose, copen = 0;
- size_t i;
-
- for (i = 0; i < link_end; ++i)
- if (data[i] == '<') {
- link_end = i;
- break;
- }
-
- while (link_end > 0) {
- if (strchr("?!.,:", data[link_end - 1]) != NULL)
- link_end--;
-
- else if (data[link_end - 1] == ';') {
- size_t new_end = link_end - 2;
-
- while (new_end > 0 && isalpha(data[new_end]))
- new_end--;
-
- if (new_end < link_end - 2 && data[new_end] == '&')
- link_end = new_end;
- else
- link_end--;
- }
- else break;
- }
-
- if (link_end == 0)
- return 0;
-
- cclose = data[link_end - 1];
-
- switch (cclose) {
- case '"': copen = '"'; break;
- case '\'': copen = '\''; break;
- case ')': copen = '('; break;
- case ']': copen = '['; break;
- case '}': copen = '{'; break;
- }
-
- if (copen != 0) {
- size_t closing = 0;
- size_t opening = 0;
- size_t i = 0;
-
- /* Try to close the final punctuation sign in this same line;
- * if we managed to close it outside of the URL, that means that it's
- * not part of the URL. If it closes inside the URL, that means it
- * is part of the URL.
- *
- * Examples:
- *
- * foo http://www.pokemon.com/Pikachu_(Electric) bar
- * => http://www.pokemon.com/Pikachu_(Electric)
- *
- * foo (http://www.pokemon.com/Pikachu_(Electric)) bar
- * => http://www.pokemon.com/Pikachu_(Electric)
- *
- * foo http://www.pokemon.com/Pikachu_(Electric)) bar
- * => http://www.pokemon.com/Pikachu_(Electric))
- *
- * (foo http://www.pokemon.com/Pikachu_(Electric)) bar
- * => foo http://www.pokemon.com/Pikachu_(Electric)
- */
-
- while (i < link_end) {
- if (data[i] == copen)
- opening++;
- else if (data[i] == cclose)
- closing++;
-
- i++;
- }
-
- if (closing != opening)
- link_end--;
- }
-
- return link_end;
-}
-
-static size_t
-check_domain(uint8_t *data, size_t size, int allow_short)
-{
- size_t i, np = 0;
-
- if (!isalnum(data[0]))
- return 0;
-
- for (i = 1; i < size - 1; ++i) {
- if (strchr(".:", data[i]) != NULL) np++;
- else if (!isalnum(data[i]) && data[i] != '-') break;
- }
-
- if (allow_short) {
- /* We don't need a valid domain in the strict sense (with
- * least one dot; so just make sure it's composed of valid
- * domain characters and return the length of the the valid
- * sequence. */
- return i;
- } else {
- /* a valid domain needs to have at least a dot.
- * that's as far as we get */
- return np ? i : 0;
- }
-}
-
-size_t
-hoedown_autolink__www(
- size_t *rewind_p,
- hoedown_buffer *link,
- uint8_t *data,
- size_t max_rewind,
- size_t size,
- hoedown_autolink_flags flags)
-{
- size_t link_end;
-
- if (max_rewind > 0 && !ispunct(data[-1]) && !isspace(data[-1]))
- return 0;
-
- if (size < 4 || memcmp(data, "www.", strlen("www.")) != 0)
- return 0;
-
- link_end = check_domain(data, size, 0);
-
- if (link_end == 0)
- return 0;
-
- while (link_end < size && !isspace(data[link_end]))
- link_end++;
-
- link_end = autolink_delim(data, link_end, max_rewind, size);
-
- if (link_end == 0)
- return 0;
-
- hoedown_buffer_put(link, data, link_end);
- *rewind_p = 0;
-
- return (int)link_end;
-}
-
-size_t
-hoedown_autolink__email(
- size_t *rewind_p,
- hoedown_buffer *link,
- uint8_t *data,
- size_t max_rewind,
- size_t size,
- hoedown_autolink_flags flags)
-{
- size_t link_end, rewind;
- int nb = 0, np = 0;
-
- for (rewind = 0; rewind < max_rewind; ++rewind) {
- uint8_t c = data[-1 - rewind];
-
- if (isalnum(c))
- continue;
-
- if (strchr(".+-_", c) != NULL)
- continue;
-
- break;
- }
-
- if (rewind == 0)
- return 0;
-
- for (link_end = 0; link_end < size; ++link_end) {
- uint8_t c = data[link_end];
-
- if (isalnum(c))
- continue;
-
- if (c == '@')
- nb++;
- else if (c == '.' && link_end < size - 1)
- np++;
- else if (c != '-' && c != '_')
- break;
- }
-
- if (link_end < 2 || nb != 1 || np == 0 ||
- !isalpha(data[link_end - 1]))
- return 0;
-
- link_end = autolink_delim(data, link_end, max_rewind, size);
-
- if (link_end == 0)
- return 0;
-
- hoedown_buffer_put(link, data - rewind, link_end + rewind);
- *rewind_p = rewind;
-
- return link_end;
-}
-
-size_t
-hoedown_autolink__url(
- size_t *rewind_p,
- hoedown_buffer *link,
- uint8_t *data,
- size_t max_rewind,
- size_t size,
- hoedown_autolink_flags flags)
-{
- size_t link_end, rewind = 0, domain_len;
-
- if (size < 4 || data[1] != '/' || data[2] != '/')
- return 0;
-
- while (rewind < max_rewind && isalpha(data[-1 - rewind]))
- rewind++;
-
- if (!hoedown_autolink_is_safe(data - rewind, size + rewind))
- return 0;
-
- link_end = strlen("://");
-
- domain_len = check_domain(
- data + link_end,
- size - link_end,
- flags & HOEDOWN_AUTOLINK_SHORT_DOMAINS);
-
- if (domain_len == 0)
- return 0;
-
- link_end += domain_len;
- while (link_end < size && !isspace(data[link_end]))
- link_end++;
-
- link_end = autolink_delim(data, link_end, max_rewind, size);
-
- if (link_end == 0)
- return 0;
-
- hoedown_buffer_put(link, data - rewind, link_end + rewind);
- *rewind_p = rewind;
-
- return link_end;
-}
diff --git a/libraries/hoedown/src/buffer.c b/libraries/hoedown/src/buffer.c
deleted file mode 100644
index 024a8bcc..00000000
--- a/libraries/hoedown/src/buffer.c
+++ /dev/null
@@ -1,308 +0,0 @@
-#include "hoedown/buffer.h"
-
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <assert.h>
-
-void *
-hoedown_malloc(size_t size)
-{
- void *ret = malloc(size);
-
- if (!ret) {
- fprintf(stderr, "Allocation failed.\n");
- abort();
- }
-
- return ret;
-}
-
-void *
-hoedown_calloc(size_t nmemb, size_t size)
-{
- void *ret = calloc(nmemb, size);
-
- if (!ret) {
- fprintf(stderr, "Allocation failed.\n");
- abort();
- }
-
- return ret;
-}
-
-void *
-hoedown_realloc(void *ptr, size_t size)
-{
- void *ret = realloc(ptr, size);
-
- if (!ret) {
- fprintf(stderr, "Allocation failed.\n");
- abort();
- }
-
- return ret;
-}
-
-void
-hoedown_buffer_init(
- hoedown_buffer *buf,
- size_t unit,
- hoedown_realloc_callback data_realloc,
- hoedown_free_callback data_free,
- hoedown_free_callback buffer_free)
-{
- assert(buf);
-
- buf->data = NULL;
- buf->size = buf->asize = 0;
- buf->unit = unit;
- buf->data_realloc = data_realloc;
- buf->data_free = data_free;
- buf->buffer_free = buffer_free;
-}
-
-void
-hoedown_buffer_uninit(hoedown_buffer *buf)
-{
- assert(buf && buf->unit);
- buf->data_free(buf->data);
-}
-
-hoedown_buffer *
-hoedown_buffer_new(size_t unit)
-{
- hoedown_buffer *ret = hoedown_malloc(sizeof (hoedown_buffer));
- hoedown_buffer_init(ret, unit, hoedown_realloc, free, free);
- return ret;
-}
-
-void
-hoedown_buffer_free(hoedown_buffer *buf)
-{
- if (!buf) return;
- assert(buf && buf->unit);
-
- buf->data_free(buf->data);
-
- if (buf->buffer_free)
- buf->buffer_free(buf);
-}
-
-void
-hoedown_buffer_reset(hoedown_buffer *buf)
-{
- assert(buf && buf->unit);
-
- buf->data_free(buf->data);
- buf->data = NULL;
- buf->size = buf->asize = 0;
-}
-
-void
-hoedown_buffer_grow(hoedown_buffer *buf, size_t neosz)
-{
- size_t neoasz;
- assert(buf && buf->unit);
-
- if (buf->asize >= neosz)
- return;
-
- neoasz = buf->asize + buf->unit;
- while (neoasz < neosz)
- neoasz += buf->unit;
-
- buf->data = (uint8_t *) buf->data_realloc(buf->data, neoasz);
- buf->asize = neoasz;
-}
-
-void
-hoedown_buffer_put(hoedown_buffer *buf, const uint8_t *data, size_t size)
-{
- assert(buf && buf->unit);
-
- if (buf->size + size > buf->asize)
- hoedown_buffer_grow(buf, buf->size + size);
-
- memcpy(buf->data + buf->size, data, size);
- buf->size += size;
-}
-
-void
-hoedown_buffer_puts(hoedown_buffer *buf, const char *str)
-{
- hoedown_buffer_put(buf, (const uint8_t *)str, strlen(str));
-}
-
-void
-hoedown_buffer_putc(hoedown_buffer *buf, uint8_t c)
-{
- assert(buf && buf->unit);
-
- if (buf->size >= buf->asize)
- hoedown_buffer_grow(buf, buf->size + 1);
-
- buf->data[buf->size] = c;
- buf->size += 1;
-}
-
-int
-hoedown_buffer_putf(hoedown_buffer *buf, FILE *file)
-{
- assert(buf && buf->unit);
-
- while (!(feof(file) || ferror(file))) {
- hoedown_buffer_grow(buf, buf->size + buf->unit);
- buf->size += fread(buf->data + buf->size, 1, buf->unit, file);
- }
-
- return ferror(file);
-}
-
-void
-hoedown_buffer_set(hoedown_buffer *buf, const uint8_t *data, size_t size)
-{
- assert(buf && buf->unit);
-
- if (size > buf->asize)
- hoedown_buffer_grow(buf, size);
-
- memcpy(buf->data, data, size);
- buf->size = size;
-}
-
-void
-hoedown_buffer_sets(hoedown_buffer *buf, const char *str)
-{
- hoedown_buffer_set(buf, (const uint8_t *)str, strlen(str));
-}
-
-int
-hoedown_buffer_eq(const hoedown_buffer *buf, const uint8_t *data, size_t size)
-{
- if (buf->size != size) return 0;
- return memcmp(buf->data, data, size) == 0;
-}
-
-int
-hoedown_buffer_eqs(const hoedown_buffer *buf, const char *str)
-{
- return hoedown_buffer_eq(buf, (const uint8_t *)str, strlen(str));
-}
-
-int
-hoedown_buffer_prefix(const hoedown_buffer *buf, const char *prefix)
-{
- size_t i;
-
- for (i = 0; i < buf->size; ++i) {
- if (prefix[i] == 0)
- return 0;
-
- if (buf->data[i] != prefix[i])
- return buf->data[i] - prefix[i];
- }
-
- return 0;
-}
-
-void
-hoedown_buffer_slurp(hoedown_buffer *buf, size_t size)
-{
- assert(buf && buf->unit);
-
- if (size >= buf->size) {
- buf->size = 0;
- return;
- }
-
- buf->size -= size;
- memmove(buf->data, buf->data + size, buf->size);
-}
-
-const char *
-hoedown_buffer_cstr(hoedown_buffer *buf)
-{
- assert(buf && buf->unit);
-
- if (buf->size < buf->asize && buf->data[buf->size] == 0)
- return (char *)buf->data;
-
- hoedown_buffer_grow(buf, buf->size + 1);
- buf->data[buf->size] = 0;
-
- return (char *)buf->data;
-}
-
-void
-hoedown_buffer_printf(hoedown_buffer *buf, const char *fmt, ...)
-{
- va_list ap;
- int n;
-
- assert(buf && buf->unit);
-
- if (buf->size >= buf->asize)
- hoedown_buffer_grow(buf, buf->size + 1);
-
- va_start(ap, fmt);
- n = vsnprintf((char *)buf->data + buf->size, buf->asize - buf->size, fmt, ap);
- va_end(ap);
-
- if (n < 0) {
-#ifndef _MSC_VER
- return;
-#else
- va_start(ap, fmt);
- n = _vscprintf(fmt, ap);
- va_end(ap);
-#endif
- }
-
- if ((size_t)n >= buf->asize - buf->size) {
- hoedown_buffer_grow(buf, buf->size + n + 1);
-
- va_start(ap, fmt);
- n = vsnprintf((char *)buf->data + buf->size, buf->asize - buf->size, fmt, ap);
- va_end(ap);
- }
-
- if (n < 0)
- return;
-
- buf->size += n;
-}
-
-void hoedown_buffer_put_utf8(hoedown_buffer *buf, unsigned int c) {
- unsigned char unichar[4];
-
- assert(buf && buf->unit);
-
- if (c < 0x80) {
- hoedown_buffer_putc(buf, c);
- }
- else if (c < 0x800) {
- unichar[0] = 192 + (c / 64);
- unichar[1] = 128 + (c % 64);
- hoedown_buffer_put(buf, unichar, 2);
- }
- else if (c - 0xd800u < 0x800) {
- HOEDOWN_BUFPUTSL(buf, "\xef\xbf\xbd");
- }
- else if (c < 0x10000) {
- unichar[0] = 224 + (c / 4096);
- unichar[1] = 128 + (c / 64) % 64;
- unichar[2] = 128 + (c % 64);
- hoedown_buffer_put(buf, unichar, 3);
- }
- else if (c < 0x110000) {
- unichar[0] = 240 + (c / 262144);
- unichar[1] = 128 + (c / 4096) % 64;
- unichar[2] = 128 + (c / 64) % 64;
- unichar[3] = 128 + (c % 64);
- hoedown_buffer_put(buf, unichar, 4);
- }
- else {
- HOEDOWN_BUFPUTSL(buf, "\xef\xbf\xbd");
- }
-}
diff --git a/libraries/hoedown/src/document.c b/libraries/hoedown/src/document.c
deleted file mode 100644
index e9e2ab11..00000000
--- a/libraries/hoedown/src/document.c
+++ /dev/null
@@ -1,2958 +0,0 @@
-#include "hoedown/document.h"
-
-#include <assert.h>
-#include <string.h>
-#include <ctype.h>
-#include <stdio.h>
-
-#include "hoedown/stack.h"
-
-#ifndef _MSC_VER
-#include <strings.h>
-#else
-#define strncasecmp _strnicmp
-#endif
-
-#define REF_TABLE_SIZE 8
-
-#define BUFFER_BLOCK 0
-#define BUFFER_SPAN 1
-
-#define HOEDOWN_LI_END 8 /* internal list flag */
-
-const char *hoedown_find_block_tag(const char *str, unsigned int len);
-
-/***************
- * LOCAL TYPES *
- ***************/
-
-/* link_ref: reference to a link */
-struct link_ref {
- unsigned int id;
-
- hoedown_buffer *link;
- hoedown_buffer *title;
-
- struct link_ref *next;
-};
-
-/* footnote_ref: reference to a footnote */
-struct footnote_ref {
- unsigned int id;
-
- int is_used;
- unsigned int num;
-
- hoedown_buffer *contents;
-};
-
-/* footnote_item: an item in a footnote_list */
-struct footnote_item {
- struct footnote_ref *ref;
- struct footnote_item *next;
-};
-
-/* footnote_list: linked list of footnote_item */
-struct footnote_list {
- unsigned int count;
- struct footnote_item *head;
- struct footnote_item *tail;
-};
-
-/* char_trigger: function pointer to render active chars */
-/* returns the number of chars taken care of */
-/* data is the pointer of the beginning of the span */
-/* offset is the number of valid chars before data */
-typedef size_t
-(*char_trigger)(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size);
-
-static size_t char_emphasis(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size);
-static size_t char_quote(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size);
-static size_t char_linebreak(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size);
-static size_t char_codespan(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size);
-static size_t char_escape(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size);
-static size_t char_entity(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size);
-static size_t char_langle_tag(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size);
-static size_t char_autolink_url(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size);
-static size_t char_autolink_email(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size);
-static size_t char_autolink_www(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size);
-static size_t char_link(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size);
-static size_t char_superscript(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size);
-static size_t char_math(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size);
-
-enum markdown_char_t {
- MD_CHAR_NONE = 0,
- MD_CHAR_EMPHASIS,
- MD_CHAR_CODESPAN,
- MD_CHAR_LINEBREAK,
- MD_CHAR_LINK,
- MD_CHAR_LANGLE,
- MD_CHAR_ESCAPE,
- MD_CHAR_ENTITY,
- MD_CHAR_AUTOLINK_URL,
- MD_CHAR_AUTOLINK_EMAIL,
- MD_CHAR_AUTOLINK_WWW,
- MD_CHAR_SUPERSCRIPT,
- MD_CHAR_QUOTE,
- MD_CHAR_MATH
-};
-
-static char_trigger markdown_char_ptrs[] = {
- NULL,
- &char_emphasis,
- &char_codespan,
- &char_linebreak,
- &char_link,
- &char_langle_tag,
- &char_escape,
- &char_entity,
- &char_autolink_url,
- &char_autolink_email,
- &char_autolink_www,
- &char_superscript,
- &char_quote,
- &char_math
-};
-
-struct hoedown_document {
- hoedown_renderer md;
- hoedown_renderer_data data;
-
- struct link_ref *refs[REF_TABLE_SIZE];
- struct footnote_list footnotes_found;
- struct footnote_list footnotes_used;
- uint8_t active_char[256];
- hoedown_stack work_bufs[2];
- hoedown_extensions ext_flags;
- size_t max_nesting;
- int in_link_body;
-};
-
-/***************************
- * HELPER FUNCTIONS *
- ***************************/
-
-static hoedown_buffer *
-newbuf(hoedown_document *doc, int type)
-{
- static const size_t buf_size[2] = {256, 64};
- hoedown_buffer *work = NULL;
- hoedown_stack *pool = &doc->work_bufs[type];
-
- if (pool->size < pool->asize &&
- pool->item[pool->size] != NULL) {
- work = pool->item[pool->size++];
- work->size = 0;
- } else {
- work = hoedown_buffer_new(buf_size[type]);
- hoedown_stack_push(pool, work);
- }
-
- return work;
-}
-
-static void
-popbuf(hoedown_document *doc, int type)
-{
- doc->work_bufs[type].size--;
-}
-
-static void
-unscape_text(hoedown_buffer *ob, hoedown_buffer *src)
-{
- size_t i = 0, org;
- while (i < src->size) {
- org = i;
- while (i < src->size && src->data[i] != '\\')
- i++;
-
- if (i > org)
- hoedown_buffer_put(ob, src->data + org, i - org);
-
- if (i + 1 >= src->size)
- break;
-
- hoedown_buffer_putc(ob, src->data[i + 1]);
- i += 2;
- }
-}
-
-static unsigned int
-hash_link_ref(const uint8_t *link_ref, size_t length)
-{
- size_t i;
- unsigned int hash = 0;
-
- for (i = 0; i < length; ++i)
- hash = tolower(link_ref[i]) + (hash << 6) + (hash << 16) - hash;
-
- return hash;
-}
-
-static struct link_ref *
-add_link_ref(
- struct link_ref **references,
- const uint8_t *name, size_t name_size)
-{
- struct link_ref *ref = hoedown_calloc(1, sizeof(struct link_ref));
-
- ref->id = hash_link_ref(name, name_size);
- ref->next = references[ref->id % REF_TABLE_SIZE];
-
- references[ref->id % REF_TABLE_SIZE] = ref;
- return ref;
-}
-
-static struct link_ref *
-find_link_ref(struct link_ref **references, uint8_t *name, size_t length)
-{
- unsigned int hash = hash_link_ref(name, length);
- struct link_ref *ref = NULL;
-
- ref = references[hash % REF_TABLE_SIZE];
-
- while (ref != NULL) {
- if (ref->id == hash)
- return ref;
-
- ref = ref->next;
- }
-
- return NULL;
-}
-
-static void
-free_link_refs(struct link_ref **references)
-{
- size_t i;
-
- for (i = 0; i < REF_TABLE_SIZE; ++i) {
- struct link_ref *r = references[i];
- struct link_ref *next;
-
- while (r) {
- next = r->next;
- hoedown_buffer_free(r->link);
- hoedown_buffer_free(r->title);
- free(r);
- r = next;
- }
- }
-}
-
-static struct footnote_ref *
-create_footnote_ref(struct footnote_list *list, const uint8_t *name, size_t name_size)
-{
- struct footnote_ref *ref = hoedown_calloc(1, sizeof(struct footnote_ref));
-
- ref->id = hash_link_ref(name, name_size);
-
- return ref;
-}
-
-static int
-add_footnote_ref(struct footnote_list *list, struct footnote_ref *ref)
-{
- struct footnote_item *item = hoedown_calloc(1, sizeof(struct footnote_item));
- if (!item)
- return 0;
- item->ref = ref;
-
- if (list->head == NULL) {
- list->head = list->tail = item;
- } else {
- list->tail->next = item;
- list->tail = item;
- }
- list->count++;
-
- return 1;
-}
-
-static struct footnote_ref *
-find_footnote_ref(struct footnote_list *list, uint8_t *name, size_t length)
-{
- unsigned int hash = hash_link_ref(name, length);
- struct footnote_item *item = NULL;
-
- item = list->head;
-
- while (item != NULL) {
- if (item->ref->id == hash)
- return item->ref;
- item = item->next;
- }
-
- return NULL;
-}
-
-static void
-free_footnote_ref(struct footnote_ref *ref)
-{
- hoedown_buffer_free(ref->contents);
- free(ref);
-}
-
-static void
-free_footnote_list(struct footnote_list *list, int free_refs)
-{
- struct footnote_item *item = list->head;
- struct footnote_item *next;
-
- while (item) {
- next = item->next;
- if (free_refs)
- free_footnote_ref(item->ref);
- free(item);
- item = next;
- }
-}
-
-
-/*
- * Check whether a char is a Markdown spacing char.
-
- * Right now we only consider spaces the actual
- * space and a newline: tabs and carriage returns
- * are filtered out during the preprocessing phase.
- *
- * If we wanted to actually be UTF-8 compliant, we
- * should instead extract an Unicode codepoint from
- * this character and check for space properties.
- */
-static int
-_isspace(int c)
-{
- return c == ' ' || c == '\n';
-}
-
-/* is_empty_all: verify that all the data is spacing */
-static int
-is_empty_all(const uint8_t *data, size_t size)
-{
- size_t i = 0;
- while (i < size && _isspace(data[i])) i++;
- return i == size;
-}
-
-/*
- * Replace all spacing characters in data with spaces. As a special
- * case, this collapses a newline with the previous space, if possible.
- */
-static void
-replace_spacing(hoedown_buffer *ob, const uint8_t *data, size_t size)
-{
- size_t i = 0, mark;
- hoedown_buffer_grow(ob, size);
- while (1) {
- mark = i;
- while (i < size && data[i] != '\n') i++;
- hoedown_buffer_put(ob, data + mark, i - mark);
-
- if (i >= size) break;
-
- if (!(i > 0 && data[i-1] == ' '))
- hoedown_buffer_putc(ob, ' ');
- i++;
- }
-}
-
-/****************************
- * INLINE PARSING FUNCTIONS *
- ****************************/
-
-/* is_mail_autolink • looks for the address part of a mail autolink and '>' */
-/* this is less strict than the original markdown e-mail address matching */
-static size_t
-is_mail_autolink(uint8_t *data, size_t size)
-{
- size_t i = 0, nb = 0;
-
- /* address is assumed to be: [-@._a-zA-Z0-9]+ with exactly one '@' */
- for (i = 0; i < size; ++i) {
- if (isalnum(data[i]))
- continue;
-
- switch (data[i]) {
- case '@':
- nb++;
-
- case '-':
- case '.':
- case '_':
- break;
-
- case '>':
- return (nb == 1) ? i + 1 : 0;
-
- default:
- return 0;
- }
- }
-
- return 0;
-}
-
-/* tag_length • returns the length of the given tag, or 0 is it's not valid */
-static size_t
-tag_length(uint8_t *data, size_t size, hoedown_autolink_type *autolink)
-{
- size_t i, j;
-
- /* a valid tag can't be shorter than 3 chars */
- if (size < 3) return 0;
-
- /* begins with a '<' optionally followed by '/', followed by letter or number */
- if (data[0] != '<') return 0;
- i = (data[1] == '/') ? 2 : 1;
-
- if (!isalnum(data[i]))
- return 0;
-
- /* scheme test */
- *autolink = HOEDOWN_AUTOLINK_NONE;
-
- /* try to find the beginning of an URI */
- while (i < size && (isalnum(data[i]) || data[i] == '.' || data[i] == '+' || data[i] == '-'))
- i++;
-
- if (i > 1 && data[i] == '@') {
- if ((j = is_mail_autolink(data + i, size - i)) != 0) {
- *autolink = HOEDOWN_AUTOLINK_EMAIL;
- return i + j;
- }
- }
-
- if (i > 2 && data[i] == ':') {
- *autolink = HOEDOWN_AUTOLINK_NORMAL;
- i++;
- }
-
- /* completing autolink test: no spacing or ' or " */
- if (i >= size)
- *autolink = HOEDOWN_AUTOLINK_NONE;
-
- else if (*autolink) {
- j = i;
-
- while (i < size) {
- if (data[i] == '\\') i += 2;
- else if (data[i] == '>' || data[i] == '\'' ||
- data[i] == '"' || data[i] == ' ' || data[i] == '\n')
- break;
- else i++;
- }
-
- if (i >= size) return 0;
- if (i > j && data[i] == '>') return i + 1;
- /* one of the forbidden chars has been found */
- *autolink = HOEDOWN_AUTOLINK_NONE;
- }
-
- /* looking for something looking like a tag end */
- while (i < size && data[i] != '>') i++;
- if (i >= size) return 0;
- return i + 1;
-}
-
-/* parse_inline • parses inline markdown elements */
-static void
-parse_inline(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size)
-{
- size_t i = 0, end = 0, consumed = 0;
- hoedown_buffer work = { 0, 0, 0, 0, NULL, NULL, NULL };
- uint8_t *active_char = doc->active_char;
-
- if (doc->work_bufs[BUFFER_SPAN].size +
- doc->work_bufs[BUFFER_BLOCK].size > doc->max_nesting)
- return;
-
- while (i < size) {
- /* copying inactive chars into the output */
- while (end < size && active_char[data[end]] == 0)
- end++;
-
- if (doc->md.normal_text) {
- work.data = data + i;
- work.size = end - i;
- doc->md.normal_text(ob, &work, &doc->data);
- }
- else
- hoedown_buffer_put(ob, data + i, end - i);
-
- if (end >= size) break;
- i = end;
-
- end = markdown_char_ptrs[ (int)active_char[data[end]] ](ob, doc, data + i, i - consumed, size - i);
- if (!end) /* no action from the callback */
- end = i + 1;
- else {
- i += end;
- end = i;
- consumed = i;
- }
- }
-}
-
-/* is_escaped • returns whether special char at data[loc] is escaped by '\\' */
-static int
-is_escaped(uint8_t *data, size_t loc)
-{
- size_t i = loc;
- while (i >= 1 && data[i - 1] == '\\')
- i--;
-
- /* odd numbers of backslashes escapes data[loc] */
- return (loc - i) % 2;
-}
-
-/* find_emph_char • looks for the next emph uint8_t, skipping other constructs */
-static size_t
-find_emph_char(uint8_t *data, size_t size, uint8_t c)
-{
- size_t i = 0;
-
- while (i < size) {
- while (i < size && data[i] != c && data[i] != '[' && data[i] != '`')
- i++;
-
- if (i == size)
- return 0;
-
- /* not counting escaped chars */
- if (is_escaped(data, i)) {
- i++; continue;
- }
-
- if (data[i] == c)
- return i;
-
- /* skipping a codespan */
- if (data[i] == '`') {
- size_t span_nb = 0, bt;
- size_t tmp_i = 0;
-
- /* counting the number of opening backticks */
- while (i < size && data[i] == '`') {
- i++; span_nb++;
- }
-
- if (i >= size) return 0;
-
- /* finding the matching closing sequence */
- bt = 0;
- while (i < size && bt < span_nb) {
- if (!tmp_i && data[i] == c) tmp_i = i;
- if (data[i] == '`') bt++;
- else bt = 0;
- i++;
- }
-
- /* not a well-formed codespan; use found matching emph char */
- if (i >= size) return tmp_i;
- }
- /* skipping a link */
- else if (data[i] == '[') {
- size_t tmp_i = 0;
- uint8_t cc;
-
- i++;
- while (i < size && data[i] != ']') {
- if (!tmp_i && data[i] == c) tmp_i = i;
- i++;
- }
-
- i++;
- while (i < size && _isspace(data[i]))
- i++;
-
- if (i >= size)
- return tmp_i;
-
- switch (data[i]) {
- case '[':
- cc = ']'; break;
-
- case '(':
- cc = ')'; break;
-
- default:
- if (tmp_i)
- return tmp_i;
- else
- continue;
- }
-
- i++;
- while (i < size && data[i] != cc) {
- if (!tmp_i && data[i] == c) tmp_i = i;
- i++;
- }
-
- if (i >= size)
- return tmp_i;
-
- i++;
- }
- }
-
- return 0;
-}
-
-/* parse_emph1 • parsing single emphase */
-/* closed by a symbol not preceded by spacing and not followed by symbol */
-static size_t
-parse_emph1(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size, uint8_t c)
-{
- size_t i = 0, len;
- hoedown_buffer *work = 0;
- int r;
-
- /* skipping one symbol if coming from emph3 */
- if (size > 1 && data[0] == c && data[1] == c) i = 1;
-
- while (i < size) {
- len = find_emph_char(data + i, size - i, c);
- if (!len) return 0;
- i += len;
- if (i >= size) return 0;
-
- if (data[i] == c && !_isspace(data[i - 1])) {
-
- if (doc->ext_flags & HOEDOWN_EXT_NO_INTRA_EMPHASIS) {
- if (i + 1 < size && isalnum(data[i + 1]))
- continue;
- }
-
- work = newbuf(doc, BUFFER_SPAN);
- parse_inline(work, doc, data, i);
-
- if (doc->ext_flags & HOEDOWN_EXT_UNDERLINE && c == '_')
- r = doc->md.underline(ob, work, &doc->data);
- else
- r = doc->md.emphasis(ob, work, &doc->data);
-
- popbuf(doc, BUFFER_SPAN);
- return r ? i + 1 : 0;
- }
- }
-
- return 0;
-}
-
-/* parse_emph2 • parsing single emphase */
-static size_t
-parse_emph2(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size, uint8_t c)
-{
- size_t i = 0, len;
- hoedown_buffer *work = 0;
- int r;
-
- while (i < size) {
- len = find_emph_char(data + i, size - i, c);
- if (!len) return 0;
- i += len;
-
- if (i + 1 < size && data[i] == c && data[i + 1] == c && i && !_isspace(data[i - 1])) {
- work = newbuf(doc, BUFFER_SPAN);
- parse_inline(work, doc, data, i);
-
- if (c == '~')
- r = doc->md.strikethrough(ob, work, &doc->data);
- else if (c == '=')
- r = doc->md.highlight(ob, work, &doc->data);
- else
- r = doc->md.double_emphasis(ob, work, &doc->data);
-
- popbuf(doc, BUFFER_SPAN);
- return r ? i + 2 : 0;
- }
- i++;
- }
- return 0;
-}
-
-/* parse_emph3 • parsing single emphase */
-/* finds the first closing tag, and delegates to the other emph */
-static size_t
-parse_emph3(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size, uint8_t c)
-{
- size_t i = 0, len;
- int r;
-
- while (i < size) {
- len = find_emph_char(data + i, size - i, c);
- if (!len) return 0;
- i += len;
-
- /* skip spacing preceded symbols */
- if (data[i] != c || _isspace(data[i - 1]))
- continue;
-
- if (i + 2 < size && data[i + 1] == c && data[i + 2] == c && doc->md.triple_emphasis) {
- /* triple symbol found */
- hoedown_buffer *work = newbuf(doc, BUFFER_SPAN);
-
- parse_inline(work, doc, data, i);
- r = doc->md.triple_emphasis(ob, work, &doc->data);
- popbuf(doc, BUFFER_SPAN);
- return r ? i + 3 : 0;
-
- } else if (i + 1 < size && data[i + 1] == c) {
- /* double symbol found, handing over to emph1 */
- len = parse_emph1(ob, doc, data - 2, size + 2, c);
- if (!len) return 0;
- else return len - 2;
-
- } else {
- /* single symbol found, handing over to emph2 */
- len = parse_emph2(ob, doc, data - 1, size + 1, c);
- if (!len) return 0;
- else return len - 1;
- }
- }
- return 0;
-}
-
-/* parse_math • parses a math span until the given ending delimiter */
-static size_t
-parse_math(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size, const char *end, size_t delimsz, int displaymode)
-{
- hoedown_buffer text = { NULL, 0, 0, 0, NULL, NULL, NULL };
- size_t i = delimsz;
-
- if (!doc->md.math)
- return 0;
-
- /* find ending delimiter */
- while (1) {
- while (i < size && data[i] != (uint8_t)end[0])
- i++;
-
- if (i >= size)
- return 0;
-
- if (!is_escaped(data, i) && !(i + delimsz > size)
- && memcmp(data + i, end, delimsz) == 0)
- break;
-
- i++;
- }
-
- /* prepare buffers */
- text.data = data + delimsz;
- text.size = i - delimsz;
-
- /* if this is a $$ and MATH_EXPLICIT is not active,
- * guess whether displaymode should be enabled from the context */
- i += delimsz;
- if (delimsz == 2 && !(doc->ext_flags & HOEDOWN_EXT_MATH_EXPLICIT))
- displaymode = is_empty_all(data - offset, offset) && is_empty_all(data + i, size - i);
-
- /* call callback */
- if (doc->md.math(ob, &text, displaymode, &doc->data))
- return i;
-
- return 0;
-}
-
-/* char_emphasis • single and double emphasis parsing */
-static size_t
-char_emphasis(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size)
-{
- uint8_t c = data[0];
- size_t ret;
-
- if (doc->ext_flags & HOEDOWN_EXT_NO_INTRA_EMPHASIS) {
- if (offset > 0 && !_isspace(data[-1]) && data[-1] != '>' && data[-1] != '(')
- return 0;
- }
-
- if (size > 2 && data[1] != c) {
- /* spacing cannot follow an opening emphasis;
- * strikethrough and highlight only takes two characters '~~' */
- if (c == '~' || c == '=' || _isspace(data[1]) || (ret = parse_emph1(ob, doc, data + 1, size - 1, c)) == 0)
- return 0;
-
- return ret + 1;
- }
-
- if (size > 3 && data[1] == c && data[2] != c) {
- if (_isspace(data[2]) || (ret = parse_emph2(ob, doc, data + 2, size - 2, c)) == 0)
- return 0;
-
- return ret + 2;
- }
-
- if (size > 4 && data[1] == c && data[2] == c && data[3] != c) {
- if (c == '~' || c == '=' || _isspace(data[3]) || (ret = parse_emph3(ob, doc, data + 3, size - 3, c)) == 0)
- return 0;
-
- return ret + 3;
- }
-
- return 0;
-}
-
-
-/* char_linebreak • '\n' preceded by two spaces (assuming linebreak != 0) */
-static size_t
-char_linebreak(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size)
-{
- if (offset < 2 || data[-1] != ' ' || data[-2] != ' ')
- return 0;
-
- /* removing the last space from ob and rendering */
- while (ob->size && ob->data[ob->size - 1] == ' ')
- ob->size--;
-
- return doc->md.linebreak(ob, &doc->data) ? 1 : 0;
-}
-
-
-/* char_codespan • '`' parsing a code span (assuming codespan != 0) */
-static size_t
-char_codespan(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size)
-{
- hoedown_buffer work = { NULL, 0, 0, 0, NULL, NULL, NULL };
- size_t end, nb = 0, i, f_begin, f_end;
-
- /* counting the number of backticks in the delimiter */
- while (nb < size && data[nb] == '`')
- nb++;
-
- /* finding the next delimiter */
- i = 0;
- for (end = nb; end < size && i < nb; end++) {
- if (data[end] == '`') i++;
- else i = 0;
- }
-
- if (i < nb && end >= size)
- return 0; /* no matching delimiter */
-
- /* trimming outside spaces */
- f_begin = nb;
- while (f_begin < end && data[f_begin] == ' ')
- f_begin++;
-
- f_end = end - nb;
- while (f_end > nb && data[f_end-1] == ' ')
- f_end--;
-
- /* real code span */
- if (f_begin < f_end) {
- work.data = data + f_begin;
- work.size = f_end - f_begin;
-
- if (!doc->md.codespan(ob, &work, &doc->data))
- end = 0;
- } else {
- if (!doc->md.codespan(ob, 0, &doc->data))
- end = 0;
- }
-
- return end;
-}
-
-/* char_quote • '"' parsing a quote */
-static size_t
-char_quote(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size)
-{
- size_t end, nq = 0, i, f_begin, f_end;
-
- /* counting the number of quotes in the delimiter */
- while (nq < size && data[nq] == '"')
- nq++;
-
- /* finding the next delimiter */
- end = nq;
- while (1) {
- i = end;
- end += find_emph_char(data + end, size - end, '"');
- if (end == i) return 0; /* no matching delimiter */
- i = end;
- while (end < size && data[end] == '"' && end - i < nq) end++;
- if (end - i >= nq) break;
- }
-
- /* trimming outside spaces */
- f_begin = nq;
- while (f_begin < end && data[f_begin] == ' ')
- f_begin++;
-
- f_end = end - nq;
- while (f_end > nq && data[f_end-1] == ' ')
- f_end--;
-
- /* real quote */
- if (f_begin < f_end) {
- hoedown_buffer *work = newbuf(doc, BUFFER_SPAN);
- parse_inline(work, doc, data + f_begin, f_end - f_begin);
-
- if (!doc->md.quote(ob, work, &doc->data))
- end = 0;
- popbuf(doc, BUFFER_SPAN);
- } else {
- if (!doc->md.quote(ob, 0, &doc->data))
- end = 0;
- }
-
- return end;
-}
-
-
-/* char_escape • '\\' backslash escape */
-static size_t
-char_escape(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size)
-{
- static const char *escape_chars = "\\`*_{}[]()#+-.!:|&<>^~=\"$";
- hoedown_buffer work = { 0, 0, 0, 0, NULL, NULL, NULL };
- size_t w;
-
- if (size > 1) {
- if (data[1] == '\\' && (doc->ext_flags & HOEDOWN_EXT_MATH) &&
- size > 2 && (data[2] == '(' || data[2] == '[')) {
- const char *end = (data[2] == '[') ? "\\\\]" : "\\\\)";
- w = parse_math(ob, doc, data, offset, size, end, 3, data[2] == '[');
- if (w) return w;
- }
-
- if (strchr(escape_chars, data[1]) == NULL)
- return 0;
-
- if (doc->md.normal_text) {
- work.data = data + 1;
- work.size = 1;
- doc->md.normal_text(ob, &work, &doc->data);
- }
- else hoedown_buffer_putc(ob, data[1]);
- } else if (size == 1) {
- hoedown_buffer_putc(ob, data[0]);
- }
-
- return 2;
-}
-
-/* char_entity • '&' escaped when it doesn't belong to an entity */
-/* valid entities are assumed to be anything matching &#?[A-Za-z0-9]+; */
-static size_t
-char_entity(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size)
-{
- size_t end = 1;
- hoedown_buffer work = { 0, 0, 0, 0, NULL, NULL, NULL };
-
- if (end < size && data[end] == '#')
- end++;
-
- while (end < size && isalnum(data[end]))
- end++;
-
- if (end < size && data[end] == ';')
- end++; /* real entity */
- else
- return 0; /* lone '&' */
-
- if (doc->md.entity) {
- work.data = data;
- work.size = end;
- doc->md.entity(ob, &work, &doc->data);
- }
- else hoedown_buffer_put(ob, data, end);
-
- return end;
-}
-
-/* char_langle_tag • '<' when tags or autolinks are allowed */
-static size_t
-char_langle_tag(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size)
-{
- hoedown_buffer work = { NULL, 0, 0, 0, NULL, NULL, NULL };
- hoedown_autolink_type altype = HOEDOWN_AUTOLINK_NONE;
- size_t end = tag_length(data, size, &altype);
- int ret = 0;
-
- work.data = data;
- work.size = end;
-
- if (end > 2) {
- if (doc->md.autolink && altype != HOEDOWN_AUTOLINK_NONE) {
- hoedown_buffer *u_link = newbuf(doc, BUFFER_SPAN);
- work.data = data + 1;
- work.size = end - 2;
- unscape_text(u_link, &work);
- ret = doc->md.autolink(ob, u_link, altype, &doc->data);
- popbuf(doc, BUFFER_SPAN);
- }
- else if (doc->md.raw_html)
- ret = doc->md.raw_html(ob, &work, &doc->data);
- }
-
- if (!ret) return 0;
- else return end;
-}
-
-static size_t
-char_autolink_www(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size)
-{
- hoedown_buffer *link, *link_url, *link_text;
- size_t link_len, rewind;
-
- if (!doc->md.link || doc->in_link_body)
- return 0;
-
- link = newbuf(doc, BUFFER_SPAN);
-
- if ((link_len = hoedown_autolink__www(&rewind, link, data, offset, size, HOEDOWN_AUTOLINK_SHORT_DOMAINS)) > 0) {
- link_url = newbuf(doc, BUFFER_SPAN);
- HOEDOWN_BUFPUTSL(link_url, "http://");
- hoedown_buffer_put(link_url, link->data, link->size);
-
- ob->size -= rewind;
- if (doc->md.normal_text) {
- link_text = newbuf(doc, BUFFER_SPAN);
- doc->md.normal_text(link_text, link, &doc->data);
- doc->md.link(ob, link_text, link_url, NULL, &doc->data);
- popbuf(doc, BUFFER_SPAN);
- } else {
- doc->md.link(ob, link, link_url, NULL, &doc->data);
- }
- popbuf(doc, BUFFER_SPAN);
- }
-
- popbuf(doc, BUFFER_SPAN);
- return link_len;
-}
-
-static size_t
-char_autolink_email(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size)
-{
- hoedown_buffer *link;
- size_t link_len, rewind;
-
- if (!doc->md.autolink || doc->in_link_body)
- return 0;
-
- link = newbuf(doc, BUFFER_SPAN);
-
- if ((link_len = hoedown_autolink__email(&rewind, link, data, offset, size, 0)) > 0) {
- ob->size -= rewind;
- doc->md.autolink(ob, link, HOEDOWN_AUTOLINK_EMAIL, &doc->data);
- }
-
- popbuf(doc, BUFFER_SPAN);
- return link_len;
-}
-
-static size_t
-char_autolink_url(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size)
-{
- hoedown_buffer *link;
- size_t link_len, rewind;
-
- if (!doc->md.autolink || doc->in_link_body)
- return 0;
-
- link = newbuf(doc, BUFFER_SPAN);
-
- if ((link_len = hoedown_autolink__url(&rewind, link, data, offset, size, 0)) > 0) {
- ob->size -= rewind;
- doc->md.autolink(ob, link, HOEDOWN_AUTOLINK_NORMAL, &doc->data);
- }
-
- popbuf(doc, BUFFER_SPAN);
- return link_len;
-}
-
-/* char_link • '[': parsing a link, a footnote or an image */
-static size_t
-char_link(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size)
-{
- int is_img = (offset && data[-1] == '!' && !is_escaped(data - offset, offset - 1));
- int is_footnote = (doc->ext_flags & HOEDOWN_EXT_FOOTNOTES && data[1] == '^');
- size_t i = 1, txt_e, link_b = 0, link_e = 0, title_b = 0, title_e = 0;
- hoedown_buffer *content = NULL;
- hoedown_buffer *link = NULL;
- hoedown_buffer *title = NULL;
- hoedown_buffer *u_link = NULL;
- size_t org_work_size = doc->work_bufs[BUFFER_SPAN].size;
- int ret = 0, in_title = 0, qtype = 0;
-
- /* checking whether the correct renderer exists */
- if ((is_footnote && !doc->md.footnote_ref) || (is_img && !doc->md.image)
- || (!is_img && !is_footnote && !doc->md.link))
- goto cleanup;
-
- /* looking for the matching closing bracket */
- i += find_emph_char(data + i, size - i, ']');
- txt_e = i;
-
- if (i < size && data[i] == ']') i++;
- else goto cleanup;
-
- /* footnote link */
- if (is_footnote) {
- hoedown_buffer id = { NULL, 0, 0, 0, NULL, NULL, NULL };
- struct footnote_ref *fr;
-
- if (txt_e < 3)
- goto cleanup;
-
- id.data = data + 2;
- id.size = txt_e - 2;
-
- fr = find_footnote_ref(&doc->footnotes_found, id.data, id.size);
-
- /* mark footnote used */
- if (fr && !fr->is_used) {
- if(!add_footnote_ref(&doc->footnotes_used, fr))
- goto cleanup;
- fr->is_used = 1;
- fr->num = doc->footnotes_used.count;
-
- /* render */
- if (doc->md.footnote_ref)
- ret = doc->md.footnote_ref(ob, fr->num, &doc->data);
- }
-
- goto cleanup;
- }
-
- /* skip any amount of spacing */
- /* (this is much more laxist than original markdown syntax) */
- while (i < size && _isspace(data[i]))
- i++;
-
- /* inline style link */
- if (i < size && data[i] == '(') {
- size_t nb_p;
-
- /* skipping initial spacing */
- i++;
-
- while (i < size && _isspace(data[i]))
- i++;
-
- link_b = i;
-
- /* looking for link end: ' " ) */
- /* Count the number of open parenthesis */
- nb_p = 0;
-
- while (i < size) {
- if (data[i] == '\\') i += 2;
- else if (data[i] == '(' && i != 0) {
- nb_p++; i++;
- }
- else if (data[i] == ')') {
- if (nb_p == 0) break;
- else nb_p--; i++;
- } else if (i >= 1 && _isspace(data[i-1]) && (data[i] == '\'' || data[i] == '"')) break;
- else i++;
- }
-
- if (i >= size) goto cleanup;
- link_e = i;
-
- /* looking for title end if present */
- if (data[i] == '\'' || data[i] == '"') {
- qtype = data[i];
- in_title = 1;
- i++;
- title_b = i;
-
- while (i < size) {
- if (data[i] == '\\') i += 2;
- else if (data[i] == qtype) {in_title = 0; i++;}
- else if ((data[i] == ')') && !in_title) break;
- else i++;
- }
-
- if (i >= size) goto cleanup;
-
- /* skipping spacing after title */
- title_e = i - 1;
- while (title_e > title_b && _isspace(data[title_e]))
- title_e--;
-
- /* checking for closing quote presence */
- if (data[title_e] != '\'' && data[title_e] != '"') {
- title_b = title_e = 0;
- link_e = i;
- }
- }
-
- /* remove spacing at the end of the link */
- while (link_e > link_b && _isspace(data[link_e - 1]))
- link_e--;
-
- /* remove optional angle brackets around the link */
- if (data[link_b] == '<') link_b++;
- if (data[link_e - 1] == '>') link_e--;
-
- /* building escaped link and title */
- if (link_e > link_b) {
- link = newbuf(doc, BUFFER_SPAN);
- hoedown_buffer_put(link, data + link_b, link_e - link_b);
- }
-
- if (title_e > title_b) {
- title = newbuf(doc, BUFFER_SPAN);
- hoedown_buffer_put(title, data + title_b, title_e - title_b);
- }
-
- i++;
- }
-
- /* reference style link */
- else if (i < size && data[i] == '[') {
- hoedown_buffer *id = newbuf(doc, BUFFER_SPAN);
- struct link_ref *lr;
-
- /* looking for the id */
- i++;
- link_b = i;
- while (i < size && data[i] != ']') i++;
- if (i >= size) goto cleanup;
- link_e = i;
-
- /* finding the link_ref */
- if (link_b == link_e)
- replace_spacing(id, data + 1, txt_e - 1);
- else
- hoedown_buffer_put(id, data + link_b, link_e - link_b);
-
- lr = find_link_ref(doc->refs, id->data, id->size);
- if (!lr)
- goto cleanup;
-
- /* keeping link and title from link_ref */
- link = lr->link;
- title = lr->title;
- i++;
- }
-
- /* shortcut reference style link */
- else {
- hoedown_buffer *id = newbuf(doc, BUFFER_SPAN);
- struct link_ref *lr;
-
- /* crafting the id */
- replace_spacing(id, data + 1, txt_e - 1);
-
- /* finding the link_ref */
- lr = find_link_ref(doc->refs, id->data, id->size);
- if (!lr)
- goto cleanup;
-
- /* keeping link and title from link_ref */
- link = lr->link;
- title = lr->title;
-
- /* rewinding the spacing */
- i = txt_e + 1;
- }
-
- /* building content: img alt is kept, only link content is parsed */
- if (txt_e > 1) {
- content = newbuf(doc, BUFFER_SPAN);
- if (is_img) {
- hoedown_buffer_put(content, data + 1, txt_e - 1);
- } else {
- /* disable autolinking when parsing inline the
- * content of a link */
- doc->in_link_body = 1;
- parse_inline(content, doc, data + 1, txt_e - 1);
- doc->in_link_body = 0;
- }
- }
-
- if (link) {
- u_link = newbuf(doc, BUFFER_SPAN);
- unscape_text(u_link, link);
- }
-
- /* calling the relevant rendering function */
- if (is_img) {
- if (ob->size && ob->data[ob->size - 1] == '!')
- ob->size -= 1;
-
- ret = doc->md.image(ob, u_link, title, content, &doc->data);
- } else {
- ret = doc->md.link(ob, content, u_link, title, &doc->data);
- }
-
- /* cleanup */
-cleanup:
- doc->work_bufs[BUFFER_SPAN].size = (int)org_work_size;
- return ret ? i : 0;
-}
-
-static size_t
-char_superscript(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size)
-{
- size_t sup_start, sup_len;
- hoedown_buffer *sup;
-
- if (!doc->md.superscript)
- return 0;
-
- if (size < 2)
- return 0;
-
- if (data[1] == '(') {
- sup_start = 2;
- sup_len = find_emph_char(data + 2, size - 2, ')') + 2;
-
- if (sup_len == size)
- return 0;
- } else {
- sup_start = sup_len = 1;
-
- while (sup_len < size && !_isspace(data[sup_len]))
- sup_len++;
- }
-
- if (sup_len - sup_start == 0)
- return (sup_start == 2) ? 3 : 0;
-
- sup = newbuf(doc, BUFFER_SPAN);
- parse_inline(sup, doc, data + sup_start, sup_len - sup_start);
- doc->md.superscript(ob, sup, &doc->data);
- popbuf(doc, BUFFER_SPAN);
-
- return (sup_start == 2) ? sup_len + 1 : sup_len;
-}
-
-static size_t
-char_math(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size)
-{
- /* double dollar */
- if (size > 1 && data[1] == '$')
- return parse_math(ob, doc, data, offset, size, "$$", 2, 1);
-
- /* single dollar allowed only with MATH_EXPLICIT flag */
- if (doc->ext_flags & HOEDOWN_EXT_MATH_EXPLICIT)
- return parse_math(ob, doc, data, offset, size, "$", 1, 0);
-
- return 0;
-}
-
-/*********************************
- * BLOCK-LEVEL PARSING FUNCTIONS *
- *********************************/
-
-/* is_empty • returns the line length when it is empty, 0 otherwise */
-static size_t
-is_empty(const uint8_t *data, size_t size)
-{
- size_t i;
-
- for (i = 0; i < size && data[i] != '\n'; i++)
- if (data[i] != ' ')
- return 0;
-
- return i + 1;
-}
-
-/* is_hrule • returns whether a line is a horizontal rule */
-static int
-is_hrule(uint8_t *data, size_t size)
-{
- size_t i = 0, n = 0;
- uint8_t c;
-
- /* skipping initial spaces */
- if (size < 3) return 0;
- if (data[0] == ' ') { i++;
- if (data[1] == ' ') { i++;
- if (data[2] == ' ') { i++; } } }
-
- /* looking at the hrule uint8_t */
- if (i + 2 >= size
- || (data[i] != '*' && data[i] != '-' && data[i] != '_'))
- return 0;
- c = data[i];
-
- /* the whole line must be the char or space */
- while (i < size && data[i] != '\n') {
- if (data[i] == c) n++;
- else if (data[i] != ' ')
- return 0;
-
- i++;
- }
-
- return n >= 3;
-}
-
-/* check if a line is a code fence; return the
- * end of the code fence. if passed, width of
- * the fence rule and character will be returned */
-static size_t
-is_codefence(uint8_t *data, size_t size, size_t *width, uint8_t *chr)
-{
- size_t i = 0, n = 1;
- uint8_t c;
-
- /* skipping initial spaces */
- if (size < 3)
- return 0;
-
- if (data[0] == ' ') { i++;
- if (data[1] == ' ') { i++;
- if (data[2] == ' ') { i++; } } }
-
- /* looking at the hrule uint8_t */
- c = data[i];
- if (i + 2 >= size || !(c=='~' || c=='`'))
- return 0;
-
- /* the fence must be that same character */
- while (++i < size && data[i] == c)
- ++n;
-
- if (n < 3)
- return 0;
-
- if (width) *width = n;
- if (chr) *chr = c;
- return i;
-}
-
-/* expects single line, checks if it's a codefence and extracts language */
-static size_t
-parse_codefence(uint8_t *data, size_t size, hoedown_buffer *lang, size_t *width, uint8_t *chr)
-{
- size_t i, w, lang_start;
-
- i = w = is_codefence(data, size, width, chr);
- if (i == 0)
- return 0;
-
- while (i < size && _isspace(data[i]))
- i++;
-
- lang_start = i;
-
- while (i < size && !_isspace(data[i]))
- i++;
-
- lang->data = data + lang_start;
- lang->size = i - lang_start;
-
- /* Avoid parsing a codespan as a fence */
- i = lang_start + 2;
- while (i < size && !(data[i] == *chr && data[i-1] == *chr && data[i-2] == *chr)) i++;
- if (i < size) return 0;
-
- return w;
-}
-
-/* is_atxheader • returns whether the line is a hash-prefixed header */
-static int
-is_atxheader(hoedown_document *doc, uint8_t *data, size_t size)
-{
- if (data[0] != '#')
- return 0;
-
- if (doc->ext_flags & HOEDOWN_EXT_SPACE_HEADERS) {
- size_t level = 0;
-
- while (level < size && level < 6 && data[level] == '#')
- level++;
-
- if (level < size && data[level] != ' ')
- return 0;
- }
-
- return 1;
-}
-
-/* is_headerline • returns whether the line is a setext-style hdr underline */
-static int
-is_headerline(uint8_t *data, size_t size)
-{
- size_t i = 0;
-
- /* test of level 1 header */
- if (data[i] == '=') {
- for (i = 1; i < size && data[i] == '='; i++);
- while (i < size && data[i] == ' ') i++;
- return (i >= size || data[i] == '\n') ? 1 : 0; }
-
- /* test of level 2 header */
- if (data[i] == '-') {
- for (i = 1; i < size && data[i] == '-'; i++);
- while (i < size && data[i] == ' ') i++;
- return (i >= size || data[i] == '\n') ? 2 : 0; }
-
- return 0;
-}
-
-static int
-is_next_headerline(uint8_t *data, size_t size)
-{
- size_t i = 0;
-
- while (i < size && data[i] != '\n')
- i++;
-
- if (++i >= size)
- return 0;
-
- return is_headerline(data + i, size - i);
-}
-
-/* prefix_quote • returns blockquote prefix length */
-static size_t
-prefix_quote(uint8_t *data, size_t size)
-{
- size_t i = 0;
- if (i < size && data[i] == ' ') i++;
- if (i < size && data[i] == ' ') i++;
- if (i < size && data[i] == ' ') i++;
-
- if (i < size && data[i] == '>') {
- if (i + 1 < size && data[i + 1] == ' ')
- return i + 2;
-
- return i + 1;
- }
-
- return 0;
-}
-
-/* prefix_code • returns prefix length for block code*/
-static size_t
-prefix_code(uint8_t *data, size_t size)
-{
- if (size > 3 && data[0] == ' ' && data[1] == ' '
- && data[2] == ' ' && data[3] == ' ') return 4;
-
- return 0;
-}
-
-/* prefix_oli • returns ordered list item prefix */
-static size_t
-prefix_oli(uint8_t *data, size_t size)
-{
- size_t i = 0;
-
- if (i < size && data[i] == ' ') i++;
- if (i < size && data[i] == ' ') i++;
- if (i < size && data[i] == ' ') i++;
-
- if (i >= size || data[i] < '0' || data[i] > '9')
- return 0;
-
- while (i < size && data[i] >= '0' && data[i] <= '9')
- i++;
-
- if (i + 1 >= size || data[i] != '.' || data[i + 1] != ' ')
- return 0;
-
- if (is_next_headerline(data + i, size - i))
- return 0;
-
- return i + 2;
-}
-
-/* prefix_uli • returns ordered list item prefix */
-static size_t
-prefix_uli(uint8_t *data, size_t size)
-{
- size_t i = 0;
-
- if (i < size && data[i] == ' ') i++;
- if (i < size && data[i] == ' ') i++;
- if (i < size && data[i] == ' ') i++;
-
- if (i + 1 >= size ||
- (data[i] != '*' && data[i] != '+' && data[i] != '-') ||
- data[i + 1] != ' ')
- return 0;
-
- if (is_next_headerline(data + i, size - i))
- return 0;
-
- return i + 2;
-}
-
-
-/* parse_block • parsing of one block, returning next uint8_t to parse */
-static void parse_block(hoedown_buffer *ob, hoedown_document *doc,
- uint8_t *data, size_t size);
-
-
-/* parse_blockquote • handles parsing of a blockquote fragment */
-static size_t
-parse_blockquote(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size)
-{
- size_t beg, end = 0, pre, work_size = 0;
- uint8_t *work_data = 0;
- hoedown_buffer *out = 0;
-
- out = newbuf(doc, BUFFER_BLOCK);
- beg = 0;
- while (beg < size) {
- for (end = beg + 1; end < size && data[end - 1] != '\n'; end++);
-
- pre = prefix_quote(data + beg, end - beg);
-
- if (pre)
- beg += pre; /* skipping prefix */
-
- /* empty line followed by non-quote line */
- else if (is_empty(data + beg, end - beg) &&
- (end >= size || (prefix_quote(data + end, size - end) == 0 &&
- !is_empty(data + end, size - end))))
- break;
-
- if (beg < end) { /* copy into the in-place working buffer */
- /* hoedown_buffer_put(work, data + beg, end - beg); */
- if (!work_data)
- work_data = data + beg;
- else if (data + beg != work_data + work_size)
- memmove(work_data + work_size, data + beg, end - beg);
- work_size += end - beg;
- }
- beg = end;
- }
-
- parse_block(out, doc, work_data, work_size);
- if (doc->md.blockquote)
- doc->md.blockquote(ob, out, &doc->data);
- popbuf(doc, BUFFER_BLOCK);
- return end;
-}
-
-static size_t
-parse_htmlblock(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size, int do_render);
-
-/* parse_blockquote • handles parsing of a regular paragraph */
-static size_t
-parse_paragraph(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size)
-{
- hoedown_buffer work = { NULL, 0, 0, 0, NULL, NULL, NULL };
- size_t i = 0, end = 0;
- int level = 0;
-
- work.data = data;
-
- while (i < size) {
- for (end = i + 1; end < size && data[end - 1] != '\n'; end++) /* empty */;
-
- if (is_empty(data + i, size - i))
- break;
-
- if ((level = is_headerline(data + i, size - i)) != 0)
- break;
-
- if (is_atxheader(doc, data + i, size - i) ||
- is_hrule(data + i, size - i) ||
- prefix_quote(data + i, size - i)) {
- end = i;
- break;
- }
-
- i = end;
- }
-
- work.size = i;
- while (work.size && data[work.size - 1] == '\n')
- work.size--;
-
- if (!level) {
- hoedown_buffer *tmp = newbuf(doc, BUFFER_BLOCK);
- parse_inline(tmp, doc, work.data, work.size);
- if (doc->md.paragraph)
- doc->md.paragraph(ob, tmp, &doc->data);
- popbuf(doc, BUFFER_BLOCK);
- } else {
- hoedown_buffer *header_work;
-
- if (work.size) {
- size_t beg;
- i = work.size;
- work.size -= 1;
-
- while (work.size && data[work.size] != '\n')
- work.size -= 1;
-
- beg = work.size + 1;
- while (work.size && data[work.size - 1] == '\n')
- work.size -= 1;
-
- if (work.size > 0) {
- hoedown_buffer *tmp = newbuf(doc, BUFFER_BLOCK);
- parse_inline(tmp, doc, work.data, work.size);
-
- if (doc->md.paragraph)
- doc->md.paragraph(ob, tmp, &doc->data);
-
- popbuf(doc, BUFFER_BLOCK);
- work.data += beg;
- work.size = i - beg;
- }
- else work.size = i;
- }
-
- header_work = newbuf(doc, BUFFER_SPAN);
- parse_inline(header_work, doc, work.data, work.size);
-
- if (doc->md.header)
- doc->md.header(ob, header_work, (int)level, &doc->data);
-
- popbuf(doc, BUFFER_SPAN);
- }
-
- return end;
-}
-
-/* parse_fencedcode • handles parsing of a block-level code fragment */
-static size_t
-parse_fencedcode(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size)
-{
- hoedown_buffer text = { 0, 0, 0, 0, NULL, NULL, NULL };
- hoedown_buffer lang = { 0, 0, 0, 0, NULL, NULL, NULL };
- size_t i = 0, text_start, line_start;
- size_t w, w2;
- size_t width, width2;
- uint8_t chr, chr2;
-
- /* parse codefence line */
- while (i < size && data[i] != '\n')
- i++;
-
- w = parse_codefence(data, i, &lang, &width, &chr);
- if (!w)
- return 0;
-
- /* search for end */
- i++;
- text_start = i;
- while ((line_start = i) < size) {
- while (i < size && data[i] != '\n')
- i++;
-
- w2 = is_codefence(data + line_start, i - line_start, &width2, &chr2);
- if (w == w2 && width == width2 && chr == chr2 &&
- is_empty(data + (line_start+w), i - (line_start+w)))
- break;
-
- i++;
- }
-
- text.data = data + text_start;
- text.size = line_start - text_start;
-
- if (doc->md.blockcode)
- doc->md.blockcode(ob, text.size ? &text : NULL, lang.size ? &lang : NULL, &doc->data);
-
- return i;
-}
-
-static size_t
-parse_blockcode(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size)
-{
- size_t beg, end, pre;
- hoedown_buffer *work = 0;
-
- work = newbuf(doc, BUFFER_BLOCK);
-
- beg = 0;
- while (beg < size) {
- for (end = beg + 1; end < size && data[end - 1] != '\n'; end++) {};
- pre = prefix_code(data + beg, end - beg);
-
- if (pre)
- beg += pre; /* skipping prefix */
- else if (!is_empty(data + beg, end - beg))
- /* non-empty non-prefixed line breaks the pre */
- break;
-
- if (beg < end) {
- /* verbatim copy to the working buffer,
- escaping entities */
- if (is_empty(data + beg, end - beg))
- hoedown_buffer_putc(work, '\n');
- else hoedown_buffer_put(work, data + beg, end - beg);
- }
- beg = end;
- }
-
- while (work->size && work->data[work->size - 1] == '\n')
- work->size -= 1;
-
- hoedown_buffer_putc(work, '\n');
-
- if (doc->md.blockcode)
- doc->md.blockcode(ob, work, NULL, &doc->data);
-
- popbuf(doc, BUFFER_BLOCK);
- return beg;
-}
-
-/* parse_listitem • parsing of a single list item */
-/* assuming initial prefix is already removed */
-static size_t
-parse_listitem(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size, hoedown_list_flags *flags)
-{
- hoedown_buffer *work = 0, *inter = 0;
- size_t beg = 0, end, pre, sublist = 0, orgpre = 0, i;
- int in_empty = 0, has_inside_empty = 0, in_fence = 0;
-
- /* keeping track of the first indentation prefix */
- while (orgpre < 3 && orgpre < size && data[orgpre] == ' ')
- orgpre++;
-
- beg = prefix_uli(data, size);
- if (!beg)
- beg = prefix_oli(data, size);
-
- if (!beg)
- return 0;
-
- /* skipping to the beginning of the following line */
- end = beg;
- while (end < size && data[end - 1] != '\n')
- end++;
-
- /* getting working buffers */
- work = newbuf(doc, BUFFER_SPAN);
- inter = newbuf(doc, BUFFER_SPAN);
-
- /* putting the first line into the working buffer */
- hoedown_buffer_put(work, data + beg, end - beg);
- beg = end;
-
- /* process the following lines */
- while (beg < size) {
- size_t has_next_uli = 0, has_next_oli = 0;
-
- end++;
-
- while (end < size && data[end - 1] != '\n')
- end++;
-
- /* process an empty line */
- if (is_empty(data + beg, end - beg)) {
- in_empty = 1;
- beg = end;
- continue;
- }
-
- /* calculating the indentation */
- i = 0;
- while (i < 4 && beg + i < end && data[beg + i] == ' ')
- i++;
-
- pre = i;
-
- if (doc->ext_flags & HOEDOWN_EXT_FENCED_CODE) {
- if (is_codefence(data + beg + i, end - beg - i, NULL, NULL))
- in_fence = !in_fence;
- }
-
- /* Only check for new list items if we are **not** inside
- * a fenced code block */
- if (!in_fence) {
- has_next_uli = prefix_uli(data + beg + i, end - beg - i);
- has_next_oli = prefix_oli(data + beg + i, end - beg - i);
- }
-
- /* checking for a new item */
- if ((has_next_uli && !is_hrule(data + beg + i, end - beg - i)) || has_next_oli) {
- if (in_empty)
- has_inside_empty = 1;
-
- /* the following item must have the same (or less) indentation */
- if (pre <= orgpre) {
- /* if the following item has different list type, we end this list */
- if (in_empty && (
- ((*flags & HOEDOWN_LIST_ORDERED) && has_next_uli) ||
- (!(*flags & HOEDOWN_LIST_ORDERED) && has_next_oli)))
- *flags |= HOEDOWN_LI_END;
-
- break;
- }
-
- if (!sublist)
- sublist = work->size;
- }
- /* joining only indented stuff after empty lines;
- * note that now we only require 1 space of indentation
- * to continue a list */
- else if (in_empty && pre == 0) {
- *flags |= HOEDOWN_LI_END;
- break;
- }
-
- if (in_empty) {
- hoedown_buffer_putc(work, '\n');
- has_inside_empty = 1;
- in_empty = 0;
- }
-
- /* adding the line without prefix into the working buffer */
- hoedown_buffer_put(work, data + beg + i, end - beg - i);
- beg = end;
- }
-
- /* render of li contents */
- if (has_inside_empty)
- *flags |= HOEDOWN_LI_BLOCK;
-
- if (*flags & HOEDOWN_LI_BLOCK) {
- /* intermediate render of block li */
- if (sublist && sublist < work->size) {
- parse_block(inter, doc, work->data, sublist);
- parse_block(inter, doc, work->data + sublist, work->size - sublist);
- }
- else
- parse_block(inter, doc, work->data, work->size);
- } else {
- /* intermediate render of inline li */
- if (sublist && sublist < work->size) {
- parse_inline(inter, doc, work->data, sublist);
- parse_block(inter, doc, work->data + sublist, work->size - sublist);
- }
- else
- parse_inline(inter, doc, work->data, work->size);
- }
-
- /* render of li itself */
- if (doc->md.listitem)
- doc->md.listitem(ob, inter, *flags, &doc->data);
-
- popbuf(doc, BUFFER_SPAN);
- popbuf(doc, BUFFER_SPAN);
- return beg;
-}
-
-
-/* parse_list • parsing ordered or unordered list block */
-static size_t
-parse_list(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size, hoedown_list_flags flags)
-{
- hoedown_buffer *work = 0;
- size_t i = 0, j;
-
- work = newbuf(doc, BUFFER_BLOCK);
-
- while (i < size) {
- j = parse_listitem(work, doc, data + i, size - i, &flags);
- i += j;
-
- if (!j || (flags & HOEDOWN_LI_END))
- break;
- }
-
- if (doc->md.list)
- doc->md.list(ob, work, flags, &doc->data);
- popbuf(doc, BUFFER_BLOCK);
- return i;
-}
-
-/* parse_atxheader • parsing of atx-style headers */
-static size_t
-parse_atxheader(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size)
-{
- size_t level = 0;
- size_t i, end, skip;
-
- while (level < size && level < 6 && data[level] == '#')
- level++;
-
- for (i = level; i < size && data[i] == ' '; i++);
-
- for (end = i; end < size && data[end] != '\n'; end++);
- skip = end;
-
- while (end && data[end - 1] == '#')
- end--;
-
- while (end && data[end - 1] == ' ')
- end--;
-
- if (end > i) {
- hoedown_buffer *work = newbuf(doc, BUFFER_SPAN);
-
- parse_inline(work, doc, data + i, end - i);
-
- if (doc->md.header)
- doc->md.header(ob, work, (int)level, &doc->data);
-
- popbuf(doc, BUFFER_SPAN);
- }
-
- return skip;
-}
-
-/* parse_footnote_def • parse a single footnote definition */
-static void
-parse_footnote_def(hoedown_buffer *ob, hoedown_document *doc, unsigned int num, uint8_t *data, size_t size)
-{
- hoedown_buffer *work = 0;
- work = newbuf(doc, BUFFER_SPAN);
-
- parse_block(work, doc, data, size);
-
- if (doc->md.footnote_def)
- doc->md.footnote_def(ob, work, num, &doc->data);
- popbuf(doc, BUFFER_SPAN);
-}
-
-/* parse_footnote_list • render the contents of the footnotes */
-static void
-parse_footnote_list(hoedown_buffer *ob, hoedown_document *doc, struct footnote_list *footnotes)
-{
- hoedown_buffer *work = 0;
- struct footnote_item *item;
- struct footnote_ref *ref;
-
- if (footnotes->count == 0)
- return;
-
- work = newbuf(doc, BUFFER_BLOCK);
-
- item = footnotes->head;
- while (item) {
- ref = item->ref;
- parse_footnote_def(work, doc, ref->num, ref->contents->data, ref->contents->size);
- item = item->next;
- }
-
- if (doc->md.footnotes)
- doc->md.footnotes(ob, work, &doc->data);
- popbuf(doc, BUFFER_BLOCK);
-}
-
-/* htmlblock_is_end • check for end of HTML block : </tag>( *)\n */
-/* returns tag length on match, 0 otherwise */
-/* assumes data starts with "<" */
-static size_t
-htmlblock_is_end(
- const char *tag,
- size_t tag_len,
- hoedown_document *doc,
- uint8_t *data,
- size_t size)
-{
- size_t i = tag_len + 3, w;
-
- /* try to match the end tag */
- /* note: we're not considering tags like "</tag >" which are still valid */
- if (i > size ||
- data[1] != '/' ||
- strncasecmp((char *)data + 2, tag, tag_len) != 0 ||
- data[tag_len + 2] != '>')
- return 0;
-
- /* rest of the line must be empty */
- if ((w = is_empty(data + i, size - i)) == 0 && i < size)
- return 0;
-
- return i + w;
-}
-
-/* htmlblock_find_end • try to find HTML block ending tag */
-/* returns the length on match, 0 otherwise */
-static size_t
-htmlblock_find_end(
- const char *tag,
- size_t tag_len,
- hoedown_document *doc,
- uint8_t *data,
- size_t size)
-{
- size_t i = 0, w;
-
- while (1) {
- while (i < size && data[i] != '<') i++;
- if (i >= size) return 0;
-
- w = htmlblock_is_end(tag, tag_len, doc, data + i, size - i);
- if (w) return i + w;
- i++;
- }
-}
-
-/* htmlblock_find_end_strict • try to find end of HTML block in strict mode */
-/* (it must be an unindented line, and have a blank line afterwads) */
-/* returns the length on match, 0 otherwise */
-static size_t
-htmlblock_find_end_strict(
- const char *tag,
- size_t tag_len,
- hoedown_document *doc,
- uint8_t *data,
- size_t size)
-{
- size_t i = 0, mark;
-
- while (1) {
- mark = i;
- while (i < size && data[i] != '\n') i++;
- if (i < size) i++;
- if (i == mark) return 0;
-
- if (data[mark] == ' ' && mark > 0) continue;
- mark += htmlblock_find_end(tag, tag_len, doc, data + mark, i - mark);
- if (mark == i && (is_empty(data + i, size - i) || i >= size)) break;
- }
-
- return i;
-}
-
-/* parse_htmlblock • parsing of inline HTML block */
-static size_t
-parse_htmlblock(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size, int do_render)
-{
- hoedown_buffer work = { NULL, 0, 0, 0, NULL, NULL, NULL };
- size_t i, j = 0, tag_len, tag_end;
- const char *curtag = NULL;
-
- work.data = data;
-
- /* identification of the opening tag */
- if (size < 2 || data[0] != '<')
- return 0;
-
- i = 1;
- while (i < size && data[i] != '>' && data[i] != ' ')
- i++;
-
- if (i < size)
- curtag = hoedown_find_block_tag((char *)data + 1, (int)i - 1);
-
- /* handling of special cases */
- if (!curtag) {
-
- /* HTML comment, laxist form */
- if (size > 5 && data[1] == '!' && data[2] == '-' && data[3] == '-') {
- i = 5;
-
- while (i < size && !(data[i - 2] == '-' && data[i - 1] == '-' && data[i] == '>'))
- i++;
-
- i++;
-
- if (i < size)
- j = is_empty(data + i, size - i);
-
- if (j) {
- work.size = i + j;
- if (do_render && doc->md.blockhtml)
- doc->md.blockhtml(ob, &work, &doc->data);
- return work.size;
- }
- }
-
- /* HR, which is the only self-closing block tag considered */
- if (size > 4 && (data[1] == 'h' || data[1] == 'H') && (data[2] == 'r' || data[2] == 'R')) {
- i = 3;
- while (i < size && data[i] != '>')
- i++;
-
- if (i + 1 < size) {
- i++;
- j = is_empty(data + i, size - i);
- if (j) {
- work.size = i + j;
- if (do_render && doc->md.blockhtml)
- doc->md.blockhtml(ob, &work, &doc->data);
- return work.size;
- }
- }
- }
-
- /* no special case recognised */
- return 0;
- }
-
- /* looking for a matching closing tag in strict mode */
- tag_len = strlen(curtag);
- tag_end = htmlblock_find_end_strict(curtag, tag_len, doc, data, size);
-
- /* if not found, trying a second pass looking for indented match */
- /* but not if tag is "ins" or "del" (following original Markdown.pl) */
- if (!tag_end && strcmp(curtag, "ins") != 0 && strcmp(curtag, "del") != 0)
- tag_end = htmlblock_find_end(curtag, tag_len, doc, data, size);
-
- if (!tag_end)
- return 0;
-
- /* the end of the block has been found */
- work.size = tag_end;
- if (do_render && doc->md.blockhtml)
- doc->md.blockhtml(ob, &work, &doc->data);
-
- return tag_end;
-}
-
-static void
-parse_table_row(
- hoedown_buffer *ob,
- hoedown_document *doc,
- uint8_t *data,
- size_t size,
- size_t columns,
- hoedown_table_flags *col_data,
- hoedown_table_flags header_flag)
-{
- size_t i = 0, col, len;
- hoedown_buffer *row_work = 0;
-
- if (!doc->md.table_cell || !doc->md.table_row)
- return;
-
- row_work = newbuf(doc, BUFFER_SPAN);
-
- if (i < size && data[i] == '|')
- i++;
-
- for (col = 0; col < columns && i < size; ++col) {
- size_t cell_start, cell_end;
- hoedown_buffer *cell_work;
-
- cell_work = newbuf(doc, BUFFER_SPAN);
-
- while (i < size && _isspace(data[i]))
- i++;
-
- cell_start = i;
-
- len = find_emph_char(data + i, size - i, '|');
- i += len ? len : size - i;
-
- cell_end = i - 1;
-
- while (cell_end > cell_start && _isspace(data[cell_end]))
- cell_end--;
-
- parse_inline(cell_work, doc, data + cell_start, 1 + cell_end - cell_start);
- doc->md.table_cell(row_work, cell_work, col_data[col] | header_flag, &doc->data);
-
- popbuf(doc, BUFFER_SPAN);
- i++;
- }
-
- for (; col < columns; ++col) {
- hoedown_buffer empty_cell = { 0, 0, 0, 0, NULL, NULL, NULL };
- doc->md.table_cell(row_work, &empty_cell, col_data[col] | header_flag, &doc->data);
- }
-
- doc->md.table_row(ob, row_work, &doc->data);
-
- popbuf(doc, BUFFER_SPAN);
-}
-
-static size_t
-parse_table_header(
- hoedown_buffer *ob,
- hoedown_document *doc,
- uint8_t *data,
- size_t size,
- size_t *columns,
- hoedown_table_flags **column_data)
-{
- int pipes;
- size_t i = 0, col, header_end, under_end;
-
- pipes = 0;
- while (i < size && data[i] != '\n')
- if (data[i++] == '|')
- pipes++;
-
- if (i == size || pipes == 0)
- return 0;
-
- header_end = i;
-
- while (header_end > 0 && _isspace(data[header_end - 1]))
- header_end--;
-
- if (data[0] == '|')
- pipes--;
-
- if (header_end && data[header_end - 1] == '|')
- pipes--;
-
- if (pipes < 0)
- return 0;
-
- *columns = pipes + 1;
- *column_data = hoedown_calloc(*columns, sizeof(hoedown_table_flags));
-
- /* Parse the header underline */
- i++;
- if (i < size && data[i] == '|')
- i++;
-
- under_end = i;
- while (under_end < size && data[under_end] != '\n')
- under_end++;
-
- for (col = 0; col < *columns && i < under_end; ++col) {
- size_t dashes = 0;
-
- while (i < under_end && data[i] == ' ')
- i++;
-
- if (data[i] == ':') {
- i++; (*column_data)[col] |= HOEDOWN_TABLE_ALIGN_LEFT;
- dashes++;
- }
-
- while (i < under_end && data[i] == '-') {
- i++; dashes++;
- }
-
- if (i < under_end && data[i] == ':') {
- i++; (*column_data)[col] |= HOEDOWN_TABLE_ALIGN_RIGHT;
- dashes++;
- }
-
- while (i < under_end && data[i] == ' ')
- i++;
-
- if (i < under_end && data[i] != '|' && data[i] != '+')
- break;
-
- if (dashes < 3)
- break;
-
- i++;
- }
-
- if (col < *columns)
- return 0;
-
- parse_table_row(
- ob, doc, data,
- header_end,
- *columns,
- *column_data,
- HOEDOWN_TABLE_HEADER
- );
-
- return under_end + 1;
-}
-
-static size_t
-parse_table(
- hoedown_buffer *ob,
- hoedown_document *doc,
- uint8_t *data,
- size_t size)
-{
- size_t i;
-
- hoedown_buffer *work = 0;
- hoedown_buffer *header_work = 0;
- hoedown_buffer *body_work = 0;
-
- size_t columns;
- hoedown_table_flags *col_data = NULL;
-
- work = newbuf(doc, BUFFER_BLOCK);
- header_work = newbuf(doc, BUFFER_SPAN);
- body_work = newbuf(doc, BUFFER_BLOCK);
-
- i = parse_table_header(header_work, doc, data, size, &columns, &col_data);
- if (i > 0) {
-
- while (i < size) {
- size_t row_start;
- int pipes = 0;
-
- row_start = i;
-
- while (i < size && data[i] != '\n')
- if (data[i++] == '|')
- pipes++;
-
- if (pipes == 0 || i == size) {
- i = row_start;
- break;
- }
-
- parse_table_row(
- body_work,
- doc,
- data + row_start,
- i - row_start,
- columns,
- col_data, 0
- );
-
- i++;
- }
-
- if (doc->md.table_header)
- doc->md.table_header(work, header_work, &doc->data);
-
- if (doc->md.table_body)
- doc->md.table_body(work, body_work, &doc->data);
-
- if (doc->md.table)
- doc->md.table(ob, work, &doc->data);
- }
-
- free(col_data);
- popbuf(doc, BUFFER_SPAN);
- popbuf(doc, BUFFER_BLOCK);
- popbuf(doc, BUFFER_BLOCK);
- return i;
-}
-
-/* parse_block • parsing of one block, returning next uint8_t to parse */
-static void
-parse_block(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size)
-{
- size_t beg, end, i;
- uint8_t *txt_data;
- beg = 0;
-
- if (doc->work_bufs[BUFFER_SPAN].size +
- doc->work_bufs[BUFFER_BLOCK].size > doc->max_nesting)
- return;
-
- while (beg < size) {
- txt_data = data + beg;
- end = size - beg;
-
- if (is_atxheader(doc, txt_data, end))
- beg += parse_atxheader(ob, doc, txt_data, end);
-
- else if (data[beg] == '<' && doc->md.blockhtml &&
- (i = parse_htmlblock(ob, doc, txt_data, end, 1)) != 0)
- beg += i;
-
- else if ((i = is_empty(txt_data, end)) != 0)
- beg += i;
-
- else if (is_hrule(txt_data, end)) {
- if (doc->md.hrule)
- doc->md.hrule(ob, &doc->data);
-
- while (beg < size && data[beg] != '\n')
- beg++;
-
- beg++;
- }
-
- else if ((doc->ext_flags & HOEDOWN_EXT_FENCED_CODE) != 0 &&
- (i = parse_fencedcode(ob, doc, txt_data, end)) != 0)
- beg += i;
-
- else if ((doc->ext_flags & HOEDOWN_EXT_TABLES) != 0 &&
- (i = parse_table(ob, doc, txt_data, end)) != 0)
- beg += i;
-
- else if (prefix_quote(txt_data, end))
- beg += parse_blockquote(ob, doc, txt_data, end);
-
- else if (!(doc->ext_flags & HOEDOWN_EXT_DISABLE_INDENTED_CODE) && prefix_code(txt_data, end))
- beg += parse_blockcode(ob, doc, txt_data, end);
-
- else if (prefix_uli(txt_data, end))
- beg += parse_list(ob, doc, txt_data, end, 0);
-
- else if (prefix_oli(txt_data, end))
- beg += parse_list(ob, doc, txt_data, end, HOEDOWN_LIST_ORDERED);
-
- else
- beg += parse_paragraph(ob, doc, txt_data, end);
- }
-}
-
-
-
-/*********************
- * REFERENCE PARSING *
- *********************/
-
-/* is_footnote • returns whether a line is a footnote definition or not */
-static int
-is_footnote(const uint8_t *data, size_t beg, size_t end, size_t *last, struct footnote_list *list)
-{
- size_t i = 0;
- hoedown_buffer *contents = 0;
- size_t ind = 0;
- int in_empty = 0;
- size_t start = 0;
-
- size_t id_offset, id_end;
-
- /* up to 3 optional leading spaces */
- if (beg + 3 >= end) return 0;
- if (data[beg] == ' ') { i = 1;
- if (data[beg + 1] == ' ') { i = 2;
- if (data[beg + 2] == ' ') { i = 3;
- if (data[beg + 3] == ' ') return 0; } } }
- i += beg;
-
- /* id part: caret followed by anything between brackets */
- if (data[i] != '[') return 0;
- i++;
- if (i >= end || data[i] != '^') return 0;
- i++;
- id_offset = i;
- while (i < end && data[i] != '\n' && data[i] != '\r' && data[i] != ']')
- i++;
- if (i >= end || data[i] != ']') return 0;
- id_end = i;
-
- /* spacer: colon (space | tab)* newline? (space | tab)* */
- i++;
- if (i >= end || data[i] != ':') return 0;
- i++;
-
- /* getting content buffer */
- contents = hoedown_buffer_new(64);
-
- start = i;
-
- /* process lines similar to a list item */
- while (i < end) {
- while (i < end && data[i] != '\n' && data[i] != '\r') i++;
-
- /* process an empty line */
- if (is_empty(data + start, i - start)) {
- in_empty = 1;
- if (i < end && (data[i] == '\n' || data[i] == '\r')) {
- i++;
- if (i < end && data[i] == '\n' && data[i - 1] == '\r') i++;
- }
- start = i;
- continue;
- }
-
- /* calculating the indentation */
- ind = 0;
- while (ind < 4 && start + ind < end && data[start + ind] == ' ')
- ind++;
-
- /* joining only indented stuff after empty lines;
- * note that now we only require 1 space of indentation
- * to continue, just like lists */
- if (ind == 0) {
- if (start == id_end + 2 && data[start] == '\t') {}
- else break;
- }
- else if (in_empty) {
- hoedown_buffer_putc(contents, '\n');
- }
-
- in_empty = 0;
-
- /* adding the line into the content buffer */
- hoedown_buffer_put(contents, data + start + ind, i - start - ind);
- /* add carriage return */
- if (i < end) {
- hoedown_buffer_putc(contents, '\n');
- if (i < end && (data[i] == '\n' || data[i] == '\r')) {
- i++;
- if (i < end && data[i] == '\n' && data[i - 1] == '\r') i++;
- }
- }
- start = i;
- }
-
- if (last)
- *last = start;
-
- if (list) {
- struct footnote_ref *ref;
- ref = create_footnote_ref(list, data + id_offset, id_end - id_offset);
- if (!ref)
- return 0;
- if (!add_footnote_ref(list, ref)) {
- free_footnote_ref(ref);
- return 0;
- }
- ref->contents = contents;
- }
-
- return 1;
-}
-
-/* is_ref • returns whether a line is a reference or not */
-static int
-is_ref(const uint8_t *data, size_t beg, size_t end, size_t *last, struct link_ref **refs)
-{
-/* int n; */
- size_t i = 0;
- size_t id_offset, id_end;
- size_t link_offset, link_end;
- size_t title_offset, title_end;
- size_t line_end;
-
- /* up to 3 optional leading spaces */
- if (beg + 3 >= end) return 0;
- if (data[beg] == ' ') { i = 1;
- if (data[beg + 1] == ' ') { i = 2;
- if (data[beg + 2] == ' ') { i = 3;
- if (data[beg + 3] == ' ') return 0; } } }
- i += beg;
-
- /* id part: anything but a newline between brackets */
- if (data[i] != '[') return 0;
- i++;
- id_offset = i;
- while (i < end && data[i] != '\n' && data[i] != '\r' && data[i] != ']')
- i++;
- if (i >= end || data[i] != ']') return 0;
- id_end = i;
-
- /* spacer: colon (space | tab)* newline? (space | tab)* */
- i++;
- if (i >= end || data[i] != ':') return 0;
- i++;
- while (i < end && data[i] == ' ') i++;
- if (i < end && (data[i] == '\n' || data[i] == '\r')) {
- i++;
- if (i < end && data[i] == '\r' && data[i - 1] == '\n') i++; }
- while (i < end && data[i] == ' ') i++;
- if (i >= end) return 0;
-
- /* link: spacing-free sequence, optionally between angle brackets */
- if (data[i] == '<')
- i++;
-
- link_offset = i;
-
- while (i < end && data[i] != ' ' && data[i] != '\n' && data[i] != '\r')
- i++;
-
- if (data[i - 1] == '>') link_end = i - 1;
- else link_end = i;
-
- /* optional spacer: (space | tab)* (newline | '\'' | '"' | '(' ) */
- while (i < end && data[i] == ' ') i++;
- if (i < end && data[i] != '\n' && data[i] != '\r'
- && data[i] != '\'' && data[i] != '"' && data[i] != '(')
- return 0;
- line_end = 0;
- /* computing end-of-line */
- if (i >= end || data[i] == '\r' || data[i] == '\n') line_end = i;
- if (i + 1 < end && data[i] == '\n' && data[i + 1] == '\r')
- line_end = i + 1;
-
- /* optional (space|tab)* spacer after a newline */
- if (line_end) {
- i = line_end + 1;
- while (i < end && data[i] == ' ') i++; }
-
- /* optional title: any non-newline sequence enclosed in '"()
- alone on its line */
- title_offset = title_end = 0;
- if (i + 1 < end
- && (data[i] == '\'' || data[i] == '"' || data[i] == '(')) {
- i++;
- title_offset = i;
- /* looking for EOL */
- while (i < end && data[i] != '\n' && data[i] != '\r') i++;
- if (i + 1 < end && data[i] == '\n' && data[i + 1] == '\r')
- title_end = i + 1;
- else title_end = i;
- /* stepping back */
- i -= 1;
- while (i > title_offset && data[i] == ' ')
- i -= 1;
- if (i > title_offset
- && (data[i] == '\'' || data[i] == '"' || data[i] == ')')) {
- line_end = title_end;
- title_end = i; } }
-
- if (!line_end || link_end == link_offset)
- return 0; /* garbage after the link empty link */
-
- /* a valid ref has been found, filling-in return structures */
- if (last)
- *last = line_end;
-
- if (refs) {
- struct link_ref *ref;
-
- ref = add_link_ref(refs, data + id_offset, id_end - id_offset);
- if (!ref)
- return 0;
-
- ref->link = hoedown_buffer_new(link_end - link_offset);
- hoedown_buffer_put(ref->link, data + link_offset, link_end - link_offset);
-
- if (title_end > title_offset) {
- ref->title = hoedown_buffer_new(title_end - title_offset);
- hoedown_buffer_put(ref->title, data + title_offset, title_end - title_offset);
- }
- }
-
- return 1;
-}
-
-static void expand_tabs(hoedown_buffer *ob, const uint8_t *line, size_t size)
-{
- /* This code makes two assumptions:
- * - Input is valid UTF-8. (Any byte with top two bits 10 is skipped,
- * whether or not it is a valid UTF-8 continuation byte.)
- * - Input contains no combining characters. (Combining characters
- * should be skipped but are not.)
- */
- size_t i = 0, tab = 0;
-
- while (i < size) {
- size_t org = i;
-
- while (i < size && line[i] != '\t') {
- /* ignore UTF-8 continuation bytes */
- if ((line[i] & 0xc0) != 0x80)
- tab++;
- i++;
- }
-
- if (i > org)
- hoedown_buffer_put(ob, line + org, i - org);
-
- if (i >= size)
- break;
-
- do {
- hoedown_buffer_putc(ob, ' '); tab++;
- } while (tab % 4);
-
- i++;
- }
-}
-
-/**********************
- * EXPORTED FUNCTIONS *
- **********************/
-
-hoedown_document *
-hoedown_document_new(
- const hoedown_renderer *renderer,
- hoedown_extensions extensions,
- size_t max_nesting)
-{
- hoedown_document *doc = NULL;
-
- assert(max_nesting > 0 && renderer);
-
- doc = hoedown_malloc(sizeof(hoedown_document));
- memcpy(&doc->md, renderer, sizeof(hoedown_renderer));
-
- doc->data.opaque = renderer->opaque;
-
- hoedown_stack_init(&doc->work_bufs[BUFFER_BLOCK], 4);
- hoedown_stack_init(&doc->work_bufs[BUFFER_SPAN], 8);
-
- memset(doc->active_char, 0x0, 256);
-
- if (extensions & HOEDOWN_EXT_UNDERLINE && doc->md.underline) {
- doc->active_char['_'] = MD_CHAR_EMPHASIS;
- }
-
- if (doc->md.emphasis || doc->md.double_emphasis || doc->md.triple_emphasis) {
- doc->active_char['*'] = MD_CHAR_EMPHASIS;
- doc->active_char['_'] = MD_CHAR_EMPHASIS;
- if (extensions & HOEDOWN_EXT_STRIKETHROUGH)
- doc->active_char['~'] = MD_CHAR_EMPHASIS;
- if (extensions & HOEDOWN_EXT_HIGHLIGHT)
- doc->active_char['='] = MD_CHAR_EMPHASIS;
- }
-
- if (doc->md.codespan)
- doc->active_char['`'] = MD_CHAR_CODESPAN;
-
- if (doc->md.linebreak)
- doc->active_char['\n'] = MD_CHAR_LINEBREAK;
-
- if (doc->md.image || doc->md.link || doc->md.footnotes || doc->md.footnote_ref)
- doc->active_char['['] = MD_CHAR_LINK;
-
- doc->active_char['<'] = MD_CHAR_LANGLE;
- doc->active_char['\\'] = MD_CHAR_ESCAPE;
- doc->active_char['&'] = MD_CHAR_ENTITY;
-
- if (extensions & HOEDOWN_EXT_AUTOLINK) {
- doc->active_char[':'] = MD_CHAR_AUTOLINK_URL;
- doc->active_char['@'] = MD_CHAR_AUTOLINK_EMAIL;
- doc->active_char['w'] = MD_CHAR_AUTOLINK_WWW;
- }
-
- if (extensions & HOEDOWN_EXT_SUPERSCRIPT)
- doc->active_char['^'] = MD_CHAR_SUPERSCRIPT;
-
- if (extensions & HOEDOWN_EXT_QUOTE)
- doc->active_char['"'] = MD_CHAR_QUOTE;
-
- if (extensions & HOEDOWN_EXT_MATH)
- doc->active_char['$'] = MD_CHAR_MATH;
-
- /* Extension data */
- doc->ext_flags = extensions;
- doc->max_nesting = max_nesting;
- doc->in_link_body = 0;
-
- return doc;
-}
-
-void
-hoedown_document_render(hoedown_document *doc, hoedown_buffer *ob, const uint8_t *data, size_t size)
-{
- static const uint8_t UTF8_BOM[] = {0xEF, 0xBB, 0xBF};
-
- hoedown_buffer *text;
- size_t beg, end;
-
- int footnotes_enabled;
-
- text = hoedown_buffer_new(64);
-
- /* Preallocate enough space for our buffer to avoid expanding while copying */
- hoedown_buffer_grow(text, size);
-
- /* reset the references table */
- memset(&doc->refs, 0x0, REF_TABLE_SIZE * sizeof(void *));
-
- footnotes_enabled = doc->ext_flags & HOEDOWN_EXT_FOOTNOTES;
-
- /* reset the footnotes lists */
- if (footnotes_enabled) {
- memset(&doc->footnotes_found, 0x0, sizeof(doc->footnotes_found));
- memset(&doc->footnotes_used, 0x0, sizeof(doc->footnotes_used));
- }
-
- /* first pass: looking for references, copying everything else */
- beg = 0;
-
- /* Skip a possible UTF-8 BOM, even though the Unicode standard
- * discourages having these in UTF-8 documents */
- if (size >= 3 && memcmp(data, UTF8_BOM, 3) == 0)
- beg += 3;
-
- while (beg < size) /* iterating over lines */
- if (footnotes_enabled && is_footnote(data, beg, size, &end, &doc->footnotes_found))
- beg = end;
- else if (is_ref(data, beg, size, &end, doc->refs))
- beg = end;
- else { /* skipping to the next line */
- end = beg;
- while (end < size && data[end] != '\n' && data[end] != '\r')
- end++;
-
- /* adding the line body if present */
- if (end > beg)
- expand_tabs(text, data + beg, end - beg);
-
- while (end < size && (data[end] == '\n' || data[end] == '\r')) {
- /* add one \n per newline */
- if (data[end] == '\n' || (end + 1 < size && data[end + 1] != '\n'))
- hoedown_buffer_putc(text, '\n');
- end++;
- }
-
- beg = end;
- }
-
- /* pre-grow the output buffer to minimize allocations */
- hoedown_buffer_grow(ob, text->size + (text->size >> 1));
-
- /* second pass: actual rendering */
- if (doc->md.doc_header)
- doc->md.doc_header(ob, 0, &doc->data);
-
- if (text->size) {
- /* adding a final newline if not already present */
- if (text->data[text->size - 1] != '\n' && text->data[text->size - 1] != '\r')
- hoedown_buffer_putc(text, '\n');
-
- parse_block(ob, doc, text->data, text->size);
- }
-
- /* footnotes */
- if (footnotes_enabled)
- parse_footnote_list(ob, doc, &doc->footnotes_used);
-
- if (doc->md.doc_footer)
- doc->md.doc_footer(ob, 0, &doc->data);
-
- /* clean-up */
- hoedown_buffer_free(text);
- free_link_refs(doc->refs);
- if (footnotes_enabled) {
- free_footnote_list(&doc->footnotes_found, 1);
- free_footnote_list(&doc->footnotes_used, 0);
- }
-
- assert(doc->work_bufs[BUFFER_SPAN].size == 0);
- assert(doc->work_bufs[BUFFER_BLOCK].size == 0);
-}
-
-void
-hoedown_document_render_inline(hoedown_document *doc, hoedown_buffer *ob, const uint8_t *data, size_t size)
-{
- size_t i = 0, mark;
- hoedown_buffer *text = hoedown_buffer_new(64);
-
- /* reset the references table */
- memset(&doc->refs, 0x0, REF_TABLE_SIZE * sizeof(void *));
-
- /* first pass: expand tabs and process newlines */
- hoedown_buffer_grow(text, size);
- while (1) {
- mark = i;
- while (i < size && data[i] != '\n' && data[i] != '\r')
- i++;
-
- expand_tabs(text, data + mark, i - mark);
-
- if (i >= size)
- break;
-
- while (i < size && (data[i] == '\n' || data[i] == '\r')) {
- /* add one \n per newline */
- if (data[i] == '\n' || (i + 1 < size && data[i + 1] != '\n'))
- hoedown_buffer_putc(text, '\n');
- i++;
- }
- }
-
- /* second pass: actual rendering */
- hoedown_buffer_grow(ob, text->size + (text->size >> 1));
-
- if (doc->md.doc_header)
- doc->md.doc_header(ob, 1, &doc->data);
-
- parse_inline(ob, doc, text->data, text->size);
-
- if (doc->md.doc_footer)
- doc->md.doc_footer(ob, 1, &doc->data);
-
- /* clean-up */
- hoedown_buffer_free(text);
-
- assert(doc->work_bufs[BUFFER_SPAN].size == 0);
- assert(doc->work_bufs[BUFFER_BLOCK].size == 0);
-}
-
-void
-hoedown_document_free(hoedown_document *doc)
-{
- size_t i;
-
- for (i = 0; i < (size_t)doc->work_bufs[BUFFER_SPAN].asize; ++i)
- hoedown_buffer_free(doc->work_bufs[BUFFER_SPAN].item[i]);
-
- for (i = 0; i < (size_t)doc->work_bufs[BUFFER_BLOCK].asize; ++i)
- hoedown_buffer_free(doc->work_bufs[BUFFER_BLOCK].item[i]);
-
- hoedown_stack_uninit(&doc->work_bufs[BUFFER_SPAN]);
- hoedown_stack_uninit(&doc->work_bufs[BUFFER_BLOCK]);
-
- free(doc);
-}
diff --git a/libraries/hoedown/src/escape.c b/libraries/hoedown/src/escape.c
deleted file mode 100644
index ce25dd54..00000000
--- a/libraries/hoedown/src/escape.c
+++ /dev/null
@@ -1,188 +0,0 @@
-#include "hoedown/escape.h"
-
-#include <assert.h>
-#include <stdio.h>
-#include <string.h>
-
-
-#define likely(x) __builtin_expect((x),1)
-#define unlikely(x) __builtin_expect((x),0)
-
-
-/*
- * The following characters will not be escaped:
- *
- * -_.+!*'(),%#@?=;:/,+&$ alphanum
- *
- * Note that this character set is the addition of:
- *
- * - The characters which are safe to be in an URL
- * - The characters which are *not* safe to be in
- * an URL because they are RESERVED characters.
- *
- * We assume (lazily) that any RESERVED char that
- * appears inside an URL is actually meant to
- * have its native function (i.e. as an URL
- * component/separator) and hence needs no escaping.
- *
- * There are two exceptions: the chacters & (amp)
- * and ' (single quote) do not appear in the table.
- * They are meant to appear in the URL as components,
- * yet they require special HTML-entity escaping
- * to generate valid HTML markup.
- *
- * All other characters will be escaped to %XX.
- *
- */
-static const uint8_t HREF_SAFE[UINT8_MAX+1] = {
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1,
- 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1,
- 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
- 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1,
- 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
- 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
-};
-
-void
-hoedown_escape_href(hoedown_buffer *ob, const uint8_t *data, size_t size)
-{
- static const char hex_chars[] = "0123456789ABCDEF";
- size_t i = 0, mark;
- char hex_str[3];
-
- hex_str[0] = '%';
-
- while (i < size) {
- mark = i;
- while (i < size && HREF_SAFE[data[i]]) i++;
-
- /* Optimization for cases where there's nothing to escape */
- if (mark == 0 && i >= size) {
- hoedown_buffer_put(ob, data, size);
- return;
- }
-
- if (likely(i > mark)) {
- hoedown_buffer_put(ob, data + mark, i - mark);
- }
-
- /* escaping */
- if (i >= size)
- break;
-
- switch (data[i]) {
- /* amp appears all the time in URLs, but needs
- * HTML-entity escaping to be inside an href */
- case '&':
- HOEDOWN_BUFPUTSL(ob, "&amp;");
- break;
-
- /* the single quote is a valid URL character
- * according to the standard; it needs HTML
- * entity escaping too */
- case '\'':
- HOEDOWN_BUFPUTSL(ob, "&#x27;");
- break;
-
- /* the space can be escaped to %20 or a plus
- * sign. we're going with the generic escape
- * for now. the plus thing is more commonly seen
- * when building GET strings */
-#if 0
- case ' ':
- hoedown_buffer_putc(ob, '+');
- break;
-#endif
-
- /* every other character goes with a %XX escaping */
- default:
- hex_str[1] = hex_chars[(data[i] >> 4) & 0xF];
- hex_str[2] = hex_chars[data[i] & 0xF];
- hoedown_buffer_put(ob, (uint8_t *)hex_str, 3);
- }
-
- i++;
- }
-}
-
-
-/**
- * According to the OWASP rules:
- *
- * & --> &amp;
- * < --> &lt;
- * > --> &gt;
- * " --> &quot;
- * ' --> &#x27; &apos; is not recommended
- * / --> &#x2F; forward slash is included as it helps end an HTML entity
- *
- */
-static const uint8_t HTML_ESCAPE_TABLE[UINT8_MAX+1] = {
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 1, 0, 0, 0, 2, 3, 0, 0, 0, 0, 0, 0, 0, 4,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 6, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
-};
-
-static const char *HTML_ESCAPES[] = {
- "",
- "&quot;",
- "&amp;",
- "&#39;",
- "&#47;",
- "&lt;",
- "&gt;"
-};
-
-void
-hoedown_escape_html(hoedown_buffer *ob, const uint8_t *data, size_t size, int secure)
-{
- size_t i = 0, mark;
-
- while (1) {
- mark = i;
- while (i < size && HTML_ESCAPE_TABLE[data[i]] == 0) i++;
-
- /* Optimization for cases where there's nothing to escape */
- if (mark == 0 && i >= size) {
- hoedown_buffer_put(ob, data, size);
- return;
- }
-
- if (likely(i > mark))
- hoedown_buffer_put(ob, data + mark, i - mark);
-
- if (i >= size) break;
-
- /* The forward slash is only escaped in secure mode */
- if (!secure && data[i] == '/') {
- hoedown_buffer_putc(ob, '/');
- } else {
- hoedown_buffer_puts(ob, HTML_ESCAPES[HTML_ESCAPE_TABLE[data[i]]]);
- }
-
- i++;
- }
-}
diff --git a/libraries/hoedown/src/html.c b/libraries/hoedown/src/html.c
deleted file mode 100644
index 8bf3358e..00000000
--- a/libraries/hoedown/src/html.c
+++ /dev/null
@@ -1,754 +0,0 @@
-#include "hoedown/html.h"
-
-#include <string.h>
-#include <stdlib.h>
-#include <stdio.h>
-#include <ctype.h>
-
-#include "hoedown/escape.h"
-
-#define USE_XHTML(opt) (opt->flags & HOEDOWN_HTML_USE_XHTML)
-
-hoedown_html_tag
-hoedown_html_is_tag(const uint8_t *data, size_t size, const char *tagname)
-{
- size_t i;
- int closed = 0;
-
- if (size < 3 || data[0] != '<')
- return HOEDOWN_HTML_TAG_NONE;
-
- i = 1;
-
- if (data[i] == '/') {
- closed = 1;
- i++;
- }
-
- for (; i < size; ++i, ++tagname) {
- if (*tagname == 0)
- break;
-
- if (data[i] != *tagname)
- return HOEDOWN_HTML_TAG_NONE;
- }
-
- if (i == size)
- return HOEDOWN_HTML_TAG_NONE;
-
- if (isspace(data[i]) || data[i] == '>')
- return closed ? HOEDOWN_HTML_TAG_CLOSE : HOEDOWN_HTML_TAG_OPEN;
-
- return HOEDOWN_HTML_TAG_NONE;
-}
-
-static void escape_html(hoedown_buffer *ob, const uint8_t *source, size_t length)
-{
- hoedown_escape_html(ob, source, length, 0);
-}
-
-static void escape_href(hoedown_buffer *ob, const uint8_t *source, size_t length)
-{
- hoedown_escape_href(ob, source, length);
-}
-
-/********************
- * GENERIC RENDERER *
- ********************/
-static int
-rndr_autolink(hoedown_buffer *ob, const hoedown_buffer *link, hoedown_autolink_type type, const hoedown_renderer_data *data)
-{
- hoedown_html_renderer_state *state = data->opaque;
-
- if (!link || !link->size)
- return 0;
-
- HOEDOWN_BUFPUTSL(ob, "<a href=\"");
- if (type == HOEDOWN_AUTOLINK_EMAIL)
- HOEDOWN_BUFPUTSL(ob, "mailto:");
- escape_href(ob, link->data, link->size);
-
- if (state->link_attributes) {
- hoedown_buffer_putc(ob, '\"');
- state->link_attributes(ob, link, data);
- hoedown_buffer_putc(ob, '>');
- } else {
- HOEDOWN_BUFPUTSL(ob, "\">");
- }
-
- /*
- * Pretty printing: if we get an email address as
- * an actual URI, e.g. `mailto:foo@bar.com`, we don't
- * want to print the `mailto:` prefix
- */
- if (hoedown_buffer_prefix(link, "mailto:") == 0) {
- escape_html(ob, link->data + 7, link->size - 7);
- } else {
- escape_html(ob, link->data, link->size);
- }
-
- HOEDOWN_BUFPUTSL(ob, "</a>");
-
- return 1;
-}
-
-static void
-rndr_blockcode(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_buffer *lang, const hoedown_renderer_data *data)
-{
- if (ob->size) hoedown_buffer_putc(ob, '\n');
-
- if (lang) {
- HOEDOWN_BUFPUTSL(ob, "<pre><code class=\"language-");
- escape_html(ob, lang->data, lang->size);
- HOEDOWN_BUFPUTSL(ob, "\">");
- } else {
- HOEDOWN_BUFPUTSL(ob, "<pre><code>");
- }
-
- if (text)
- escape_html(ob, text->data, text->size);
-
- HOEDOWN_BUFPUTSL(ob, "</code></pre>\n");
-}
-
-static void
-rndr_blockquote(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)
-{
- if (ob->size) hoedown_buffer_putc(ob, '\n');
- HOEDOWN_BUFPUTSL(ob, "<blockquote>\n");
- if (content) hoedown_buffer_put(ob, content->data, content->size);
- HOEDOWN_BUFPUTSL(ob, "</blockquote>\n");
-}
-
-static int
-rndr_codespan(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data)
-{
- HOEDOWN_BUFPUTSL(ob, "<code>");
- if (text) escape_html(ob, text->data, text->size);
- HOEDOWN_BUFPUTSL(ob, "</code>");
- return 1;
-}
-
-static int
-rndr_strikethrough(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)
-{
- if (!content || !content->size)
- return 0;
-
- HOEDOWN_BUFPUTSL(ob, "<del>");
- hoedown_buffer_put(ob, content->data, content->size);
- HOEDOWN_BUFPUTSL(ob, "</del>");
- return 1;
-}
-
-static int
-rndr_double_emphasis(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)
-{
- if (!content || !content->size)
- return 0;
-
- HOEDOWN_BUFPUTSL(ob, "<strong>");
- hoedown_buffer_put(ob, content->data, content->size);
- HOEDOWN_BUFPUTSL(ob, "</strong>");
-
- return 1;
-}
-
-static int
-rndr_emphasis(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)
-{
- if (!content || !content->size) return 0;
- HOEDOWN_BUFPUTSL(ob, "<em>");
- if (content) hoedown_buffer_put(ob, content->data, content->size);
- HOEDOWN_BUFPUTSL(ob, "</em>");
- return 1;
-}
-
-static int
-rndr_underline(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)
-{
- if (!content || !content->size)
- return 0;
-
- HOEDOWN_BUFPUTSL(ob, "<u>");
- hoedown_buffer_put(ob, content->data, content->size);
- HOEDOWN_BUFPUTSL(ob, "</u>");
-
- return 1;
-}
-
-static int
-rndr_highlight(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)
-{
- if (!content || !content->size)
- return 0;
-
- HOEDOWN_BUFPUTSL(ob, "<mark>");
- hoedown_buffer_put(ob, content->data, content->size);
- HOEDOWN_BUFPUTSL(ob, "</mark>");
-
- return 1;
-}
-
-static int
-rndr_quote(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)
-{
- if (!content || !content->size)
- return 0;
-
- HOEDOWN_BUFPUTSL(ob, "<q>");
- hoedown_buffer_put(ob, content->data, content->size);
- HOEDOWN_BUFPUTSL(ob, "</q>");
-
- return 1;
-}
-
-static int
-rndr_linebreak(hoedown_buffer *ob, const hoedown_renderer_data *data)
-{
- hoedown_html_renderer_state *state = data->opaque;
- hoedown_buffer_puts(ob, USE_XHTML(state) ? "<br/>\n" : "<br>\n");
- return 1;
-}
-
-static void
-rndr_header(hoedown_buffer *ob, const hoedown_buffer *content, int level, const hoedown_renderer_data *data)
-{
- hoedown_html_renderer_state *state = data->opaque;
-
- if (ob->size)
- hoedown_buffer_putc(ob, '\n');
-
- if (level <= state->toc_data.nesting_level)
- hoedown_buffer_printf(ob, "<h%d id=\"toc_%d\">", level, state->toc_data.header_count++);
- else
- hoedown_buffer_printf(ob, "<h%d>", level);
-
- if (content) hoedown_buffer_put(ob, content->data, content->size);
- hoedown_buffer_printf(ob, "</h%d>\n", level);
-}
-
-static int
-rndr_link(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_buffer *link, const hoedown_buffer *title, const hoedown_renderer_data *data)
-{
- hoedown_html_renderer_state *state = data->opaque;
-
- HOEDOWN_BUFPUTSL(ob, "<a href=\"");
-
- if (link && link->size)
- escape_href(ob, link->data, link->size);
-
- if (title && title->size) {
- HOEDOWN_BUFPUTSL(ob, "\" title=\"");
- escape_html(ob, title->data, title->size);
- }
-
- if (state->link_attributes) {
- hoedown_buffer_putc(ob, '\"');
- state->link_attributes(ob, link, data);
- hoedown_buffer_putc(ob, '>');
- } else {
- HOEDOWN_BUFPUTSL(ob, "\">");
- }
-
- if (content && content->size) hoedown_buffer_put(ob, content->data, content->size);
- HOEDOWN_BUFPUTSL(ob, "</a>");
- return 1;
-}
-
-static void
-rndr_list(hoedown_buffer *ob, const hoedown_buffer *content, hoedown_list_flags flags, const hoedown_renderer_data *data)
-{
- if (ob->size) hoedown_buffer_putc(ob, '\n');
- hoedown_buffer_put(ob, (const uint8_t *)(flags & HOEDOWN_LIST_ORDERED ? "<ol>\n" : "<ul>\n"), 5);
- if (content) hoedown_buffer_put(ob, content->data, content->size);
- hoedown_buffer_put(ob, (const uint8_t *)(flags & HOEDOWN_LIST_ORDERED ? "</ol>\n" : "</ul>\n"), 6);
-}
-
-static void
-rndr_listitem(hoedown_buffer *ob, const hoedown_buffer *content, hoedown_list_flags flags, const hoedown_renderer_data *data)
-{
- HOEDOWN_BUFPUTSL(ob, "<li>");
- if (content) {
- size_t size = content->size;
- while (size && content->data[size - 1] == '\n')
- size--;
-
- hoedown_buffer_put(ob, content->data, size);
- }
- HOEDOWN_BUFPUTSL(ob, "</li>\n");
-}
-
-static void
-rndr_paragraph(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)
-{
- hoedown_html_renderer_state *state = data->opaque;
- size_t i = 0;
-
- if (ob->size) hoedown_buffer_putc(ob, '\n');
-
- if (!content || !content->size)
- return;
-
- while (i < content->size && isspace(content->data[i])) i++;
-
- if (i == content->size)
- return;
-
- HOEDOWN_BUFPUTSL(ob, "<p>");
- if (state->flags & HOEDOWN_HTML_HARD_WRAP) {
- size_t org;
- while (i < content->size) {
- org = i;
- while (i < content->size && content->data[i] != '\n')
- i++;
-
- if (i > org)
- hoedown_buffer_put(ob, content->data + org, i - org);
-
- /*
- * do not insert a line break if this newline
- * is the last character on the paragraph
- */
- if (i >= content->size - 1)
- break;
-
- rndr_linebreak(ob, data);
- i++;
- }
- } else {
- hoedown_buffer_put(ob, content->data + i, content->size - i);
- }
- HOEDOWN_BUFPUTSL(ob, "</p>\n");
-}
-
-static void
-rndr_raw_block(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data)
-{
- size_t org, sz;
-
- if (!text)
- return;
-
- /* FIXME: Do we *really* need to trim the HTML? How does that make a difference? */
- sz = text->size;
- while (sz > 0 && text->data[sz - 1] == '\n')
- sz--;
-
- org = 0;
- while (org < sz && text->data[org] == '\n')
- org++;
-
- if (org >= sz)
- return;
-
- if (ob->size)
- hoedown_buffer_putc(ob, '\n');
-
- hoedown_buffer_put(ob, text->data + org, sz - org);
- hoedown_buffer_putc(ob, '\n');
-}
-
-static int
-rndr_triple_emphasis(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)
-{
- if (!content || !content->size) return 0;
- HOEDOWN_BUFPUTSL(ob, "<strong><em>");
- hoedown_buffer_put(ob, content->data, content->size);
- HOEDOWN_BUFPUTSL(ob, "</em></strong>");
- return 1;
-}
-
-static void
-rndr_hrule(hoedown_buffer *ob, const hoedown_renderer_data *data)
-{
- hoedown_html_renderer_state *state = data->opaque;
- if (ob->size) hoedown_buffer_putc(ob, '\n');
- hoedown_buffer_puts(ob, USE_XHTML(state) ? "<hr/>\n" : "<hr>\n");
-}
-
-static int
-rndr_image(hoedown_buffer *ob, const hoedown_buffer *link, const hoedown_buffer *title, const hoedown_buffer *alt, const hoedown_renderer_data *data)
-{
- hoedown_html_renderer_state *state = data->opaque;
- if (!link || !link->size) return 0;
-
- HOEDOWN_BUFPUTSL(ob, "<img src=\"");
- escape_href(ob, link->data, link->size);
- HOEDOWN_BUFPUTSL(ob, "\" alt=\"");
-
- if (alt && alt->size)
- escape_html(ob, alt->data, alt->size);
-
- if (title && title->size) {
- HOEDOWN_BUFPUTSL(ob, "\" title=\"");
- escape_html(ob, title->data, title->size); }
-
- hoedown_buffer_puts(ob, USE_XHTML(state) ? "\"/>" : "\">");
- return 1;
-}
-
-static int
-rndr_raw_html(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data)
-{
- hoedown_html_renderer_state *state = data->opaque;
-
- /* ESCAPE overrides SKIP_HTML. It doesn't look to see if
- * there are any valid tags, just escapes all of them. */
- if((state->flags & HOEDOWN_HTML_ESCAPE) != 0) {
- escape_html(ob, text->data, text->size);
- return 1;
- }
-
- if ((state->flags & HOEDOWN_HTML_SKIP_HTML) != 0)
- return 1;
-
- hoedown_buffer_put(ob, text->data, text->size);
- return 1;
-}
-
-static void
-rndr_table(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)
-{
- if (ob->size) hoedown_buffer_putc(ob, '\n');
- HOEDOWN_BUFPUTSL(ob, "<table>\n");
- hoedown_buffer_put(ob, content->data, content->size);
- HOEDOWN_BUFPUTSL(ob, "</table>\n");
-}
-
-static void
-rndr_table_header(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)
-{
- if (ob->size) hoedown_buffer_putc(ob, '\n');
- HOEDOWN_BUFPUTSL(ob, "<thead>\n");
- hoedown_buffer_put(ob, content->data, content->size);
- HOEDOWN_BUFPUTSL(ob, "</thead>\n");
-}
-
-static void
-rndr_table_body(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)
-{
- if (ob->size) hoedown_buffer_putc(ob, '\n');
- HOEDOWN_BUFPUTSL(ob, "<tbody>\n");
- hoedown_buffer_put(ob, content->data, content->size);
- HOEDOWN_BUFPUTSL(ob, "</tbody>\n");
-}
-
-static void
-rndr_tablerow(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)
-{
- HOEDOWN_BUFPUTSL(ob, "<tr>\n");
- if (content) hoedown_buffer_put(ob, content->data, content->size);
- HOEDOWN_BUFPUTSL(ob, "</tr>\n");
-}
-
-static void
-rndr_tablecell(hoedown_buffer *ob, const hoedown_buffer *content, hoedown_table_flags flags, const hoedown_renderer_data *data)
-{
- if (flags & HOEDOWN_TABLE_HEADER) {
- HOEDOWN_BUFPUTSL(ob, "<th");
- } else {
- HOEDOWN_BUFPUTSL(ob, "<td");
- }
-
- switch (flags & HOEDOWN_TABLE_ALIGNMASK) {
- case HOEDOWN_TABLE_ALIGN_CENTER:
- HOEDOWN_BUFPUTSL(ob, " style=\"text-align: center\">");
- break;
-
- case HOEDOWN_TABLE_ALIGN_LEFT:
- HOEDOWN_BUFPUTSL(ob, " style=\"text-align: left\">");
- break;
-
- case HOEDOWN_TABLE_ALIGN_RIGHT:
- HOEDOWN_BUFPUTSL(ob, " style=\"text-align: right\">");
- break;
-
- default:
- HOEDOWN_BUFPUTSL(ob, ">");
- }
-
- if (content)
- hoedown_buffer_put(ob, content->data, content->size);
-
- if (flags & HOEDOWN_TABLE_HEADER) {
- HOEDOWN_BUFPUTSL(ob, "</th>\n");
- } else {
- HOEDOWN_BUFPUTSL(ob, "</td>\n");
- }
-}
-
-static int
-rndr_superscript(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)
-{
- if (!content || !content->size) return 0;
- HOEDOWN_BUFPUTSL(ob, "<sup>");
- hoedown_buffer_put(ob, content->data, content->size);
- HOEDOWN_BUFPUTSL(ob, "</sup>");
- return 1;
-}
-
-static void
-rndr_normal_text(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)
-{
- if (content)
- escape_html(ob, content->data, content->size);
-}
-
-static void
-rndr_footnotes(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)
-{
- hoedown_html_renderer_state *state = data->opaque;
-
- if (ob->size) hoedown_buffer_putc(ob, '\n');
- HOEDOWN_BUFPUTSL(ob, "<div class=\"footnotes\">\n");
- hoedown_buffer_puts(ob, USE_XHTML(state) ? "<hr/>\n" : "<hr>\n");
- HOEDOWN_BUFPUTSL(ob, "<ol>\n");
-
- if (content) hoedown_buffer_put(ob, content->data, content->size);
-
- HOEDOWN_BUFPUTSL(ob, "\n</ol>\n</div>\n");
-}
-
-static void
-rndr_footnote_def(hoedown_buffer *ob, const hoedown_buffer *content, unsigned int num, const hoedown_renderer_data *data)
-{
- size_t i = 0;
- int pfound = 0;
-
- /* insert anchor at the end of first paragraph block */
- if (content) {
- while ((i+3) < content->size) {
- if (content->data[i++] != '<') continue;
- if (content->data[i++] != '/') continue;
- if (content->data[i++] != 'p' && content->data[i] != 'P') continue;
- if (content->data[i] != '>') continue;
- i -= 3;
- pfound = 1;
- break;
- }
- }
-
- hoedown_buffer_printf(ob, "\n<li id=\"fn%d\">\n", num);
- if (pfound) {
- hoedown_buffer_put(ob, content->data, i);
- hoedown_buffer_printf(ob, "&nbsp;<a href=\"#fnref%d\" rev=\"footnote\">&#8617;</a>", num);
- hoedown_buffer_put(ob, content->data + i, content->size - i);
- } else if (content) {
- hoedown_buffer_put(ob, content->data, content->size);
- }
- HOEDOWN_BUFPUTSL(ob, "</li>\n");
-}
-
-static int
-rndr_footnote_ref(hoedown_buffer *ob, unsigned int num, const hoedown_renderer_data *data)
-{
- hoedown_buffer_printf(ob, "<sup id=\"fnref%d\"><a href=\"#fn%d\" rel=\"footnote\">%d</a></sup>", num, num, num);
- return 1;
-}
-
-static int
-rndr_math(hoedown_buffer *ob, const hoedown_buffer *text, int displaymode, const hoedown_renderer_data *data)
-{
- hoedown_buffer_put(ob, (const uint8_t *)(displaymode ? "\\[" : "\\("), 2);
- escape_html(ob, text->data, text->size);
- hoedown_buffer_put(ob, (const uint8_t *)(displaymode ? "\\]" : "\\)"), 2);
- return 1;
-}
-
-static void
-toc_header(hoedown_buffer *ob, const hoedown_buffer *content, int level, const hoedown_renderer_data *data)
-{
- hoedown_html_renderer_state *state = data->opaque;
-
- if (level <= state->toc_data.nesting_level) {
- /* set the level offset if this is the first header
- * we're parsing for the document */
- if (state->toc_data.current_level == 0)
- state->toc_data.level_offset = level - 1;
-
- level -= state->toc_data.level_offset;
-
- if (level > state->toc_data.current_level) {
- while (level > state->toc_data.current_level) {
- HOEDOWN_BUFPUTSL(ob, "<ul>\n<li>\n");
- state->toc_data.current_level++;
- }
- } else if (level < state->toc_data.current_level) {
- HOEDOWN_BUFPUTSL(ob, "</li>\n");
- while (level < state->toc_data.current_level) {
- HOEDOWN_BUFPUTSL(ob, "</ul>\n</li>\n");
- state->toc_data.current_level--;
- }
- HOEDOWN_BUFPUTSL(ob,"<li>\n");
- } else {
- HOEDOWN_BUFPUTSL(ob,"</li>\n<li>\n");
- }
-
- hoedown_buffer_printf(ob, "<a href=\"#toc_%d\">", state->toc_data.header_count++);
- if (content) hoedown_buffer_put(ob, content->data, content->size);
- HOEDOWN_BUFPUTSL(ob, "</a>\n");
- }
-}
-
-static int
-toc_link(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_buffer *link, const hoedown_buffer *title, const hoedown_renderer_data *data)
-{
- if (content && content->size) hoedown_buffer_put(ob, content->data, content->size);
- return 1;
-}
-
-static void
-toc_finalize(hoedown_buffer *ob, int inline_render, const hoedown_renderer_data *data)
-{
- hoedown_html_renderer_state *state;
-
- if (inline_render)
- return;
-
- state = data->opaque;
-
- while (state->toc_data.current_level > 0) {
- HOEDOWN_BUFPUTSL(ob, "</li>\n</ul>\n");
- state->toc_data.current_level--;
- }
-
- state->toc_data.header_count = 0;
-}
-
-hoedown_renderer *
-hoedown_html_toc_renderer_new(int nesting_level)
-{
- static const hoedown_renderer cb_default = {
- NULL,
-
- NULL,
- NULL,
- toc_header,
- NULL,
- NULL,
- NULL,
- NULL,
- NULL,
- NULL,
- NULL,
- NULL,
- NULL,
- NULL,
- NULL,
- NULL,
-
- NULL,
- rndr_codespan,
- rndr_double_emphasis,
- rndr_emphasis,
- rndr_underline,
- rndr_highlight,
- rndr_quote,
- NULL,
- NULL,
- toc_link,
- rndr_triple_emphasis,
- rndr_strikethrough,
- rndr_superscript,
- NULL,
- NULL,
- NULL,
-
- NULL,
- rndr_normal_text,
-
- NULL,
- toc_finalize
- };
-
- hoedown_html_renderer_state *state;
- hoedown_renderer *renderer;
-
- /* Prepare the state pointer */
- state = hoedown_malloc(sizeof(hoedown_html_renderer_state));
- memset(state, 0x0, sizeof(hoedown_html_renderer_state));
-
- state->toc_data.nesting_level = nesting_level;
-
- /* Prepare the renderer */
- renderer = hoedown_malloc(sizeof(hoedown_renderer));
- memcpy(renderer, &cb_default, sizeof(hoedown_renderer));
-
- renderer->opaque = state;
- return renderer;
-}
-
-hoedown_renderer *
-hoedown_html_renderer_new(hoedown_html_flags render_flags, int nesting_level)
-{
- static const hoedown_renderer cb_default = {
- NULL,
-
- rndr_blockcode,
- rndr_blockquote,
- rndr_header,
- rndr_hrule,
- rndr_list,
- rndr_listitem,
- rndr_paragraph,
- rndr_table,
- rndr_table_header,
- rndr_table_body,
- rndr_tablerow,
- rndr_tablecell,
- rndr_footnotes,
- rndr_footnote_def,
- rndr_raw_block,
-
- rndr_autolink,
- rndr_codespan,
- rndr_double_emphasis,
- rndr_emphasis,
- rndr_underline,
- rndr_highlight,
- rndr_quote,
- rndr_image,
- rndr_linebreak,
- rndr_link,
- rndr_triple_emphasis,
- rndr_strikethrough,
- rndr_superscript,
- rndr_footnote_ref,
- rndr_math,
- rndr_raw_html,
-
- NULL,
- rndr_normal_text,
-
- NULL,
- NULL
- };
-
- hoedown_html_renderer_state *state;
- hoedown_renderer *renderer;
-
- /* Prepare the state pointer */
- state = hoedown_malloc(sizeof(hoedown_html_renderer_state));
- memset(state, 0x0, sizeof(hoedown_html_renderer_state));
-
- state->flags = render_flags;
- state->toc_data.nesting_level = nesting_level;
-
- /* Prepare the renderer */
- renderer = hoedown_malloc(sizeof(hoedown_renderer));
- memcpy(renderer, &cb_default, sizeof(hoedown_renderer));
-
- if (render_flags & HOEDOWN_HTML_SKIP_HTML || render_flags & HOEDOWN_HTML_ESCAPE)
- renderer->blockhtml = NULL;
-
- renderer->opaque = state;
- return renderer;
-}
-
-void
-hoedown_html_renderer_free(hoedown_renderer *renderer)
-{
- free(renderer->opaque);
- free(renderer);
-}
diff --git a/libraries/hoedown/src/html_blocks.c b/libraries/hoedown/src/html_blocks.c
deleted file mode 100644
index f5e9dce9..00000000
--- a/libraries/hoedown/src/html_blocks.c
+++ /dev/null
@@ -1,240 +0,0 @@
-/* ANSI-C code produced by gperf version 3.0.3 */
-/* Command-line: gperf -L ANSI-C -N hoedown_find_block_tag -c -C -E -S 1 --ignore-case -m100 html_block_names.gperf */
-/* Computed positions: -k'1-2' */
-
-#if !((' ' == 32) && ('!' == 33) && ('"' == 34) && ('#' == 35) \
- && ('%' == 37) && ('&' == 38) && ('\'' == 39) && ('(' == 40) \
- && (')' == 41) && ('*' == 42) && ('+' == 43) && (',' == 44) \
- && ('-' == 45) && ('.' == 46) && ('/' == 47) && ('0' == 48) \
- && ('1' == 49) && ('2' == 50) && ('3' == 51) && ('4' == 52) \
- && ('5' == 53) && ('6' == 54) && ('7' == 55) && ('8' == 56) \
- && ('9' == 57) && (':' == 58) && (';' == 59) && ('<' == 60) \
- && ('=' == 61) && ('>' == 62) && ('?' == 63) && ('A' == 65) \
- && ('B' == 66) && ('C' == 67) && ('D' == 68) && ('E' == 69) \
- && ('F' == 70) && ('G' == 71) && ('H' == 72) && ('I' == 73) \
- && ('J' == 74) && ('K' == 75) && ('L' == 76) && ('M' == 77) \
- && ('N' == 78) && ('O' == 79) && ('P' == 80) && ('Q' == 81) \
- && ('R' == 82) && ('S' == 83) && ('T' == 84) && ('U' == 85) \
- && ('V' == 86) && ('W' == 87) && ('X' == 88) && ('Y' == 89) \
- && ('Z' == 90) && ('[' == 91) && ('\\' == 92) && (']' == 93) \
- && ('^' == 94) && ('_' == 95) && ('a' == 97) && ('b' == 98) \
- && ('c' == 99) && ('d' == 100) && ('e' == 101) && ('f' == 102) \
- && ('g' == 103) && ('h' == 104) && ('i' == 105) && ('j' == 106) \
- && ('k' == 107) && ('l' == 108) && ('m' == 109) && ('n' == 110) \
- && ('o' == 111) && ('p' == 112) && ('q' == 113) && ('r' == 114) \
- && ('s' == 115) && ('t' == 116) && ('u' == 117) && ('v' == 118) \
- && ('w' == 119) && ('x' == 120) && ('y' == 121) && ('z' == 122) \
- && ('{' == 123) && ('|' == 124) && ('}' == 125) && ('~' == 126))
-/* The character set is not based on ISO-646. */
-#error "gperf generated tables don't work with this execution character set. Please report a bug to <bug-gnu-gperf@gnu.org>."
-#endif
-
-/* maximum key range = 24, duplicates = 0 */
-
-#ifndef GPERF_DOWNCASE
-#define GPERF_DOWNCASE 1
-static unsigned char gperf_downcase[256] =
- {
- 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
- 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29,
- 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44,
- 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59,
- 60, 61, 62, 63, 64, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106,
- 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121,
- 122, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104,
- 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119,
- 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134,
- 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149,
- 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164,
- 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179,
- 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194,
- 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209,
- 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224,
- 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239,
- 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254,
- 255
- };
-#endif
-
-#ifndef GPERF_CASE_STRNCMP
-#define GPERF_CASE_STRNCMP 1
-static int
-gperf_case_strncmp (register const char *s1, register const char *s2, register unsigned int n)
-{
- for (; n > 0;)
- {
- unsigned char c1 = gperf_downcase[(unsigned char)*s1++];
- unsigned char c2 = gperf_downcase[(unsigned char)*s2++];
- if (c1 != 0 && c1 == c2)
- {
- n--;
- continue;
- }
- return (int)c1 - (int)c2;
- }
- return 0;
-}
-#endif
-
-#ifdef __GNUC__
-__inline
-#else
-#ifdef __cplusplus
-inline
-#endif
-#endif
-static unsigned int
-hash (register const char *str, register unsigned int len)
-{
- static const unsigned char asso_values[] =
- {
- 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
- 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
- 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
- 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
- 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
- 22, 21, 19, 18, 16, 0, 25, 25, 25, 25,
- 25, 25, 25, 25, 25, 25, 1, 25, 0, 25,
- 1, 0, 0, 13, 0, 25, 25, 11, 2, 1,
- 0, 25, 25, 5, 0, 2, 25, 25, 25, 25,
- 25, 25, 25, 25, 25, 25, 25, 25, 1, 25,
- 0, 25, 1, 0, 0, 13, 0, 25, 25, 11,
- 2, 1, 0, 25, 25, 5, 0, 2, 25, 25,
- 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
- 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
- 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
- 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
- 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
- 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
- 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
- 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
- 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
- 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
- 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
- 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
- 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
- 25, 25, 25, 25, 25, 25, 25
- };
- register int hval = (int)len;
-
- switch (hval)
- {
- default:
- hval += asso_values[(unsigned char)str[1]+1];
- /*FALLTHROUGH*/
- case 1:
- hval += asso_values[(unsigned char)str[0]];
- break;
- }
- return hval;
-}
-
-#ifdef __GNUC__
-__inline
-#ifdef __GNUC_STDC_INLINE__
-__attribute__ ((__gnu_inline__))
-#endif
-#endif
-const char *
-hoedown_find_block_tag (register const char *str, register unsigned int len)
-{
- enum
- {
- TOTAL_KEYWORDS = 24,
- MIN_WORD_LENGTH = 1,
- MAX_WORD_LENGTH = 10,
- MIN_HASH_VALUE = 1,
- MAX_HASH_VALUE = 24
- };
-
- if (len <= MAX_WORD_LENGTH && len >= MIN_WORD_LENGTH)
- {
- register int key = hash (str, len);
-
- if (key <= MAX_HASH_VALUE && key >= MIN_HASH_VALUE)
- {
- register const char *resword;
-
- switch (key - 1)
- {
- case 0:
- resword = "p";
- goto compare;
- case 1:
- resword = "h6";
- goto compare;
- case 2:
- resword = "div";
- goto compare;
- case 3:
- resword = "del";
- goto compare;
- case 4:
- resword = "form";
- goto compare;
- case 5:
- resword = "table";
- goto compare;
- case 6:
- resword = "figure";
- goto compare;
- case 7:
- resword = "pre";
- goto compare;
- case 8:
- resword = "fieldset";
- goto compare;
- case 9:
- resword = "noscript";
- goto compare;
- case 10:
- resword = "script";
- goto compare;
- case 11:
- resword = "style";
- goto compare;
- case 12:
- resword = "dl";
- goto compare;
- case 13:
- resword = "ol";
- goto compare;
- case 14:
- resword = "ul";
- goto compare;
- case 15:
- resword = "math";
- goto compare;
- case 16:
- resword = "ins";
- goto compare;
- case 17:
- resword = "h5";
- goto compare;
- case 18:
- resword = "iframe";
- goto compare;
- case 19:
- resword = "h4";
- goto compare;
- case 20:
- resword = "h3";
- goto compare;
- case 21:
- resword = "blockquote";
- goto compare;
- case 22:
- resword = "h2";
- goto compare;
- case 23:
- resword = "h1";
- goto compare;
- }
- return 0;
- compare:
- if ((((unsigned char)*str ^ (unsigned char)*resword) & ~32) == 0 && !gperf_case_strncmp (str, resword, len) && resword[len] == '\0')
- return resword;
- }
- }
- return 0;
-}
diff --git a/libraries/hoedown/src/html_smartypants.c b/libraries/hoedown/src/html_smartypants.c
deleted file mode 100644
index d89624f3..00000000
--- a/libraries/hoedown/src/html_smartypants.c
+++ /dev/null
@@ -1,435 +0,0 @@
-#include "hoedown/html.h"
-
-#include <string.h>
-#include <stdlib.h>
-#include <stdio.h>
-#include <ctype.h>
-
-#ifdef _MSC_VER
-#define snprintf _snprintf
-#endif
-
-struct smartypants_data {
- int in_squote;
- int in_dquote;
-};
-
-static size_t smartypants_cb__ltag(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size);
-static size_t smartypants_cb__dquote(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size);
-static size_t smartypants_cb__amp(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size);
-static size_t smartypants_cb__period(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size);
-static size_t smartypants_cb__number(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size);
-static size_t smartypants_cb__dash(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size);
-static size_t smartypants_cb__parens(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size);
-static size_t smartypants_cb__squote(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size);
-static size_t smartypants_cb__backtick(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size);
-static size_t smartypants_cb__escape(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size);
-
-static size_t (*smartypants_cb_ptrs[])
- (hoedown_buffer *, struct smartypants_data *, uint8_t, const uint8_t *, size_t) =
-{
- NULL, /* 0 */
- smartypants_cb__dash, /* 1 */
- smartypants_cb__parens, /* 2 */
- smartypants_cb__squote, /* 3 */
- smartypants_cb__dquote, /* 4 */
- smartypants_cb__amp, /* 5 */
- smartypants_cb__period, /* 6 */
- smartypants_cb__number, /* 7 */
- smartypants_cb__ltag, /* 8 */
- smartypants_cb__backtick, /* 9 */
- smartypants_cb__escape, /* 10 */
-};
-
-static const uint8_t smartypants_cb_chars[UINT8_MAX+1] = {
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 4, 0, 0, 0, 5, 3, 2, 0, 0, 0, 0, 1, 6, 0,
- 0, 7, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0,
- 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
-};
-
-static int
-word_boundary(uint8_t c)
-{
- return c == 0 || isspace(c) || ispunct(c);
-}
-
-/*
- If 'text' begins with any kind of single quote (e.g. "'" or "&apos;" etc.),
- returns the length of the sequence of characters that makes up the single-
- quote. Otherwise, returns zero.
-*/
-static size_t
-squote_len(const uint8_t *text, size_t size)
-{
- static char* single_quote_list[] = { "'", "&#39;", "&#x27;", "&apos;", NULL };
- char** p;
-
- for (p = single_quote_list; *p; ++p) {
- size_t len = strlen(*p);
- if (size >= len && memcmp(text, *p, len) == 0) {
- return len;
- }
- }
-
- return 0;
-}
-
-/* Converts " or ' at very beginning or end of a word to left or right quote */
-static int
-smartypants_quotes(hoedown_buffer *ob, uint8_t previous_char, uint8_t next_char, uint8_t quote, int *is_open)
-{
- char ent[8];
-
- if (*is_open && !word_boundary(next_char))
- return 0;
-
- if (!(*is_open) && !word_boundary(previous_char))
- return 0;
-
- snprintf(ent, sizeof(ent), "&%c%cquo;", (*is_open) ? 'r' : 'l', quote);
- *is_open = !(*is_open);
- hoedown_buffer_puts(ob, ent);
- return 1;
-}
-
-/*
- Converts ' to left or right single quote; but the initial ' might be in
- different forms, e.g. &apos; or &#39; or &#x27;.
- 'squote_text' points to the original single quote, and 'squote_size' is its length.
- 'text' points at the last character of the single-quote, e.g. ' or ;
-*/
-static size_t
-smartypants_squote(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size,
- const uint8_t *squote_text, size_t squote_size)
-{
- if (size >= 2) {
- uint8_t t1 = tolower(text[1]);
- size_t next_squote_len = squote_len(text+1, size-1);
-
- /* convert '' to &ldquo; or &rdquo; */
- if (next_squote_len > 0) {
- uint8_t next_char = (size > 1+next_squote_len) ? text[1+next_squote_len] : 0;
- if (smartypants_quotes(ob, previous_char, next_char, 'd', &smrt->in_dquote))
- return next_squote_len;
- }
-
- /* Tom's, isn't, I'm, I'd */
- if ((t1 == 's' || t1 == 't' || t1 == 'm' || t1 == 'd') &&
- (size == 3 || word_boundary(text[2]))) {
- HOEDOWN_BUFPUTSL(ob, "&rsquo;");
- return 0;
- }
-
- /* you're, you'll, you've */
- if (size >= 3) {
- uint8_t t2 = tolower(text[2]);
-
- if (((t1 == 'r' && t2 == 'e') ||
- (t1 == 'l' && t2 == 'l') ||
- (t1 == 'v' && t2 == 'e')) &&
- (size == 4 || word_boundary(text[3]))) {
- HOEDOWN_BUFPUTSL(ob, "&rsquo;");
- return 0;
- }
- }
- }
-
- if (smartypants_quotes(ob, previous_char, size > 0 ? text[1] : 0, 's', &smrt->in_squote))
- return 0;
-
- hoedown_buffer_put(ob, squote_text, squote_size);
- return 0;
-}
-
-/* Converts ' to left or right single quote. */
-static size_t
-smartypants_cb__squote(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size)
-{
- return smartypants_squote(ob, smrt, previous_char, text, size, text, 1);
-}
-
-/* Converts (c), (r), (tm) */
-static size_t
-smartypants_cb__parens(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size)
-{
- if (size >= 3) {
- uint8_t t1 = tolower(text[1]);
- uint8_t t2 = tolower(text[2]);
-
- if (t1 == 'c' && t2 == ')') {
- HOEDOWN_BUFPUTSL(ob, "&copy;");
- return 2;
- }
-
- if (t1 == 'r' && t2 == ')') {
- HOEDOWN_BUFPUTSL(ob, "&reg;");
- return 2;
- }
-
- if (size >= 4 && t1 == 't' && t2 == 'm' && text[3] == ')') {
- HOEDOWN_BUFPUTSL(ob, "&trade;");
- return 3;
- }
- }
-
- hoedown_buffer_putc(ob, text[0]);
- return 0;
-}
-
-/* Converts "--" to em-dash, etc. */
-static size_t
-smartypants_cb__dash(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size)
-{
- if (size >= 3 && text[1] == '-' && text[2] == '-') {
- HOEDOWN_BUFPUTSL(ob, "&mdash;");
- return 2;
- }
-
- if (size >= 2 && text[1] == '-') {
- HOEDOWN_BUFPUTSL(ob, "&ndash;");
- return 1;
- }
-
- hoedown_buffer_putc(ob, text[0]);
- return 0;
-}
-
-/* Converts &quot; etc. */
-static size_t
-smartypants_cb__amp(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size)
-{
- size_t len;
- if (size >= 6 && memcmp(text, "&quot;", 6) == 0) {
- if (smartypants_quotes(ob, previous_char, size >= 7 ? text[6] : 0, 'd', &smrt->in_dquote))
- return 5;
- }
-
- len = squote_len(text, size);
- if (len > 0) {
- return (len-1) + smartypants_squote(ob, smrt, previous_char, text+(len-1), size-(len-1), text, len);
- }
-
- if (size >= 4 && memcmp(text, "&#0;", 4) == 0)
- return 3;
-
- hoedown_buffer_putc(ob, '&');
- return 0;
-}
-
-/* Converts "..." to ellipsis */
-static size_t
-smartypants_cb__period(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size)
-{
- if (size >= 3 && text[1] == '.' && text[2] == '.') {
- HOEDOWN_BUFPUTSL(ob, "&hellip;");
- return 2;
- }
-
- if (size >= 5 && text[1] == ' ' && text[2] == '.' && text[3] == ' ' && text[4] == '.') {
- HOEDOWN_BUFPUTSL(ob, "&hellip;");
- return 4;
- }
-
- hoedown_buffer_putc(ob, text[0]);
- return 0;
-}
-
-/* Converts `` to opening double quote */
-static size_t
-smartypants_cb__backtick(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size)
-{
- if (size >= 2 && text[1] == '`') {
- if (smartypants_quotes(ob, previous_char, size >= 3 ? text[2] : 0, 'd', &smrt->in_dquote))
- return 1;
- }
-
- hoedown_buffer_putc(ob, text[0]);
- return 0;
-}
-
-/* Converts 1/2, 1/4, 3/4 */
-static size_t
-smartypants_cb__number(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size)
-{
- if (word_boundary(previous_char) && size >= 3) {
- if (text[0] == '1' && text[1] == '/' && text[2] == '2') {
- if (size == 3 || word_boundary(text[3])) {
- HOEDOWN_BUFPUTSL(ob, "&frac12;");
- return 2;
- }
- }
-
- if (text[0] == '1' && text[1] == '/' && text[2] == '4') {
- if (size == 3 || word_boundary(text[3]) ||
- (size >= 5 && tolower(text[3]) == 't' && tolower(text[4]) == 'h')) {
- HOEDOWN_BUFPUTSL(ob, "&frac14;");
- return 2;
- }
- }
-
- if (text[0] == '3' && text[1] == '/' && text[2] == '4') {
- if (size == 3 || word_boundary(text[3]) ||
- (size >= 6 && tolower(text[3]) == 't' && tolower(text[4]) == 'h' && tolower(text[5]) == 's')) {
- HOEDOWN_BUFPUTSL(ob, "&frac34;");
- return 2;
- }
- }
- }
-
- hoedown_buffer_putc(ob, text[0]);
- return 0;
-}
-
-/* Converts " to left or right double quote */
-static size_t
-smartypants_cb__dquote(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size)
-{
- if (!smartypants_quotes(ob, previous_char, size > 0 ? text[1] : 0, 'd', &smrt->in_dquote))
- HOEDOWN_BUFPUTSL(ob, "&quot;");
-
- return 0;
-}
-
-static size_t
-smartypants_cb__ltag(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size)
-{
- static const char *skip_tags[] = {
- "pre", "code", "var", "samp", "kbd", "math", "script", "style"
- };
- static const size_t skip_tags_count = 8;
-
- size_t tag, i = 0;
-
- /* This is a comment. Copy everything verbatim until --> or EOF is seen. */
- if (i + 4 < size && memcmp(text, "<!--", 4) == 0) {
- i += 4;
- while (i + 3 < size && memcmp(text + i, "-->", 3) != 0)
- i++;
- i += 3;
- hoedown_buffer_put(ob, text, i + 1);
- return i;
- }
-
- while (i < size && text[i] != '>')
- i++;
-
- for (tag = 0; tag < skip_tags_count; ++tag) {
- if (hoedown_html_is_tag(text, size, skip_tags[tag]) == HOEDOWN_HTML_TAG_OPEN)
- break;
- }
-
- if (tag < skip_tags_count) {
- for (;;) {
- while (i < size && text[i] != '<')
- i++;
-
- if (i == size)
- break;
-
- if (hoedown_html_is_tag(text + i, size - i, skip_tags[tag]) == HOEDOWN_HTML_TAG_CLOSE)
- break;
-
- i++;
- }
-
- while (i < size && text[i] != '>')
- i++;
- }
-
- hoedown_buffer_put(ob, text, i + 1);
- return i;
-}
-
-static size_t
-smartypants_cb__escape(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size)
-{
- if (size < 2)
- return 0;
-
- switch (text[1]) {
- case '\\':
- case '"':
- case '\'':
- case '.':
- case '-':
- case '`':
- hoedown_buffer_putc(ob, text[1]);
- return 1;
-
- default:
- hoedown_buffer_putc(ob, '\\');
- return 0;
- }
-}
-
-#if 0
-static struct {
- uint8_t c0;
- const uint8_t *pattern;
- const uint8_t *entity;
- int skip;
-} smartypants_subs[] = {
- { '\'', "'s>", "&rsquo;", 0 },
- { '\'', "'t>", "&rsquo;", 0 },
- { '\'', "'re>", "&rsquo;", 0 },
- { '\'', "'ll>", "&rsquo;", 0 },
- { '\'', "'ve>", "&rsquo;", 0 },
- { '\'', "'m>", "&rsquo;", 0 },
- { '\'', "'d>", "&rsquo;", 0 },
- { '-', "--", "&mdash;", 1 },
- { '-', "<->", "&ndash;", 0 },
- { '.', "...", "&hellip;", 2 },
- { '.', ". . .", "&hellip;", 4 },
- { '(', "(c)", "&copy;", 2 },
- { '(', "(r)", "&reg;", 2 },
- { '(', "(tm)", "&trade;", 3 },
- { '3', "<3/4>", "&frac34;", 2 },
- { '3', "<3/4ths>", "&frac34;", 2 },
- { '1', "<1/2>", "&frac12;", 2 },
- { '1', "<1/4>", "&frac14;", 2 },
- { '1', "<1/4th>", "&frac14;", 2 },
- { '&', "&#0;", 0, 3 },
-};
-#endif
-
-void
-hoedown_html_smartypants(hoedown_buffer *ob, const uint8_t *text, size_t size)
-{
- size_t i;
- struct smartypants_data smrt = {0, 0};
-
- if (!text)
- return;
-
- hoedown_buffer_grow(ob, size);
-
- for (i = 0; i < size; ++i) {
- size_t org;
- uint8_t action = 0;
-
- org = i;
- while (i < size && (action = smartypants_cb_chars[text[i]]) == 0)
- i++;
-
- if (i > org)
- hoedown_buffer_put(ob, text + org, i - org);
-
- if (i < size) {
- i += smartypants_cb_ptrs[(int)action]
- (ob, &smrt, i ? text[i - 1] : 0, text + i, size - i);
- }
- }
-}
diff --git a/libraries/hoedown/src/stack.c b/libraries/hoedown/src/stack.c
deleted file mode 100644
index 0523c11b..00000000
--- a/libraries/hoedown/src/stack.c
+++ /dev/null
@@ -1,79 +0,0 @@
-#include "hoedown/stack.h"
-
-#include "hoedown/buffer.h"
-
-#include <stdlib.h>
-#include <string.h>
-#include <assert.h>
-
-void
-hoedown_stack_init(hoedown_stack *st, size_t initial_size)
-{
- assert(st);
-
- st->item = NULL;
- st->size = st->asize = 0;
-
- if (!initial_size)
- initial_size = 8;
-
- hoedown_stack_grow(st, initial_size);
-}
-
-void
-hoedown_stack_uninit(hoedown_stack *st)
-{
- assert(st);
-
- free(st->item);
-}
-
-void
-hoedown_stack_grow(hoedown_stack *st, size_t neosz)
-{
- assert(st);
-
- if (st->asize >= neosz)
- return;
-
- st->item = hoedown_realloc(st->item, neosz * sizeof(void *));
- memset(st->item + st->asize, 0x0, (neosz - st->asize) * sizeof(void *));
-
- st->asize = neosz;
-
- if (st->size > neosz)
- st->size = neosz;
-}
-
-void
-hoedown_stack_push(hoedown_stack *st, void *item)
-{
- assert(st);
-
- if (st->size >= st->asize)
- hoedown_stack_grow(st, st->size * 2);
-
- st->item[st->size++] = item;
-}
-
-void *
-hoedown_stack_pop(hoedown_stack *st)
-{
- assert(st);
-
- if (!st->size)
- return NULL;
-
- return st->item[--st->size];
-}
-
-void *
-hoedown_stack_top(const hoedown_stack *st)
-{
- assert(st);
-
- if (!st->size)
- return NULL;
-
- return st->item[st->size - 1];
-}
diff --git a/libraries/hoedown/src/version.c b/libraries/hoedown/src/version.c
deleted file mode 100644
index 10d36cb9..00000000
--- a/libraries/hoedown/src/version.c
+++ /dev/null
@@ -1,9 +0,0 @@
-#include "hoedown/version.h"
-
-void
-hoedown_version(int *major, int *minor, int *revision)
-{
- *major = HOEDOWN_VERSION_MAJOR;
- *minor = HOEDOWN_VERSION_MINOR;
- *revision = HOEDOWN_VERSION_REVISION;
-}
diff --git a/libraries/katabasis/CMakeLists.txt b/libraries/katabasis/CMakeLists.txt
index f764feb6..643244ed 100644
--- a/libraries/katabasis/CMakeLists.txt
+++ b/libraries/katabasis/CMakeLists.txt
@@ -38,6 +38,15 @@ set( katabasis_PUBLIC
include/katabasis/RequestParameter.h
)
+ecm_qt_declare_logging_category(katabasis_PRIVATE
+ HEADER KatabasisLogging.h # NOTE: this won't be in src/, but CMAKE_BINARY_DIR/src isn't included by default so this should be fine
+ IDENTIFIER katabasisCredentials
+ CATEGORY_NAME "katabasis.credentials"
+ DEFAULT_SEVERITY Warning
+ DESCRIPTION "Secrets and credentials from Katabasis"
+ EXPORT "Katabasis"
+)
+
add_library( Katabasis STATIC ${katabasis_PRIVATE} ${katabasis_PUBLIC} )
target_link_libraries(Katabasis Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Network)
diff --git a/libraries/katabasis/include/katabasis/DeviceFlow.h b/libraries/katabasis/include/katabasis/DeviceFlow.h
index b68c92e0..0401df3c 100644
--- a/libraries/katabasis/include/katabasis/DeviceFlow.h
+++ b/libraries/katabasis/include/katabasis/DeviceFlow.h
@@ -1,5 +1,6 @@
#pragma once
+#include <QLoggingCategory>
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
diff --git a/libraries/katabasis/src/DeviceFlow.cpp b/libraries/katabasis/src/DeviceFlow.cpp
index f78fd620..f49fcb7d 100644
--- a/libraries/katabasis/src/DeviceFlow.cpp
+++ b/libraries/katabasis/src/DeviceFlow.cpp
@@ -19,9 +19,11 @@
#include "katabasis/PollServer.h"
#include "katabasis/Globals.h"
+#include "KatabasisLogging.h"
#include "JsonResponse.h"
namespace {
+
// ref: https://tools.ietf.org/html/rfc8628#section-3.2
// Exception: Google sign-in uses "verification_url" instead of "*_uri" - we'll accept both.
bool hasMandatoryDeviceAuthParams(const QVariantMap& params)
@@ -333,9 +335,7 @@ QString DeviceFlow::refreshToken() {
}
void DeviceFlow::setRefreshToken(const QString &v) {
-#ifndef NDEBUG
- qDebug() << "DeviceFlow::setRefreshToken" << v << "...";
-#endif
+ qCDebug(katabasisCredentials) << "new refresh token:" << v;
token_.refresh_token = v;
}
diff --git a/nix/default.nix b/nix/default.nix
index 6050fd37..f6ab1332 100644
--- a/nix/default.nix
+++ b/nix/default.nix
@@ -2,7 +2,7 @@
, stdenv
, cmake
, jdk8
-, jdk
+, jdk17
, zlib
, file
, wrapQtAppsHook
@@ -16,15 +16,16 @@
, glfw
, openal
, extra-cmake-modules
+, tomlplusplus
, ghc_filesystem
+, cmark
, msaClientID ? ""
-, jdks ? [ jdk jdk8 ]
+, jdks ? [ jdk17 jdk8 ]
# flake
, self
, version
, libnbtplusplus
-, tomlplusplus
}:
stdenv.mkDerivation rec {
@@ -33,13 +34,15 @@ stdenv.mkDerivation rec {
src = lib.cleanSource self;
- nativeBuildInputs = [ extra-cmake-modules cmake file jdk wrapQtAppsHook ];
+ nativeBuildInputs = [ extra-cmake-modules cmake file jdk17 wrapQtAppsHook ];
buildInputs = [
qtbase
qtsvg
zlib
quazip
ghc_filesystem
+ tomlplusplus
+ cmark
] ++ lib.optional (lib.versionAtLeast qtbase.version "6") qtwayland;
cmakeFlags = lib.optionals (msaClientID != "") [ "-DLauncher_MSA_CLIENT_ID=${msaClientID}" ]
@@ -52,11 +55,6 @@ stdenv.mkDerivation rec {
ln -s ${libnbtplusplus}/* source/libraries/libnbtplusplus
chmod -R +r+w source/libraries/libnbtplusplus
chown -R $USER: source/libraries/libnbtplusplus
- rm -rf source/libraries/tomlplusplus
- mkdir source/libraries/tomlplusplus
- ln -s ${tomlplusplus}/* source/libraries/tomlplusplus
- chmod -R +r+w source/libraries/tomlplusplus
- chown -R $USER: source/libraries/tomlplusplus
'';
postInstall =
diff --git a/program_info/org.prismlauncher.PrismLauncher.metainfo.xml.in b/program_info/org.prismlauncher.PrismLauncher.metainfo.xml.in
index 13a860d9..96708960 100644
--- a/program_info/org.prismlauncher.PrismLauncher.metainfo.xml.in
+++ b/program_info/org.prismlauncher.PrismLauncher.metainfo.xml.in
@@ -19,38 +19,49 @@
<p>Features:</p>
<ul>
<li>Easily install game modifications, such as Fabric, Forge and Quilt</li>
- <li>Control your Java settings</li>
+ <li>Easily install and update modpacks from the Launcher</li>
+ <li>Control your Java settings, and enable Mangohud or Gamemode with a toggle</li>
<li>Manage worlds and resource packs from the launcher</li>
- <li>See logs and other details easily</li>
+ <li>See logs and other details easily through a dashboard</li>
<li>Kill Minecraft in case of a crash/freeze</li>
<li>Isolate Minecraft instances to keep everything clean</li>
<li>Install and update mods directly from the launcher</li>
+ <li>Customize the launcher with themes, and more</li>
+ <li>And cat :3</li>
</ul>
</description>
<screenshots>
<screenshot type="default">
<caption>The main Prism Launcher window</caption>
- <image type="source" width="976" height="764">https://prismlauncher.org/img/screenshots/LauncherDark.png</image>
+ <image type="source" width="1030" height="764">https://prismlauncher.org/img/screenshots/LauncherDark.png</image>
</screenshot>
<screenshot>
<caption>Modpack installation</caption>
- <image type="source" width="1103" height="954">https://prismlauncher.org/img/screenshots/ModpackInstallDark.png</image>
+ <image type="source" width="1126" height="850">https://prismlauncher.org/img/screenshots/ModpackInstallDark.png</image>
+ </screenshot>
+ <screenshot>
+ <caption>Modpack updating</caption>
+ <image type="source" width="930" height="677">https://prismlauncher.org/img/screenshots/ModpackUpdateDark.png</image>
</screenshot>
<screenshot>
<caption>Mod installation</caption>
- <image type="source" width="1036" height="700">https://prismlauncher.org/img/screenshots/ModInstallDark.png</image>
+ <image type="source" width="848" height="558">https://prismlauncher.org/img/screenshots/ModInstallDark.png</image>
</screenshot>
<screenshot>
<caption>Mod updating</caption>
- <image type="source" width="930" height="858">https://prismlauncher.org/img/screenshots/ModUpdateDark.png</image>
+ <image type="source" width="860" height="748">https://prismlauncher.org/img/screenshots/ModUpdateDark.png</image>
</screenshot>
<screenshot>
<caption>Instance management</caption>
- <image type="source" width="1083" height="735">https://prismlauncher.org/img/screenshots/PropertiesDark.png</image>
+ <image type="source" width="960" height="659">https://prismlauncher.org/img/screenshots/PropertiesDark.png</image>
+ </screenshot>
+ <screenshot>
+ <caption>Cat :3</caption>
+ <image type="source" width="1042" height="754">https://prismlauncher.org/img/screenshots/LauncherCatDark.png</image>
</screenshot>
<screenshot>
- <caption>Cat :)</caption>
- <image type="source" width="931" height="759">https://prismlauncher.org/img/screenshots/LauncherCatDark.png</image>
+ <caption>Customization</caption>
+ <image type="source" width="1040" height="752">https://prismlauncher.org/img/screenshots/CustomizeDark.png</image>
</screenshot>
</screenshots>
<releases>
diff --git a/program_info/prismlauncher.6.scd b/program_info/prismlauncher.6.scd
index f979e457..e1ebfff3 100644
--- a/program_info/prismlauncher.6.scd
+++ b/program_info/prismlauncher.6.scd
@@ -41,6 +41,24 @@ Here are the current features of Prism Launcher.
*-a, --profile*=PROFILE
Use the account specified by PROFILE (only valid in combination with --launch).
+# ENVIRONMENT
+
+The behavior of the launcher can be customized by the following environment
+variables, besides other common Qt variables:
+
+*QT_LOGGING_RULES*
+ Specifies which logging categories are shown in the logs. One can
+ enable/disable multiple categories by separating them with a semicolon (;).
+
+ The specific syntax, and alternatives to this setting, can be found at
+ https://doc.qt.io/qt-6/qloggingcategory.html#configuring-categories.
+
+*QT_MESSAGE_PATTERN*
+ Specifies the format in which the console output will be shown.
+
+ Available options, as well as syntax, can be viewed at
+ https://doc.qt.io/qt-6/qtglobal.html#qSetMessagePattern.
+
# EXIT STATUS
*0*
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 630f1200..3d0d2dca 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -24,9 +24,21 @@ ecm_add_test(ResourceFolderModel_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_V
ecm_add_test(ResourcePackParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
TEST_NAME ResourcePackParse)
+ecm_add_test(ResourceModel_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
+ TEST_NAME ResourceModel)
+
ecm_add_test(TexturePackParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
TEST_NAME TexturePackParse)
+ecm_add_test(DataPackParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
+ TEST_NAME DataPackParse)
+
+ecm_add_test(ShaderPackParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
+ TEST_NAME ShaderPackParse)
+
+ecm_add_test(WorldSaveParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
+ TEST_NAME WorldSaveParse)
+
ecm_add_test(ParseUtils_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
TEST_NAME ParseUtils)
diff --git a/tests/DataPackParse_test.cpp b/tests/DataPackParse_test.cpp
new file mode 100644
index 00000000..61ce1e2b
--- /dev/null
+++ b/tests/DataPackParse_test.cpp
@@ -0,0 +1,79 @@
+// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <QTest>
+#include <QTimer>
+
+#include <FileSystem.h>
+
+#include <minecraft/mod/DataPack.h>
+#include <minecraft/mod/tasks/LocalDataPackParseTask.h>
+
+class DataPackParseTest : public QObject {
+ Q_OBJECT
+
+ private slots:
+ void test_parseZIP()
+ {
+ QString source = QFINDTESTDATA("testdata/DataPackParse");
+
+ QString zip_dp = FS::PathCombine(source, "test_data_pack_boogaloo.zip");
+ DataPack pack { QFileInfo(zip_dp) };
+
+ bool valid = DataPackUtils::processZIP(pack);
+
+ QVERIFY(pack.packFormat() == 4);
+ QVERIFY(pack.description() == "Some data pack 2 boobgaloo");
+ QVERIFY(valid == true);
+ }
+
+ void test_parseFolder()
+ {
+ QString source = QFINDTESTDATA("testdata/DataPackParse");
+
+ QString folder_dp = FS::PathCombine(source, "test_folder");
+ DataPack pack { QFileInfo(folder_dp) };
+
+ bool valid = DataPackUtils::processFolder(pack);
+
+ QVERIFY(pack.packFormat() == 10);
+ QVERIFY(pack.description() == "Some data pack, maybe");
+ QVERIFY(valid == true);
+ }
+
+ void test_parseFolder2()
+ {
+ QString source = QFINDTESTDATA("testdata/DataPackParse");
+
+ QString folder_dp = FS::PathCombine(source, "another_test_folder");
+ DataPack pack { QFileInfo(folder_dp) };
+
+ bool valid = DataPackUtils::process(pack);
+
+ QVERIFY(pack.packFormat() == 6);
+ QVERIFY(pack.description() == "Some data pack three, leaves on the tree");
+ QVERIFY(valid == true);
+ }
+};
+
+QTEST_GUILESS_MAIN(DataPackParseTest)
+
+#include "DataPackParse_test.moc"
diff --git a/tests/DummyResourceAPI.h b/tests/DummyResourceAPI.h
new file mode 100644
index 00000000..e91be96c
--- /dev/null
+++ b/tests/DummyResourceAPI.h
@@ -0,0 +1,47 @@
+#pragma once
+
+#include <QJsonDocument>
+
+#include <modplatform/ResourceAPI.h>
+
+class SearchTask : public Task {
+ Q_OBJECT
+
+ public:
+ void executeTask() override { emitSucceeded(); }
+};
+
+class DummyResourceAPI : public ResourceAPI {
+ public:
+ static auto searchRequestResult()
+ {
+ static QByteArray json_response =
+ "{\"hits\":["
+ "{"
+ "\"author\":\"flowln\","
+ "\"description\":\"the bestest mod\","
+ "\"project_id\":\"something\","
+ "\"project_type\":\"mod\","
+ "\"slug\":\"bip_bop\","
+ "\"title\":\"AAAAAAAA\","
+ "\"versions\":[\"2.71\"]"
+ "}"
+ "]}";
+
+ return QJsonDocument::fromJson(json_response);
+ }
+
+ DummyResourceAPI() : ResourceAPI() {}
+ [[nodiscard]] auto getSortingMethods() const -> QList<SortingMethod> override { return {}; };
+
+ [[nodiscard]] Task::Ptr searchProjects(SearchArgs&&, SearchCallbacks&& callbacks) const override
+ {
+ auto task = new SearchTask;
+ QObject::connect(task, &Task::succeeded, [=] {
+ auto json = searchRequestResult();
+ callbacks.on_succeed(json);
+ });
+ QObject::connect(task, &Task::finished, task, &Task::deleteLater);
+ return task;
+ }
+};
diff --git a/tests/Packwiz_test.cpp b/tests/Packwiz_test.cpp
index 098e8f89..29289469 100644
--- a/tests/Packwiz_test.cpp
+++ b/tests/Packwiz_test.cpp
@@ -48,7 +48,7 @@ class PackwizTest : public QObject {
QCOMPARE(metadata.hash_format, "sha512");
QCOMPARE(metadata.hash, "c8fe6e15ddea32668822dddb26e1851e5f03834be4bcb2eff9c0da7fdc086a9b6cead78e31a44d3bc66335cba11144ee0337c6d5346f1ba63623064499b3188d");
- QCOMPARE(metadata.provider, ModPlatform::Provider::MODRINTH);
+ QCOMPARE(metadata.provider, ModPlatform::ResourceProvider::MODRINTH);
QCOMPARE(metadata.version(), "ug2qKTPR");
QCOMPARE(metadata.mod_id(), "kYq5qkSL");
}
@@ -76,7 +76,7 @@ class PackwizTest : public QObject {
QCOMPARE(metadata.hash_format, "murmur2");
QCOMPARE(metadata.hash, "1781245820");
- QCOMPARE(metadata.provider, ModPlatform::Provider::FLAME);
+ QCOMPARE(metadata.provider, ModPlatform::ResourceProvider::FLAME);
QCOMPARE(metadata.file_id, 3509043);
QCOMPARE(metadata.project_id, 327154);
}
diff --git a/tests/ResourceModel_test.cpp b/tests/ResourceModel_test.cpp
new file mode 100644
index 00000000..716bf853
--- /dev/null
+++ b/tests/ResourceModel_test.cpp
@@ -0,0 +1,88 @@
+#include <QAbstractItemModelTester>
+#include <QTest>
+#include <QTimer>
+
+#include <Json.h>
+
+#include <ui/pages/modplatform/ResourceModel.h>
+
+#include "DummyResourceAPI.h"
+
+using ResourceDownload::ResourceModel;
+
+#define EXEC_TASK(EXEC) \
+ QEventLoop loop; \
+ \
+ connect(model, &ResourceModel::dataChanged, &loop, &QEventLoop::quit); \
+ \
+ QTimer expire_timer; \
+ expire_timer.callOnTimeout(&loop, &QEventLoop::quit); \
+ expire_timer.setSingleShot(true); \
+ expire_timer.start(4000); \
+ \
+ EXEC; \
+ if (model->hasActiveSearchJob()) \
+ loop.exec(); \
+ \
+ QVERIFY2(expire_timer.isActive(), "Timer has expired. The search never finished."); \
+ expire_timer.stop(); \
+ \
+ disconnect(model, nullptr, &loop, nullptr)
+
+class ResourceModelTest;
+
+class DummyResourceModel : public ResourceModel {
+ Q_OBJECT
+
+ friend class ResourceModelTest;
+
+ public:
+ DummyResourceModel() : ResourceModel(new DummyResourceAPI) {}
+
+ [[nodiscard]] auto metaEntryBase() const -> QString override { return ""; };
+
+ ResourceAPI::SearchArgs createSearchArguments() override { return {}; };
+ ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) override { return {}; };
+ ResourceAPI::ProjectInfoArgs createInfoArguments(QModelIndex&) override { return {}; };
+
+ QJsonArray documentToArray(QJsonDocument& doc) const override { return doc.object().value("hits").toArray(); }
+
+ void loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) override
+ {
+ pack.authors.append({ Json::requireString(obj, "author") });
+ pack.description = Json::requireString(obj, "description");
+ pack.addonId = Json::requireString(obj, "project_id");
+ }
+};
+
+class ResourceModelTest : public QObject {
+ Q_OBJECT
+ private slots:
+ void test_abstract_item_model() { [[maybe_unused]] auto tester = new QAbstractItemModelTester(new DummyResourceModel); }
+
+ void test_search()
+ {
+ auto model = new DummyResourceModel;
+
+ QVERIFY(model->m_packs.isEmpty());
+
+ EXEC_TASK(model->search());
+
+ QVERIFY(model->m_packs.size() == 1);
+ QVERIFY(model->m_search_state == DummyResourceModel::SearchState::Finished);
+
+ auto processed_pack = model->m_packs.at(0);
+ auto search_json = DummyResourceAPI::searchRequestResult();
+ auto processed_response = model->documentToArray(search_json).first().toObject();
+
+ QVERIFY(processed_pack.addonId.toString() == Json::requireString(processed_response, "project_id"));
+ QVERIFY(processed_pack.description == Json::requireString(processed_response, "description"));
+ QVERIFY(processed_pack.authors.first().name == Json::requireString(processed_response, "author"));
+ }
+};
+
+QTEST_GUILESS_MAIN(ResourceModelTest)
+
+#include "ResourceModel_test.moc"
+
+#include "moc_DummyResourceAPI.cpp"
diff --git a/tests/ResourcePackParse_test.cpp b/tests/ResourcePackParse_test.cpp
index 568c3b63..7f2f86bf 100644
--- a/tests/ResourcePackParse_test.cpp
+++ b/tests/ResourcePackParse_test.cpp
@@ -35,10 +35,11 @@ class ResourcePackParseTest : public QObject {
QString zip_rp = FS::PathCombine(source, "test_resource_pack_idk.zip");
ResourcePack pack { QFileInfo(zip_rp) };
- ResourcePackUtils::processZIP(pack);
+ bool valid = ResourcePackUtils::processZIP(pack, ResourcePackUtils::ProcessingLevel::BasicInfoOnly);
QVERIFY(pack.packFormat() == 3);
QVERIFY(pack.description() == "um dois, feijão com arroz, três quatro, feijão no prato, cinco seis, café inglês, sete oito, comer biscoito, nove dez comer pastéis!!");
+ QVERIFY(valid == true);
}
void test_parseFolder()
@@ -48,10 +49,11 @@ class ResourcePackParseTest : public QObject {
QString folder_rp = FS::PathCombine(source, "test_folder");
ResourcePack pack { QFileInfo(folder_rp) };
- ResourcePackUtils::processFolder(pack);
+ bool valid = ResourcePackUtils::processFolder(pack, ResourcePackUtils::ProcessingLevel::BasicInfoOnly);
QVERIFY(pack.packFormat() == 1);
QVERIFY(pack.description() == "Some resource pack maybe");
+ QVERIFY(valid == true);
}
void test_parseFolder2()
@@ -61,10 +63,11 @@ class ResourcePackParseTest : public QObject {
QString folder_rp = FS::PathCombine(source, "another_test_folder");
ResourcePack pack { QFileInfo(folder_rp) };
- ResourcePackUtils::process(pack);
+ bool valid = ResourcePackUtils::process(pack, ResourcePackUtils::ProcessingLevel::BasicInfoOnly);
QVERIFY(pack.packFormat() == 6);
QVERIFY(pack.description() == "o quartel pegou fogo, policia deu sinal, acode acode acode a bandeira nacional");
+ QVERIFY(valid == false); // no assets dir
}
};
diff --git a/tests/ShaderPackParse_test.cpp b/tests/ShaderPackParse_test.cpp
new file mode 100644
index 00000000..7df105c6
--- /dev/null
+++ b/tests/ShaderPackParse_test.cpp
@@ -0,0 +1,77 @@
+
+// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <QTest>
+#include <QTimer>
+
+#include <FileSystem.h>
+
+#include <minecraft/mod/ShaderPack.h>
+#include <minecraft/mod/tasks/LocalShaderPackParseTask.h>
+
+class ShaderPackParseTest : public QObject {
+ Q_OBJECT
+
+ private slots:
+ void test_parseZIP()
+ {
+ QString source = QFINDTESTDATA("testdata/ShaderPackParse");
+
+ QString zip_sp = FS::PathCombine(source, "shaderpack1.zip");
+ ShaderPack pack { QFileInfo(zip_sp) };
+
+ bool valid = ShaderPackUtils::processZIP(pack);
+
+ QVERIFY(pack.packFormat() == ShaderPackFormat::VALID);
+ QVERIFY(valid == true);
+ }
+
+ void test_parseFolder()
+ {
+ QString source = QFINDTESTDATA("testdata/ShaderPackParse");
+
+ QString folder_sp = FS::PathCombine(source, "shaderpack2");
+ ShaderPack pack { QFileInfo(folder_sp) };
+
+ bool valid = ShaderPackUtils::processFolder(pack);
+
+ QVERIFY(pack.packFormat() == ShaderPackFormat::VALID);
+ QVERIFY(valid == true);
+ }
+
+ void test_parseZIP2()
+ {
+ QString source = QFINDTESTDATA("testdata/ShaderPackParse");
+
+ QString folder_sp = FS::PathCombine(source, "shaderpack3.zip");
+ ShaderPack pack { QFileInfo(folder_sp) };
+
+ bool valid = ShaderPackUtils::process(pack);
+
+ QVERIFY(pack.packFormat() == ShaderPackFormat::INVALID);
+ QVERIFY(valid == false);
+ }
+};
+
+QTEST_GUILESS_MAIN(ShaderPackParseTest)
+
+#include "ShaderPackParse_test.moc"
diff --git a/tests/Task_test.cpp b/tests/Task_test.cpp
index 80bba02f..6649b724 100644
--- a/tests/Task_test.cpp
+++ b/tests/Task_test.cpp
@@ -1,4 +1,6 @@
#include <QTest>
+#include <QTimer>
+#include <QThread>
#include <tasks/ConcurrentTask.h>
#include <tasks/MultipleOptionsTask.h>
@@ -11,6 +13,9 @@ class BasicTask : public Task {
friend class TaskTest;
+ public:
+ BasicTask(bool show_debug_log = true) : Task(nullptr, show_debug_log) {}
+
private:
void executeTask() override
{
@@ -30,6 +35,42 @@ class BasicTask_MultiStep : public Task {
void executeTask() override {};
};
+class BigConcurrentTask : public QThread {
+ Q_OBJECT
+
+ ConcurrentTask big_task;
+
+ void run() override
+ {
+ QTimer deadline;
+ deadline.setInterval(10000);
+ connect(&deadline, &QTimer::timeout, this, [this]{ passed_the_deadline = true; });
+ deadline.start();
+
+ // NOTE: Arbitrary value that manages to trigger a problem when there is one.
+ static const unsigned s_num_tasks = 1 << 14;
+ auto sub_tasks = new BasicTask*[s_num_tasks];
+
+ for (unsigned i = 0; i < s_num_tasks; i++) {
+ sub_tasks[i] = new BasicTask(false);
+ big_task.addTask(sub_tasks[i]);
+ }
+
+ big_task.run();
+
+ while (!big_task.isFinished() && !passed_the_deadline)
+ QCoreApplication::processEvents();
+
+ emit finished();
+ }
+
+ public:
+ bool passed_the_deadline = false;
+
+ signals:
+ void finished();
+};
+
class TaskTest : public QObject {
Q_OBJECT
@@ -183,6 +224,25 @@ class TaskTest : public QObject {
return t.isFinished();
}, 1000), "Task didn't finish as it should.");
}
+
+ void test_stackOverflowInConcurrentTask()
+ {
+ QEventLoop loop;
+
+ auto thread = new BigConcurrentTask;
+ // NOTE: This is an arbitrary value, big enough to not cause problems on normal execution, but low enough
+ // so that the number of tasks that needs to get ran to potentially cause a problem isn't too big.
+ thread->setStackSize(32 * 1024);
+
+ connect(thread, &BigConcurrentTask::finished, &loop, &QEventLoop::quit);
+
+ thread->start();
+
+ loop.exec();
+
+ QVERIFY(!thread->passed_the_deadline);
+ thread->deleteLater();
+ }
};
QTEST_GUILESS_MAIN(TaskTest)
diff --git a/tests/TexturePackParse_test.cpp b/tests/TexturePackParse_test.cpp
index 0771f79f..4ddc0a3a 100644
--- a/tests/TexturePackParse_test.cpp
+++ b/tests/TexturePackParse_test.cpp
@@ -36,9 +36,10 @@ class TexturePackParseTest : public QObject {
QString zip_rp = FS::PathCombine(source, "test_texture_pack_idk.zip");
TexturePack pack { QFileInfo(zip_rp) };
- TexturePackUtils::processZIP(pack);
+ bool valid = TexturePackUtils::processZIP(pack);
QVERIFY(pack.description() == "joe biden, wake up");
+ QVERIFY(valid == true);
}
void test_parseFolder()
@@ -48,9 +49,10 @@ class TexturePackParseTest : public QObject {
QString folder_rp = FS::PathCombine(source, "test_texturefolder");
TexturePack pack { QFileInfo(folder_rp) };
- TexturePackUtils::processFolder(pack);
+ bool valid = TexturePackUtils::processFolder(pack, TexturePackUtils::ProcessingLevel::BasicInfoOnly);
QVERIFY(pack.description() == "Some texture pack surely");
+ QVERIFY(valid == true);
}
void test_parseFolder2()
@@ -60,9 +62,10 @@ class TexturePackParseTest : public QObject {
QString folder_rp = FS::PathCombine(source, "another_test_texturefolder");
TexturePack pack { QFileInfo(folder_rp) };
- TexturePackUtils::process(pack);
+ bool valid = TexturePackUtils::process(pack, TexturePackUtils::ProcessingLevel::BasicInfoOnly);
QVERIFY(pack.description() == "quieres\nfor real");
+ QVERIFY(valid == true);
}
};
diff --git a/tests/WorldSaveParse_test.cpp b/tests/WorldSaveParse_test.cpp
new file mode 100644
index 00000000..4a8c3d29
--- /dev/null
+++ b/tests/WorldSaveParse_test.cpp
@@ -0,0 +1,94 @@
+
+// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
+//
+// SPDX-License-Identifier: GPL-3.0-only
+
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <QTest>
+#include <QTimer>
+
+#include <FileSystem.h>
+
+#include <minecraft/mod/WorldSave.h>
+#include <minecraft/mod/tasks/LocalWorldSaveParseTask.h>
+
+class WorldSaveParseTest : public QObject {
+ Q_OBJECT
+
+ private slots:
+ void test_parseZIP()
+ {
+ QString source = QFINDTESTDATA("testdata/WorldSaveParse");
+
+ QString zip_ws = FS::PathCombine(source, "minecraft_save_1.zip") ;
+ WorldSave save { QFileInfo(zip_ws) };
+
+ bool valid = WorldSaveUtils::processZIP(save);
+
+ QVERIFY(save.saveFormat() == WorldSaveFormat::SINGLE);
+ QVERIFY(save.saveDirName() == "world_1");
+ QVERIFY(valid == true);
+ }
+
+ void test_parse_ZIP2()
+ {
+ QString source = QFINDTESTDATA("testdata/WorldSaveParse");
+
+ QString zip_ws = FS::PathCombine(source, "minecraft_save_2.zip") ;
+ WorldSave save { QFileInfo(zip_ws) };
+
+ bool valid = WorldSaveUtils::processZIP(save);
+
+ QVERIFY(save.saveFormat() == WorldSaveFormat::MULTI);
+ QVERIFY(save.saveDirName() == "world_2");
+ QVERIFY(valid == true);
+ }
+
+ void test_parseFolder()
+ {
+ QString source = QFINDTESTDATA("testdata/WorldSaveParse");
+
+ QString folder_ws = FS::PathCombine(source, "minecraft_save_3");
+ WorldSave save { QFileInfo(folder_ws) };
+
+ bool valid = WorldSaveUtils::processFolder(save);
+
+ QVERIFY(save.saveFormat() == WorldSaveFormat::SINGLE);
+ QVERIFY(save.saveDirName() == "world_3");
+ QVERIFY(valid == true);
+ }
+
+ void test_parseFolder2()
+ {
+ QString source = QFINDTESTDATA("testdata/WorldSaveParse");
+
+ QString folder_ws = FS::PathCombine(source, "minecraft_save_4");
+ WorldSave save { QFileInfo(folder_ws) };
+
+ bool valid = WorldSaveUtils::process(save);
+
+ QVERIFY(save.saveFormat() == WorldSaveFormat::MULTI);
+ QVERIFY(save.saveDirName() == "world_4");
+ QVERIFY(valid == true);
+ }
+};
+
+QTEST_GUILESS_MAIN(WorldSaveParseTest)
+
+#include "WorldSaveParse_test.moc"
diff --git a/tests/testdata/DataPackParse/another_test_folder/data/dummy/tags/item/foo_proof/bar.json b/tests/testdata/DataPackParse/another_test_folder/data/dummy/tags/item/foo_proof/bar.json
new file mode 100644
index 00000000..9e26dfee
--- /dev/null
+++ b/tests/testdata/DataPackParse/another_test_folder/data/dummy/tags/item/foo_proof/bar.json
@@ -0,0 +1 @@
+{} \ No newline at end of file
diff --git a/tests/testdata/DataPackParse/another_test_folder/pack.mcmeta b/tests/testdata/DataPackParse/another_test_folder/pack.mcmeta
new file mode 100644
index 00000000..5509d007
--- /dev/null
+++ b/tests/testdata/DataPackParse/another_test_folder/pack.mcmeta
@@ -0,0 +1,6 @@
+{
+ "pack": {
+ "pack_format": 6,
+ "description": "Some data pack three, leaves on the tree"
+ }
+}
diff --git a/tests/testdata/DataPackParse/test_data_pack_boogaloo.zip b/tests/testdata/DataPackParse/test_data_pack_boogaloo.zip
new file mode 100644
index 00000000..cb0b9f3c
--- /dev/null
+++ b/tests/testdata/DataPackParse/test_data_pack_boogaloo.zip
Binary files differ
diff --git a/tests/testdata/DataPackParse/test_folder/data/dummy/tags/item/foo_proof/bar.json b/tests/testdata/DataPackParse/test_folder/data/dummy/tags/item/foo_proof/bar.json
new file mode 100644
index 00000000..9e26dfee
--- /dev/null
+++ b/tests/testdata/DataPackParse/test_folder/data/dummy/tags/item/foo_proof/bar.json
@@ -0,0 +1 @@
+{} \ No newline at end of file
diff --git a/tests/testdata/DataPackParse/test_folder/pack.mcmeta b/tests/testdata/DataPackParse/test_folder/pack.mcmeta
new file mode 100644
index 00000000..dbfc7e9b
--- /dev/null
+++ b/tests/testdata/DataPackParse/test_folder/pack.mcmeta
@@ -0,0 +1,6 @@
+{
+ "pack": {
+ "pack_format": 10,
+ "description": "Some data pack, maybe"
+ }
+}
diff --git a/tests/testdata/ResourcePackParse/test_resource_pack_idk.zip b/tests/testdata/ResourcePackParse/test_resource_pack_idk.zip
index 52b91cdc..b4e66a60 100644
--- a/tests/testdata/ResourcePackParse/test_resource_pack_idk.zip
+++ b/tests/testdata/ResourcePackParse/test_resource_pack_idk.zip
Binary files differ
diff --git a/tests/testdata/ShaderPackParse/shaderpack1.zip b/tests/testdata/ShaderPackParse/shaderpack1.zip
new file mode 100644
index 00000000..9a8fb186
--- /dev/null
+++ b/tests/testdata/ShaderPackParse/shaderpack1.zip
Binary files differ
diff --git a/tests/testdata/ShaderPackParse/shaderpack2/shaders/shaders.properties b/tests/testdata/ShaderPackParse/shaderpack2/shaders/shaders.properties
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/tests/testdata/ShaderPackParse/shaderpack2/shaders/shaders.properties
diff --git a/tests/testdata/ShaderPackParse/shaderpack3.zip b/tests/testdata/ShaderPackParse/shaderpack3.zip
new file mode 100644
index 00000000..dbec042d
--- /dev/null
+++ b/tests/testdata/ShaderPackParse/shaderpack3.zip
Binary files differ
diff --git a/tests/testdata/WorldSaveParse/minecraft_save_1.zip b/tests/testdata/WorldSaveParse/minecraft_save_1.zip
new file mode 100644
index 00000000..832a243d
--- /dev/null
+++ b/tests/testdata/WorldSaveParse/minecraft_save_1.zip
Binary files differ
diff --git a/tests/testdata/WorldSaveParse/minecraft_save_2.zip b/tests/testdata/WorldSaveParse/minecraft_save_2.zip
new file mode 100644
index 00000000..6c895176
--- /dev/null
+++ b/tests/testdata/WorldSaveParse/minecraft_save_2.zip
Binary files differ
diff --git a/tests/testdata/WorldSaveParse/minecraft_save_3/world_3/level.dat b/tests/testdata/WorldSaveParse/minecraft_save_3/world_3/level.dat
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/tests/testdata/WorldSaveParse/minecraft_save_3/world_3/level.dat
diff --git a/tests/testdata/WorldSaveParse/minecraft_save_4/saves/world_4/level.dat b/tests/testdata/WorldSaveParse/minecraft_save_4/saves/world_4/level.dat
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/tests/testdata/WorldSaveParse/minecraft_save_4/saves/world_4/level.dat