Commit 7855d8b9 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Release packaging, update feed, engine patches and screen polish

- Release builds: scripts/build-macos.sh (app + DMG, engine patches applied
  idempotently) and scripts/cross-build-windows.sh (llvm-mingw cross-compile,
  Qt deploy, NSIS installer and portable zip).
- Engine patches: step over arm64 debug breaks, ignore SIGPIPE, Ctrl+Cmd+F
  fullscreen on macOS, optional non-Retina rendering.
- Launcher updates come from a configurable AL-Arcade release feed
  (ALARCADE_UPDATE_FEED_URL) and show in the sidebar on both platforms.
- Restyled the remaining upstream screens (sign-in, profile editor, game log,
  mods, troubleshooting, versions, storage) onto the shared theme.
- README, NOTICE and the GPL-3.0 LICENSE at the repository root.
Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
parent dfa036f8
......@@ -4,3 +4,4 @@
.DS_Store
*.dmg
/.cache/
*.log
This diff is collapsed.
# Notices
AL-Arcade MC Launcher is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3 (see `LICENSE`).
**NOT AN OFFICIAL MINECRAFT PRODUCT. NOT APPROVED BY OR ASSOCIATED WITH MOJANG OR
MICROSOFT.** Minecraft is a trademark of Mojang Synergies AB. Players need to own
the game: on macOS through Google Play (Android edition), on Windows through the
Microsoft Store or the Xbox app.
## Based on
| Project | License | Used for |
|---|---|---|
| [mcpelauncher](https://github.com/minecraft-linux/mcpelauncher-manifest) by MCMrARM, ChristopherHX and contributors | GPL-3.0 | Game engine (`engine/`), launcher UI the app grew from (`launcher/`), Google Play libraries (`deps/`) |
| [Qt 6](https://www.qt.io) | LGPL-3.0 | User interface |
| [ANGLE](https://chromium.googlesource.com/angle/angle) | BSD-3-Clause | OpenGL ES on Metal and Vulkan (macOS) |
| [MoltenVK](https://github.com/KhronosGroup/MoltenVK) | Apache-2.0 | Vulkan on Metal (macOS) |
| [SDL 3](https://libsdl.org) | Zlib | Audio in the game engine |
| [OpenSSL](https://www.openssl.org) | Apache-2.0 | Encryption of stored sign-in data, TLS in the engine |
| [protobuf](https://protobuf.dev), [Abseil](https://abseil.io) | BSD-3-Clause, Apache-2.0 | Google Play protocol |
| [libzip](https://libzip.org), [miniz](https://github.com/richgel999/miniz) | BSD-3-Clause, MIT | Zip archives |
| [Inter](https://rsms.me/inter/), [IBM Plex Sans Arabic](https://github.com/IBM/plex) | OFL-1.1 | Typefaces (`launcher/fonts`) |
| [Lucide](https://lucide.dev) | ISC | Icons (`launcher/icons`) |
The ANGLE and MoltenVK binaries in the macOS app are taken unmodified from the
mcpelauncher macOS release named in `scripts/build-macos.sh` and verified by
checksum. License texts ship next to each component (`launcher/fonts/*-OFL.txt`,
`launcher/icons/LICENSE.txt`, `launcher/third_party/miniz/LICENSE`).
# AL-Arcade MC Launcher
A launcher for **Minecraft: Bedrock Edition** on **macOS** and **Windows**, with
world backups, pack management and a redesigned interface. It grew out of
[mcpelauncher](https://github.com/minecraft-linux/mcpelauncher-manifest) and stays
GPL-3.0 like it.
> NOT AN OFFICIAL MINECRAFT PRODUCT. NOT APPROVED BY OR ASSOCIATED WITH MOJANG OR
> MICROSOFT. You need to own Minecraft to play.
## How it plays the game
| | macOS (Apple Silicon) | Windows 10 / 11 (x64) |
|---|---|---|
| Game | The Android edition, run natively by the mcpelauncher engine (no emulator) | The official Minecraft for Windows |
| Ownership | Google Play purchase, checked at sign-in | Microsoft Store / Xbox app purchase |
| Download | From Google Play, inside the launcher | Installed by the Microsoft Store or the Xbox app |
| Keyboard and mouse | Yes. Fullscreen: Ctrl+Cmd+F | Yes (native) |
| Versions and profiles | Any supported version, per-profile data folders | Release or Preview edition |
| Mods | Engine mods (upstream mod system) | No |
Everything else is shared by both builds:
- **Worlds**: list with size, game mode and last played; back up, export and import
`.mcworld`; restore any backup next to the original; move to Trash / Recycle Bin.
- **Automatic backups**: when the game closes, every world that changed is saved.
Keeps the last 5 per world by default (Settings > Backups).
- **Packs**: resource packs, behavior packs and skin packs; import `.mcpack` and
`.mcaddon` (an add-on with several packs is unpacked into each), replace a pack
with a newer copy of the same pack, remove packs.
- **Updates**: optional launcher update check against an AL-Arcade release feed
(see [Update feed](#update-feed)).
- **Recovery**: after a launcher crash it restarts once in safe mode.
- Right-to-left layout when the system language is Arabic or Hebrew. The interface
is translatable with Qt Linguist; the translations carried over from upstream
cover only part of the new screens.
## Installing
**macOS**: open `AL-Arcade-MC-Launcher-<version>-macOS-arm64.dmg` and drag the app
to Applications. Builds are not notarized yet, so the first launch needs
right-click > Open (or System Settings > Privacy & Security > Open Anyway).
Sign in with the Google account that bought Minecraft on Google Play. The
sign-in is stored encrypted on disk; its key lives in the macOS Keychain.
**Windows**: run `AL-Arcade-MC-Launcher-<version>-windows-x64-setup.exe` (per-user
install, no administrator rights needed) or unpack the portable `.zip`. The
installer is not code-signed yet, so SmartScreen asks for More info > Run anyway.
Install Minecraft from the Microsoft Store or the Xbox app first; the launcher
finds it and starts it.
## Building
The repository uses submodules for the engine and the Google Play libraries:
```bash
git clone --recursive https://gitlab.caprover.al-arcade.com/root/al-arcade-mc-launcher.git
# or, in an existing clone:
git submodule update --init --recursive
```
### macOS app and disk image
Requirements: Xcode command line tools and
```bash
brew install cmake ninja pkgconf automake qtbase qtdeclarative qtsvg qttools \
qtshadertools openssl@3 protobuf abseil libzip
```
```bash
scripts/build-macos.sh --dmg # dist/AL-Arcade MC Launcher.app + .dmg
scripts/build-macos.sh --skip-engine # launcher only, reuses the last engine build
```
The script applies the engine patches in `patches/engine/`, builds the engine
and the launcher, compiles the Swift helpers (Xbox sign-in and error windows),
bundles Qt with `macdeployqt`, adds ANGLE and MoltenVK from the upstream macOS
release (fixed version, verified by SHA-256) and signs the app ad hoc.
### Windows (cross-compiled on macOS)
```bash
brew install cmake ninja makensis qtbase qtdeclarative qtsvg qttools qtshadertools
scripts/cross-build-windows.sh
```
The first run downloads [llvm-mingw](https://github.com/mstorsjo/llvm-mingw) and
Qt for Windows into `.cache/` (checksummed; nothing is installed system-wide). It
produces the installer, a portable `.zip` and their SHA-256 sums in `dist/`.
The Homebrew Qt version must match `QT_VERSION` (default 6.11.2).
A native build on Windows uses the same CMake project:
`cmake -B build -G Ninja -DALARCADE_BACKEND=windows -DCMAKE_PREFIX_PATH=<Qt>`.
It is not scripted yet.
### Build options
| CMake option / variable | Effect |
|---|---|
| `ALARCADE_BACKEND` | `android` (engine; default on macOS) or `windows` (default on Windows) |
| `ALARCADE_UPDATE_FEED_URL` | HTTPS URL of the release feed. Empty: no update checks. Both build scripts pass it through from the environment |
| `LAUNCHER_VERSION_NAME`, `LAUNCHER_VERSION_CODE` | Set by the scripts from `VERSION` and the commit count (`BUILD_NUMBER` overrides) |
| `MACOSX_DEPLOYMENT_TARGET` | Requested minimum macOS for the engine and launcher (default 12.0) |
## Update feed
The launcher compares its build number with a JSON file:
```json
{
"version": "1.1.0",
"build": 42,
"notes": "What changed",
"macos": { "url": "https://.../AL-Arcade-MC-Launcher-1.1.0-macOS-arm64.dmg", "sha256": "..." },
"windows": { "url": "https://.../AL-Arcade-MC-Launcher-1.1.0-windows-x64-setup.exe", "sha256": "..." }
}
```
When `build` is higher than the running build and the platform entry has an
`https://` URL, a sidebar entry offers the download. `ALARCADE_UPDATE_FEED=<url>`
overrides the built-in URL for testing.
## Repository layout
| Path | Contents |
|---|---|
| `launcher/` | The launcher (Qt 6 / QML). `qml/main.qml` is the shared shell; `qml/android/` and `qml/windows/` are the two game backends; `qml/pages/` the shared pages; `core/` worlds, packs, zip and level.dat code |
| `launcher/macos/`, `launcher/windows/` | Native code: Keychain and Credential Manager storage, Google sign-in window (WebKit), Minecraft for Windows detection and launch, crash handler |
| `engine/` | mcpelauncher engine (submodule), patched by `patches/engine/` at build time |
| `deps/` | Google Play API and sign-in libraries (submodules) |
| `macos/` | Info.plist template and the Swift helper apps |
| `windows/` | NSIS installer script |
| `branding/`, `tools/brand/` | App icons and the scripts that render them |
| `scripts/` | Release builds for macOS and Windows |
| `tools/dev/screenshots.sh` | Renders every page to PNG for design review |
### Engine patches
| Patch | Fixes |
|---|---|
| `mcpelauncher-core/0001-arm64-step-over-debug-breaks` | Debug break instructions in the game crashed or hung it on Apple Silicon; they are now stepped over |
| `mcpelauncher-client/0001-ignore-sigpipe` | The game quit when a network connection dropped (for example with a VPN) |
| `mcpelauncher-client/0002-macos-fullscreen-shortcut` | Ctrl+Cmd+F toggles fullscreen; F11 is taken by macOS |
| `game-window/0001-optional-non-retina-rendering` | Setting to render at half resolution on Retina screens for more frames per second |
## Known limitations
- macOS builds are Apple Silicon only. Their minimum macOS version follows the
Homebrew libraries they bundle (currently macOS 27); building those
dependencies for an older deployment target is still to do.
- The Windows build is cross-compiled and checked on macOS; it has not yet been
run on a Windows machine.
- Builds are unsigned: no Apple notarization, no Authenticode signature.
- World thumbnails that the game saves as a single color are shown as a
placeholder icon.
## License
GPL-3.0, see `LICENSE`. Credits and third-party licenses are in `NOTICE.md`.
Minecraft is a trademark of Mojang Synergies AB.
2026-10-05 06:50:01,146 - aqt.main - INFO - installer 8471887744 aqtinstall(aqt) v3.3.0 on Python 3.14.7 [CPython Clang 21.0.0 (clang-2100.1.1.101)]
2026-10-05 06:50:01,147 - aqt.helper - DEBUG - helper 8471887744 Attempt to download checksum at https://download.qt.io/online/qtsdkrepository/windows_x86/desktop/qt6_6112/qt6_6112/Updates.xml.sha256
2026-10-05 06:50:01,740 - aqt.helper - DEBUG - helper 8471887744 Attempt to download checksum at https://download.qt.io/online/qtsdkrepository/windows_x86/desktop/qt6_6112/qt6_6112/Updates.xml.sha256
2026-10-05 06:50:02,159 - aqt.helper - DEBUG - helper 8471887744 Attempt to download checksum at https://download.qt.io/online/qtsdkrepository/windows_x86/desktop/qt6_6112/qt6_6112/Updates.xml.sha256
2026-10-05 06:50:02,542 - aqt.helper - DEBUG - helper 8471887744 Attempt to download checksum at https://download.qt.io/online/qtsdkrepository/windows_x86/desktop/qt6_6112/qt6_6112/Updates.xml.sha256
2026-10-05 06:50:02,975 - aqt.helper - DEBUG - helper 8471887744 Attempt to download checksum at https://download.qt.io/online/qtsdkrepository/windows_x86/desktop/qt6_6112/qt6_6112/Updates.xml.sha256
2026-10-05 06:50:03,408 - aqt.archives - WARNING - archives 8471887744 Failed to download checksum for the file 'online/qtsdkrepository/windows_x86/desktop/qt6_6112/qt6_6112/Updates.xml'. This may happen on unofficial mirrors.
2026-10-05 06:50:03,828 - aqt.main - ERROR - installer 8471887744 Failed to locate XML data for Qt version '6.11.2'.
==============================Suggested follow-up:==============================
* Please use 'aqt list-qt windows desktop' to show versions available.
2026-10-05 06:51:28,664 - aqt.main - INFO - installer 8471887744 aqtinstall(aqt) v3.3.0 on Python 3.14.7 [CPython Clang 21.0.0 (clang-2100.1.1.101)]
2026-10-05 06:51:29,946 - aqt.helper - DEBUG - helper 8471887744 Attempt to download checksum at https://download.qt.io/online/qtsdkrepository/windows_x86/desktop/qt6_6112/qt6_6112/Updates.xml.sha256
2026-10-05 06:51:30,766 - aqt.helper - DEBUG - helper 8471887744 Attempt to download checksum at https://download.qt.io/online/qtsdkrepository/windows_x86/desktop/qt6_6112/qt6_6112/Updates.xml.sha256
2026-10-05 06:51:31,643 - aqt.helper - DEBUG - helper 8471887744 Attempt to download checksum at https://download.qt.io/online/qtsdkrepository/windows_x86/desktop/qt6_6112/qt6_6112/Updates.xml.sha256
2026-10-05 06:51:32,636 - aqt.helper - DEBUG - helper 8471887744 Attempt to download checksum at https://download.qt.io/online/qtsdkrepository/windows_x86/desktop/qt6_6112/qt6_6112/Updates.xml.sha256
2026-10-05 06:51:33,909 - aqt.helper - DEBUG - helper 8471887744 Attempt to download checksum at https://download.qt.io/online/qtsdkrepository/windows_x86/desktop/qt6_6112/qt6_6112/Updates.xml.sha256
2026-10-05 06:51:35,056 - aqt.main - ERROR - installer 8471887744 Failed to download checksum for the file 'Updates.xml' from mirrors '['https://download.qt.io']
==============================Suggested follow-up:==============================
* Check your internet connection
* Consider modifying `requests.max_retries_to_retrieve_hash` in settings.ini
* Consider modifying `mirrors.trusted_mirrors` in settings.ini (see https://aqtinstall.readthedocs.io/en/stable/configuration.html#configuration)
* Please use 'aqt list-qt windows desktop' to show versions of Qt available.
2026-10-05 06:51:35,257 - aqt.helper - DEBUG - helper 8471887744 Attempt to download checksum at https://download.qt.io/online/qtsdkrepository/windows_x86/desktop/qt6_6111/qt6_6111/Updates.xml.sha256
2026-10-05 06:51:36,334 - aqt.helper - DEBUG - helper 8471887744 Attempt to download checksum at https://download.qt.io/online/qtsdkrepository/windows_x86/desktop/qt6_6111/qt6_6111/Updates.xml.sha256
2026-10-05 06:51:37,471 - aqt.helper - DEBUG - helper 8471887744 Attempt to download checksum at https://download.qt.io/online/qtsdkrepository/windows_x86/desktop/qt6_6111/qt6_6111/Updates.xml.sha256
2026-10-05 06:51:38,432 - aqt.helper - DEBUG - helper 8471887744 Attempt to download checksum at https://download.qt.io/online/qtsdkrepository/windows_x86/desktop/qt6_6111/qt6_6111/Updates.xml.sha256
2026-10-05 06:51:39,525 - aqt.helper - DEBUG - helper 8471887744 Attempt to download checksum at https://download.qt.io/online/qtsdkrepository/windows_x86/desktop/qt6_6111/qt6_6111/Updates.xml.sha256
2026-10-05 06:51:40,736 - aqt.main - ERROR - installer 8471887744 Failed to download checksum for the file 'Updates.xml' from mirrors '['https://download.qt.io']
==============================Suggested follow-up:==============================
* Check your internet connection
* Consider modifying `requests.max_retries_to_retrieve_hash` in settings.ini
* Consider modifying `mirrors.trusted_mirrors` in settings.ini (see https://aqtinstall.readthedocs.io/en/stable/configuration.html#configuration)
* Please use 'aqt list-qt windows desktop' to show versions of Qt available.
2026-10-05 06:51:40,939 - aqt.helper - DEBUG - helper 8471887744 Attempt to download checksum at https://download.qt.io/online/qtsdkrepository/windows_x86/desktop/qt6_6110/qt6_6110/Updates.xml.sha256
2026-10-05 06:51:42,121 - aqt.helper - DEBUG - helper 8471887744 Attempt to download checksum at https://download.qt.io/online/qtsdkrepository/windows_x86/desktop/qt6_6110/qt6_6110/Updates.xml.sha256
2026-10-05 06:51:43,352 - aqt.helper - DEBUG - helper 8471887744 Attempt to download checksum at https://download.qt.io/online/qtsdkrepository/windows_x86/desktop/qt6_6110/qt6_6110/Updates.xml.sha256
2026-10-05 06:51:44,304 - aqt.helper - DEBUG - helper 8471887744 Attempt to download checksum at https://download.qt.io/online/qtsdkrepository/windows_x86/desktop/qt6_6110/qt6_6110/Updates.xml.sha256
2026-10-05 06:51:45,443 - aqt.helper - DEBUG - helper 8471887744 Attempt to download checksum at https://download.qt.io/online/qtsdkrepository/windows_x86/desktop/qt6_6110/qt6_6110/Updates.xml.sha256
2026-10-05 06:51:46,645 - aqt.main - ERROR - installer 8471887744 Failed to download checksum for the file 'Updates.xml' from mirrors '['https://download.qt.io']
==============================Suggested follow-up:==============================
* Check your internet connection
* Consider modifying `requests.max_retries_to_retrieve_hash` in settings.ini
* Consider modifying `mirrors.trusted_mirrors` in settings.ini (see https://aqtinstall.readthedocs.io/en/stable/configuration.html#configuration)
* Please use 'aqt list-qt windows desktop' to show versions of Qt available.
2026-10-05 06:52:03,219 - aqt.helper - DEBUG - helper 8471887744 Attempt to download checksum at https://download.qt.io/online/qtsdkrepository/windows_x86/desktop/qt6_6112/qt6_6112/Updates.xml.sha256
2026-10-05 06:52:04,357 - aqt.helper - DEBUG - helper 8471887744 Attempt to download checksum at https://download.qt.io/online/qtsdkrepository/windows_x86/desktop/qt6_6112/qt6_6112/Updates.xml.sha256
2026-10-05 06:52:05,406 - aqt.helper - DEBUG - helper 8471887744 Attempt to download checksum at https://download.qt.io/online/qtsdkrepository/windows_x86/desktop/qt6_6112/qt6_6112/Updates.xml.sha256
2026-10-05 06:52:06,555 - aqt.helper - DEBUG - helper 8471887744 Attempt to download checksum at https://download.qt.io/online/qtsdkrepository/windows_x86/desktop/qt6_6112/qt6_6112/Updates.xml.sha256
2026-10-05 06:52:07,486 - aqt.helper - DEBUG - helper 8471887744 Attempt to download checksum at https://download.qt.io/online/qtsdkrepository/windows_x86/desktop/qt6_6112/qt6_6112/Updates.xml.sha256
2026-10-05 06:52:08,561 - aqt.main - ERROR - installer 8471887744 Failed to download checksum for the file 'Updates.xml' from mirrors '['https://download.qt.io']
==============================Suggested follow-up:==============================
* Check your internet connection
* Consider modifying `requests.max_retries_to_retrieve_hash` in settings.ini
* Consider modifying `mirrors.trusted_mirrors` in settings.ini (see https://aqtinstall.readthedocs.io/en/stable/configuration.html#configuration)
* Please use 'aqt list-qt windows desktop' to show versions of Qt available.
2026-10-05 06:52:08,764 - aqt.helper - DEBUG - helper 8471887744 Attempt to download checksum at https://download.qt.io/online/qtsdkrepository/windows_x86/desktop/qt6_6112/qt6_6112/Updates.xml.sha256
2026-10-05 06:52:09,865 - aqt.helper - DEBUG - helper 8471887744 Attempt to download checksum at https://download.qt.io/online/qtsdkrepository/windows_x86/desktop/qt6_6112/qt6_6112/Updates.xml.sha256
2026-10-05 06:52:11,016 - aqt.helper - DEBUG - helper 8471887744 Attempt to download checksum at https://download.qt.io/online/qtsdkrepository/windows_x86/desktop/qt6_6112/qt6_6112/Updates.xml.sha256
2026-10-05 06:52:12,077 - aqt.helper - DEBUG - helper 8471887744 Attempt to download checksum at https://download.qt.io/online/qtsdkrepository/windows_x86/desktop/qt6_6112/qt6_6112/Updates.xml.sha256
2026-10-05 06:52:13,231 - aqt.helper - DEBUG - helper 8471887744 Attempt to download checksum at https://download.qt.io/online/qtsdkrepository/windows_x86/desktop/qt6_6112/qt6_6112/Updates.xml.sha256
2026-10-05 06:52:14,313 - aqt.main - ERROR - installer 8471887744 Failed to download checksum for the file 'Updates.xml' from mirrors '['https://download.qt.io']
==============================Suggested follow-up:==============================
* Check your internet connection
* Consider modifying `requests.max_retries_to_retrieve_hash` in settings.ini
* Consider modifying `mirrors.trusted_mirrors` in settings.ini (see https://aqtinstall.readthedocs.io/en/stable/configuration.html#configuration)
* Please use 'aqt list-qt windows desktop' to show versions of Qt available.
* Please use 'aqt list-qt windows desktop --arch <QT_VERSION>' to list valid architectures.
......@@ -15,6 +15,7 @@ find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Network Quick QuickControl
set(LAUNCHER_VERSION_NAME "${PROJECT_VERSION}" CACHE STRING "Version name shown in the app")
set(LAUNCHER_VERSION_CODE "1" CACHE STRING "Integer build number, used for changelog and update checks")
set(LAUNCHER_CHANGE_LOG "" CACHE STRING "Changelog shown after an update")
set(ALARCADE_UPDATE_FEED_URL "" CACHE STRING "HTTPS URL of the release feed (latest.json); empty disables update checks")
option(LAUNCHER_DISABLE_DEV_MODE "Hide the developer settings unless started with --enable-devmode" ON)
# ---------------------------------------------------------------- shared
......@@ -168,6 +169,9 @@ target_compile_definitions(${APP_TARGET} PRIVATE
if(ALARCADE_BACKEND STREQUAL "android" AND NOT "${LAUNCHER_VERSIONDB_URL}" STREQUAL "")
target_compile_definitions(${APP_TARGET} PRIVATE LAUNCHER_VERSIONDB_URL="${LAUNCHER_VERSIONDB_URL}")
endif()
if(NOT "${ALARCADE_UPDATE_FEED_URL}" STREQUAL "")
target_compile_definitions(${APP_TARGET} PRIVATE ALARCADE_UPDATE_FEED_URL="${ALARCADE_UPDATE_FEED_URL}")
endif()
if(NOT "${LAUNCHER_CHANGE_LOG}" STREQUAL "")
target_compile_definitions(${APP_TARGET} PRIVATE LAUNCHER_CHANGE_LOG="${LAUNCHER_CHANGE_LOG}")
endif()
......
#include "gamelauncher.h"
#include "launchersettings.h"
#include "profilemanager.h"
#include "EnvPathUtil.h"
#include <QFile>
......@@ -31,6 +32,10 @@ void GameLauncher::start(bool disableGameLog, QString arch, bool hasVerifiedLice
QStringList args;
QStringList cargs;
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
#ifdef __APPLE__
if (!LauncherSettings().retinaRendering())
env.insert("ALARCADE_RETINA", "0");
#endif
if (m_gameDir.length() > 0) {
args.append("-dg");
args.append(m_gameDir);
......
......@@ -32,6 +32,7 @@ class LauncherSettings : public QObject {
Q_PROPERTY(bool trialMode READ trialMode WRITE setTrialMode NOTIFY settingsChanged)
Q_PROPERTY(bool welcomeDone READ welcomeDone WRITE setWelcomeDone NOTIFY settingsChanged)
Q_PROPERTY(QString windowsEdition READ windowsEdition WRITE setWindowsEdition NOTIFY settingsChanged)
Q_PROPERTY(bool retinaRendering READ retinaRendering WRITE setRetinaRendering NOTIFY settingsChanged)
private:
QSettings settings;
......@@ -113,6 +114,10 @@ public:
bool welcomeDone() const { return settings.value("welcomeDone", false).toBool(); }
void setWelcomeDone(bool value) { settings.setValue("welcomeDone", value); emit settingsChanged(); }
// macOS engine: render at full Retina resolution (off = 1x, faster).
bool retinaRendering() const { return settings.value("retinaRendering", true).toBool(); }
void setRetinaRendering(bool value) { settings.setValue("retinaRendering", value); emit settingsChanged(); }
// Windows backend: "release" or "preview"
QString windowsEdition() const { return settings.value("windowsEdition", "release").toString(); }
void setWindowsEdition(QString value) { settings.setValue("windowsEdition", value); emit settingsChanged(); }
......
......@@ -24,9 +24,9 @@ BaseScreen {
padding: 15
textFormat: TextEdit.RichText
text: "<b>Welcome to the new Minecraft Linux Launcher Update</b><br/><br/>" + LAUNCHER_CHANGE_LOG
color: "#fff"
color: Theme.text
readOnly: true
font.pointSize: 10
font.pixelSize: Theme.fontBody
wrapMode: Text.WordWrap
selectByMouse: true
}
......@@ -35,7 +35,7 @@ BaseScreen {
Rectangle {
Layout.fillWidth: true
Layout.minimumHeight: pbutton.height + 10 * 2
color: "#242424"
color: Theme.background
MButton {
id: pbutton
text: qsTr("Continue")
......
......@@ -2,7 +2,8 @@ import QtQuick
import QtQuick.Templates as T
// Navigation entry in the sidebar. Collapses to an icon with a tooltip when
// `showText` is false.
// `showText` is false. `highlighted` tints an entry that is an action rather
// than a page (no current-page marker).
T.Button {
id: control
......@@ -19,7 +20,7 @@ T.Button {
background: Rectangle {
radius: Theme.radius
color: control.checked ? Theme.accentSoft : control.down ? Theme.surfacePressed : control.hovered ? Theme.surfaceHover : "transparent"
color: control.checked || control.highlighted ? Theme.accentSoft : control.down ? Theme.surfacePressed : control.hovered ? Theme.surfaceHover : "transparent"
Behavior on color {
ColorAnimation { duration: Theme.durationFast }
......@@ -55,16 +56,16 @@ T.Button {
anchors.verticalCenter: parent.verticalCenter
iconName: control.iconName
size: 18
color: control.checked ? Theme.accentText : control.hovered ? Theme.text : Theme.textSecondary
color: control.checked || control.highlighted ? Theme.accentText : control.hovered ? Theme.text : Theme.textSecondary
}
Text {
anchors.verticalCenter: parent.verticalCenter
visible: control.showText
text: control.text
color: control.checked ? Theme.text : control.hovered ? Theme.text : Theme.textSecondary
color: control.checked || control.highlighted || control.hovered ? Theme.text : Theme.textSecondary
font.pixelSize: Theme.fontBody
font.weight: control.checked ? Font.DemiBold : Font.Medium
font.weight: control.checked || control.highlighted ? Font.DemiBold : Font.Medium
}
}
......
......@@ -67,6 +67,7 @@ QtObject {
readonly property int fontBody: 14
readonly property int fontSmall: 12
readonly property int fontCaption: 11
readonly property string fontMono: Qt.platform.os === "osx" ? "Menlo" : Qt.platform.os === "windows" ? "Consolas" : "monospace"
// Spacing (4 px grid)
readonly property int space1: 4
......
......@@ -26,8 +26,8 @@ BaseScreen {
Layout.margins: 15
textFormat: TextEdit.RichText
text: errorScreen.message
font.pointSize: 10
color: "#fff"
font.pixelSize: Theme.fontBody
color: Theme.text
readOnly: true
wrapMode: Text.WordWrap
selectByMouse: true
......@@ -37,7 +37,7 @@ BaseScreen {
Layout.fillWidth: true
Layout.minimumHeight: pbutton.height + 10 * 2
color: "#242424"
color: Theme.background
MButton {
id: pbutton
......
......@@ -48,7 +48,7 @@ ColumnLayout {
Layout.fillHeight: true
Layout.columnSpan: articleImage.ratio > 1.5 ? 2 : 1
Layout.rowSpan: articleImage.ratio < 0.5 ? 2 : 1
color: "#222"
color: Theme.surface
Image {
id: articleImage
......@@ -66,7 +66,7 @@ ColumnLayout {
width: parent.width
height: descriptionContent.height
anchors.bottom: parent.bottom
color: "#111"
color: Theme.background
Column {
id: descriptionContent
width: parent.width
......@@ -75,16 +75,16 @@ ColumnLayout {
Text {
text: name
width: parent.width - 2 * parent.padding
color: "#fff"
font.pointSize: 13
color: Theme.text
font.pixelSize: Theme.fontHeading
font.weight: Font.Bold
wrapMode: Text.WordWrap
}
Text {
text: description
width: parent.width - 2 * parent.padding
color: "#bbb"
font.pointSize: 10
color: Theme.textSecondary
font.pixelSize: Theme.fontBody
wrapMode: Text.WordWrap
}
}
......
......@@ -50,7 +50,7 @@ ColumnLayout {
if (section.revealEmail || mail.length < 4)
return mail
const at = mail.indexOf("@")
return mail.substring(0, 2) + "•".repeat(Math.max(3, (at > 2 ? at : mail.length) - 2)) + (at > 0 ? mail.substring(at) : "")
return mail.substring(0, 2) + "*".repeat(Math.max(3, (at > 2 ? at : mail.length) - 2)) + (at > 0 ? mail.substring(at) : "")
}
color: Theme.textMuted
font.pixelSize: Theme.fontSmall + 1
......@@ -132,6 +132,24 @@ ColumnLayout {
}
}
SectionTitle {
Layout.topMargin: Theme.space2
text: qsTr("Performance")
}
Card {
Layout.fillWidth: true
visible: Platform.isMac
SettingRow {
width: parent.width
title: qsTr("High-resolution rendering")
description: qsTr("Draws the game at full Retina resolution. Turn it off for a smoother frame rate, especially on M1 and M2 Macs. Takes effect the next time the game starts.")
MSwitch {
checked: launcherSettings.retinaRendering
onToggled: launcherSettings.retinaRendering = checked
}
}
}
SectionTitle {
Layout.topMargin: Theme.space2
text: qsTr("Troubleshooting")
......
......@@ -15,7 +15,7 @@ Window {
flags: Qt.Dialog
title: "Minecraft .apk import"
visible: apkImportHelper.extractingApk
color: "#333"
color: Theme.surfaceRaised
property bool allowIncompatible: false
......
......@@ -10,70 +10,42 @@ ColumnLayout {
spacing: 0
property var launcher: null
BaseHeader {
title: qsTr("Game Log")
MButton {
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: 10
height: 34
width: 34
onClicked: {
function copyLog() {
var text = ""
for (var i = 0; i < gameLog.count; i++) {
for (var i = 0; i < gameLog.count; i++)
text += gameLog.get(i).display + "\n"
}
launcherSettings.clipboard = text
window.notify(qsTr("Copied %n line(s) of the game log.", "", gameLog.count), "success")
}
Image {
height: 18
width: 18
anchors.centerIn: parent
source: "qrc:/Resources/icon-copy.png"
smooth: false
}
BaseHeader {
title: qsTr("Game Log")
actions: [
MButton {
text: qsTr("Copy log")
iconName: "copy"
enabled: gameLog.count > 0
onClicked: layout.copyLog()
}
]
}
Rectangle {
id: rectangle
property int padding: 20
color: "#821"
ColumnLayout {
Layout.fillWidth: true
Layout.preferredHeight: children[0].implicitHeight + padding * 2
Layout.alignment: Qt.AlignTop
Layout.leftMargin: Theme.space8
Layout.rightMargin: Theme.space8
Layout.bottomMargin: Theme.space4
visible: launcher.crashed
z: 2
spacing: Theme.space3
ColumnLayout {
x: rectangle.padding
y: rectangle.padding
width: parent.width - rectangle.padding * 2
MText {
Layout.fillWidth: true
Layout.bottomMargin: 4
wrapMode: Text.WordWrap
font.weight: Font.Bold
text: qsTr("Minecraft stopped working")
}
MText {
Layout.fillWidth: true
wrapMode: Text.WordWrap
text: (launcherSettings.disableGameLog || incompatibleStateText.visible) ? qsTr("Please don't report this error.") : qsTr("Game has exited with a non-zero error code. <a href='%1'>Click here if you would like to open an issue.</a>").arg("https://github.com/minecraft-linux/mcpelauncher-manifest/issues/new/choose")
linkColor: hoveredLink ? "#acf" : "#9bf"
onLinkActivated: Qt.openUrlExternally(link)
HoverHandler {
enabled: parent.hoveredLink
cursorShape: Qt.PointingHandCursor
}
}
MText {
id: incompatibleStateText
NotifyBanner {
Layout.fillWidth: true
wrapMode: Text.WordWrap
text: {
var msg = qsTr("You may be running an incompatible version. Uncheck the following toggles in settings and relaunch the game: ")
tone: "danger"
dismissible: false
title: qsTr("Minecraft stopped working")
description: {
if (launcherSettings.disableGameLog)
return qsTr("The game log is turned off. Turn it on in Settings > Game and play again to see what went wrong.")
var toggles = []
if (launcherSettings.showUnsupported)
toggles.push(qsTr("Show incompatible versions"))
......@@ -81,22 +53,36 @@ ColumnLayout {
toggles.push(qsTr("Show unverified versions"))
if (launcherSettings.showBetaVersions)
toggles.push(qsTr("Show beta versions"))
return msg + toggles.join(", ") + "."
if (toggles.length > 0)
return qsTr("You may be running a version the launcher does not support. Turn off these settings and play again: %1.").arg(toggles.join(", "))
return qsTr("The game exited with an error. The log below shows what happened; copy it into a bug report.")
}
visible: launcherSettings.showUnsupported || launcherSettings.showUnverified || launcherSettings.showBetaVersions
actionText: gameLog.count > 0 ? qsTr("Copy log") : ""
onClicked: layout.copyLog()
}
MText {
Layout.fillWidth: true
wrapMode: Text.WordWrap
text: qsTr("Enable game log in settings and relaunch the game to get error log.")
visible: launcherSettings.disableGameLog
}
// A wrapper that fills the page: EmptyState is a layout and cannot grow.
Item {
Layout.fillWidth: true
Layout.fillHeight: true
visible: gameLog.count === 0
EmptyState {
anchors.centerIn: parent
width: Math.min(parent.width - 2 * Theme.space8, 440)
iconName: "square-terminal"
title: qsTr("No game output yet")
description: qsTr("While Minecraft runs, the engine's log shows up here. It helps when something goes wrong: copy it into a bug report.")
}
}
ListView {
Layout.fillHeight: true
Layout.fillWidth: true
Layout.leftMargin: Theme.space8
Layout.rightMargin: Theme.space4
visible: gameLog.count > 0
id: view
model: gameLog
delegate: TextArea {
......@@ -108,10 +94,11 @@ ColumnLayout {
selectByMouse: false
selectByKeyboard: false
property int myIndex: index
color: "#ddd"
color: Theme.text
background: null
selectionColor: "#842"
font.family: "monospace"
selectionColor: Theme.accent
font.family: Theme.fontMono
font.pixelSize: Theme.fontSmall
Connections {
target: selectionArea
......
......@@ -12,7 +12,7 @@ ApplicationWindow {
minimumWidth: 400
minimumHeight: 300
title: qsTr("Gamepad Tool")
color: "#333"
color: Theme.surfaceRaised
property var currentGamepad: GamepadManager.gamepads.length > 0 ? GamepadManager.gamepads[0] : null
......@@ -23,7 +23,7 @@ ApplicationWindow {
Layout.alignment: Qt.AlignHCenter
text: qsTr("No Gamepads Found!")
font.bold: true
font.pointSize: 12
font.pixelSize: Theme.fontHeading
}
MText {
Layout.alignment: Qt.AlignHCenter
......@@ -80,7 +80,7 @@ ApplicationWindow {
Layout.fillWidth: true
readOnly: true
text: currentGamepad ? currentGamepad.guid : ""
color: "#888"
color: Theme.textMuted
}
}
ColumnLayout {
......@@ -93,7 +93,7 @@ ApplicationWindow {
MTextField {
Layout.fillWidth: true
readOnly: true
color: "#888"
color: Theme.textMuted
text: currentGamepad && currentGamepad.hasMapping ? "True" : "False"
}
}
......@@ -113,8 +113,8 @@ ApplicationWindow {
model: ["a", "b", "x", "y", "leftshoulder", "rightshoulder", "righttrigger", "lefttrigger", "back", "start", "leftstick", "rightstick", "guide", "dpleft", "dpdown", "dpright", "dpup", "leftx", "lefty", "rightx", "righty"]
Rectangle {
id: field
color: "#222"
border.color: "#444"
color: Theme.surface
border.color: Theme.border
Layout.fillWidth: true
Layout.minimumHeight: 48
......@@ -137,7 +137,7 @@ ApplicationWindow {
}
MText {
text: field.waiting ? qsTr("Waiting") : field.key.toUpperCase()
color: field.waiting ? "#888" : "#af8"
color: field.waiting ? Theme.textMuted : Theme.success
Layout.rightMargin: 5
}
MButton {
......@@ -212,7 +212,7 @@ ApplicationWindow {
Layout.fillWidth: true
Layout.preferredHeight: 40
readOnly: true
color: "#888"
color: Theme.textMuted
text: {
if (!currentGamepad)
return ""
......@@ -252,7 +252,7 @@ ApplicationWindow {
footer: Rectangle {
height: pbutton.height + 20
color: "#242424"
color: Theme.background
MButton {
id: pbutton
anchors.right: parent.right
......
......@@ -335,24 +335,6 @@ Item {
visible: (googleLoginHelper.account !== null) && launcherLatestVersionBase().versionCode > playVerChannel.latestVersionCode && launcherSettings.showNotifications
}
NotifyBanner {
Layout.fillWidth: true
tone: "accent"
iconName: "download"
visible: hasUpdate && !(progressbarVisible || updateChecker.active) && launcherSettings.showNotifications
title: qsTr("Launcher update available")
description: qsTr("A newer %1 is ready to download.").arg(APP_NAME)
actionText: qsTr("Download")
onClicked: {
if (updateDownloadUrl.length == 0) {
updateCheckerConnectorBase.enabled = true
updateChecker.startUpdate()
} else {
Qt.openUrlExternally(updateDownloadUrl)
}
}
}
NotifyBanner {
Layout.fillWidth: true
tone: "accent"
......
......@@ -4,7 +4,8 @@ import QtQuick.Dialogs
import "../Components"
import io.mrarm.mcpelauncher 1.0
Item {
// First-run Google Play sign-in for the Android backend.
Rectangle {
id: root
property GoogleLoginHelper googleLoginHelper
......@@ -14,142 +15,157 @@ Item {
signal finished
Component.onCompleted: {
launcherSettings.trialMode = false
}
color: Theme.background
Component.onCompleted: launcherSettings.trialMode = false
Image {
anchors.fill: parent
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
height: parent.height * 0.6
source: "qrc:/Resources/hero.png"
fillMode: Image.PreserveAspectCrop
smooth: false
fillMode: Image.Tile
source: "qrc:/Resources/noise.png"
opacity: 0.8
}
Rectangle {
width: 420
height: container.implicitHeight + 70
anchors.fill: parent
gradient: Gradient {
GradientStop { position: 0.0; color: "#00000000" }
GradientStop { position: 0.3; color: Qt.rgba(0.055, 0.035, 0.08, 0.4) }
GradientStop { position: 0.62; color: Theme.background }
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
}
Card {
anchors.centerIn: parent
radius: 4
visible: !extractingApk
color: "#222"
width: Math.min(parent.width - 2 * Theme.space8, 460)
padding: Theme.space8
visible: !root.extractingApk
ColumnLayout {
id: container
spacing: 10
x: 35
y: 35
width: parent.width - 70
width: parent.width
spacing: Theme.space4
Image {
Layout.alignment: Qt.AlignHCenter
source: "qrc:/Resources/logo-mark.svg"
sourceSize: Qt.size(44, 44)
smooth: false
}
Text {
text: qsTr("Sign in")
font.pointSize: 16
font.bold: true
color: "#fff"
Layout.fillWidth: true
text: qsTr("Sign in to Google Play")
color: Theme.text
font.pixelSize: Theme.fontTitle
font.weight: Font.Bold
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
Text {
Layout.fillWidth: true
text: qsTr("Use the Google account that bought Minecraft on Google Play. The launcher checks your purchase and downloads the Android edition from there.")
color: Theme.textSecondary
font.pixelSize: Theme.fontBody
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
lineHeight: 1.15
}
ColumnLayout {
Layout.fillWidth: true
Layout.topMargin: Theme.space1
spacing: Theme.space2
Repeater {
model: [
{ icon: "lock", text: qsTr("Your password goes only to Google.") },
{ icon: "key-round", text: Platform.isMac ? qsTr("The sign-in is stored encrypted, with its key in your Keychain.") : qsTr("The sign-in is stored encrypted on this computer.") },
]
RowLayout {
required property var modelData
Layout.fillWidth: true
spacing: Theme.space2
AppIcon {
iconName: modelData.icon
size: 16
color: Theme.accentText
}
Text {
text: qsTr("To use this launcher, you must either purchase Minecraft Android or claim the free Minecraft Trial on Google Play and sign in.\n\nThe free Minecraft (ChromeOS) Trial Mode can be enabled after sign in inside Settings.")
wrapMode: Text.WordWrap
font.pointSize: 11
color: "#fff"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
Layout.alignment: Qt.AlignTop
Layout.fillWidth: true
Layout.topMargin: 10
text: modelData.text
color: Theme.textMuted
font.pixelSize: Theme.fontSmall + 1
wrapMode: Text.Wrap
}
}
}
}
PlayButton {
text: qsTr("Sign in with Google")
MButton {
Layout.fillWidth: true
Layout.topMargin: 10
onClicked: function () {
acquiringAccount = true
Layout.topMargin: Theme.space2
kind: "primary"
large: true
text: qsTr("Sign in with Google")
iconName: "log-in"
enabled: !root.acquiringAccount
onClicked: {
root.acquiringAccount = true
googleLoginHelper.acquireAccount(window)
}
}
RowLayout {
id: alternativeOptions
Layout.alignment: Qt.AlignHCenter
spacing: 15
spacing: Theme.space2
TransparentButton {
text: qsTr("Use trial .apk").toUpperCase()
textColor: "#0aa82f"
Layout.fillWidth: true
font.pointSize: 11
text: qsTr("Use the free trial")
onClicked: apkImportHelper.pickFile()
}
TransparentButton {
text: qsTr("Get help").toUpperCase()
textColor: "#0aa82f"
Layout.fillWidth: true
font.pointSize: 11
onClicked: Qt.openUrlExternally("https://minecraft-linux.github.io")
text: qsTr("Not now")
onClicked: root.finished()
}
}
}
}
Rectangle {
width: 400
height: extractContainer.height
visible: extractingApk
color: "#222"
Card {
anchors.centerIn: parent
width: Math.min(parent.width - 2 * Theme.space8, 420)
visible: root.extractingApk
ColumnLayout {
id: extractContainer
spacing: 0
width: parent.width
spacing: Theme.space3
Text {
text: qsTr("Extracting apk")
color: "#fff"
font.pointSize: 15
font.bold: true
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
Layout.alignment: Qt.AlignTop
Layout.fillWidth: true
Layout.topMargin: 15
text: qsTr("Unpacking the trial")
color: Theme.text
font.pixelSize: Theme.fontHeading
font.weight: Font.DemiBold
}
MProgressBar {
id: apkExtractionProgressBar
indeterminate: true
Layout.fillWidth: true
Layout.margins: 15
Layout.preferredHeight: 20
indeterminate: true
}
}
}
Text {
text: qsTr("This is an unofficial Linux launcher for the Minecraft Bedrock codebase.\nThis project is not affiliated with Minecraft, Mojang or Microsoft.")
color: "#fff"
anchors.bottom: parent.bottom
anchors.bottomMargin: 10
width: parent.width
wrapMode: Text.WordWrap
font.pointSize: 10
anchors.bottomMargin: Theme.space5
anchors.horizontalCenter: parent.horizontalCenter
width: Math.min(parent.width - 2 * Theme.space8, 560)
text: APP_DISCLAIMER
color: Theme.textMuted
font.pixelSize: Theme.fontCaption
font.letterSpacing: 0.3
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
Rectangle {
anchors.fill: parent
color: "#000"
opacity: 0.3
visible: acquiringAccount
MouseArea {
anchors.fill: parent
hoverEnabled: true
}
wrapMode: Text.Wrap
}
ApkImportHelper {
......@@ -178,16 +194,9 @@ Item {
Connections {
target: googleLoginHelper
function onAccountAcquireFinished(acc) {
acquiringAccount = false
root.acquiringAccount = false
if (acc)
root.finished()
}
}
Connections {
target: window
function onClosing() {
application.quit()
}
}
}
......@@ -33,7 +33,7 @@ ScrollView {
Layout.fillHeight: true
Layout.columnSpan: 1
Layout.rowSpan: 1
color: "#222"
color: Theme.surface
height: iconImage.height + 20
Item {
......@@ -61,8 +61,8 @@ ScrollView {
text: modelData.name
width: parent.width
font.bold: true
color: "#fff"
font.pointSize: 13
color: Theme.text
font.pixelSize: Theme.fontHeading
font.weight: Font.Bold
wrapMode: Text.Wrap
elide: Text.ElideRight
......@@ -73,8 +73,8 @@ ScrollView {
text: modelData.description
width: parent.width
height: parent.height - titleText.height - 20
color: "#bbb"
font.pointSize: 10
color: Theme.textSecondary
font.pixelSize: Theme.fontBody
wrapMode: Text.Wrap
elide: Text.ElideRight
}
......
......@@ -86,7 +86,7 @@ AnimatedStackLayout {
MText {
text: qsTr("Welcome to our experimental mods section.")
font.bold: true
font.pointSize: 12
font.pixelSize: Theme.fontHeading
}
TextEdit {
focus: true
......@@ -94,8 +94,8 @@ AnimatedStackLayout {
textFormat: Text.RichText
wrapMode: Text.WordWrap
text: "<style type=\"text/css\">a { color: #6af; }</style>" + qsTr("Managing mods is not yet supported. To contribute your mod, please open a pull request on <a href=\"https://github.com/minecraft-linux/mcpelauncher-moddb\">minecraft-linux/mcpelauncher-moddb</a>.<br/><br/>Mods are a collection of .so files (also on macOS) placed inside the `mods` folder. This folder is located within your data root, which you can find in Settings > Storage. The `mods` folder does not exist by default, so you'll need to create it. Extract zip files directly into the `mods` folder without creating subfolders. .so files should be directly below the `mods` folder.<br/><br/><font color=\"#f66\">Do not report crashes to the launcher's issue tracker when mods are enabled.</font>")
font.pointSize: 10
color: "#fff"
font.pixelSize: Theme.fontBody
color: Theme.text
readOnly: true
selectByMouse: true
onLinkActivated: Qt.openUrlExternally(link)
......@@ -175,7 +175,7 @@ AnimatedStackLayout {
}
MText {
font.bold: true
font.pointSize: 17
font.pixelSize: Theme.fontBody
text: qsTr("Mods") + " / " + (stack.elem && stack.elem.name || qsTr("Untitled Mod"))
}
}
......@@ -203,7 +203,7 @@ AnimatedStackLayout {
MText {
Layout.fillWidth: true
wrapMode: TextEdit.Wrap
color: "#bbb"
color: Theme.textSecondary
text: stack.elem && stack.elem.url || ""
}
......@@ -218,7 +218,7 @@ AnimatedStackLayout {
readOnly: true
selectByMouse: true
onLinkActivated: Qt.openUrlExternally(link)
font.pointSize: 10
font.pixelSize: Theme.fontBody
MouseArea {
anchors.fill: parent
......@@ -244,7 +244,7 @@ AnimatedStackLayout {
delegate: ItemDelegate {
id: control
width: parent.width
font.pointSize: 11
font.pixelSize: Theme.fontBody
contentItem: RowLayout {
MText {
text: modelData.version
......@@ -321,7 +321,7 @@ AnimatedStackLayout {
onClicked: downloads.currentIndex = index
highlighted: ListView.isCurrentItem
background: Rectangle {
color: control.highlighted ? "#226322" : (control.down ? "#338833" : (control.hovered ? "#222" : "transparent"))
color: control.highlighted ? Theme.accentSoft : (control.down ? Theme.surfacePressed : (control.hovered ? Theme.surfaceHover : "transparent"))
}
}
highlightResizeVelocity: -1
......
......@@ -55,7 +55,7 @@ MComboBox {
id: separator
width: parent.width
height: 1
color: "#444"
color: Theme.border
visible: hasSeparator
radius: 2
}
......@@ -66,14 +66,14 @@ MComboBox {
anchors.rightMargin: parent.padding
anchors.topMargin: (separator.visible ? separator.height : 0)
text: modelData
font.pointSize: 10
color: "#fff"
font.pixelSize: Theme.fontBody
color: Theme.text
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
anchors.fill: parent
color: highlighted ? "#283" : (parent.hovered ? "#333" : "#1e1e1e")
color: highlighted ? Theme.accentSoft : (parent.hovered ? Theme.surfaceRaised : Theme.surface)
}
}
......
......@@ -23,12 +23,15 @@ Popup {
focus: true
background: Rectangle {
color: "#333"
color: Theme.surface
radius: Theme.radiusXL
border.width: 1
border.color: Theme.borderStrong
}
Overlay.modal: Rectangle {
id: popupOverlay
color: "#8f181818"
color: Theme.overlay
}
enter: Transition {
......@@ -80,17 +83,18 @@ Popup {
BaseHeader {
id: baseHeader
title: profile == null ? qsTr("Create profile") : qsTr("Edit profile")
actions: [
MButton {
kind: "danger"
text: qsTr("Delete")
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: 10
iconName: "trash-2"
visible: profile !== null && profile !== profileManager.defaultProfile
onClicked: {
profileManager.deleteProfile(profile)
close()
}
}
]
}
ScrollView {
......@@ -353,7 +357,7 @@ Popup {
}
MText {
text: "x"
font.pointSize: 11
font.pixelSize: Theme.fontBody
opacity: windowSizeCheck.checked ? 1.0 : 0.3
}
MTextField {
......@@ -386,8 +390,11 @@ Popup {
verticalAlignment: Text.AlignTop
}
Rectangle {
color: "#222"
Layout.preferredHeight: 120
color: Theme.surfaceRaised
radius: Theme.radius
border.width: 1
border.color: Theme.border
Layout.preferredHeight: 140
Layout.fillWidth: true
ListView {
id: envs
......@@ -403,7 +410,7 @@ Popup {
})
}
delegate: Rectangle {
color: "#222"
color: "transparent"
width: parent.width
height: keyField.implicitHeight
MTextField {
......@@ -433,29 +440,22 @@ Popup {
})
}
}
MButton {
IconButton {
visible: !add
id: del
height: parent.height
anchors.right: parent.right
Image {
height: 20
width: 20
anchors.centerIn: parent
source: "qrc:/Resources/icon-delete.png"
smooth: false
}
onClicked: {
console.log(index)
envs.model.remove(index, 1)
}
iconName: "trash-2"
tooltip: qsTr("Remove variable")
onClicked: envs.model.remove(index, 1)
}
MButton {
visible: !!add
width: parent.width
text: qsTr("Add New Variable")
kind: "ghost"
iconName: "plus"
text: qsTr("Add variable")
onClicked: {
console.log(index)
envs.model.insert(envs.model.count - 1, {
"key": "",
"value": ""
......@@ -466,7 +466,7 @@ Popup {
highlightResizeVelocity: -1
highlightMoveVelocity: -1
currentIndex: -1
ScrollBar.vertical: ScrollBar {}
ScrollBar.vertical: MScrollBar {}
}
}
}
......@@ -474,11 +474,11 @@ Popup {
RowLayout {
id: buttons
Layout.topMargin: 15
TransparentButton {
MButton {
Layout.alignment: Qt.AlignVCenter
text: advancedColumn.visible ? qsTr("Collapse advanced 🞁") : qsTr("Expand advanced 🞃")
textColor: hovered ? "#ccc" : "#888"
font.bold: true
kind: "ghost"
iconName: advancedColumn.visible ? "chevron-up" : "chevron-down"
text: advancedColumn.visible ? qsTr("Hide advanced options") : qsTr("Show advanced options")
onClicked: {
advancedColumn.visible = !advancedColumn.visible
advancedColumn.Layout.preferredHeight = advancedColumn.height
......@@ -503,6 +503,7 @@ Popup {
}
}
MButton {
kind: "primary"
text: qsTr("Save")
enabled: !profileName.enabled || profileName.text.length > 0
onClicked: {
......
......@@ -13,7 +13,7 @@ Window {
width: 640
height: 480
title: qsTr("Request Google Credentials")
color: "#333333"
color: Theme.background
property bool hasUpdate: false
property bool hasAskedForKey: false
property string updateDownloadUrl: ""
......
......@@ -4,13 +4,13 @@ import "../Components"
import io.mrarm.mcpelauncher 1.0
ColumnLayout {
property color labelColor: "#fff"
property color labelColor: Theme.text
property int labelFontSize: 10
width: parent.width
spacing: 10
Rectangle {
color: "#30ff8000"
color: Theme.warningSoft
Layout.fillWidth: true
Layout.bottomMargin: 15
Layout.minimumHeight: warningText.height
......
......@@ -51,7 +51,7 @@ ColumnLayout {
MText {
text: qsTr("Game Directories")
font.bold: true
font.pointSize: 11
font.pixelSize: Theme.fontBody
Layout.topMargin: 10
}
......@@ -83,7 +83,7 @@ ColumnLayout {
MText {
text: qsTr("Default Game Directories")
font.bold: true
font.pointSize: 11
font.pixelSize: Theme.fontBody
Layout.topMargin: 10
Layout.bottomMargin: 10
}
......@@ -118,7 +118,7 @@ ColumnLayout {
Layout.fillHeight: true
text: QmlUrlUtils.urlToLocalFile(modelData.path)
readOnly: true
color: "#aaa"
color: Theme.textSecondary
hoverEnabled: false
}
MButton {
......
......@@ -16,7 +16,7 @@ ColumnLayout {
Popup {
id: downloadApk
background: Rectangle {
color: "#333"
color: Theme.surfaceRaised
}
height: Math.min(scope.implicitHeight + 20, layout.outerHeight - 20)
width: layout.width
......@@ -33,7 +33,7 @@ ColumnLayout {
Overlay.modal: Rectangle {
id: popupOverlay
color: "#8f181818"
color: Theme.overlay
}
ScrollView {
anchors.fill: parent
......@@ -45,7 +45,7 @@ ColumnLayout {
width: downloadApk.width - 40
Rectangle {
color: "#53ba1300"
color: Theme.warningSoft
Layout.fillWidth: true
Layout.bottomMargin: 15
Layout.minimumHeight: warningText.height
......@@ -54,7 +54,7 @@ ColumnLayout {
id: warningText
padding: 10
text: qsTr("You’re most likely mistaken here — to download and install different versions, open the launcher’s Home page and click the Profile Edit pencil icon.")
color: "#fff"
color: Theme.text
font.pointSize: labelFontSize
wrapMode: Text.WordWrap
width: parent.width
......@@ -342,7 +342,7 @@ ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumHeight: Math.max(window.height - 180, 150)
color: "#1e1e1e"
color: Theme.surface
ListView {
id: versions
......@@ -357,17 +357,17 @@ ColumnLayout {
id: control
width: parent.width
height: 32
font.pointSize: 11
font.pixelSize: Theme.fontBody
contentItem: Text {
text: modelData.versionName + " (" + modelData.archs.join(", ") + ")"
color: "#fff"
font.pointSize: 10
color: Theme.text
font.pixelSize: Theme.fontBody
verticalAlignment: Text.AlignVCenter
}
onClicked: versions.currentIndex = index
highlighted: ListView.isCurrentItem
background: Rectangle {
color: control.highlighted ? "#226322" : (control.down ? "#338833" : (control.hovered ? "#222" : "transparent"))
color: control.highlighted ? Theme.accentSoft : (control.down ? Theme.surfacePressed : (control.hovered ? Theme.surfaceHover : "transparent"))
}
}
highlightResizeVelocity: -1
......
......@@ -20,7 +20,7 @@ Window {
property GoogleLoginHelper googleLoginHelper
property GoogleVersionChannel playVerChannel
property GooglePlayApi playApi
color: "#333333"
color: Theme.background
ColumnLayout {
id: layout
......@@ -52,17 +52,17 @@ Window {
text: modelData.shortDesc
Layout.fillWidth: true
font.bold: true
font.pointSize: 10
color: "#fff"
font.pixelSize: Theme.fontBody
color: Theme.text
wrapMode: Text.WordWrap
}
Text {
text: modelData.longDesc
Layout.fillWidth: true
wrapMode: Text.WordWrap
color: "#fff"
linkColor: "#57f"
font.pointSize: 10
color: Theme.text
linkColor: Theme.accentText
font.pixelSize: Theme.fontBody
onLinkActivated: Qt.openUrlExternally(link)
MouseArea {
anchors.fill: parent
......@@ -74,11 +74,11 @@ Window {
text: qsTr("<a href=\"%1\">Go to wiki</a>").arg(modelData.wikiUrl)
Layout.fillWidth: true
wrapMode: Text.WordWrap
color: "#fff"
linkColor: "#57f"
color: Theme.text
linkColor: Theme.accentText
onLinkActivated: Qt.openUrlExternally(link)
visible: modelData.wikiUrl.length > 0
font.pointSize: 10
font.pixelSize: Theme.fontBody
MouseArea {
anchors.fill: parent
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
......
......@@ -44,7 +44,7 @@ BaseScreen {
Rectangle {
id: warning
opacity: 0
color: "#30ff8000"
color: Theme.warningSoft
Layout.fillWidth: true
Layout.bottomMargin: 15
Layout.minimumHeight: warningText.height
......@@ -80,7 +80,7 @@ BaseScreen {
enabled: !continueInvalidCredentials.checked
onAccepted: attemptUnlock()
onTextChanged: {
pwd.color = "#fff"
pwd.color = Theme.text
}
NumberAnimation on x {
id: shakeAnim
......@@ -95,7 +95,7 @@ BaseScreen {
Rectangle {
Layout.fillWidth: true
Layout.minimumHeight: pbutton.height + 10 * 2
color: "#242424"
color: Theme.background
MButton {
id: pbutton
text: qsTr("Continue")
......@@ -114,7 +114,7 @@ BaseScreen {
warning.opacity = 1
warningAnim.restart()
shakeAnim.restart()
pwd.color = "#f88"
pwd.color = Theme.danger
pwd.forceActiveFocus()
}
}
......
......@@ -26,8 +26,8 @@ BaseScreen {
Layout.margins: 15
textFormat: TextEdit.RichText
text: qsTr("<b><font color=\"#f66\">Sorry your Computer cannot run Minecraft with this Launcher</font></b>, this CPU is too old.<br/><br/>Details:<br/>%1").arg(googleLoginHelper.GetSupportReport())
font.pointSize: 10
color: "#fff"
font.pixelSize: Theme.fontBody
color: Theme.text
readOnly: true
wrapMode: Text.WordWrap
selectByMouse: true
......@@ -37,7 +37,7 @@ BaseScreen {
Layout.fillWidth: true
Layout.minimumHeight: pbutton.height + 10 * 2
color: "#242424"
color: Theme.background
MButton {
id: pbutton
......
......@@ -93,6 +93,7 @@ Window {
onUpdateAvailable: function (downloadUrl) {
window.hasUpdate = true
window.updateDownloadUrl = downloadUrl
window.notify(qsTr("%1 %2 is available.").arg(APP_NAME).arg(updateChecker.latestVersion), "info")
}
}
......@@ -216,6 +217,16 @@ Window {
Item { Layout.fillHeight: true }
MSideBarItem {
Layout.fillWidth: true
visible: window.hasUpdate
text: updateChecker.latestVersion.length > 0 ? qsTr("Update to %1").arg(updateChecker.latestVersion) : qsTr("Update available")
iconName: "download"
showText: !sidebar.compact
highlighted: true
onClicked: updateChecker.startUpdate()
}
// Game status at a glance.
Rectangle {
Layout.fillWidth: true
......
......@@ -61,8 +61,9 @@ ColumnLayout {
onToggled: launcherSettings.showNotifications = checked
}
}
HorizontalDivider {}
HorizontalDivider { visible: updateChecker.enabled }
SettingRow {
visible: updateChecker.enabled
title: qsTr("Check for launcher updates")
description: qsTr("Looks for a newer AL-Arcade MC Launcher when it starts.")
MSwitch {
......
#include "updatechecker.h"
#include <QDesktopServices>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkReply>
#include <sstream>
#include <QObject>
#include <QNetworkRequest>
#include <QUrl>
UpdateChecker::UpdateChecker(QObject* parent) : QObject(parent) {
connect(&netAccessManager, &QNetworkAccessManager::finished, this, &UpdateChecker::onRequestFinished);
m_active = false;
}
#ifndef SPARKLE_UPDATE_CHECK
void UpdateChecker::checkForUpdates() {
#ifdef APPIMAGE_UPDATE_CHECK
auto updater = this->updater;
auto oldthread = std::make_shared<std::thread>();
std::swap(*oldthread, updatethread);
updatethread = std::thread([this, updater, oldthread]() mutable {
try {
if (oldthread->joinable()) {
oldthread->join();
}
if (!updater) {
char * appimage = getenv("APPIMAGE");
if (appimage) {
#ifndef NDEBUG
printf("Appimage create updater\n");
#endif
this->updater = updater = std::make_shared<appimage::update::Updater>(appimage, true);
} else {
#ifndef NDEBUG
printf("Appimage cannot be updated\n");
#endif
emit updateError(QObject::tr("Appimage cannot be updated<br/>Expected Environmentvariable 'APPIMAGE' to be set to the path of the AppImage"));
return;
}
}
bool _updateAvailable = false;
#ifndef NDEBUG
printf("Appimage check for changes\n");
#endif
if (!updater->checkForChanges(_updateAvailable)) {
#ifndef NDEBUG
printf("Appimage Error\n");
#endif
std::stringstream errorstream;
std::string nextMessage;
while (updater->nextStatusMessage(nextMessage)) {
#ifndef NDEBUG
printf("appimage update error %s\n", nextMessage.data());
#endif
errorstream << nextMessage << "<br/>";
}
emit updateError(QObject::tr("Appimage cannot be updated") + "<br/>" + QString::fromStdString(errorstream.str()));
return;
}
#ifndef NDEBUG
printf("Appimage Found Update? %d\n", (int)_updateAvailable);
#ifndef ALARCADE_UPDATE_FEED_URL
#define ALARCADE_UPDATE_FEED_URL ""
#endif
if (_updateAvailable) {
emit updateAvailable("");
}
emit updateCheck(_updateAvailable);
} catch (...) {
emit updateError(QObject::tr("Appimage cannot be updated<br/>Unknown Error"));
}
});
#elif defined(UPDATE_CHECK)
QNetworkRequest request(QStringLiteral(UPDATE_CHECK_URL));
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
netAccessManager.get(request);
static const char *platformKey() {
#if defined(Q_OS_MACOS)
return "macos";
#elif defined(Q_OS_WIN)
return "windows";
#else
emit updateError(QObject::tr("Launcher cannot be updated<br/>You have to check your packagemanager for updates or recompile your Open Source build with newer sources"));
return "linux";
#endif
}
#endif
void UpdateChecker::onRequestFinished(QNetworkReply* reply) {
#ifdef UPDATE_CHECK
if (reply->error() != QNetworkReply::NoError) {
emit updateError(QObject::tr("Failed to check for update<br/>Failed to connect to update server"));
return;
}
auto redirect = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
if (redirect.isValid()) {
QNetworkRequest request(redirect);
netAccessManager.get(request);
return;
}
QMap<QString, QString> props;
QString replyText = QString::fromUtf8(reply->readAll());
for (QStringRef const& line : replyText.splitRef('\n')) {
auto iof = line.indexOf('=');
if (iof == -1)
continue;
props[line.left(iof).toString()] = line.mid(iof + 1).trimmed().toString();
}
#ifndef NDEBUG
printf("server build id: %i\n", props["build_id"].toInt());
#endif
bool updateavailable = props["build_id"].toInt() > UPDATE_CHECK_BUILD_ID;
if (updateavailable)
emit updateAvailable(props["download_url"]);
emit updateCheck(updateavailable);
#endif
UpdateChecker::UpdateChecker(QObject *parent) : QObject(parent) {
connect(&m_network, &QNetworkAccessManager::finished, this, &UpdateChecker::onFinished);
}
void UpdateChecker::startUpdate() {
#ifdef APPIMAGE_UPDATE_CHECK
if (updater) {
auto oldthread = std::make_shared<std::thread>();
std::swap(*oldthread, updatethread);
updatethread = std::thread([this, oldthread] {
if (oldthread->joinable()) {
oldthread->join();
}
m_active = true;
emit activeChanged();
updater->start();
std::stringstream errorstream;
while (!updater->isDone()) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
QString UpdateChecker::feedUrl() {
const QString overrideUrl = qEnvironmentVariable("ALARCADE_UPDATE_FEED");
return overrideUrl.isEmpty() ? QStringLiteral(ALARCADE_UPDATE_FEED_URL) : overrideUrl;
}
double _progress;
if (!updater->progress(_progress)) {
#ifndef NDEBUG
printf("appimage startUpdate Call to progress() failed\n");
#endif
m_active = false;
emit activeChanged();
void UpdateChecker::checkForUpdates() {
const QString url = feedUrl();
if (url.isEmpty())
return;
}
emit progress(_progress);
// fetch all status messages
// this is basically the same as before
std::string nextMessage;
while (updater->nextStatusMessage(nextMessage)) {
#ifndef NDEBUG
printf("appimage startUpdate %s\n", nextMessage.data());
#endif
errorstream << nextMessage << "<br/>";
}
}
QNetworkRequest request{QUrl(url)};
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
request.setTransferTimeout(15000);
m_network.get(request);
}
m_active = false;
emit activeChanged();
if (updater->hasError()) {
#ifndef NDEBUG
printf("Error occurred. See previous messages for details.\n");
#endif
emit updateError(QObject::tr("Appimage cannot be updated") + "<br/>" + QString::fromStdString(errorstream.str()));
} else {
updater->copyPermissionsToNewFile();
emit requestRestart();
}
});
void UpdateChecker::onFinished(QNetworkReply *reply) {
reply->deleteLater();
if (reply->error() != QNetworkReply::NoError) {
emit updateCheck(false);
return;
}
#endif
const QJsonObject feed = QJsonDocument::fromJson(reply->readAll()).object();
const QJsonObject download = feed.value(QLatin1String(platformKey())).toObject();
const int latestBuild = feed.value("build").toInt();
const int currentBuild = QStringLiteral(LAUNCHER_VERSION_CODE).toInt();
m_latestVersion = feed.value("version").toString();
m_notes = feed.value("notes").toString();
m_downloadUrl = download.value("url").toString();
const bool newer = latestBuild > currentBuild && m_downloadUrl.startsWith(QLatin1String("https://"));
if (newer)
emit updateAvailable(m_downloadUrl);
emit updateCheck(newer);
}
#ifdef APPIMAGE_UPDATE_CHECK
UpdateChecker::~UpdateChecker() {
if (updatethread.joinable()) {
updatethread.join();
}
void UpdateChecker::startUpdate() {
// Updates are installed by downloading the new installer or disk image.
if (!m_downloadUrl.isEmpty())
QDesktopServices::openUrl(QUrl(m_downloadUrl));
}
#endif
\ No newline at end of file
#ifndef UPDATECHECKER_H
#define UPDATECHECKER_H
#include <QObject>
#include <QNetworkAccessManager>
#include <QObject>
#ifdef APPIMAGE_UPDATE_CHECK
#include <thread>
#include <appimage/update.h>
#endif
class QNetworkReply;
// Checks an AL-Arcade release feed (JSON) for a newer launcher build:
//
// { "version": "1.1.0", "build": 42, "notes": "...",
// "macos": { "url": "https://.../AL-Arcade-MC-Launcher-1.1.0-macOS-arm64.dmg", "sha256": "..." },
// "windows": { "url": "https://.../AL-Arcade-MC-Launcher-1.1.0-windows-x64-setup.exe", "sha256": "..." } }
//
// The feed URL is set at build time (ALARCADE_UPDATE_FEED_URL) and can be
// overridden with the ALARCADE_UPDATE_FEED environment variable. Without one,
// update checks are skipped.
class UpdateChecker : public QObject {
Q_OBJECT
Q_PROPERTY(bool active READ active NOTIFY activeChanged)
private:
QNetworkAccessManager netAccessManager;
#ifdef APPIMAGE_UPDATE_CHECK
std::shared_ptr<appimage::update::Updater> updater;
std::thread updatethread;
#endif
std::atomic_bool m_active;
void onRequestFinished(QNetworkReply* reply);
Q_PROPERTY(QString latestVersion READ latestVersion NOTIFY updateCheck)
Q_PROPERTY(QString releaseNotes READ releaseNotes NOTIFY updateCheck)
Q_PROPERTY(bool enabled READ enabled CONSTANT)
public:
UpdateChecker(QObject* parent = nullptr);
explicit UpdateChecker(QObject *parent = nullptr);
#ifdef APPIMAGE_UPDATE_CHECK
~UpdateChecker();
#endif
bool active() const { return m_active; }
bool active() const { return false; }
bool enabled() const { return !feedUrl().isEmpty(); }
QString latestVersion() const { return m_latestVersion; }
QString releaseNotes() const { return m_notes; }
signals:
void updateAvailable(QString downloadUrl);
......@@ -42,6 +41,15 @@ signals:
public slots:
void checkForUpdates();
void startUpdate();
private:
static QString feedUrl();
void onFinished(QNetworkReply *reply);
QNetworkAccessManager m_network;
QString m_downloadUrl;
QString m_latestVersion;
QString m_notes;
};
#endif // UPDATECHECKER_H
Subject: macOS: ALARCADE_RETINA=0 renders at 1x for more frames per second
diff --git a/src/window_glfw.cpp b/src/window_glfw.cpp
index 7e0a85b..9c6b513 100644
--- a/src/window_glfw.cpp
+++ b/src/window_glfw.cpp
@@ -25,6 +25,12 @@ GLFWGameWindow::GLFWGameWindow(const std::string& title, int width, int height,
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
}
+#ifdef __APPLE__
+ // ALARCADE_RETINA=0 renders at 1x on Retina displays: a quarter of the
+ // pixels, for a large frame-rate gain on older Apple Silicon.
+ const char* retina = getenv("ALARCADE_RETINA");
+ glfwWindowHint(GLFW_COCOA_RETINA_FRAMEBUFFER, (retina && retina[0] == '0') ? GLFW_FALSE : GLFW_TRUE);
+#endif
window = glfwCreateWindow(width, height, title.c_str(), nullptr, nullptr);
if(window == nullptr && api == GraphicsApi::OPENGL_ES2) {
// Failed to get es3 request es2
Subject: Ignore SIGPIPE like Android does, so VPNs cannot kill the game (#2051)
diff --git a/src/main.cpp b/src/main.cpp
index e24c581..5f25e0c 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -40,6 +40,7 @@
#include "thread_mover.h"
#include <FileUtil.h>
#include <properties/property.h>
+#include <csignal>
#include <fstream>
#include "glad/glad.h"
// For getpid
@@ -138,6 +139,10 @@ int main(int argc, char* argv[]) {
}
CrashHandler::registerCrashHandler();
+ // Android ignores SIGPIPE in every app and Minecraft relies on it: a write
+ // to a reset socket (common behind VPNs) must fail with EPIPE instead of
+ // killing the game. https://github.com/minecraft-linux/mcpelauncher-manifest/issues/2051
+ signal(SIGPIPE, SIG_IGN);
MinecraftUtils::workaroundLocaleBug();
argparser::arg_parser p;
Subject: macOS: toggle fullscreen with Control-Command-F (F11 is Show Desktop)
diff --git a/src/window_callbacks.cpp b/src/window_callbacks.cpp
index 2838736..9b2688c 100644
--- a/src/window_callbacks.cpp
+++ b/src/window_callbacks.cpp
@@ -441,6 +441,14 @@ void WindowCallbacks::onKeyboard(KeyCode key, KeyAction action, int mods) {
if(key == KeyCode::FN11 && action == KeyAction::PRESS)
setFullscreen(!Settings::fullscreen);
+#ifdef __APPLE__
+ // macOS binds F11 to Show Desktop; also accept the system's own
+ // full-screen shortcut, Control-Command-F.
+ if(key == KeyCode::F && action == KeyAction::PRESS && (mods & KEY_MOD_SUPER) && (mods & KEY_MOD_CTRL)) {
+ setFullscreen(!Settings::fullscreen);
+ return;
+ }
+#endif
if(useDirectKeyboardInput && (action == KeyAction::PRESS || action == KeyAction::RELEASE)) {
if(Keyboard::useLegacyKeyboard) {
Subject: arm64: step over debug breaks instead of spinning on an ignored SIGTRAP (#1929)
diff --git a/include/mcpelauncher/crash_handler.h b/include/mcpelauncher/crash_handler.h
index 4d3f9a6..b9a91c4 100644
--- a/include/mcpelauncher/crash_handler.h
+++ b/include/mcpelauncher/crash_handler.h
@@ -9,6 +9,9 @@ private:
#if defined(__x86_64__) && defined(__APPLE__)
static void handle_fs_fault(int sig, void *si, void *ucp);
#endif
+#if defined(__aarch64__)
+ static void handle_trap(int sig, void *si, void *ucp);
+#endif
public:
static void registerCrashHandler();
diff --git a/src/crash_handler.cpp b/src/crash_handler.cpp
index 11f0e62..fba0e1d 100644
--- a/src/crash_handler.cpp
+++ b/src/crash_handler.cpp
@@ -8,6 +8,10 @@
#include <execinfo.h>
#include <mcpelauncher/linker.h>
#include <thread>
+#include <cstdint>
+#if defined(__aarch64__) && !defined(__APPLE__)
+#include <sys/ucontext.h> // macOS gets ucontext_t from <signal.h>
+#endif
bool CrashHandler::hasCrashed = false;
@@ -96,6 +100,32 @@ void CrashHandler::handle_fs_fault(int sig, void *si, void *ucp) {
}
#endif
+#if defined(__aarch64__)
+// x86 builds ignore SIGTRAP so the game's leftover debug breaks (int3) are
+// skipped: the CPU has already moved past int3 when the signal arrives. On
+// arm64, brk leaves the pc on the instruction itself, so an ignored SIGTRAP
+// re-executes it forever and pins a core at 100% CPU, the orphaned clients of
+// https://github.com/minecraft-linux/mcpelauncher-manifest/issues/1929.
+// Step over debug breaks (brk #0xf000, what __builtin_debugtrap emits) the way
+// x86 skips int3, and treat any other brk (__builtin_trap emits brk #1) as the
+// crash it is.
+void CrashHandler::handle_trap(int sig, void *si, void *ucp) {
+ ucontext_t *uc = (ucontext_t *)ucp;
+#if defined(__APPLE__)
+ uint64_t *pc = (uint64_t *)&uc->uc_mcontext->__ss.__pc;
+#else
+ uint64_t *pc = (uint64_t *)&uc->uc_mcontext.pc;
+#endif
+ const uint32_t insn = *(const uint32_t *)*pc;
+ const bool isBrk = (insn & 0xFFE0001F) == 0xD4200000;
+ if (isBrk && ((insn >> 5) & 0xFFFF) == 0xF000) {
+ *pc += 4;
+ return;
+ }
+ handleSignal(sig, si);
+}
+#endif
+
void CrashHandler::registerCrashHandler() {
struct sigaction act;
sigemptyset(&act.sa_mask);
@@ -119,7 +149,15 @@ void CrashHandler::registerCrashHandler() {
sigemptyset(&act.sa_mask);
act.sa_handler = SIG_IGN;
act.sa_flags = 0;
+#if defined(__aarch64__)
+ struct sigaction trap;
+ sigemptyset(&trap.sa_mask);
+ trap.sa_sigaction = (void (*)(int, siginfo_t *, void *)) handle_trap;
+ trap.sa_flags = SA_SIGINFO;
+ sigaction(SIGTRAP, &trap, 0);
+#else
sigaction(SIGTRAP, &act, 0);
+#endif
#if defined(__APPLE__)
// Try to ignore this for macOS 10.12 and older and Minecraft 1.16.230+
// macOS 12.3 bug reports, crash with sigsys
......@@ -95,7 +95,8 @@ build_launcher() {
-DCMAKE_PREFIX_PATH="$BREW" \
-DOPENSSL_ROOT_DIR="$BREW/opt/openssl@3" \
-DLAUNCHER_VERSION_NAME="$VERSION" \
-DLAUNCHER_VERSION_CODE="$BUILD_NUMBER" >/dev/null
-DLAUNCHER_VERSION_CODE="$BUILD_NUMBER" \
-DALARCADE_UPDATE_FEED_URL="${ALARCADE_UPDATE_FEED_URL:-}" >/dev/null
ninja -C "$BUILD/launcher" -j "$JOBS" >/dev/null
}
......
#!/bin/bash
# Builds the Windows (x64) launcher on macOS: llvm-mingw cross-compile against
# Qt for Windows, then deploys Qt next to the .exe and packages
# dist/AL-Arcade-MC-Launcher-<version>-windows-x64-setup.exe (installer)
# dist/AL-Arcade-MC-Launcher-<version>-windows-x64.zip (portable)
#
# usage: scripts/cross-build-windows.sh
# Requirements: cmake, ninja, makensis (brew install makensis), Homebrew Qt of
# the same version as QT_VERSION (its moc/rcc/qmlcachegen are the host tools).
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
CACHE="$ROOT/.cache"
BUILD="$ROOT/build/windows"
DIST="$ROOT/dist"
QT_VERSION="${QT_VERSION:-6.11.2}"
QT_WIN="$CACHE/qt-windows/$QT_VERSION"
VERSION="$(tr -d '[:space:]' < "$ROOT/VERSION")"
BUILD_NUMBER="${BUILD_NUMBER:-$(git -C "$ROOT" rev-list --count HEAD 2>/dev/null || echo 1)}"
BREW="$(brew --prefix)"
APP_NAME="AL-Arcade MC Launcher"
APP_DIR="$DIST/windows/$APP_NAME"
BASENAME="AL-Arcade-MC-Launcher-$VERSION-windows-x64"
step() { printf '\n\033[1;35m==> %s\033[0m\n' "$1"; }
step "Toolchain"
"$ROOT/scripts/fetch-windows-toolchain.sh"
[ -f "$QT_WIN/lib/cmake/Qt6/Qt6Config.cmake" ] || python3 "$ROOT/tools/qt-windows-install.py" "$QT_VERSION" "$QT_WIN"
step "Building"
cmake -S "$ROOT" -B "$BUILD" -G Ninja \
-DCMAKE_TOOLCHAIN_FILE="$ROOT/cmake/toolchains/llvm-mingw-x86_64.cmake" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH="$QT_WIN" \
-DQT_HOST_PATH="$BREW" \
-DQT_HOST_PATH_CMAKE_DIR="$BREW/lib/cmake" \
-DALARCADE_BACKEND=windows \
-DLAUNCHER_VERSION_NAME="$VERSION" \
-DLAUNCHER_VERSION_CODE="$BUILD_NUMBER" \
-DALARCADE_UPDATE_FEED_URL="${ALARCADE_UPDATE_FEED_URL:-}" >/dev/null
ninja -C "$BUILD" >/dev/null
step "Deploying Qt"
python3 "$ROOT/tools/windows-deploy.py" \
--exe "$BUILD/launcher/$APP_NAME.exe" \
--qt "$QT_WIN" \
--mingw "$CACHE/llvm-mingw" \
--qml-root "$ROOT/launcher/qml" \
--scanner "$BREW/share/qt/libexec/qmlimportscanner" \
--out "$APP_DIR"
cp "$ROOT/LICENSE" "$APP_DIR/LICENSE.txt"
cp "$ROOT/NOTICE.md" "$APP_DIR/NOTICE.md"
step "Packaging"
rm -f "$DIST/$BASENAME.zip" "$DIST/$BASENAME-setup.exe"
(cd "$DIST/windows" && zip -qr -9 "$DIST/$BASENAME.zip" "$APP_NAME")
makensis -V2 \
-DAPP_VERSION="$VERSION" \
-DSOURCE_DIR="$APP_DIR" \
-DOUT_FILE="$DIST/$BASENAME-setup.exe" \
-DICON="$ROOT/branding/app.ico" \
-DLICENSE_FILE="$ROOT/LICENSE" \
"$ROOT/windows/installer.nsi"
(cd "$DIST" && shasum -a 256 "$BASENAME-setup.exe" "$BASENAME.zip" | tee "$BASENAME.sha256")
step "Done"
#!/bin/bash
# Fetches what scripts/cross-build-windows.sh needs to build the Windows launcher
# on macOS or Linux: the llvm-mingw toolchain and Qt for Windows (llvm-mingw build).
# on macOS: the llvm-mingw toolchain and Qt for Windows (llvm-mingw build).
# Everything goes to .cache/; nothing is installed system-wide.
set -euo pipefail
......@@ -38,9 +38,8 @@ if [ ! -x "$CACHE/llvm-mingw/bin/x86_64-w64-mingw32-clang++" ]; then
xattr -cr "$CACHE/llvm-mingw" 2>/dev/null || true
fi
if [ ! -d "$CACHE/qt-windows/$QT_VERSION/llvm-mingw_64" ]; then
if [ ! -f "$CACHE/qt-windows/$QT_VERSION/lib/cmake/Qt6/Qt6Config.cmake" ]; then
echo "==> Qt $QT_VERSION for Windows (llvm-mingw)"
[ -x "$CACHE/aqt-venv/bin/aqt" ] || { python3 -m venv "$CACHE/aqt-venv"; "$CACHE/aqt-venv/bin/pip" install -q aqtinstall; }
"$CACHE/aqt-venv/bin/aqt" install-qt windows desktop "$QT_VERSION" win64_llvm_mingw -O "$CACHE/qt-windows"
python3 "$ROOT/tools/qt-windows-install.py" "$QT_VERSION" "$CACHE/qt-windows/$QT_VERSION"
fi
echo "toolchain ready: $CACHE/llvm-mingw, $CACHE/qt-windows/$QT_VERSION"
#!/usr/bin/env python3
"""Assembles the Windows app folder next to the .exe: Qt DLLs, plugins, QML
modules and the llvm-mingw runtime. Does what windeployqt does, but runs on
macOS and Linux, so the Windows build can be packaged where it is built.
usage: windows-deploy.py --exe app.exe --qt <qt-windows> --mingw <llvm-mingw>
--qml-root launcher/qml --scanner <qmlimportscanner>
--out dist/windows/<App>
"""
import argparse
import json
import os
import shutil
import subprocess
import sys
PLUGINS = [
"platforms/qwindows.dll",
"imageformats/qjpeg.dll",
"imageformats/qsvg.dll",
"imageformats/qico.dll",
"imageformats/qgif.dll",
"iconengines/qsvgicon.dll",
"tls/qschannelbackend.dll",
"tls/qcertonlybackend.dll",
"networkinformation/qnetworklistmanager.dll",
"styles/qmodernwindowsstyle.dll",
]
# The launcher forces the Basic style; the others would only add size.
UNUSED_QML = (
"QtQuick/Controls/FluentWinUI3", "QtQuick/Controls/Fusion", "QtQuick/Controls/Imagine",
"QtQuick/Controls/Material", "QtQuick/Controls/Universal", "QtQuick/Controls/Windows",
"QtQuick/NativeStyle",
)
def imports_of(objdump, path):
result = subprocess.run([objdump, "-p", path], capture_output=True, text=True, check=True)
return [line.split("DLL Name:")[1].strip() for line in result.stdout.splitlines() if "DLL Name:" in line]
def copy_module_dir(src, dst):
"""Copies one QML module directory, without its nested modules."""
os.makedirs(dst, exist_ok=True)
for name in os.listdir(src):
path = os.path.join(src, name)
if os.path.isfile(path) and not name.endswith((".pdb", ".debug")):
shutil.copy2(path, os.path.join(dst, name))
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--exe", required=True)
ap.add_argument("--qt", required=True)
ap.add_argument("--mingw", required=True)
ap.add_argument("--qml-root", required=True)
ap.add_argument("--scanner", required=True)
ap.add_argument("--out", required=True)
args = ap.parse_args()
objdump = os.path.join(args.mingw, "bin", "llvm-objdump")
search_dirs = [os.path.join(args.qt, "bin"), os.path.join(args.mingw, "x86_64-w64-mingw32", "bin")]
if os.path.isdir(args.out):
shutil.rmtree(args.out)
os.makedirs(args.out)
binaries = [shutil.copy2(args.exe, args.out)]
for plugin in PLUGINS:
src = os.path.join(args.qt, "plugins", plugin)
if os.path.exists(src):
dst = os.path.join(args.out, plugin)
os.makedirs(os.path.dirname(dst), exist_ok=True)
binaries.append(shutil.copy2(src, dst))
scan = subprocess.run([args.scanner, "-rootPath", args.qml_root, "-importPath", os.path.join(args.qt, "qml")],
capture_output=True, text=True, check=True)
modules = 0
for entry in json.loads(scan.stdout):
rel = entry.get("relativePath")
if entry.get("type") != "module" or not rel or rel == "QML" or rel.startswith(UNUSED_QML):
continue
src = os.path.join(args.qt, "qml", rel)
if not os.path.isdir(src):
continue
dst = os.path.join(args.out, "qml", rel)
copy_module_dir(src, dst)
modules += 1
binaries += [os.path.join(dst, f) for f in os.listdir(dst) if f.endswith(".dll")]
# Pull in every DLL the binaries need that ships with Qt or the toolchain;
# anything not found there is a Windows system library.
copied = set()
pending = list(binaries)
while pending:
for dll in imports_of(objdump, pending.pop()):
key = dll.lower()
if key in copied:
continue
for directory in search_dirs:
candidate = os.path.join(directory, dll)
if os.path.exists(candidate):
copied.add(key)
pending.append(shutil.copy2(candidate, os.path.join(args.out, dll)))
break
size = sum(os.path.getsize(os.path.join(root, f)) for root, _, files in os.walk(args.out) for f in files)
print(f"deployed {len(copied)} DLLs, {modules} QML modules, {size / 1048576:.1f} MB into {args.out}")
if __name__ == "__main__":
sys.exit(main())
; AL-Arcade MC Launcher installer (NSIS 3). Built by scripts/cross-build-windows.sh
; or scripts/build-windows.ps1, which pass:
; APP_VERSION 1.2.3 SOURCE_DIR deployed app folder
; OUT_FILE setup.exe ICON / LICENSE_FILE paths in the repository
; Installs per user (no administrator prompt) into %LOCALAPPDATA%\Programs.
Unicode true
SetCompressor /SOLID lzma
!include "MUI2.nsh"
!define APP_NAME "AL-Arcade MC Launcher"
!define APP_EXE "AL-Arcade MC Launcher.exe"
!define COMPANY "AL-Arcade"
!define UNINSTALL_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}"
Name "${APP_NAME}"
OutFile "${OUT_FILE}"
InstallDir "$LOCALAPPDATA\Programs\${APP_NAME}"
InstallDirRegKey HKCU "Software\${COMPANY}\${APP_NAME} Installer" "InstallDir"
RequestExecutionLevel user
BrandingText "${COMPANY}"
VIProductVersion "${APP_VERSION}.0"
VIAddVersionKey "ProductName" "${APP_NAME}"
VIAddVersionKey "CompanyName" "${COMPANY}"
VIAddVersionKey "FileDescription" "${APP_NAME} Setup"
VIAddVersionKey "FileVersion" "${APP_VERSION}"
VIAddVersionKey "ProductVersion" "${APP_VERSION}"
VIAddVersionKey "LegalCopyright" "Copyright 2026 AL-Arcade. GPL-3.0. Not an official Minecraft product."
!define MUI_ICON "${ICON}"
!define MUI_UNICON "${ICON}"
!define MUI_ABORTWARNING
!define MUI_FINISHPAGE_RUN "$INSTDIR\${APP_EXE}"
!define MUI_FINISHPAGE_RUN_TEXT "Start ${APP_NAME}"
!insertmacro MUI_PAGE_WELCOME
!insertmacro MUI_PAGE_LICENSE "${LICENSE_FILE}"
!insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_INSTFILES
!insertmacro MUI_PAGE_FINISH
!insertmacro MUI_UNPAGE_CONFIRM
!insertmacro MUI_UNPAGE_INSTFILES
!insertmacro MUI_LANGUAGE "English"
!insertmacro MUI_LANGUAGE "Arabic"
Section "Install"
; Replace an earlier version's files completely.
RMDir /r "$INSTDIR\qml"
SetOutPath "$INSTDIR"
File /r "${SOURCE_DIR}/*.*"
WriteUninstaller "$INSTDIR\Uninstall.exe"
CreateShortcut "$SMPROGRAMS\${APP_NAME}.lnk" "$INSTDIR\${APP_EXE}"
CreateShortcut "$DESKTOP\${APP_NAME}.lnk" "$INSTDIR\${APP_EXE}"
WriteRegStr HKCU "Software\${COMPANY}\${APP_NAME} Installer" "InstallDir" "$INSTDIR"
WriteRegStr HKCU "${UNINSTALL_KEY}" "DisplayName" "${APP_NAME}"
WriteRegStr HKCU "${UNINSTALL_KEY}" "DisplayVersion" "${APP_VERSION}"
WriteRegStr HKCU "${UNINSTALL_KEY}" "Publisher" "${COMPANY}"
WriteRegStr HKCU "${UNINSTALL_KEY}" "DisplayIcon" "$INSTDIR\${APP_EXE}"
WriteRegStr HKCU "${UNINSTALL_KEY}" "InstallLocation" "$INSTDIR"
WriteRegStr HKCU "${UNINSTALL_KEY}" "UninstallString" "$\"$INSTDIR\Uninstall.exe$\""
WriteRegStr HKCU "${UNINSTALL_KEY}" "URLInfoAbout" "https://al-arcade.com"
WriteRegDWORD HKCU "${UNINSTALL_KEY}" "NoModify" 1
WriteRegDWORD HKCU "${UNINSTALL_KEY}" "NoRepair" 1
SectionEnd
Section "Uninstall"
Delete "$SMPROGRAMS\${APP_NAME}.lnk"
Delete "$DESKTOP\${APP_NAME}.lnk"
RMDir /r "$INSTDIR"
DeleteRegKey HKCU "${UNINSTALL_KEY}"
DeleteRegKey HKCU "Software\${COMPANY}\${APP_NAME} Installer"
; Settings (HKCU\Software\AL-Arcade\AL-Arcade MC Launcher) and world backups
; in Documents are the player's data and stay.
SectionEnd
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment