Compare commits
No commits in common. "main" and "oneclient-2.1.4" have entirely different histories.
main
...
oneclient-
|
|
@ -1,27 +0,0 @@
|
||||||
[target.x86_64-unknown-linux-gnu]
|
|
||||||
linker = "clang"
|
|
||||||
rustflags = ["-C", "link-arg=-fuse-ld=lld"]
|
|
||||||
|
|
||||||
[target.aarch64-apple-darwin]
|
|
||||||
linker = "aarch64-apple-darwin-clang"
|
|
||||||
rustflags = [
|
|
||||||
"-C", "link-arg=-fuse-ld=lld",
|
|
||||||
"-C", "link-arg=-isysroot",
|
|
||||||
"-C", "link-arg=/home/oiupoyt/.local/share/MacOSX11.3.sdk",
|
|
||||||
]
|
|
||||||
|
|
||||||
[target.x86_64-apple-darwin]
|
|
||||||
linker = "x86_64-apple-darwin-clang"
|
|
||||||
rustflags = [
|
|
||||||
"-C", "link-arg=-fuse-ld=lld",
|
|
||||||
"-C", "link-arg=-isysroot",
|
|
||||||
"-C", "link-arg=/home/oiupoyt/.local/share/MacOSX11.3.sdk",
|
|
||||||
]
|
|
||||||
|
|
||||||
# Linux and Macos by default have 8MB of stack storage.
|
|
||||||
# Windows has 1MB, it isn't enough, so it's changed to 8MB also.
|
|
||||||
[target.x86_64-pc-windows-msvc]
|
|
||||||
rustflags = ["-C", "link-arg=/STACK:8388608"]
|
|
||||||
|
|
||||||
[target.aarch64-pc-windows-msvc]
|
|
||||||
rustflags = ["-C", "link-arg=/STACK:8388608"]
|
|
||||||
|
|
@ -1,122 +0,0 @@
|
||||||
name: Release Binaries (Forgejo)
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- 'v*'
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
tag:
|
|
||||||
description: 'Release Tag (e.g. v2.8.0-c1)'
|
|
||||||
required: false
|
|
||||||
default: 'v2.8.0-c1'
|
|
||||||
type: string
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
env:
|
|
||||||
SQLX_OFFLINE: true
|
|
||||||
SQLX_OFFLINE_DIR: packages/oneclient_db/.sqlx
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-linux:
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
with:
|
|
||||||
targets: x86_64-unknown-linux-gnu
|
|
||||||
|
|
||||||
- name: Rust Cache
|
|
||||||
uses: Swatinem/rust-cache@v2
|
|
||||||
with:
|
|
||||||
key: x86_64-unknown-linux-gnu
|
|
||||||
cache-all-crates: true
|
|
||||||
|
|
||||||
- name: Install Linux build dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y \
|
|
||||||
libwebkit2gtk-4.1-dev \
|
|
||||||
build-essential \
|
|
||||||
curl \
|
|
||||||
wget \
|
|
||||||
file \
|
|
||||||
libxdo-dev \
|
|
||||||
libssl-dev \
|
|
||||||
libayatana-appindicator3-dev \
|
|
||||||
librsvg2-dev \
|
|
||||||
libfontconfig1-dev \
|
|
||||||
libfreetype6-dev \
|
|
||||||
libgl1-mesa-dev \
|
|
||||||
libvulkan-dev \
|
|
||||||
libasound2-dev
|
|
||||||
|
|
||||||
- name: Install cargo-packager
|
|
||||||
run: cargo install cargo-packager --locked || true
|
|
||||||
|
|
||||||
- name: Build Release Binary
|
|
||||||
run: cargo build --release --bin oneclient_app --target x86_64-unknown-linux-gnu
|
|
||||||
|
|
||||||
- name: Package Application
|
|
||||||
run: cargo packager --release --target x86_64-unknown-linux-gnu --formats deb,appimage
|
|
||||||
continue-on-error: true
|
|
||||||
|
|
||||||
- name: Generate RPM Package (Linux)
|
|
||||||
run: |
|
|
||||||
cargo install cargo-generate-rpm --locked || true
|
|
||||||
cargo generate-rpm --target x86_64-unknown-linux-gnu -p oneclient_app || true
|
|
||||||
|
|
||||||
- name: Determine Release Tag
|
|
||||||
id: release_tag
|
|
||||||
run: |
|
|
||||||
if [[ "${{ github.ref_name }}" =~ ^v ]]; then
|
|
||||||
echo "tag=${{ github.ref_name }}" >> $GITHUB_OUTPUT
|
|
||||||
elif [ -n "${{ inputs.tag }}" ]; then
|
|
||||||
echo "tag=${{ inputs.tag }}" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "tag=v2.8.0-c1" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Publish Assets to Forgejo Release
|
|
||||||
run: |
|
|
||||||
TAG="${{ steps.release_tag.outputs.tag }}"
|
|
||||||
API_URL="${{ github.server_url }}/api/v1/repos/${{ github.repository }}"
|
|
||||||
echo "Connecting to $API_URL for release $TAG"
|
|
||||||
|
|
||||||
# 1. Fetch or create release on Forgejo
|
|
||||||
RELEASE_ID=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" "$API_URL/releases/tags/$TAG" | jq -r '.id // empty')
|
|
||||||
if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "null" ]; then
|
|
||||||
RESP=$(curl -s -X POST "$API_URL/releases" \
|
|
||||||
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "{\"tag_name\":\"$TAG\",\"name\":\"OneLauncher-Cracked $TAG\",\"draft\":false,\"prerelease\":false,\"target_commitish\":\"main\"}")
|
|
||||||
RELEASE_ID=$(echo "$RESP" | jq -r '.id // empty')
|
|
||||||
fi
|
|
||||||
echo "Target Release ID: $RELEASE_ID"
|
|
||||||
|
|
||||||
# 2. Upload generated packages
|
|
||||||
shopt -s nullglob
|
|
||||||
FILES=(
|
|
||||||
target/x86_64-unknown-linux-gnu/release/*.deb
|
|
||||||
target/x86_64-unknown-linux-gnu/release/*.AppImage
|
|
||||||
target/x86_64-unknown-linux-gnu/generate-rpm/*.rpm
|
|
||||||
target/release/*.deb
|
|
||||||
target/release/*.AppImage
|
|
||||||
target/generate-rpm/*.rpm
|
|
||||||
)
|
|
||||||
|
|
||||||
for f in "${FILES[@]}"; do
|
|
||||||
if [ -f "$f" ]; then
|
|
||||||
fname=$(basename "$f")
|
|
||||||
echo "Uploading asset $fname..."
|
|
||||||
curl -s -X POST "$API_URL/releases/$RELEASE_ID/assets?name=$fname" \
|
|
||||||
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
|
||||||
-F "attachment=@$f"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
echo "Release upload completed!"
|
|
||||||
4
.gitattributes
vendored
|
|
@ -1,2 +1,6 @@
|
||||||
* text=auto eol=lf
|
* text=auto eol=lf
|
||||||
Cargo.lock -diff linguist-generated
|
Cargo.lock -diff linguist-generated
|
||||||
|
pnpm-lock.yaml -diff linguist-generated
|
||||||
|
packages/client/src/bindings.ts -diff linguist-generated=true
|
||||||
|
.github/actions/publish-artifacts/dist/index.js -diff linguist-generated=true
|
||||||
|
.github/actions/clippy/dist/index.js -diff linguist-generated=true
|
||||||
|
|
|
||||||
14
.github/actions/install-cargo-tool/action.yml
vendored
|
|
@ -17,13 +17,9 @@ runs:
|
||||||
- name: Check for an already-installed ${{ inputs.tool }}
|
- name: Check for an already-installed ${{ inputs.tool }}
|
||||||
id: check
|
id: check
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
|
||||||
TOOL: ${{ inputs.tool }}
|
|
||||||
VERSION: ${{ inputs.version }}
|
|
||||||
run: |
|
run: |
|
||||||
if command -v "$TOOL" > /dev/null 2>&1 \
|
if cargo install --list | grep -qF "${{ inputs.tool }} v${{ inputs.version }}:"; then
|
||||||
&& cargo install --list | grep -qF "$TOOL v$VERSION:"; then
|
echo "Found ${{ inputs.tool }} v${{ inputs.version }} in \$CARGO_HOME/bin"
|
||||||
echo "Found $TOOL v$VERSION in \$CARGO_HOME/bin"
|
|
||||||
echo "installed=true" >> "$GITHUB_OUTPUT"
|
echo "installed=true" >> "$GITHUB_OUTPUT"
|
||||||
else
|
else
|
||||||
echo "installed=false" >> "$GITHUB_OUTPUT"
|
echo "installed=false" >> "$GITHUB_OUTPUT"
|
||||||
|
|
@ -31,7 +27,7 @@ runs:
|
||||||
|
|
||||||
- name: Setup cargo-binstall
|
- name: Setup cargo-binstall
|
||||||
if: steps.check.outputs.installed == 'false'
|
if: steps.check.outputs.installed == 'false'
|
||||||
uses: cargo-bins/cargo-binstall@e00d2c94cc0067b77737821097a62d91c0301baa # v1.21.1
|
uses: cargo-bins/cargo-binstall@main
|
||||||
|
|
||||||
- name: Install ${{ inputs.tool }}
|
- name: Install ${{ inputs.tool }}
|
||||||
if: steps.check.outputs.installed == 'false'
|
if: steps.check.outputs.installed == 'false'
|
||||||
|
|
@ -43,5 +39,5 @@ runs:
|
||||||
# Prebuilt download takes seconds; building cargo-packager from source
|
# Prebuilt download takes seconds; building cargo-packager from source
|
||||||
# takes minutes on every leg of the matrix. Fall back to a source build
|
# takes minutes on every leg of the matrix. Fall back to a source build
|
||||||
# so a missing prebuilt artifact can never break a release.
|
# so a missing prebuilt artifact can never break a release.
|
||||||
cargo binstall --no-confirm --force "${TOOL}@${VERSION}" \
|
cargo binstall --no-confirm "${TOOL}@${VERSION}" \
|
||||||
|| cargo install "${TOOL}" --version "${VERSION}" --locked --force
|
|| cargo install "${TOOL}" --version "${VERSION}" --locked
|
||||||
|
|
|
||||||
14
.github/actions/setup-rust/action.yml
vendored
|
|
@ -13,19 +13,11 @@ inputs:
|
||||||
description: Whether to restore the Rust cache
|
description: Whether to restore the Rust cache
|
||||||
required: false
|
required: false
|
||||||
default: 'true'
|
default: 'true'
|
||||||
cache-namespace:
|
|
||||||
description: >
|
|
||||||
Cache namespace. Workflows that compile with different profile settings
|
|
||||||
MUST use different namespaces - cargo fingerprints every dependency by its
|
|
||||||
profile, so sharing one would mean each workflow evicts artifacts the other
|
|
||||||
cannot use.
|
|
||||||
required: false
|
|
||||||
default: release-cache
|
|
||||||
runs:
|
runs:
|
||||||
using: composite
|
using: composite
|
||||||
steps:
|
steps:
|
||||||
- name: Install Rust (stable)
|
- name: Install Rust (stable)
|
||||||
uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable branch @ 2026-07-16
|
uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
toolchain: stable
|
toolchain: stable
|
||||||
targets: ${{ inputs.target }}
|
targets: ${{ inputs.target }}
|
||||||
|
|
@ -33,11 +25,11 @@ runs:
|
||||||
|
|
||||||
- name: Cache Rust
|
- name: Cache Rust
|
||||||
if: ${{ inputs.restore-cache == 'true' }}
|
if: ${{ inputs.restore-cache == 'true' }}
|
||||||
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
key: ${{ inputs.target }}
|
key: ${{ inputs.target }}
|
||||||
save-if: ${{ inputs.save-cache }}
|
save-if: ${{ inputs.save-cache }}
|
||||||
shared-key: ${{ inputs.cache-namespace }}
|
shared-key: release-cache
|
||||||
# Release builds are long and can fail late (signing, notarization,
|
# Release builds are long and can fail late (signing, notarization,
|
||||||
# upload). Without this the whole compile is thrown away on those runs.
|
# upload). Without this the whole compile is thrown away on those runs.
|
||||||
cache-on-failure: true
|
cache-on-failure: true
|
||||||
|
|
|
||||||
BIN
.github/media/RepoBanner.png
vendored
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 54 KiB |
8
.github/workflows/aur_publish.yml
vendored
|
|
@ -30,7 +30,7 @@ jobs:
|
||||||
needs: resolve
|
needs: resolve
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Compute source sha256
|
- name: Compute source sha256
|
||||||
id: hash
|
id: hash
|
||||||
|
|
@ -52,7 +52,7 @@ jobs:
|
||||||
sed -i "s/^sha256sums=.*/sha256sums=('${SHA256}')/" "$PKGBUILD"
|
sed -i "s/^sha256sums=.*/sha256sums=('${SHA256}')/" "$PKGBUILD"
|
||||||
|
|
||||||
- name: Publish to AUR
|
- name: Publish to AUR
|
||||||
uses: KSXGitHub/github-actions-deploy-aur@a97f56a8425a7a7f3b8c58607f769c69b089cadb # v3.0.1
|
uses: KSXGitHub/github-actions-deploy-aur@v3
|
||||||
with:
|
with:
|
||||||
pkgname: oneclient
|
pkgname: oneclient
|
||||||
pkgbuild: distribution/aur/oneclient/PKGBUILD
|
pkgbuild: distribution/aur/oneclient/PKGBUILD
|
||||||
|
|
@ -65,7 +65,7 @@ jobs:
|
||||||
needs: resolve
|
needs: resolve
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Compute deb sha256
|
- name: Compute deb sha256
|
||||||
id: hash
|
id: hash
|
||||||
|
|
@ -87,7 +87,7 @@ jobs:
|
||||||
sed -i "s/^sha256sums=.*/sha256sums=('${SHA256}')/" "$PKGBUILD"
|
sed -i "s/^sha256sums=.*/sha256sums=('${SHA256}')/" "$PKGBUILD"
|
||||||
|
|
||||||
- name: Publish to AUR
|
- name: Publish to AUR
|
||||||
uses: KSXGitHub/github-actions-deploy-aur@a97f56a8425a7a7f3b8c58607f769c69b089cadb # v3.0.1
|
uses: KSXGitHub/github-actions-deploy-aur@v3
|
||||||
with:
|
with:
|
||||||
pkgname: oneclient-bin
|
pkgname: oneclient-bin
|
||||||
pkgbuild: distribution/aur/oneclient-bin/PKGBUILD
|
pkgbuild: distribution/aur/oneclient-bin/PKGBUILD
|
||||||
|
|
|
||||||
35
.github/workflows/changelog_publish.yml
vendored
|
|
@ -1,35 +0,0 @@
|
||||||
name: Publish Changelog
|
|
||||||
|
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types: [published, edited]
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: changelog-publish
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
publish:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
GH_REPO: ${{ github.repository }}
|
|
||||||
steps:
|
|
||||||
# The launcher reads releases/latest/download/changelog.json, so the
|
|
||||||
# snapshot of the last 50 release bodies always lives on the latest release.
|
|
||||||
- name: Build changelog.json from release bodies
|
|
||||||
run: |
|
|
||||||
gh api "repos/$GH_REPO/releases?per_page=50" --jq '[.[]
|
|
||||||
| select(.draft | not)
|
|
||||||
| select(.tag_name | startswith("oneclient-"))
|
|
||||||
| {
|
|
||||||
version: (.tag_name | ltrimstr("oneclient-")),
|
|
||||||
body: ((.body // "") | sub("^Automated release for OneClient [^\n]*\\s*"; ""))
|
|
||||||
}]' > changelog.json
|
|
||||||
jq -r '.[].version' changelog.json
|
|
||||||
|
|
||||||
- name: Upload to latest release
|
|
||||||
run: gh release upload "$(gh release view --json tagName -q .tagName)" changelog.json --clobber
|
|
||||||
23
.github/workflows/changeset_check.yml
vendored
|
|
@ -1,23 +0,0 @@
|
||||||
name: Changeset Check
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [opened, synchronize, reopened, labeled, unlabeled]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
changeset:
|
|
||||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no changeset') }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Require a change file
|
|
||||||
env:
|
|
||||||
BASE_REF: ${{ github.base_ref }}
|
|
||||||
run: |
|
|
||||||
if ! git diff --name-only --diff-filter=A "origin/$BASE_REF...HEAD" -- '.changeset/*.md' | grep -q .; then
|
|
||||||
echo "::error::Add a change file with \`knope document-change\`, or label the PR 'no changeset' if it isn't user-facing."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
60
.github/workflows/copr_publish.yml
vendored
|
|
@ -1,60 +0,0 @@
|
||||||
name: Publish COPR Package
|
|
||||||
|
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
tag:
|
|
||||||
description: 'Release tag (e.g. oneclient-2.0.1)'
|
|
||||||
required: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
resolve:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
outputs:
|
|
||||||
version: ${{ steps.parse.outputs.version }}
|
|
||||||
steps:
|
|
||||||
- name: Parse release tag
|
|
||||||
id: parse
|
|
||||||
run: |
|
|
||||||
TAG="${{ github.event.release.tag_name || inputs.tag }}"
|
|
||||||
if [[ "$TAG" =~ ^oneclient-(.+)$ ]]; then
|
|
||||||
echo "version=${BASH_REMATCH[1]}" >> "$GITHUB_OUTPUT"
|
|
||||||
else
|
|
||||||
echo "::error::Tag '$TAG' does not match oneclient-*"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
publish-bin:
|
|
||||||
needs: resolve
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container: fedora:latest
|
|
||||||
steps:
|
|
||||||
- name: Install tooling
|
|
||||||
run: dnf -y install rpm-build rpmdevtools copr-cli bsdtar git
|
|
||||||
|
|
||||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
||||||
|
|
||||||
- name: Prepare spec
|
|
||||||
env:
|
|
||||||
VERSION: ${{ needs.resolve.outputs.version }}
|
|
||||||
run: |
|
|
||||||
SPEC="distribution/copr/oneclient-bin.spec"
|
|
||||||
sed -i "s/^Version:.*/Version: ${VERSION}/" "$SPEC"
|
|
||||||
sed -i "s/^Release:.*/Release: 1%{?dist}/" "$SPEC"
|
|
||||||
|
|
||||||
- name: Build SRPM
|
|
||||||
run: |
|
|
||||||
rpmdev-setuptree
|
|
||||||
spectool -g -R distribution/copr/oneclient-bin.spec
|
|
||||||
rpmbuild -bs distribution/copr/oneclient-bin.spec
|
|
||||||
|
|
||||||
- name: Submit to COPR
|
|
||||||
env:
|
|
||||||
COPR_CONFIG_CONTENT: ${{ secrets.COPR_CONFIG }}
|
|
||||||
run: |
|
|
||||||
mkdir -p ~/.config
|
|
||||||
printf '%s' "$COPR_CONFIG_CONTENT" > ~/.config/copr
|
|
||||||
chmod 600 ~/.config/copr
|
|
||||||
copr-cli build polyfrost/OneClient ~/rpmbuild/SRPMS/oneclient-bin-*.src.rpm
|
|
||||||
145
.github/workflows/oneclient_nightly.yml
vendored
|
|
@ -10,22 +10,6 @@ concurrency:
|
||||||
group: oneclient-nightly-${{ github.ref }}
|
group: oneclient-nightly-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
# Nightlies are unsigned test builds, so they do not need the release profile's
|
|
||||||
# whole-program optimization - and that optimization is exactly what makes them
|
|
||||||
# slow. `lto = "fat"` + `codegen-units = 1` force a single-threaded re-optimization
|
|
||||||
# of the entire dependency graph at link time, on every run, which no cache can
|
|
||||||
# ever skip. Thin LTO keeps nearly all of the runtime benefit at a fraction of the
|
|
||||||
# link cost, and 16 codegen units lets the whole compile use every runner core.
|
|
||||||
#
|
|
||||||
# These are cargo's documented profile overrides, so `target/release` stays the
|
|
||||||
# output directory and every path below is unaffected. The shipped release binary
|
|
||||||
# is untouched: oneclient_release.yml still builds the fat-LTO profile from
|
|
||||||
# Cargo.toml. That difference is why the two workflows must not share a cache
|
|
||||||
# namespace - see `cache-namespace` on the Setup Rust step.
|
|
||||||
env:
|
|
||||||
CARGO_PROFILE_RELEASE_LTO: thin
|
|
||||||
CARGO_PROFILE_RELEASE_CODEGEN_UNITS: '16'
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
permissions:
|
permissions:
|
||||||
|
|
@ -58,7 +42,7 @@ jobs:
|
||||||
|
|
||||||
runs-on: ${{ matrix.platform }}
|
runs-on: ${{ matrix.platform }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: (macOS) Pin deployment target
|
- name: (macOS) Pin deployment target
|
||||||
if: matrix.macos_deployment_target != ''
|
if: matrix.macos_deployment_target != ''
|
||||||
|
|
@ -71,10 +55,6 @@ jobs:
|
||||||
target: ${{ matrix.target }}
|
target: ${{ matrix.target }}
|
||||||
save-cache: true
|
save-cache: true
|
||||||
restore-cache: true
|
restore-cache: true
|
||||||
# Separate namespace: nightly's thin-LTO dependency artifacts have
|
|
||||||
# different cargo fingerprints than the release workflow's fat-LTO ones,
|
|
||||||
# so a shared cache would just thrash between the two.
|
|
||||||
cache-namespace: nightly-cache
|
|
||||||
|
|
||||||
- name: (Linux) Install system dependencies
|
- name: (Linux) Install system dependencies
|
||||||
if: matrix.platform == 'ubuntu-22.04'
|
if: matrix.platform == 'ubuntu-22.04'
|
||||||
|
|
@ -85,7 +65,6 @@ jobs:
|
||||||
libfontconfig-dev libfreetype-dev \
|
libfontconfig-dev libfreetype-dev \
|
||||||
libgl1-mesa-dev libx11-dev libxcursor-dev libxi-dev libxrandr-dev \
|
libgl1-mesa-dev libx11-dev libxcursor-dev libxi-dev libxrandr-dev \
|
||||||
libxkbcommon-dev libxkbcommon-x11-dev libwayland-dev libdbus-1-dev \
|
libxkbcommon-dev libxkbcommon-x11-dev libwayland-dev libdbus-1-dev \
|
||||||
libgtk-3-dev libayatana-appindicator3-dev \
|
|
||||||
libfuse2
|
libfuse2
|
||||||
|
|
||||||
- name: Install cargo-packager
|
- name: Install cargo-packager
|
||||||
|
|
@ -116,47 +95,6 @@ jobs:
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: (macOS) Compile app icon catalog
|
|
||||||
if: startsWith(matrix.platform, 'macos')
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
out="packages/oneclient_app/icons/generated"
|
|
||||||
mkdir -p "$out"
|
|
||||||
xcrun actool packages/oneclient_app/icons/OneClient.icon \
|
|
||||||
--compile "$out" \
|
|
||||||
--app-icon OneClient \
|
|
||||||
--output-partial-info-plist "$out/partial.plist" \
|
|
||||||
--platform macosx \
|
|
||||||
--minimum-deployment-target 26.0 \
|
|
||||||
--errors --warnings --notices
|
|
||||||
rm -f "$out/OneClient.icns"
|
|
||||||
test -f "$out/Assets.car" || { echo "::error::actool produced no Assets.car"; exit 1; }
|
|
||||||
|
|
||||||
- name: (Windows) Stage app-local VC++ runtime
|
|
||||||
if: matrix.platform == 'windows-latest'
|
|
||||||
shell: bash
|
|
||||||
run: python3 distribution/windows/bundle-crt.py
|
|
||||||
|
|
||||||
- name: (Windows) Verify staged runtime is Microsoft-signed
|
|
||||||
if: matrix.platform == 'windows-latest'
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
$dlls = @(Get-ChildItem 'target/crt/*.dll')
|
|
||||||
if ($dlls.Count -eq 0) { Write-Error 'no CRT DLLs staged'; exit 1 }
|
|
||||||
foreach ($dll in $dlls) {
|
|
||||||
$sig = Get-AuthenticodeSignature $dll.FullName
|
|
||||||
Write-Host "$($dll.Name): $($sig.Status) $($sig.SignerCertificate.Subject)"
|
|
||||||
if ($sig.Status -ne 'Valid') {
|
|
||||||
Write-Error "$($dll.Name) signature is $($sig.Status), refusing to ship it"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
if ($sig.SignerCertificate.Subject -notmatch 'O=Microsoft Corporation') {
|
|
||||||
Write-Error "$($dll.Name) is not signed by Microsoft"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- name: Package (unsigned)
|
- name: Package (unsigned)
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
|
|
@ -190,38 +128,9 @@ jobs:
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cp target/release/.cargo-packager/deb/*/data/usr/share/applications/oneclient_app.desktop \
|
|
||||||
target/release/oneclient_app.desktop
|
|
||||||
cargo generate-rpm -p packages/oneclient_app
|
cargo generate-rpm -p packages/oneclient_app
|
||||||
cp target/generate-rpm/*.rpm "${{ steps.outdir.outputs.dir }}/"
|
cp target/generate-rpm/*.rpm "${{ steps.outdir.outputs.dir }}/"
|
||||||
|
|
||||||
- name: (Windows) Smoke-test installer layout
|
|
||||||
if: matrix.platform == 'windows-latest'
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
$dest = 'C:\oneclient-smoke'
|
|
||||||
Remove-Item -Recurse -Force $dest -ErrorAction SilentlyContinue
|
|
||||||
$exe = Resolve-Path (Get-ChildItem '${{ steps.outdir.outputs.dir }}/*-setup.exe')[0].FullName
|
|
||||||
# NSIS: /S is silent, /D= sets the install dir and must be the last arg.
|
|
||||||
$p = Start-Process -FilePath $exe -ArgumentList '/S', "/D=$dest" -Wait -PassThru
|
|
||||||
if ($p.ExitCode -ne 0) { Write-Error "installer exited $($p.ExitCode)"; exit 1 }
|
|
||||||
|
|
||||||
Write-Host '--- installed layout ---'
|
|
||||||
Get-ChildItem -Recurse $dest | ForEach-Object { Write-Host $_.FullName }
|
|
||||||
|
|
||||||
if (-not (Test-Path "$dest\oneclient_app.exe")) {
|
|
||||||
Write-Error "oneclient_app.exe is not at the root of $dest"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
foreach ($dll in @(Get-ChildItem 'target/crt/*.dll')) {
|
|
||||||
if (-not (Test-Path (Join-Path $dest $dll.Name))) {
|
|
||||||
Write-Error "$($dll.Name) did not land beside oneclient_app.exe - cargo-packager put resources elsewhere, so app-local CRT loading will not work"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Write-Host "OK: $($(Get-ChildItem 'target/crt/*.dll').Count) CRT DLLs sit beside oneclient_app.exe"
|
|
||||||
|
|
||||||
# Upload debug info (dSYM / .dwp / .pdb produced by the release profile's
|
# Upload debug info (dSYM / .dwp / .pdb produced by the release profile's
|
||||||
# split-debuginfo) to Sentry so nightly crash reports get real file/function/
|
# split-debuginfo) to Sentry so nightly crash reports get real file/function/
|
||||||
# line stack traces instead of raw addresses. Runs on every main push, so
|
# line stack traces instead of raw addresses. Runs on every main push, so
|
||||||
|
|
@ -264,61 +173,21 @@ jobs:
|
||||||
for f in "${{ steps.outdir.outputs.dir }}"/*.dmg \
|
for f in "${{ steps.outdir.outputs.dir }}"/*.dmg \
|
||||||
"${{ steps.outdir.outputs.dir }}"/*.deb \
|
"${{ steps.outdir.outputs.dir }}"/*.deb \
|
||||||
"${{ steps.outdir.outputs.dir }}"/*.rpm \
|
"${{ steps.outdir.outputs.dir }}"/*.rpm \
|
||||||
"${{ steps.outdir.outputs.dir }}"/*.AppImage; do
|
"${{ steps.outdir.outputs.dir }}"/*.AppImage \
|
||||||
|
"${{ steps.outdir.outputs.dir }}"/*-setup.exe; do
|
||||||
[ -e "$f" ] || continue
|
[ -e "$f" ] || continue
|
||||||
cp "$f" dist/
|
cp "$f" dist/
|
||||||
done
|
done
|
||||||
# Raw executable, renamed per platform so the unzipped uploads don't collide.
|
|
||||||
bin="${{ steps.outdir.outputs.dir }}/oneclient_app"
|
|
||||||
ext=""
|
|
||||||
[ -e "$bin.exe" ] && ext=".exe"
|
|
||||||
cp "$bin$ext" "dist/oneclient_app-${{ matrix.os_key }}-${{ matrix.arch }}$ext"
|
|
||||||
if [ -z "$(ls -A dist)" ]; then
|
if [ -z "$(ls -A dist)" ]; then
|
||||||
echo "::error::No installers found in ${{ steps.outdir.outputs.dir }}"
|
echo "::error::No installers found in ${{ steps.outdir.outputs.dir }}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
ls -la dist
|
ls -la dist
|
||||||
|
|
||||||
- name: Upload unsigned executable
|
- name: Upload unsigned installers
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
path: dist/oneclient_app-${{ matrix.os_key }}-${{ matrix.arch }}*
|
name: oneclient-nightly-${{ matrix.os_key }}-${{ matrix.arch }}
|
||||||
archive: false
|
path: dist/*
|
||||||
if-no-files-found: error
|
|
||||||
retention-days: 14
|
|
||||||
|
|
||||||
- name: (macOS) Upload unsigned dmg
|
|
||||||
if: matrix.os_key == 'darwin'
|
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
||||||
with:
|
|
||||||
path: dist/*.dmg
|
|
||||||
archive: false
|
|
||||||
if-no-files-found: error
|
|
||||||
retention-days: 14
|
|
||||||
|
|
||||||
- name: (Linux) Upload unsigned deb
|
|
||||||
if: matrix.os_key == 'linux'
|
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
||||||
with:
|
|
||||||
path: dist/*.deb
|
|
||||||
archive: false
|
|
||||||
if-no-files-found: error
|
|
||||||
retention-days: 14
|
|
||||||
|
|
||||||
- name: (Linux) Upload unsigned rpm
|
|
||||||
if: matrix.os_key == 'linux'
|
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
||||||
with:
|
|
||||||
path: dist/*.rpm
|
|
||||||
archive: false
|
|
||||||
if-no-files-found: error
|
|
||||||
retention-days: 14
|
|
||||||
|
|
||||||
- name: (Linux) Upload unsigned AppImage
|
|
||||||
if: matrix.os_key == 'linux'
|
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
||||||
with:
|
|
||||||
path: dist/*.AppImage
|
|
||||||
archive: false
|
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 14
|
retention-days: 14
|
||||||
|
|
|
||||||
194
.github/workflows/oneclient_release.yml
vendored
|
|
@ -3,11 +3,22 @@ name: OneClient Release Build
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
bump:
|
||||||
|
description: 'Bump the workspace version in Cargo.toml before releasing'
|
||||||
|
type: choice
|
||||||
|
required: false
|
||||||
|
default: none
|
||||||
|
options:
|
||||||
|
- none
|
||||||
|
- patch
|
||||||
|
- minor
|
||||||
|
- major
|
||||||
|
|
||||||
# NOTE: CARGO_PACKAGER_SIGN_PRIVATE_KEY* are deliberately NOT declared at
|
# NOTE: CARGO_PACKAGER_SIGN_PRIVATE_KEY* are deliberately NOT declared at
|
||||||
# workflow level. Swatinem/rust-cache folds every `CARGO*`-prefixed environment
|
# workflow level. Swatinem/rust-cache folds every `CARGO*`-prefixed environment
|
||||||
# variable into its cache key, so a workflow-level declaration gave this workflow
|
# variable into its cache key, so a workflow-level declaration gave this workflow
|
||||||
# a cache namespace that the nightly workflow could never populate - every
|
# a cache namespace that the nightly workflow could never populate — every
|
||||||
# release built the whole dependency graph from scratch. They are scoped to the
|
# release built the whole dependency graph from scratch. They are scoped to the
|
||||||
# individual steps that actually need them instead.
|
# individual steps that actually need them instead.
|
||||||
|
|
||||||
|
|
@ -25,26 +36,19 @@ jobs:
|
||||||
tag: ${{ steps.version.outputs.tag }}
|
tag: ${{ steps.version.outputs.tag }}
|
||||||
sha: ${{ steps.version.outputs.sha }}
|
sha: ${{ steps.version.outputs.sha }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
- uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
# Knope finds the commit (and so the PR author) that added each change file
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Setup Rust
|
- name: Setup Rust
|
||||||
|
if: inputs.bump != 'none'
|
||||||
uses: ./.github/actions/setup-rust
|
uses: ./.github/actions/setup-rust
|
||||||
with:
|
with:
|
||||||
restore-cache: false
|
restore-cache: false
|
||||||
|
|
||||||
- name: Install Knope
|
- name: Resolve version, bump if requested, refuse to clobber a release
|
||||||
uses: ./.github/actions/install-cargo-tool
|
|
||||||
with:
|
|
||||||
tool: knope
|
|
||||||
version: 0.23.0
|
|
||||||
|
|
||||||
- name: Consume changesets
|
|
||||||
id: version
|
id: version
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
BUMP: ${{ inputs.bump }}
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
@ -53,29 +57,63 @@ jobs:
|
||||||
sed -n -E '/^\[workspace\.package\]/,/^\[/ s/^version[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' Cargo.toml | head -n1
|
sed -n -E '/^\[workspace\.package\]/,/^\[/ s/^version[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' Cargo.toml | head -n1
|
||||||
}
|
}
|
||||||
|
|
||||||
# No change files means a re-run of a release whose version commit
|
|
||||||
# already landed, so carry on with the current version.
|
|
||||||
RELEASED=0
|
|
||||||
if compgen -G ".changeset/*.md" > /dev/null; then
|
|
||||||
knope prepare-release
|
|
||||||
cargo update --workspace --offline || cargo update --workspace
|
|
||||||
RELEASED=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
VERSION="$(read_version)"
|
VERSION="$(read_version)"
|
||||||
if [ -z "$VERSION" ]; then
|
if [ -z "$VERSION" ]; then
|
||||||
echo "::error::Could not read version from [workspace.package] in Cargo.toml"
|
echo "::error::Could not read version from [workspace.package] in Cargo.toml"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
echo "Current version: $VERSION"
|
||||||
|
|
||||||
|
if [ "$BUMP" != "none" ]; then
|
||||||
|
if ! [[ "$VERSION" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
|
||||||
|
echo "::error::Version '$VERSION' is not plain semver major.minor.patch; cannot bump automatically."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
MAJOR="${BASH_REMATCH[1]}"; MINOR="${BASH_REMATCH[2]}"; PATCH="${BASH_REMATCH[3]}"
|
||||||
|
case "$BUMP" in
|
||||||
|
major) MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0 ;;
|
||||||
|
minor) MINOR=$((MINOR + 1)); PATCH=0 ;;
|
||||||
|
patch) PATCH=$((PATCH + 1)) ;;
|
||||||
|
esac
|
||||||
|
NEW_VERSION="$MAJOR.$MINOR.$PATCH"
|
||||||
|
echo "Bumping ($BUMP): $VERSION -> $NEW_VERSION"
|
||||||
|
|
||||||
|
# Only rewrite the `version` key inside [workspace.package]; the
|
||||||
|
# dependency pins further down the file must not be touched.
|
||||||
|
NEW_VERSION="$NEW_VERSION" python3 - <<'PY'
|
||||||
|
import os, re
|
||||||
|
new = os.environ["NEW_VERSION"]
|
||||||
|
src = open("Cargo.toml").read()
|
||||||
|
start = src.index("[workspace.package]")
|
||||||
|
end = src.find("\n[", start + 1)
|
||||||
|
end = len(src) if end == -1 else end
|
||||||
|
section, count = re.subn(
|
||||||
|
r'(?m)^version\s*=\s*"[^"]+"', f'version = "{new}"', src[start:end], count=1
|
||||||
|
)
|
||||||
|
assert count == 1, "no version key found in [workspace.package]"
|
||||||
|
open("Cargo.toml", "w").write(src[:start] + section + src[end:])
|
||||||
|
PY
|
||||||
|
|
||||||
|
# Workspace members inherit `version.workspace = true`, so only the
|
||||||
|
# lockfile's own entries for those members need refreshing.
|
||||||
|
cargo update --workspace --offline || cargo update --workspace
|
||||||
|
|
||||||
|
ACTUAL="$(read_version)"
|
||||||
|
if [ "$ACTUAL" != "$NEW_VERSION" ]; then
|
||||||
|
echo "::error::Version rewrite failed (Cargo.toml still reports '$ACTUAL')"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
VERSION="$NEW_VERSION"
|
||||||
|
fi
|
||||||
|
|
||||||
TAG="oneclient-$VERSION"
|
TAG="oneclient-$VERSION"
|
||||||
echo "Releasing OneClient $VERSION"
|
|
||||||
|
|
||||||
# Abort rather than clobber: a published release (or an existing tag,
|
# Abort rather than clobber: a published release (or an existing tag,
|
||||||
# which only appears once a release is published) means this version
|
# which only appears once a release is published) means this version
|
||||||
# already shipped. A *draft* is a previous failed/partial run of this
|
# already shipped. A *draft* is a previous failed/partial run of this
|
||||||
# same workflow, so reuse it.
|
# same workflow, so reuse it.
|
||||||
if git ls-remote --exit-code --tags origin "refs/tags/$TAG" >/dev/null 2>&1; then
|
if git ls-remote --exit-code --tags origin "refs/tags/$TAG" >/dev/null 2>&1; then
|
||||||
echo "::error::Tag $TAG already exists on origin - OneClient $VERSION has already been released. Add a change file with \`knope document-change\` first."
|
echo "::error::Tag $TAG already exists on origin — OneClient $VERSION has already been released. Re-run with a bump, or bump the version in Cargo.toml."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -85,17 +123,17 @@ jobs:
|
||||||
RELEASE_EXISTS=1
|
RELEASE_EXISTS=1
|
||||||
fi
|
fi
|
||||||
if [ "$RELEASE_EXISTS" = "1" ] && [ "$IS_DRAFT" != "true" ]; then
|
if [ "$RELEASE_EXISTS" = "1" ] && [ "$IS_DRAFT" != "true" ]; then
|
||||||
echo "::error::Release $TAG already exists and is published - refusing to overwrite OneClient $VERSION. Add a change file with \`knope document-change\` first."
|
echo "::error::Release $TAG already exists and is published — refusing to overwrite OneClient $VERSION. Re-run with a bump, or bump the version in Cargo.toml."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Commit only after the guards pass, so a rejected release does not
|
# Commit the bump only after the guards pass, so a rejected release
|
||||||
# leave a stray version bump on the branch.
|
# does not leave a stray version bump on the branch.
|
||||||
if [ "$RELEASED" = "1" ]; then
|
if [ "$BUMP" != "none" ]; then
|
||||||
git config user.name "github-actions[bot]"
|
git config user.name "github-actions[bot]"
|
||||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
git add -A Cargo.toml Cargo.lock .changeset CHANGELOG.md
|
git add Cargo.toml Cargo.lock
|
||||||
git commit -m "chore: release $VERSION"
|
git commit -m "chore: bump to $VERSION"
|
||||||
git push origin "HEAD:${GITHUB_REF_NAME}"
|
git push origin "HEAD:${GITHUB_REF_NAME}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -105,18 +143,11 @@ jobs:
|
||||||
echo "Reusing existing draft release $TAG"
|
echo "Reusing existing draft release $TAG"
|
||||||
gh release edit "$TAG" --target "$SHA"
|
gh release edit "$TAG" --target "$SHA"
|
||||||
else
|
else
|
||||||
# The body is what the launcher's changelog page shows
|
|
||||||
awk -v heading="## $VERSION " '/^## /{p = index($0, heading) == 1; next} p' \
|
|
||||||
CHANGELOG.md > notes.md 2>/dev/null || true
|
|
||||||
if ! grep -q '[^[:space:]]' notes.md; then
|
|
||||||
echo "::error::No changelog entry for $VERSION. Add a change file with \`knope document-change\` first."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
gh release create "$TAG" \
|
gh release create "$TAG" \
|
||||||
--draft \
|
--draft \
|
||||||
--target "$SHA" \
|
--target "$SHA" \
|
||||||
--title "OneClient $VERSION" \
|
--title "OneClient $VERSION" \
|
||||||
--notes-file notes.md
|
--notes "Automated release for OneClient $VERSION."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||||
|
|
@ -158,9 +189,9 @@ jobs:
|
||||||
|
|
||||||
runs-on: ${{ matrix.platform }}
|
runs-on: ${{ matrix.platform }}
|
||||||
steps:
|
steps:
|
||||||
# Pin to the commit `prepare` resolved - after a release that is the version
|
# Pin to the commit `prepare` resolved — with `bump` that is the version
|
||||||
# commit it just pushed, which github.sha would not point at.
|
# commit it just pushed, which github.sha would not point at.
|
||||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: ${{ needs.prepare.outputs.sha }}
|
ref: ${{ needs.prepare.outputs.sha }}
|
||||||
|
|
||||||
|
|
@ -185,7 +216,6 @@ jobs:
|
||||||
libfontconfig-dev libfreetype-dev \
|
libfontconfig-dev libfreetype-dev \
|
||||||
libgl1-mesa-dev libx11-dev libxcursor-dev libxi-dev libxrandr-dev \
|
libgl1-mesa-dev libx11-dev libxcursor-dev libxi-dev libxrandr-dev \
|
||||||
libxkbcommon-dev libxkbcommon-x11-dev libwayland-dev libdbus-1-dev \
|
libxkbcommon-dev libxkbcommon-x11-dev libwayland-dev libdbus-1-dev \
|
||||||
libgtk-3-dev libayatana-appindicator3-dev \
|
|
||||||
libfuse2
|
libfuse2
|
||||||
|
|
||||||
- name: Install cargo-packager
|
- name: Install cargo-packager
|
||||||
|
|
@ -255,14 +285,14 @@ jobs:
|
||||||
- name: (Windows) Upload unsigned app binary artifact
|
- name: (Windows) Upload unsigned app binary artifact
|
||||||
if: matrix.platform == 'windows-latest'
|
if: matrix.platform == 'windows-latest'
|
||||||
id: win_upload_unsigned_bin
|
id: win_upload_unsigned_bin
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: oneclient-windows-binary-unsigned
|
name: oneclient-windows-binary-unsigned
|
||||||
path: target/release/oneclient_app.exe
|
path: target/release/oneclient_app.exe
|
||||||
|
|
||||||
- name: (Windows) Submit SignPath signing request for app binary
|
- name: (Windows) Submit SignPath signing request for app binary
|
||||||
if: matrix.platform == 'windows-latest'
|
if: matrix.platform == 'windows-latest'
|
||||||
uses: signpath/github-action-submit-signing-request@ced31329c0317e779dad2eec2a7c3bb46ea1343e # v1.3
|
uses: signpath/github-action-submit-signing-request@v1
|
||||||
with:
|
with:
|
||||||
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
|
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
|
||||||
organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }}
|
organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }}
|
||||||
|
|
@ -298,47 +328,6 @@ jobs:
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
- name: (macOS) Compile app icon catalog
|
|
||||||
if: startsWith(matrix.platform, 'macos')
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
out="packages/oneclient_app/icons/generated"
|
|
||||||
mkdir -p "$out"
|
|
||||||
xcrun actool packages/oneclient_app/icons/OneClient.icon \
|
|
||||||
--compile "$out" \
|
|
||||||
--app-icon OneClient \
|
|
||||||
--output-partial-info-plist "$out/partial.plist" \
|
|
||||||
--platform macosx \
|
|
||||||
--minimum-deployment-target 26.0 \
|
|
||||||
--errors --warnings --notices
|
|
||||||
rm -f "$out/OneClient.icns"
|
|
||||||
test -f "$out/Assets.car" || { echo "::error::actool produced no Assets.car"; exit 1; }
|
|
||||||
|
|
||||||
- name: (Windows) Stage app-local VC++ runtime
|
|
||||||
if: matrix.platform == 'windows-latest'
|
|
||||||
shell: bash
|
|
||||||
run: python3 distribution/windows/bundle-crt.py
|
|
||||||
|
|
||||||
- name: (Windows) Verify staged runtime is Microsoft-signed
|
|
||||||
if: matrix.platform == 'windows-latest'
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
$dlls = @(Get-ChildItem 'target/crt/*.dll')
|
|
||||||
if ($dlls.Count -eq 0) { Write-Error 'no CRT DLLs staged'; exit 1 }
|
|
||||||
foreach ($dll in $dlls) {
|
|
||||||
$sig = Get-AuthenticodeSignature $dll.FullName
|
|
||||||
Write-Host "$($dll.Name): $($sig.Status) $($sig.SignerCertificate.Subject)"
|
|
||||||
if ($sig.Status -ne 'Valid') {
|
|
||||||
Write-Error "$($dll.Name) signature is $($sig.Status), refusing to ship it"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
if ($sig.SignerCertificate.Subject -notmatch 'O=Microsoft Corporation') {
|
|
||||||
Write-Error "$($dll.Name) is not signed by Microsoft"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- name: Package
|
- name: Package
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
|
|
@ -377,8 +366,6 @@ jobs:
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cp target/release/.cargo-packager/deb/*/data/usr/share/applications/oneclient_app.desktop \
|
|
||||||
target/release/oneclient_app.desktop
|
|
||||||
cargo generate-rpm -p packages/oneclient_app
|
cargo generate-rpm -p packages/oneclient_app
|
||||||
cp target/generate-rpm/*.rpm "${{ steps.outdir.outputs.dir }}/"
|
cp target/generate-rpm/*.rpm "${{ steps.outdir.outputs.dir }}/"
|
||||||
|
|
||||||
|
|
@ -416,44 +403,17 @@ jobs:
|
||||||
shell: bash
|
shell: bash
|
||||||
run: echo "path=$(ls ${{ steps.outdir.outputs.dir }}/*-setup.exe | head -n1)" >> "$GITHUB_OUTPUT"
|
run: echo "path=$(ls ${{ steps.outdir.outputs.dir }}/*-setup.exe | head -n1)" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: (Windows) Smoke-test installer layout
|
|
||||||
if: matrix.platform == 'windows-latest'
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
$dest = 'C:\oneclient-smoke'
|
|
||||||
Remove-Item -Recurse -Force $dest -ErrorAction SilentlyContinue
|
|
||||||
$exe = Resolve-Path '${{ steps.win_installer.outputs.path }}'
|
|
||||||
# NSIS: /S is silent, /D= sets the install dir and must be the last arg.
|
|
||||||
$p = Start-Process -FilePath $exe -ArgumentList '/S', "/D=$dest" -Wait -PassThru
|
|
||||||
if ($p.ExitCode -ne 0) { Write-Error "installer exited $($p.ExitCode)"; exit 1 }
|
|
||||||
|
|
||||||
Write-Host '--- installed layout ---'
|
|
||||||
Get-ChildItem -Recurse $dest | ForEach-Object { Write-Host $_.FullName }
|
|
||||||
|
|
||||||
if (-not (Test-Path "$dest\oneclient_app.exe")) {
|
|
||||||
Write-Error "oneclient_app.exe is not at the root of $dest"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
foreach ($dll in @(Get-ChildItem 'target/crt/*.dll')) {
|
|
||||||
if (-not (Test-Path (Join-Path $dest $dll.Name))) {
|
|
||||||
Write-Error "$($dll.Name) did not land beside oneclient_app.exe - cargo-packager put resources elsewhere, so app-local CRT loading will not work"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Write-Host "OK: $($(Get-ChildItem 'target/crt/*.dll').Count) CRT DLLs sit beside oneclient_app.exe"
|
|
||||||
|
|
||||||
- name: (Windows) Upload unsigned installer artifact
|
- name: (Windows) Upload unsigned installer artifact
|
||||||
if: matrix.platform == 'windows-latest'
|
if: matrix.platform == 'windows-latest'
|
||||||
id: win_upload_unsigned
|
id: win_upload_unsigned
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: oneclient-windows-installer-unsigned
|
name: oneclient-windows-installer-unsigned
|
||||||
path: ${{ steps.win_installer.outputs.path }}
|
path: ${{ steps.win_installer.outputs.path }}
|
||||||
|
|
||||||
- name: (Windows) Submit SignPath signing request
|
- name: (Windows) Submit SignPath signing request
|
||||||
if: matrix.platform == 'windows-latest'
|
if: matrix.platform == 'windows-latest'
|
||||||
uses: signpath/github-action-submit-signing-request@ced31329c0317e779dad2eec2a7c3bb46ea1343e # v1.3
|
uses: signpath/github-action-submit-signing-request@v1
|
||||||
with:
|
with:
|
||||||
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
|
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
|
||||||
organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }}
|
organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }}
|
||||||
|
|
@ -496,7 +456,7 @@ jobs:
|
||||||
# Identify the single updater artifact for this platform (the file with a
|
# Identify the single updater artifact for this platform (the file with a
|
||||||
# sibling .sig): -setup.exe (nsis), *.app.tar.gz (app), *.AppImage (appimage).
|
# sibling .sig): -setup.exe (nsis), *.app.tar.gz (app), *.AppImage (appimage).
|
||||||
# NOTE: nullglob is on, so an unmatched glob expands to nothing. Use arrays
|
# NOTE: nullglob is on, so an unmatched glob expands to nothing. Use arrays
|
||||||
# rather than `ls <glob> | head` - the latter falls back to listing the cwd
|
# rather than `ls <glob> | head` — the latter falls back to listing the cwd
|
||||||
# when nothing matches, silently picking an unrelated file.
|
# when nothing matches, silently picking an unrelated file.
|
||||||
fmt=""; ext=""; matches=()
|
fmt=""; ext=""; matches=()
|
||||||
case "$OS_KEY" in
|
case "$OS_KEY" in
|
||||||
|
|
@ -567,7 +527,7 @@ jobs:
|
||||||
PY
|
PY
|
||||||
|
|
||||||
- name: Upload manifest fragment
|
- name: Upload manifest fragment
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: manifest-${{ matrix.os_key }}-${{ matrix.arch }}
|
name: manifest-${{ matrix.os_key }}-${{ matrix.arch }}
|
||||||
path: fragments/*.json
|
path: fragments/*.json
|
||||||
|
|
@ -579,7 +539,7 @@ jobs:
|
||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Download all manifest fragments
|
- name: Download all manifest fragments
|
||||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
pattern: manifest-*
|
pattern: manifest-*
|
||||||
path: fragments
|
path: fragments
|
||||||
|
|
|
||||||
171
.github/workflows/release.yml
vendored
|
|
@ -1,171 +0,0 @@
|
||||||
name: Release Multiplatform Binaries
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- 'v*'
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
tag:
|
|
||||||
description: 'Release Tag (e.g. v2.5.1)'
|
|
||||||
required: false
|
|
||||||
default: 'v2.5.1-c3'
|
|
||||||
type: string
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
env:
|
|
||||||
SQLX_OFFLINE: true
|
|
||||||
SQLX_OFFLINE_DIR: packages/oneclient_db/.sqlx
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
# Linux x86_64
|
|
||||||
- os: ubuntu-22.04
|
|
||||||
target: x86_64-unknown-linux-gnu
|
|
||||||
name: linux-x86_64
|
|
||||||
formats: deb,appimage
|
|
||||||
|
|
||||||
# Windows x86_64
|
|
||||||
- os: windows-latest
|
|
||||||
target: x86_64-pc-windows-msvc
|
|
||||||
name: windows-x86_64
|
|
||||||
formats: nsis
|
|
||||||
|
|
||||||
# macOS Apple Silicon
|
|
||||||
- os: macos-14
|
|
||||||
target: aarch64-apple-darwin
|
|
||||||
name: macos-arm64
|
|
||||||
formats: dmg,app
|
|
||||||
|
|
||||||
# macOS Intel (cross-compiled on macos-14)
|
|
||||||
- os: macos-14
|
|
||||||
target: x86_64-apple-darwin
|
|
||||||
name: macos-x86_64
|
|
||||||
formats: dmg,app
|
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
with:
|
|
||||||
targets: ${{ matrix.target }}
|
|
||||||
|
|
||||||
- name: Rust Cache
|
|
||||||
uses: Swatinem/rust-cache@v2
|
|
||||||
with:
|
|
||||||
key: ${{ matrix.target }}
|
|
||||||
cache-all-crates: true
|
|
||||||
|
|
||||||
- name: Install Linux build dependencies
|
|
||||||
if: runner.os == 'Linux'
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y \
|
|
||||||
libwebkit2gtk-4.1-dev \
|
|
||||||
build-essential \
|
|
||||||
curl \
|
|
||||||
wget \
|
|
||||||
file \
|
|
||||||
libxdo-dev \
|
|
||||||
libssl-dev \
|
|
||||||
libayatana-appindicator3-dev \
|
|
||||||
librsvg2-dev \
|
|
||||||
libfontconfig1-dev \
|
|
||||||
libfreetype6-dev \
|
|
||||||
libgl1-mesa-dev \
|
|
||||||
libvulkan-dev \
|
|
||||||
libasound2-dev
|
|
||||||
|
|
||||||
- name: Install cargo-packager
|
|
||||||
run: cargo install cargo-packager --locked || true
|
|
||||||
|
|
||||||
- name: Build Release Binary
|
|
||||||
run: cargo build --release --bin oneclient_app --target ${{ matrix.target }}
|
|
||||||
|
|
||||||
- name: Package Application
|
|
||||||
run: cargo packager --release --target ${{ matrix.target }} --formats ${{ matrix.formats }}
|
|
||||||
continue-on-error: true
|
|
||||||
|
|
||||||
- name: Generate RPM Package (Linux)
|
|
||||||
if: runner.os == 'Linux'
|
|
||||||
run: |
|
|
||||||
cargo install cargo-generate-rpm --locked || true
|
|
||||||
cargo generate-rpm --target ${{ matrix.target }} -p oneclient_app || true
|
|
||||||
|
|
||||||
- name: Upload Build Artifacts
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: binaries-${{ matrix.name }}
|
|
||||||
if-no-files-found: warn
|
|
||||||
path: |
|
|
||||||
target/${{ matrix.target }}/release/*.deb
|
|
||||||
target/${{ matrix.target }}/release/*.AppImage
|
|
||||||
target/${{ matrix.target }}/release/*.dmg
|
|
||||||
target/${{ matrix.target }}/release/*-setup.exe
|
|
||||||
target/${{ matrix.target }}/generate-rpm/*.rpm
|
|
||||||
target/generate-rpm/*.rpm
|
|
||||||
target/release/*.deb
|
|
||||||
target/release/*.AppImage
|
|
||||||
target/release/*.dmg
|
|
||||||
target/release/*-setup.exe
|
|
||||||
|
|
||||||
create-release:
|
|
||||||
needs: build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'
|
|
||||||
steps:
|
|
||||||
- name: Download all artifacts
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
path: ./artifacts
|
|
||||||
merge-multiple: true
|
|
||||||
|
|
||||||
- name: Determine Release Tag
|
|
||||||
id: release_tag
|
|
||||||
run: |
|
|
||||||
if [[ "${{ github.ref_name }}" =~ ^v ]]; then
|
|
||||||
echo "tag=${{ github.ref_name }}" >> $GITHUB_OUTPUT
|
|
||||||
elif [ -n "${{ inputs.tag }}" ]; then
|
|
||||||
echo "tag=${{ inputs.tag }}" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "tag=v2.5.1-c3" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Clean up previous release if exists
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: |
|
|
||||||
gh release delete "${{ steps.release_tag.outputs.tag }}" -y --cleanup-tag=false || true
|
|
||||||
|
|
||||||
- name: Publish GitHub Release
|
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
with:
|
|
||||||
files: ./artifacts/**/*
|
|
||||||
tag_name: ${{ steps.release_tag.outputs.tag }}
|
|
||||||
name: "OneLauncher-Cracked ${{ steps.release_tag.outputs.tag }}"
|
|
||||||
body: |
|
|
||||||
### What's New in ${{ steps.release_tag.outputs.tag }}
|
|
||||||
- **Complete Multiplayer Server Skin Support**: Native integration with client-side `OfflineSkins Reloaded` for Fabric 1.21.11, allowing skins to render directly on cracked multiplayer servers (e.g. `alwto.net`) when the server transmits empty player profiles.
|
|
||||||
- **Proper Isolated Offline Skin System (Authlib-Injector)**: Embedded, zero-network-dependency Authlib-Injector session provider with RSA signature verification. Your skin is attached strictly and exclusively to your player profile, completely eliminating the bug where other players or NPCs on offline servers would receive your skin.
|
|
||||||
- **Dynamic Version Compatibility**: Automatically gates offline skin mod integration strictly to matching versions, while Authlib-Injector handles clean, zero-dependency offline profile signing across all Minecraft versions.
|
|
||||||
- **Purged Incompatible Mods**: Completely cleaned and prevented CustomSkinLoader crashes on modern Fabric Loader (0.17+) and Sponge Mixin 0.8.7.
|
|
||||||
- **Vanilla & Multi-server Integrity**: Other offline players remain authentic vanilla Steve/Alex, while online players on hybrid servers display their legitimate Mojang skins via transparent caching session proxying.
|
|
||||||
- **Automatic Legacy Cleanup**: Restores pristine `client.jar` and removes stale skin resource packs from `options.txt` on launch.
|
|
||||||
- **Guaranteed Offline Game Launch**: Direct offline launching with 0 Microsoft accounts required.
|
|
||||||
- **In-Launcher 3D Skin Changer**: Full 3D skin preview, slim/classic arm model toggle, local file import, and online skin downloads.
|
|
||||||
- **Rebased on Upstream 2.5.1**: All upstream 2.5.1 features, hook error reporting, and metadata self-healing.
|
|
||||||
draft: false
|
|
||||||
prerelease: false
|
|
||||||
generate_release_notes: false
|
|
||||||
make_latest: true
|
|
||||||
fail_on_unmatched_files: false
|
|
||||||
3
.gitignore
vendored
|
|
@ -1,7 +1,4 @@
|
||||||
/target
|
/target
|
||||||
/dist
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
/.idea
|
/.idea
|
||||||
OneLauncher.iml
|
OneLauncher.iml
|
||||||
|
|
||||||
/packages/oneclient_app/icons/generated/
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "\n INSERT OR IGNORE INTO cluster_bundle_choices (cluster_id, bundle_name, accepted)\n SELECT ?, bundle_name, accepted\n FROM cluster_bundle_choices\n WHERE cluster_id = ?\n ",
|
|
||||||
"describe": {
|
|
||||||
"columns": [],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 2
|
|
||||||
},
|
|
||||||
"nullable": []
|
|
||||||
},
|
|
||||||
"hash": "001a994c740f7a015152319eb16fc113954af25ebd2a44d55e95833adda53268"
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "UPDATE cluster_artifacts SET seen_status = ? WHERE seen_status <> ?",
|
|
||||||
"describe": {
|
|
||||||
"columns": [],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 2
|
|
||||||
},
|
|
||||||
"nullable": []
|
|
||||||
},
|
|
||||||
"hash": "011448f2ce79e981700ee75c8cd5db5e0fcb793b72671c20a925b73ee6fb9456"
|
|
||||||
}
|
|
||||||
|
|
@ -1,110 +0,0 @@
|
||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "\n\t\tSELECT\n\t\t\ttarget_cluster_id, provider, project_id, content_type,\n\t\t\tsource_hash, display_name, expires_at, enabled\n\t\tFROM release_migration_waitlist\n\t\t",
|
|
||||||
"describe": {
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"name": "target_cluster_id",
|
|
||||||
"ordinal": 0,
|
|
||||||
"type_info": "Integer",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "release_migration_waitlist",
|
|
||||||
"name": "target_cluster_id"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "provider",
|
|
||||||
"ordinal": 1,
|
|
||||||
"type_info": "Integer",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "release_migration_waitlist",
|
|
||||||
"name": "provider"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "project_id",
|
|
||||||
"ordinal": 2,
|
|
||||||
"type_info": "Text",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "release_migration_waitlist",
|
|
||||||
"name": "project_id"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "content_type",
|
|
||||||
"ordinal": 3,
|
|
||||||
"type_info": "Integer",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "release_migration_waitlist",
|
|
||||||
"name": "content_type"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "source_hash",
|
|
||||||
"ordinal": 4,
|
|
||||||
"type_info": "Text",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "release_migration_waitlist",
|
|
||||||
"name": "source_hash"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "display_name",
|
|
||||||
"ordinal": 5,
|
|
||||||
"type_info": "Text",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "release_migration_waitlist",
|
|
||||||
"name": "display_name"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "expires_at",
|
|
||||||
"ordinal": 6,
|
|
||||||
"type_info": "Text",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "release_migration_waitlist",
|
|
||||||
"name": "expires_at"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "enabled",
|
|
||||||
"ordinal": 7,
|
|
||||||
"type_info": "Integer",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "release_migration_waitlist",
|
|
||||||
"name": "enabled"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 0
|
|
||||||
},
|
|
||||||
"nullable": [
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"hash": "15dc690e91fe0bc7e1932a5051f8edd855deaeb31e52209ae436680962dea954"
|
|
||||||
}
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "\n SELECT bundle_name, accepted\n FROM cluster_bundle_choices\n WHERE cluster_id = ?\n ",
|
|
||||||
"describe": {
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"name": "bundle_name",
|
|
||||||
"ordinal": 0,
|
|
||||||
"type_info": "Text",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "cluster_bundle_choices",
|
|
||||||
"name": "bundle_name"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "accepted",
|
|
||||||
"ordinal": 1,
|
|
||||||
"type_info": "Integer",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "cluster_bundle_choices",
|
|
||||||
"name": "accepted"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 1
|
|
||||||
},
|
|
||||||
"nullable": [
|
|
||||||
false,
|
|
||||||
false
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"hash": "171c6771d06656c2fe375f6a0da889f828f400e950f103f0ed44f5f98a48dc50"
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "DELETE FROM release_migration_waitlist WHERE target_cluster_id = ? AND provider = ? AND project_id = ?",
|
|
||||||
"describe": {
|
|
||||||
"columns": [],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 3
|
|
||||||
},
|
|
||||||
"nullable": []
|
|
||||||
},
|
|
||||||
"hash": "1e43f93b56bb495e13826c734fe5fb64515cf1d7c28bc6a1bf1fb1645fdba2a1"
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"db_name": "SQLite",
|
"db_name": "SQLite",
|
||||||
"query": "\n\t\tINSERT INTO cluster_artifacts (cluster_id, hash, cluster_file_name, enabled)\n\t\tVALUES (?, ?, ?, 1)\n\t\tON CONFLICT(cluster_id, hash) DO UPDATE SET\n\t\t\tcluster_file_name = excluded.cluster_file_name\n\t\tRETURNING cluster_id, hash, cluster_file_name, enabled, seen_status\n\t\t",
|
"query": "\n\t\tINSERT INTO cluster_artifacts (cluster_id, hash, cluster_file_name, enabled)\n\t\tVALUES (?, ?, ?, 1)\n\t\tON CONFLICT(cluster_id, hash) DO UPDATE SET\n\t\t\tcluster_file_name = excluded.cluster_file_name\n\t\tRETURNING cluster_id, hash, cluster_file_name, enabled\n\t\t",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
|
|
@ -46,17 +46,6 @@
|
||||||
"name": "enabled"
|
"name": "enabled"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "seen_status",
|
|
||||||
"ordinal": 4,
|
|
||||||
"type_info": "Integer",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "cluster_artifacts",
|
|
||||||
"name": "seen_status"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
|
|
@ -66,9 +55,8 @@
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
|
||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "25259e9d6eda7f5113e0d88f00b24b28b98de1eda40a1e25d48bbcf212af42b5"
|
"hash": "24209a44c9a51e8a318b080aa0c79a00a6002a35b47ec3723e3aa6a327cb7f51"
|
||||||
}
|
}
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "\n INSERT INTO cluster_optional_mods\n (cluster_id, bundle_name, package_id, bundle_version_id, seen_status)\n VALUES (?, ?, ?, ?, ?)\n ON CONFLICT(cluster_id, package_id) DO UPDATE SET\n bundle_name = excluded.bundle_name,\n bundle_version_id = excluded.bundle_version_id\n ",
|
|
||||||
"describe": {
|
|
||||||
"columns": [],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 5
|
|
||||||
},
|
|
||||||
"nullable": []
|
|
||||||
},
|
|
||||||
"hash": "3b87531a1231c467072ec4d1f9afd3c93114777031250c7f9e74f6b9eef4ab9f"
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "\n DELETE FROM cluster_bundle_overrides\n WHERE cluster_id = ? AND package_id = ? AND override_type IN ('disabled', 'removed')\n ",
|
|
||||||
"describe": {
|
|
||||||
"columns": [],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 2
|
|
||||||
},
|
|
||||||
"nullable": []
|
|
||||||
},
|
|
||||||
"hash": "43521070cb5de5b2fe5e25c88e683b933c2f6788e2d2482ca9fd7d71ee67381a"
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"db_name": "SQLite",
|
"db_name": "SQLite",
|
||||||
"query": "\n\t\tUPDATE cluster_artifacts\n\t\tSET cluster_file_name = ?, enabled = ?\n\t\tWHERE cluster_id = ? AND hash = ?\n\t\tRETURNING cluster_id, hash, cluster_file_name, enabled, seen_status\n\t\t",
|
"query": "\n\t\tUPDATE cluster_artifacts\n\t\tSET cluster_file_name = ?, enabled = ?\n\t\tWHERE cluster_id = ? AND hash = ?\n\t\tRETURNING cluster_id, hash, cluster_file_name, enabled\n\t\t",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
|
|
@ -46,17 +46,6 @@
|
||||||
"name": "enabled"
|
"name": "enabled"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "seen_status",
|
|
||||||
"ordinal": 4,
|
|
||||||
"type_info": "Integer",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "cluster_artifacts",
|
|
||||||
"name": "seen_status"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
|
|
@ -66,9 +55,8 @@
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
|
||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "b68194fc40d9fdd508ab6cc354fff662ba4b7ca17c1c4be3e51e30fcb3d0cb16"
|
"hash": "4f209d73ce580a5e305b6ccdbe47ce139b331506d1c53ed0f4011596c810252f"
|
||||||
}
|
}
|
||||||
|
|
@ -1,218 +0,0 @@
|
||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "\n\t\tINSERT INTO clusters (\n\t\t\tname, folder_name, mc_version, mc_loader, mc_loader_version,\n\t\t\tsetting_profile_name, stage, created_at,\n\t\t\tkind, user_created, description, tags, cover_path\n\t\t)\n\t\tVALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n\t\tRETURNING\n\t\t\tid, name, folder_name, setting_profile_name, mc_version, mc_loader,\n\t\t\tstage, mc_loader_version, created_at, last_played, overall_played, linked_modpack_hash,\n\t\t\tkind, user_created, description, tags, cover_path\n\t\t",
|
|
||||||
"describe": {
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"name": "id",
|
|
||||||
"ordinal": 0,
|
|
||||||
"type_info": "Integer",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "clusters",
|
|
||||||
"name": "id"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "name",
|
|
||||||
"ordinal": 1,
|
|
||||||
"type_info": "Text",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "clusters",
|
|
||||||
"name": "name"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "folder_name",
|
|
||||||
"ordinal": 2,
|
|
||||||
"type_info": "Text",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "clusters",
|
|
||||||
"name": "folder_name"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "setting_profile_name",
|
|
||||||
"ordinal": 3,
|
|
||||||
"type_info": "Text",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "clusters",
|
|
||||||
"name": "setting_profile_name"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "mc_version",
|
|
||||||
"ordinal": 4,
|
|
||||||
"type_info": "Text",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "clusters",
|
|
||||||
"name": "mc_version"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "mc_loader",
|
|
||||||
"ordinal": 5,
|
|
||||||
"type_info": "Integer",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "clusters",
|
|
||||||
"name": "mc_loader"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "stage",
|
|
||||||
"ordinal": 6,
|
|
||||||
"type_info": "Integer",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "clusters",
|
|
||||||
"name": "stage"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "mc_loader_version",
|
|
||||||
"ordinal": 7,
|
|
||||||
"type_info": "Text",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "clusters",
|
|
||||||
"name": "mc_loader_version"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "created_at",
|
|
||||||
"ordinal": 8,
|
|
||||||
"type_info": "Text",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "clusters",
|
|
||||||
"name": "created_at"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "last_played",
|
|
||||||
"ordinal": 9,
|
|
||||||
"type_info": "Text",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "clusters",
|
|
||||||
"name": "last_played"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "overall_played",
|
|
||||||
"ordinal": 10,
|
|
||||||
"type_info": "Integer",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "clusters",
|
|
||||||
"name": "overall_played"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "linked_modpack_hash",
|
|
||||||
"ordinal": 11,
|
|
||||||
"type_info": "Text",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "clusters",
|
|
||||||
"name": "linked_modpack_hash"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "kind",
|
|
||||||
"ordinal": 12,
|
|
||||||
"type_info": "Integer",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "clusters",
|
|
||||||
"name": "kind"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "user_created",
|
|
||||||
"ordinal": 13,
|
|
||||||
"type_info": "Integer",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "clusters",
|
|
||||||
"name": "user_created"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "description",
|
|
||||||
"ordinal": 14,
|
|
||||||
"type_info": "Text",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "clusters",
|
|
||||||
"name": "description"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "tags",
|
|
||||||
"ordinal": 15,
|
|
||||||
"type_info": "Text",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "clusters",
|
|
||||||
"name": "tags"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "cover_path",
|
|
||||||
"ordinal": 16,
|
|
||||||
"type_info": "Text",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "clusters",
|
|
||||||
"name": "cover_path"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 13
|
|
||||||
},
|
|
||||||
"nullable": [
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
true
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"hash": "54dea3f4093ceee9dcd5f0ae9bf072ec3615954b60bffa33bff7fd3f9bf14210"
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "\n INSERT OR IGNORE INTO cluster_bundle_type_opt_outs (cluster_id, bundle_name, content_type)\n VALUES (?, ?, ?)\n ",
|
|
||||||
"describe": {
|
|
||||||
"columns": [],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 3
|
|
||||||
},
|
|
||||||
"nullable": []
|
|
||||||
},
|
|
||||||
"hash": "59a29c97c422e8721c29b9517ae834ea97776c57bb129a9d67007cdd979ae841"
|
|
||||||
}
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "\n SELECT bundle_name, content_type\n FROM cluster_bundle_type_opt_outs\n WHERE cluster_id = ?\n ",
|
|
||||||
"describe": {
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"name": "bundle_name",
|
|
||||||
"ordinal": 0,
|
|
||||||
"type_info": "Text",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "cluster_bundle_type_opt_outs",
|
|
||||||
"name": "bundle_name"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "content_type",
|
|
||||||
"ordinal": 1,
|
|
||||||
"type_info": "Integer",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "cluster_bundle_type_opt_outs",
|
|
||||||
"name": "content_type"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 1
|
|
||||||
},
|
|
||||||
"nullable": [
|
|
||||||
false,
|
|
||||||
false
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"hash": "5f45ecf4e5ac27ad3a7ef4e5f9c2e6466ef51af11c332a2e6089c90759b9871f"
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"db_name": "SQLite",
|
"db_name": "SQLite",
|
||||||
"query": "\n\t\tINSERT INTO java_versions (absolute_path, major, version, vendor, os_arch, is_jdk, probe_version)\n\t\tVALUES (?, ?, ?, ?, ?, ?, ?)\n\t\tON CONFLICT(absolute_path) DO UPDATE SET\n\t\t\tmajor = excluded.major,\n\t\t\tversion = excluded.version,\n\t\t\tvendor = excluded.vendor,\n\t\t\tos_arch = excluded.os_arch,\n\t\t\tis_jdk = excluded.is_jdk,\n\t\t\tprobe_version = excluded.probe_version\n\t\tRETURNING absolute_path, major, version, vendor, os_arch, is_jdk as \"is_jdk: bool\", probe_version\n\t\t",
|
"query": "\n\t\tINSERT INTO java_versions (absolute_path, major, version, vendor, os_arch)\n\t\tVALUES (?, ?, ?, ?, ?)\n\t\tON CONFLICT(absolute_path) DO UPDATE SET\n\t\t\tmajor = excluded.major,\n\t\t\tversion = excluded.version,\n\t\t\tvendor = excluded.vendor,\n\t\t\tos_arch = excluded.os_arch\n\t\tRETURNING absolute_path, major, version, vendor, os_arch\n\t\t",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
|
|
@ -57,42 +57,18 @@
|
||||||
"name": "os_arch"
|
"name": "os_arch"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "is_jdk: bool",
|
|
||||||
"ordinal": 5,
|
|
||||||
"type_info": "Integer",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "java_versions",
|
|
||||||
"name": "is_jdk"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "probe_version",
|
|
||||||
"ordinal": 6,
|
|
||||||
"type_info": "Integer",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "java_versions",
|
|
||||||
"name": "probe_version"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Right": 7
|
"Right": 5
|
||||||
},
|
},
|
||||||
"nullable": [
|
"nullable": [
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "f5dbcf7798afb495efe228a4ba7d1ddd9904cc25f34bef3c16f29cfb141158f0"
|
"hash": "a3bd6cf3048a6a5ed9d29e6bbb7f517676fe715c8af45eb5c58d69073e2d0304"
|
||||||
}
|
}
|
||||||
|
|
@ -1,218 +0,0 @@
|
||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "\n\t\tUPDATE clusters\n\t\tSET mc_version = ?,\n\t\t mc_loader = ?,\n\t\t mc_loader_version = ?,\n\t\t stage = ?,\n\t\t name = ?,\n\t\t folder_name = ?\n\t\tWHERE id = ?\n\t\tRETURNING\n\t\t\tid, name, folder_name, setting_profile_name, mc_version, mc_loader,\n\t\t\tstage, mc_loader_version, created_at, last_played, overall_played, linked_modpack_hash,\n\t\t\tkind, user_created, description, tags, cover_path\n\t\t",
|
|
||||||
"describe": {
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"name": "id",
|
|
||||||
"ordinal": 0,
|
|
||||||
"type_info": "Integer",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "clusters",
|
|
||||||
"name": "id"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "name",
|
|
||||||
"ordinal": 1,
|
|
||||||
"type_info": "Text",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "clusters",
|
|
||||||
"name": "name"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "folder_name",
|
|
||||||
"ordinal": 2,
|
|
||||||
"type_info": "Text",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "clusters",
|
|
||||||
"name": "folder_name"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "setting_profile_name",
|
|
||||||
"ordinal": 3,
|
|
||||||
"type_info": "Text",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "clusters",
|
|
||||||
"name": "setting_profile_name"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "mc_version",
|
|
||||||
"ordinal": 4,
|
|
||||||
"type_info": "Text",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "clusters",
|
|
||||||
"name": "mc_version"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "mc_loader",
|
|
||||||
"ordinal": 5,
|
|
||||||
"type_info": "Integer",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "clusters",
|
|
||||||
"name": "mc_loader"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "stage",
|
|
||||||
"ordinal": 6,
|
|
||||||
"type_info": "Integer",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "clusters",
|
|
||||||
"name": "stage"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "mc_loader_version",
|
|
||||||
"ordinal": 7,
|
|
||||||
"type_info": "Text",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "clusters",
|
|
||||||
"name": "mc_loader_version"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "created_at",
|
|
||||||
"ordinal": 8,
|
|
||||||
"type_info": "Text",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "clusters",
|
|
||||||
"name": "created_at"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "last_played",
|
|
||||||
"ordinal": 9,
|
|
||||||
"type_info": "Text",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "clusters",
|
|
||||||
"name": "last_played"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "overall_played",
|
|
||||||
"ordinal": 10,
|
|
||||||
"type_info": "Integer",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "clusters",
|
|
||||||
"name": "overall_played"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "linked_modpack_hash",
|
|
||||||
"ordinal": 11,
|
|
||||||
"type_info": "Text",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "clusters",
|
|
||||||
"name": "linked_modpack_hash"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "kind",
|
|
||||||
"ordinal": 12,
|
|
||||||
"type_info": "Integer",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "clusters",
|
|
||||||
"name": "kind"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "user_created",
|
|
||||||
"ordinal": 13,
|
|
||||||
"type_info": "Integer",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "clusters",
|
|
||||||
"name": "user_created"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "description",
|
|
||||||
"ordinal": 14,
|
|
||||||
"type_info": "Text",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "clusters",
|
|
||||||
"name": "description"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "tags",
|
|
||||||
"ordinal": 15,
|
|
||||||
"type_info": "Text",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "clusters",
|
|
||||||
"name": "tags"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "cover_path",
|
|
||||||
"ordinal": 16,
|
|
||||||
"type_info": "Text",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "clusters",
|
|
||||||
"name": "cover_path"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 7
|
|
||||||
},
|
|
||||||
"nullable": [
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
true
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"hash": "aeb2fbb78cfa536d6478878ccafb4f4754dfe057468fe06c22d87c3a062132b7"
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "\n DELETE FROM cluster_bundle_overrides\n WHERE cluster_id = ? AND package_id = ? AND override_type = 'disabled'\n ",
|
|
||||||
"describe": {
|
|
||||||
"columns": [],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 2
|
|
||||||
},
|
|
||||||
"nullable": []
|
|
||||||
},
|
|
||||||
"hash": "af0567c14b689a3f53207179ba9ee2e369b99542369fa5401aa4057d0b326b86"
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"db_name": "SQLite",
|
"db_name": "SQLite",
|
||||||
"query": "\n\t\tSELECT absolute_path, major, version, vendor, os_arch, is_jdk as \"is_jdk: bool\", probe_version\n\t\tFROM java_versions\n\t\tWHERE major = ?\n\t\tORDER BY version DESC\n\t\tLIMIT 1\n\t\t",
|
"query": "\n\t\tSELECT absolute_path, major, version, vendor, os_arch\n\t\tFROM java_versions\n\t\tWHERE major = ?\n\t\tORDER BY version DESC\n\t\tLIMIT 1\n\t\t",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
|
|
@ -57,28 +57,6 @@
|
||||||
"name": "os_arch"
|
"name": "os_arch"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "is_jdk: bool",
|
|
||||||
"ordinal": 5,
|
|
||||||
"type_info": "Integer",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "java_versions",
|
|
||||||
"name": "is_jdk"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "probe_version",
|
|
||||||
"ordinal": 6,
|
|
||||||
"type_info": "Integer",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "java_versions",
|
|
||||||
"name": "probe_version"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
|
|
@ -89,10 +67,8 @@
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "14ab205c5b8d4c1c435814e4b7a155acbae41a67925b6b179b82dea1758d621a"
|
"hash": "b153f762849649456dd97b6a5cd35fb7dafc3d5539441328e03442cea195483a"
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"db_name": "SQLite",
|
"db_name": "SQLite",
|
||||||
"query": "\n\t\tSELECT cluster_id, hash, cluster_file_name, enabled, seen_status\n\t\tFROM cluster_artifacts\n\t\tWHERE cluster_id = ? AND hash = ?\n\t\t",
|
"query": "\n\t\tSELECT cluster_id, hash, cluster_file_name, enabled\n\t\tFROM cluster_artifacts\n\t\tWHERE cluster_id = ? AND hash = ?\n\t\t",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
|
|
@ -46,17 +46,6 @@
|
||||||
"name": "enabled"
|
"name": "enabled"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "seen_status",
|
|
||||||
"ordinal": 4,
|
|
||||||
"type_info": "Integer",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "cluster_artifacts",
|
|
||||||
"name": "seen_status"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
|
|
@ -66,9 +55,8 @@
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
|
||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "071d02aba0d2eb073b8ca1e6488087101255147c064612fad99500d3c993f61f"
|
"hash": "b5935e0e5d751a7d33e898f7cc1e0f985a66434365f0939f7572434af40e43af"
|
||||||
}
|
}
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "\n INSERT OR REPLACE INTO cluster_bundle_choices (cluster_id, bundle_name, accepted)\n VALUES (?, ?, ?)\n ",
|
|
||||||
"describe": {
|
|
||||||
"columns": [],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 3
|
|
||||||
},
|
|
||||||
"nullable": []
|
|
||||||
},
|
|
||||||
"hash": "c06af5530a824d31e6e18a3a52cd57639b74e7e4d2ae30e29a6c49fda9521b4d"
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"db_name": "SQLite",
|
"db_name": "SQLite",
|
||||||
"query": "\n SELECT absolute_path, major, version, vendor, os_arch, is_jdk as \"is_jdk: bool\", probe_version\n FROM java_versions\n ORDER BY major DESC, version DESC\n ",
|
"query": "\n SELECT absolute_path, major, version, vendor, os_arch FROM java_versions \n ORDER BY major DESC, version DESC\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
|
|
@ -57,28 +57,6 @@
|
||||||
"name": "os_arch"
|
"name": "os_arch"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "is_jdk: bool",
|
|
||||||
"ordinal": 5,
|
|
||||||
"type_info": "Integer",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "java_versions",
|
|
||||||
"name": "is_jdk"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "probe_version",
|
|
||||||
"ordinal": 6,
|
|
||||||
"type_info": "Integer",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "java_versions",
|
|
||||||
"name": "probe_version"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
|
|
@ -89,10 +67,8 @@
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "0c3651cf175cb900c2466f0e97beca338b95637d43d9d067b974327f7720637b"
|
"hash": "c1d3cf930d5f38e8f1046da1a8f895ee1de2d3fe9a40ecb47731618962d2a4ad"
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"db_name": "SQLite",
|
"db_name": "SQLite",
|
||||||
"query": "\n SELECT absolute_path, major, version, vendor, os_arch, is_jdk as \"is_jdk: bool\", probe_version\n FROM java_versions\n WHERE absolute_path = ?\n ",
|
"query": "\n SELECT absolute_path, major, version, vendor, os_arch FROM java_versions \n WHERE absolute_path = ?\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
|
|
@ -57,28 +57,6 @@
|
||||||
"name": "os_arch"
|
"name": "os_arch"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "is_jdk: bool",
|
|
||||||
"ordinal": 5,
|
|
||||||
"type_info": "Integer",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "java_versions",
|
|
||||||
"name": "is_jdk"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "probe_version",
|
|
||||||
"ordinal": 6,
|
|
||||||
"type_info": "Integer",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "java_versions",
|
|
||||||
"name": "probe_version"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
|
|
@ -89,10 +67,8 @@
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "331bee3da454f39a8a2cc8ed4e5338b743a7cd033fdef023c4b3b93827248be1"
|
"hash": "c570e309195b6063cf0fbee866d68972de05107769dab983524fd2e74a7ae31a"
|
||||||
}
|
}
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "\n\t\tUPDATE cluster_artifacts\n\t\tSET seen_status = ?\n\t\tWHERE cluster_id = ? AND hash = ?\n\t\t",
|
|
||||||
"describe": {
|
|
||||||
"columns": [],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 3
|
|
||||||
},
|
|
||||||
"nullable": []
|
|
||||||
},
|
|
||||||
"hash": "cd9609d707fa11bc4c246cd69c879159d23af897bdcb0bd9f04df73a7805a713"
|
|
||||||
}
|
|
||||||
|
|
@ -1,158 +0,0 @@
|
||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "\n\t\tSELECT\n\t\t\tca.hash AS \"hash!: String\",\n\t\t\tca.cluster_file_name AS \"cluster_file_name!: String\",\n\t\t\tca.enabled AS \"enabled!: i64\",\n\t\t\tca.seen_status AS \"seen_status!: i64\",\n\t\t\ta.content_type AS \"content_type!: i64\",\n\t\t\ta.file_name AS \"file_name!: String\",\n\t\t\tpr.provider AS \"provider?: i64\",\n\t\t\tpr.project_id AS \"project_id?: String\",\n\t\t\tpr.version_id AS \"version_id?: String\",\n\t\t\tpr.display_name AS \"display_name?: String\",\n\t\t\tpr.display_version AS \"display_version?: String\",\n\t\t\tpr.published_at AS \"published_at?: String\"\n\t\tFROM cluster_artifacts ca\n\t\tJOIN artifacts a ON a.hash = ca.hash\n\t\tLEFT JOIN provider_releases pr ON pr.rowid = (\n\t\t\tSELECT rowid\n\t\t\tFROM provider_releases\n\t\t\tWHERE hash = ca.hash\n\t\t\tORDER BY published_at DESC, provider, project_id, version_id\n\t\t\tLIMIT 1\n\t\t)\n\t\tWHERE ca.cluster_id = ?\n\t\t",
|
|
||||||
"describe": {
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"name": "hash!: String",
|
|
||||||
"ordinal": 0,
|
|
||||||
"type_info": "Text",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "cluster_artifacts",
|
|
||||||
"name": "hash"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "cluster_file_name!: String",
|
|
||||||
"ordinal": 1,
|
|
||||||
"type_info": "Text",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "cluster_artifacts",
|
|
||||||
"name": "cluster_file_name"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "enabled!: i64",
|
|
||||||
"ordinal": 2,
|
|
||||||
"type_info": "Integer",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "cluster_artifacts",
|
|
||||||
"name": "enabled"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "seen_status!: i64",
|
|
||||||
"ordinal": 3,
|
|
||||||
"type_info": "Integer",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "cluster_artifacts",
|
|
||||||
"name": "seen_status"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "content_type!: i64",
|
|
||||||
"ordinal": 4,
|
|
||||||
"type_info": "Integer",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "artifacts",
|
|
||||||
"name": "content_type"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "file_name!: String",
|
|
||||||
"ordinal": 5,
|
|
||||||
"type_info": "Text",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "artifacts",
|
|
||||||
"name": "file_name"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "provider?: i64",
|
|
||||||
"ordinal": 6,
|
|
||||||
"type_info": "Integer",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "provider_releases",
|
|
||||||
"name": "provider"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "project_id?: String",
|
|
||||||
"ordinal": 7,
|
|
||||||
"type_info": "Text",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "provider_releases",
|
|
||||||
"name": "project_id"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "version_id?: String",
|
|
||||||
"ordinal": 8,
|
|
||||||
"type_info": "Text",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "provider_releases",
|
|
||||||
"name": "version_id"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "display_name?: String",
|
|
||||||
"ordinal": 9,
|
|
||||||
"type_info": "Text",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "provider_releases",
|
|
||||||
"name": "display_name"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "display_version?: String",
|
|
||||||
"ordinal": 10,
|
|
||||||
"type_info": "Text",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "provider_releases",
|
|
||||||
"name": "display_version"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "published_at?: String",
|
|
||||||
"ordinal": 11,
|
|
||||||
"type_info": "Text",
|
|
||||||
"origin": {
|
|
||||||
"Table": {
|
|
||||||
"table": "provider_releases",
|
|
||||||
"name": "published_at"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 1
|
|
||||||
},
|
|
||||||
"nullable": [
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
true
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"hash": "d1fc4c93f507d459e590cda5b5830a1e26e3047139db08a9f6b90c84185402a6"
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"db_name": "SQLite",
|
"db_name": "SQLite",
|
||||||
"query": "\n SELECT 1 FROM cluster_artifacts\n WHERE cluster_id = ? AND bundle_name IS NOT NULL AND package_id = ?\n LIMIT 1\n ",
|
"query": "\n SELECT 1 FROM cluster_artifacts\n WHERE cluster_id = ? AND bundle_name = ? AND package_id = ?\n LIMIT 1\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
|
|
@ -11,11 +11,11 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Right": 2
|
"Right": 3
|
||||||
},
|
},
|
||||||
"nullable": [
|
"nullable": [
|
||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "ab6dcea84a9502d5f8396a207ae613a4c41eb729ca22249f15eb45e6c6030094"
|
"hash": "dc93cdd57e23e48f8e3c333db4ec396b0ee319adec619e5a7d147805573ae2b2"
|
||||||
}
|
}
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "\n DELETE FROM cluster_bundle_type_opt_outs\n WHERE cluster_id = ? AND bundle_name = ? AND content_type = ?\n ",
|
|
||||||
"describe": {
|
|
||||||
"columns": [],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 3
|
|
||||||
},
|
|
||||||
"nullable": []
|
|
||||||
},
|
|
||||||
"hash": "dff3f2fe8e50eaa7ffebab7490306bc40e846ad708975991346aab83870e0968"
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "\n UPDATE cluster_optional_mods SET seen_status = ?\n WHERE cluster_id = ? AND package_id = ?\n ",
|
|
||||||
"describe": {
|
|
||||||
"columns": [],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 3
|
|
||||||
},
|
|
||||||
"nullable": []
|
|
||||||
},
|
|
||||||
"hash": "e03d85ee97b3a632d5946019865d9efb5d665e521519d05b16a4b66b066987ca"
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "\n\t\tINSERT INTO release_migration_waitlist (\n\t\t\ttarget_cluster_id, provider, project_id, content_type,\n\t\t\tsource_hash, display_name, added_at, expires_at, enabled\n\t\t)\n\t\tVALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)\n\t\tON CONFLICT(target_cluster_id, provider, project_id) DO UPDATE SET\n\t\t\tcontent_type = excluded.content_type,\n\t\t\tsource_hash = excluded.source_hash,\n\t\t\tdisplay_name = excluded.display_name,\n\t\t\tadded_at = excluded.added_at,\n\t\t\texpires_at = excluded.expires_at,\n\t\t\tenabled = excluded.enabled\n\t\t",
|
|
||||||
"describe": {
|
|
||||||
"columns": [],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 9
|
|
||||||
},
|
|
||||||
"nullable": []
|
|
||||||
},
|
|
||||||
"hash": "e12409b04c8a3eea09a4bfc1014bca6fe5fbc1256b3537ed37195ed20075f944"
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "\n DELETE FROM cluster_optional_mods\n WHERE cluster_id = ? AND package_id = ?\n ",
|
|
||||||
"describe": {
|
|
||||||
"columns": [],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 2
|
|
||||||
},
|
|
||||||
"nullable": []
|
|
||||||
},
|
|
||||||
"hash": "e2ee6fbe534dd47ce658d547c2bfc3a4e86dfce86da3a4ffe35d25d1fa412d13"
|
|
||||||
}
|
|
||||||
62
.sqlx/query-e372eec993f6e82238fe95b1c74dbb377ee08e44c7e8c3b5eaa6f3b8b229fddf.json
generated
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n\t\tSELECT cluster_id, hash, cluster_file_name, enabled\n\t\tFROM cluster_artifacts\n\t\tWHERE cluster_id = ?\n\t\t",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "cluster_id",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Integer",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "cluster_artifacts",
|
||||||
|
"name": "cluster_id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "hash",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Text",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "cluster_artifacts",
|
||||||
|
"name": "hash"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "cluster_file_name",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Text",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "cluster_artifacts",
|
||||||
|
"name": "cluster_file_name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "enabled",
|
||||||
|
"ordinal": 3,
|
||||||
|
"type_info": "Integer",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "cluster_artifacts",
|
||||||
|
"name": "enabled"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "e372eec993f6e82238fe95b1c74dbb377ee08e44c7e8c3b5eaa6f3b8b229fddf"
|
||||||
|
}
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "DELETE FROM release_migration_waitlist WHERE expires_at <= ?",
|
|
||||||
"describe": {
|
|
||||||
"columns": [],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 1
|
|
||||||
},
|
|
||||||
"nullable": []
|
|
||||||
},
|
|
||||||
"hash": "ff07b78c0e97f1f38a6900092391d22e475471578b5ef73927e5427c2c0abc77"
|
|
||||||
}
|
|
||||||
25
AGENTS.md
|
|
@ -1,25 +0,0 @@
|
||||||
# OneLauncher-Cracked Development & Release Guidelines
|
|
||||||
|
|
||||||
## 1. Release Versioning Policy
|
|
||||||
- **Suffix Progression**: Every fix, rebuild, or update to an existing release MUST increment the cracked revision suffix monotonically:
|
|
||||||
- Example: `v2.8.0-c1` -> `v2.8.0-c2` -> `v2.8.0-c3`
|
|
||||||
- **Tag Immutability**: Never force-push or re-upload assets over an existing release tag. The in-launcher `CrackedVersion` SemVer parser relies on strictly increasing revisions to detect and download updates.
|
|
||||||
- **Cargo.toml Version**: When bumping the suffix, update the workspace `version = "2.8.0-c<N>"` in the root `Cargo.toml`.
|
|
||||||
|
|
||||||
## 2. Remote Synchronization (Dual-Push)
|
|
||||||
- Primary Forgejo remote: `https://git.oiupoyt.space/oiupoyt/OneLauncher-Cracked.git` (remote: `forgejo`)
|
|
||||||
- GitHub remote: `https://github.com/oiupoyt/OneLauncher-Cracked.git` (remote: `origin`)
|
|
||||||
- Always push to both remotes:
|
|
||||||
```bash
|
|
||||||
git push origin <branch> --tags
|
|
||||||
git push forgejo <branch> --tags
|
|
||||||
```
|
|
||||||
|
|
||||||
## 3. Build & Compiler Performance
|
|
||||||
- **ThinLTO**: Use `lto = "thin"` with `codegen-units = 16` and `debug = false` in `[profile.release]`. Do not use `lto = "fat"` (causes 10m+ single-threaded linking stalls).
|
|
||||||
- **Fast Linker**: On Linux, use `clang -fuse-ld=lld` configured in `.cargo/config.toml`.
|
|
||||||
- **Offline Mode & Skin Pipeline**:
|
|
||||||
- Preserve `account_for_launch` bypass in `oneclient_auth`.
|
|
||||||
- Always verify `paths::all_skins_dirs()` for multi-directory skin search.
|
|
||||||
- Keep `--userProperties` argument injection in `oneclient_core/src/game/launch.rs`.
|
|
||||||
- Ensure TCP socket `flush()` and `shutdown()` in `skin_server.rs` before connection termination.
|
|
||||||
34
CHANGELOG.md
|
|
@ -1,34 +0,0 @@
|
||||||
## 2.8.0 (2026-10-09)
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- Added safe multiple minecraft instances launching, only dedicated clusters tho by [rozwader](https://github.com/rozwader)
|
|
||||||
|
|
||||||
### Fixes
|
|
||||||
|
|
||||||
- Added better informations about errors from mclogs. by [rozwader](https://github.com/rozwader)
|
|
||||||
- Added status bar showing up if a player is logged out or he has a stale refresh token by [rozwader](https://github.com/rozwader)
|
|
||||||
- Added a filter for choosing modpack loaders by [rozwader](https://github.com/rozwader)
|
|
||||||
- SkyBlock content installed without asking is removed from 26.3 clusters once, and turning an optional bundle down removes its mods
|
|
||||||
- Fixed shield.io badges not rendering properly in markdown viewers by [rozwader](https://github.com/rozwader)
|
|
||||||
|
|
||||||
## 2.7.0 (2026-10-05)
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- Code blocks are selectable, and the open link confirmation has a copy button by [LynithDev](https://github.com/LynithDev)
|
|
||||||
- Game settings have a Game Arguments field for passing extra arguments to Minecraft, globally or per cluster by [Wyvest](https://github.com/Wyvest)
|
|
||||||
- Game logs hide account tokens, and the game output starts with the launch command ready to paste into a terminal by [LynithDev](https://github.com/LynithDev)
|
|
||||||
- GitHub hosted mods have a "View in browser" option that opens their repository by [LynithDev](https://github.com/LynithDev)
|
|
||||||
- The changelog page now shows each GitHub release's notes by [LynithDev](https://github.com/LynithDev)
|
|
||||||
- The mods folder now syncs with the launcher: jars added, renamed or replaced by hand show up in the Mods list, and jars deleted by hand are switched off instead of being put back by [Wyvest](https://github.com/Wyvest)
|
|
||||||
|
|
||||||
- Choose which GPU the game renders on by [LynithDev](https://github.com/LynithDev)
|
|
||||||
|
|
||||||
### Fixes
|
|
||||||
|
|
||||||
- Icons show on Linux systems with a comma decimal locale by [LynithDev](https://github.com/LynithDev)
|
|
||||||
- The "Uploaded to mclo.gs" notification no longer repeats when switching back to the Logs tab by [LynithDev](https://github.com/LynithDev)
|
|
||||||
- Open folder and open in browser work again on Linux by [LynithDev](https://github.com/LynithDev)
|
|
||||||
- Optional bundles are now offered before the first launch of every new version instead of being installed or skipped silently by [Wyvest](https://github.com/Wyvest)
|
|
||||||
- System information in the settings sidebar shows a tooltip explaining it can be clicked to copy by [LynithDev](https://github.com/LynithDev)
|
|
||||||
|
|
@ -32,7 +32,7 @@ To find an issue that interests you, you can browse through our [existing issues
|
||||||
#### Making Changes Locally
|
#### Making Changes Locally
|
||||||
|
|
||||||
This project is a pure Rust [`Cargo`] workspace (edition 2024). Make sure you have a
|
This project is a pure Rust [`Cargo`] workspace (edition 2024). Make sure you have a
|
||||||
recent Rust toolchain (`rustc` **1.97+**) installed before proceeding - the easiest way
|
recent Rust toolchain (`rustc` **1.85+**) installed before proceeding — the easiest way
|
||||||
is via [rustup].
|
is via [rustup].
|
||||||
|
|
||||||
To make changes locally, follow these steps:
|
To make changes locally, follow these steps:
|
||||||
|
|
@ -73,15 +73,6 @@ and per-OS prerequisites.
|
||||||
Once you have finished making your changes, create a pull request (PR) to submit them.
|
Once you have finished making your changes, create a pull request (PR) to submit them.
|
||||||
|
|
||||||
- Fill out the template to help reviewers understand your changes and the purpose of your PR.
|
- Fill out the template to help reviewers understand your changes and the purpose of your PR.
|
||||||
- If your change is user-facing, add a change file with `knope document-change`, or create `.changeset/<anything>.md` by hand:
|
|
||||||
```md
|
|
||||||
---
|
|
||||||
default: patch
|
|
||||||
---
|
|
||||||
|
|
||||||
# Fixed the thing players noticed
|
|
||||||
```
|
|
||||||
Use `patch` for fixes, `minor` for features and `major` for breaking changes. The title becomes your line in the release notes. PRs without one fail the `Changeset Check` unless a maintainer labels them `no changeset`.
|
|
||||||
- If you are addressing an existing issue, don't forget to [link your PR to the issue].
|
- If you are addressing an existing issue, don't forget to [link your PR to the issue].
|
||||||
- Enable the checkbox to [allow maintainer edits] so that the branch can be updated for merging.
|
- Enable the checkbox to [allow maintainer edits] so that the branch can be updated for merging.
|
||||||
- Once you submit your PR, a team member will review your proposal. They may ask questions or request additional information.
|
- Once you submit your PR, a team member will review your proposal. They may ask questions or request additional information.
|
||||||
|
|
|
||||||
3213
Cargo.lock
generated
184
Cargo.toml
|
|
@ -9,38 +9,42 @@ authors = [
|
||||||
"Polyfrost",
|
"Polyfrost",
|
||||||
"Polyfrost Contributors",
|
"Polyfrost Contributors",
|
||||||
]
|
]
|
||||||
documentation = "https://git.oiupoyt.space/oiupoyt/OneLauncher-Cracked"
|
documentation = "https://github.com/Polyfrost/OneLauncher"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
homepage = "https://polyfrost.org/projects/oneclient"
|
homepage = "https://polyfrost.org/projects/oneclient"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
repository = "https://git.oiupoyt.space/oiupoyt/OneLauncher-Cracked"
|
repository = "https://github.com/Polyfrost/OneLauncher"
|
||||||
rust-version = "1.97"
|
version = "2.1.4"
|
||||||
version = "2.8.0-c1"
|
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
# core
|
# core
|
||||||
oneclient_auth = {path = "./packages/oneclient_auth"}
|
polyio = { path = "./packages/polyio" }
|
||||||
oneclient_cluster = {path = "./packages/oneclient_cluster"}
|
oneclient_auth = { path = "./packages/oneclient_auth" }
|
||||||
oneclient_common = {path = "./packages/oneclient_common"}
|
oneclient_cluster = { path = "./packages/oneclient_cluster" }
|
||||||
oneclient_content = {path = "./packages/oneclient_content"}
|
oneclient_common = { path = "./packages/oneclient_common" }
|
||||||
oneclient_core = {path = "./packages/oneclient_core"}
|
oneclient_content = { path = "./packages/oneclient_content" }
|
||||||
oneclient_db = {path = "./packages/oneclient_db"}
|
oneclient_discord = { path = "./packages/oneclient_discord" }
|
||||||
oneclient_discord = {path = "./packages/oneclient_discord"}
|
oneclient_events = { path = "./packages/oneclient_events" }
|
||||||
oneclient_events = {path = "./packages/oneclient_events"}
|
oneclient_java = { path = "./packages/oneclient_java" }
|
||||||
oneclient_java = {path = "./packages/oneclient_java"}
|
oneclient_mc = { path = "./packages/oneclient_mc" }
|
||||||
oneclient_macro = {path = "./packages/oneclient_macro"}
|
oneclient_net = { path = "./packages/oneclient_net" }
|
||||||
oneclient_mc = {path = "./packages/oneclient_mc"}
|
oneclient_polyplus = { path = "./packages/oneclient_polyplus" }
|
||||||
oneclient_net = {path = "./packages/oneclient_net"}
|
oneclient_core = { path = "./packages/oneclient_core" }
|
||||||
oneclient_polyplus = {path = "./packages/oneclient_polyplus"}
|
oneclient_db = { path = "./packages/oneclient_db" }
|
||||||
polyio = {path = "./packages/polyio"}
|
oneclient_macro = { path = "./packages/oneclient_macro" }
|
||||||
|
|
||||||
# core (extra)
|
# core (extra)
|
||||||
|
interfrost = {version = "=1.3.2"}
|
||||||
discord-rich-presence = {version = "=1.1.0"}
|
discord-rich-presence = {version = "=1.1.0"}
|
||||||
interfrost = {version = "=1.5.0"}
|
|
||||||
|
|
||||||
# logging
|
# logging
|
||||||
sentry = {version = "=0.49.3", default-features = false, features = [
|
tracing = {version = "=0.1.44"}
|
||||||
|
tracing-futures = {version = "=0.2.5"}
|
||||||
|
tracing-subscriber = {version = "=0.3.23", features = ["env-filter", "fmt", "ansi", "json", "parking_lot", "registry"]}
|
||||||
|
|
||||||
|
sentry = {version = "=0.48.5", default-features = false, features = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"contexts",
|
"contexts",
|
||||||
"debug-images",
|
"debug-images",
|
||||||
|
|
@ -49,10 +53,7 @@ sentry = {version = "=0.49.3", default-features = false, features = [
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rustls",
|
"rustls",
|
||||||
]}
|
]}
|
||||||
sentry-tracing = {version = "=0.49.3"}
|
sentry-tracing = {version = "=0.48.5"}
|
||||||
tracing = {version = "=0.1.44"}
|
|
||||||
tracing-futures = {version = "=0.2.5"}
|
|
||||||
tracing-subscriber = {version = "=0.3.23", features = ["env-filter", "fmt", "ansi", "json", "parking_lot", "registry"]}
|
|
||||||
|
|
||||||
# database
|
# database
|
||||||
sqlx = {version = "=0.9.0", default-features = false, features = [
|
sqlx = {version = "=0.9.0", default-features = false, features = [
|
||||||
|
|
@ -68,15 +69,10 @@ sqlx = {version = "=0.9.0", default-features = false, features = [
|
||||||
]}
|
]}
|
||||||
|
|
||||||
# async
|
# async
|
||||||
astral-tokio-tar = {version = "=0.7.0"}
|
async-trait = {version = "=0.1.89"}
|
||||||
async-compression = {version = "=0.4.48", features = ["tokio", "gzip"]}
|
futures-util = {version = "=0.3.32", features = ["tokio-io"]}
|
||||||
async-stream = {version = "=0.3.6"}
|
futures-lite = { version = "=2.6.1" }
|
||||||
async-tempfile = {version = "=0.8.0", features = ["uuid"]}
|
reqwest = {version = "=0.13.3", default-features = false, features = [
|
||||||
async-trait = {version = "=0.1.92"}
|
|
||||||
async_zip = {version = "=0.0.19", features = ["tracing", "chrono", "tokio", "tokio-fs", "deflate", "deflate64"]}
|
|
||||||
futures-lite = {version = "=2.6.1"}
|
|
||||||
futures-util = {version = "=0.3.34", features = ["tokio-io"]}
|
|
||||||
reqwest = {version = "=0.13.5", default-features = false, features = [
|
|
||||||
"json",
|
"json",
|
||||||
"stream",
|
"stream",
|
||||||
"multipart",
|
"multipart",
|
||||||
|
|
@ -88,95 +84,81 @@ reqwest = {version = "=0.13.5", default-features = false, features = [
|
||||||
"gzip",
|
"gzip",
|
||||||
"brotli",
|
"brotli",
|
||||||
"form",
|
"form",
|
||||||
|
"hickory-dns",
|
||||||
|
# Without this reqwest sees only the HTTP_PROXY/HTTPS_PROXY env vars, and
|
||||||
|
# ignores the proxy configured in Windows Internet Options or macOS network
|
||||||
|
# preferences — which is how school and workplace networks are set up. Those
|
||||||
|
# users could reach every service in a browser but not from here.
|
||||||
"system-proxy",
|
"system-proxy",
|
||||||
]}
|
]}
|
||||||
reqwest-websocket = {version = "=0.6.0"}
|
reqwest-websocket = {version = "=0.6.0"}
|
||||||
tokio = {version = "=1.53.1", features = ["full"]}
|
tokio = {version = "=1.52.3", features = ["full"]}
|
||||||
tokio-stream = {version = "=0.1.19", features = ["full"]}
|
tokio-stream = {version = "=0.1.18", features = ["full"]}
|
||||||
tokio-util = {version = "=0.7.19", features = ["full"]}
|
tokio-util = {version = "=0.7.18", features = ["full"]}
|
||||||
|
async_zip = { version = "=0.0.18", features = ["chrono", "tokio", "tokio-fs", "deflate", "deflate64"]}
|
||||||
|
astral-tokio-tar = { version = "=0.6.2" }
|
||||||
|
async-tempfile = { version = "=0.7.0", features = ["uuid"] }
|
||||||
|
async-stream = { version = "=0.3.6" }
|
||||||
|
async-compression = { version = "=0.4.42", features = ["tokio", "gzip"] }
|
||||||
|
|
||||||
# crypto (content hashing)
|
# crypto (content hashing)
|
||||||
byteorder = {version = "=1.5.0"}
|
digest = { version = "=0.10.7", features = ["std"] }
|
||||||
digest = {version = "=0.11.3"}
|
sha1 = { version = "=0.10.6", default-features = false }
|
||||||
md-5 = {version = "=0.11.0", default-features = false}
|
sha2 = { version = "=0.10.9", default-features = false }
|
||||||
p256 = {version = "=0.14.0", features = ["ecdsa", "pkcs8"]}
|
md-5 = { version = "=0.10.6", default-features = false }
|
||||||
sha1 = {version = "=0.11.0", default-features = false}
|
p256 = { version = "=0.13.2", features = ["ecdsa", "pkcs8"] }
|
||||||
sha2 = {version = "=0.11.0", default-features = false}
|
byteorder = { version = "=1.5.0" }
|
||||||
|
|
||||||
# util
|
# util
|
||||||
arboard = {version = "=3.6.1", features = ["wayland-data-control"]}
|
base64 = {version = "=0.22.1"}
|
||||||
arc-swap = {version = "=1.9.2"}
|
chrono = {version = "=0.4.44", features = ["serde"]}
|
||||||
base64 = {version = "=0.23.1"}
|
|
||||||
bytes = {version = "=1.12.1"}
|
|
||||||
chrono = {version = "=0.4.45", features = ["serde"]}
|
|
||||||
directories = {version = "=6.0.0"}
|
directories = {version = "=6.0.0"}
|
||||||
dunce = {version = "=1.0.5"}
|
rand = {version = "=0.10.1"}
|
||||||
htmd = {version = "=0.5.5"}
|
serde = {version = "=1.0.228"}
|
||||||
indicatif = {version = "=0.18.6", features = ["tokio"]}
|
serde_json = {version = "=1.0.150"}
|
||||||
junction = {version = "=2.1.0"}
|
|
||||||
libc = {version = "=0.2.189"}
|
|
||||||
notify = {version = "=8.2.0"}
|
|
||||||
open = {version = "=5.4.4", features = ["shellexecute-on-windows"]}
|
|
||||||
parking_lot = {version = "=0.12.5"}
|
|
||||||
pulldown-cmark = {version = "=0.13.4", default-features = false}
|
|
||||||
rand = {version = "=0.10.3"}
|
|
||||||
regex = {version = "=1.13.1"}
|
|
||||||
rfd = {version = "=0.17.2"}
|
|
||||||
rust-embed = {version = "=8.12.0", features = ["include-exclude", "compression", "debug-embed"]}
|
|
||||||
sanitize-filename = {version = "=0.6.0"}
|
|
||||||
serde = {version = "=1.0.229"}
|
|
||||||
serde_json = {version = "=1.0.151"}
|
|
||||||
serde_repr = {version = "=0.1.21"}
|
|
||||||
sysinfo = {version = "=0.39.6"}
|
|
||||||
toml = {version = "=1.1.6", default-features = false, features = ["parse", "serde"]}
|
|
||||||
trash = {version = "=5.2.9"}
|
|
||||||
percent-encoding = {version = "=2.3.2"}
|
|
||||||
url = {version = "=2.5.8", features = ["serde"]}
|
url = {version = "=2.5.8", features = ["serde"]}
|
||||||
uuid = {version = "=1.26.1", features = ["serde", "v4"]}
|
uuid = {version = "=1.23.1", features = ["serde", "v4"]}
|
||||||
windows-sys = {version = "=0.61.2", features = ["Win32_Foundation", "Win32_Storage_FileSystem"]}
|
sanitize-filename = { version = "=0.6.0" }
|
||||||
|
regex = { version = "=1.12.3" }
|
||||||
|
parking_lot = { version = "=0.12.5" }
|
||||||
|
arc-swap = { version = "=1.7.1" }
|
||||||
|
dunce = { version = "=1.0.5" }
|
||||||
|
junction = { version = "=2.0.0" }
|
||||||
|
bytes = { version = "=1.11.1" }
|
||||||
|
indicatif = { version = "=0.18.4", features = [ "tokio" ]}
|
||||||
|
sysinfo = { version = "=0.39.5" }
|
||||||
|
|
||||||
# util (codegen/macro)
|
# util (codegen/macro)
|
||||||
anyhow = {version = "=1.0.104"}
|
proc-macro2 = {version = "=1.0.106"}
|
||||||
proc-macro2 = {version = "=1.0.107"}
|
quote = {version = "=1.0.45"}
|
||||||
quote = {version = "=1.0.47"}
|
thiserror = {version = "=2.0.18" }
|
||||||
|
anyhow = {version = "=1.0.102"}
|
||||||
strum = {version = "=0.28.0", features = ["derive"]}
|
strum = {version = "=0.28.0", features = ["derive"]}
|
||||||
syn = {version = "=3.0.6"}
|
syn = {version = "=2.0.117"}
|
||||||
thiserror = {version = "=2.0.21"}
|
|
||||||
|
|
||||||
# ui
|
|
||||||
freya = {git = "https://github.com/Polyfrost/freya", rev = "12f696f68c4936617b8627c862b39acb3d9cca2b", features = ["engine", "winit", "router", "remote-asset", "sdk-tokio", "query", "radio", "borderless", "titlebar", "tray", "metrics"]}
|
|
||||||
freya-core = {git = "https://github.com/Polyfrost/freya", rev = "12f696f68c4936617b8627c862b39acb3d9cca2b"}
|
|
||||||
freya-testing = {git = "https://github.com/Polyfrost/freya", rev = "12f696f68c4936617b8627c862b39acb3d9cca2b"}
|
|
||||||
freya-skia-safe = "=0.103.2" # required for the SKSL player renderer
|
|
||||||
image = {version = "=0.25.10", default-features = false, features = ["png", "jpeg"]}
|
image = {version = "=0.25.10", default-features = false, features = ["png", "jpeg"]}
|
||||||
|
htmd = {version = "=0.5.4"}
|
||||||
|
arboard = {version = "=3.6.1"}
|
||||||
|
trash = {version = "=5.2.6"}
|
||||||
|
|
||||||
|
# required for the SKSL player renderer
|
||||||
|
freya-skia-safe = "=0.98.0"
|
||||||
|
freya = { git = "https://github.com/marc2332/freya.git", branch = "fix/blur", features = ["engine", "winit", "router", "remote-asset", "sdk", "markdown", "query", "radio", "titlebar"] }
|
||||||
|
|
||||||
|
[patch.crates-io]
|
||||||
|
freya = { git = "https://github.com/marc2332/freya.git", branch = "fix/blur" }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
codegen-units = 16
|
|
||||||
debug = false
|
|
||||||
lto = "thin"
|
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
panic = "abort"
|
lto = "fat"
|
||||||
split-debuginfo = "packed"
|
codegen-units = 1
|
||||||
strip = true
|
strip = true
|
||||||
|
panic = "abort"
|
||||||
|
debug = "line-tables-only"
|
||||||
|
split-debuginfo = "packed"
|
||||||
|
|
||||||
[profile.release-profiling]
|
[profile.release-profiling]
|
||||||
inherits = "release"
|
inherits = "release"
|
||||||
|
|
||||||
debug = true
|
|
||||||
strip = false
|
strip = false
|
||||||
|
|
||||||
[profile.release-fastprofiling]
|
|
||||||
inherits = "release"
|
|
||||||
|
|
||||||
codegen-units = 16
|
|
||||||
debug = true
|
debug = true
|
||||||
lto = false
|
|
||||||
strip = false
|
|
||||||
|
|
||||||
[profile.dev]
|
|
||||||
opt-level = 0
|
|
||||||
debug = 0
|
|
||||||
strip = "debuginfo"
|
|
||||||
|
|
||||||
[profile.dev.package."*"]
|
|
||||||
opt-level = 2
|
|
||||||
|
|
|
||||||
70
README.md
|
|
@ -1,20 +1,74 @@
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
<img src="https://github.com/user-attachments/assets/e8359baa-23fb-457a-8910-169974487a98" alt="OneLauncher Logo" width="220" style="border-radius: 24px;" />
|
<img src=".github/media/RepoBanner.png" alt="Repository Banner" />
|
||||||
|
|
||||||
forked from [OneLauncher](https://github.com/Polyfrost/OneLauncher) allowing for uh offline authentication
|
# OneLauncher | OneClient
|
||||||
|
The monorepo containing the code for OneLauncher, OneClient, and their core backend.
|
||||||
|
|
||||||
|
OneClient is a Minecraft client featuring fully 100% open-source components, offering many packaged and pre-configured mods in one click.
|
||||||
|
OneLauncher is a WIP Minecraft launcher giving power-users the greatest customization whilst featuring a clean UI.
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## download from [**Releases Page**](https://git.oiupoyt.space/oiupoyt/OneLauncher-Cracked/releases).
|
> [!NOTE]
|
||||||
|
> This is **OneLauncher/OneClient v2** — a full rewrite in Rust using [Freya](https://freyaui.dev/).
|
||||||
|
> All previous web-based (React / Tauri) code has been removed.
|
||||||
|
|
||||||
yes the actual client performs normally aswell
|
## Technologies Used
|
||||||
|
- [**Rust**](https://www.rust-lang.org/) (edition 2024)
|
||||||
|
- [**Freya**](https://freyaui.dev/) - native GUI framework for Rust
|
||||||
|
|
||||||
> [!WARNING]
|
## Contributing
|
||||||
> DO NOT use official Microsoft/Mojang accounts with this launcher. This bypasses authentication and is intended for offline play only. Using with official servers may violate Minecraft's Terms of Service.
|
We welcome contributions! Please read our [contributing guidelines](CONTRIBUTING.md) before getting started.
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
The project targets **Rust edition 2024**, so a recent Rust toolchain is required.
|
||||||
|
|
||||||
#### The Trojan:Win32/Wacatac.B!ml detection is a false positive triggered by Microsoft Defender's machine learning heuristic on newly compiled, unsigned Rust NSIS installers. The software contains no malware, telemetry, or malicious payloads.
|
- Install Rust via [rustup](https://rustup.rs/)
|
||||||
|
- Use a toolchain that supports edition 2024 (`rustc` **1.85+**)
|
||||||
|
|
||||||
intended for offline usage only.
|
### Building & Running
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Run the app
|
||||||
|
cargo run -p oneclient_app
|
||||||
|
|
||||||
|
# Build a release binary
|
||||||
|
cargo build --release -p oneclient_app
|
||||||
|
```
|
||||||
|
|
||||||
|
### Packaging / Releasing
|
||||||
|
|
||||||
|
Installers are produced with [**cargo-packager**](https://github.com/crabnebula-dev/cargo-packager)
|
||||||
|
(the standalone bundler spun out of the Tauri bundler). Config lives in
|
||||||
|
[`packages/oneclient_app/Cargo.toml`](./packages/oneclient_app/Cargo.toml) under
|
||||||
|
`[package.metadata.packager]`.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo install cargo-packager --locked
|
||||||
|
|
||||||
|
# Build the binary, then bundle it for the current OS:
|
||||||
|
cargo build --release -p oneclient_app
|
||||||
|
cargo packager --release -p oneclient_app --formats <targets>
|
||||||
|
# Windows: nsis macOS: app,dmg Linux: deb,appimage
|
||||||
|
```
|
||||||
|
|
||||||
|
### Versioning
|
||||||
|
|
||||||
|
The workspace shares a single version, defined in the root [`Cargo.toml`](./Cargo.toml)
|
||||||
|
under `[workspace.package]`. Current version: **2.0.0**.
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
|
||||||
|
All crates live under **`packages/`** in a single Cargo workspace.
|
||||||
|
|
||||||
|
- [**`oneclient_app/`**](./packages/oneclient_app/) - The Freya desktop application (UI, routes, entry point).
|
||||||
|
- [**`oneclient_core/`**](./packages/oneclient_core/) - Launcher core. Contains the entire launcher logic.
|
||||||
|
- [**`oneclient_db/`**](./packages/oneclient_db/) - SQLx-based database layer.
|
||||||
|
- [**`oneclient_macro/`**](./packages/oneclient_macro/) - Macro definitions to simplify some code.
|
||||||
|
- [**`polyio/`**](./packages/polyio/) - PolyIO-rs. Shared IO utilities (archives, files, system helpers).
|
||||||
|
|
||||||
|
## Code signing
|
||||||
|
|
||||||
|
This program uses free code signing provided by [SignPath.io](https://signpath.io?utm_source=foundation&utm_medium=github&utm_campaign=0install), and a certificate by the [SignPath Foundation](https://signpath.org?utm_source=foundation&utm_medium=github&utm_campaign=0install). We thank them very much to their contributions to OSS software!
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ depends=(
|
||||||
'wayland'
|
'wayland'
|
||||||
'dbus'
|
'dbus'
|
||||||
'gtk3'
|
'gtk3'
|
||||||
'libayatana-appindicator'
|
|
||||||
)
|
)
|
||||||
provides=('oneclient')
|
provides=('oneclient')
|
||||||
conflicts=('oneclient')
|
conflicts=('oneclient')
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ depends=(
|
||||||
'wayland'
|
'wayland'
|
||||||
'dbus'
|
'dbus'
|
||||||
'gtk3'
|
'gtk3'
|
||||||
'libayatana-appindicator'
|
|
||||||
)
|
)
|
||||||
makedepends=(
|
makedepends=(
|
||||||
'rust'
|
'rust'
|
||||||
|
|
@ -29,10 +28,7 @@ makedepends=(
|
||||||
)
|
)
|
||||||
provides=('oneclient')
|
provides=('oneclient')
|
||||||
conflicts=('oneclient-bin')
|
conflicts=('oneclient-bin')
|
||||||
options=(
|
options=('!lto')
|
||||||
'!lto'
|
|
||||||
'!debug'
|
|
||||||
)
|
|
||||||
source=("$pkgname-$pkgver.tar.gz::${url}/archive/refs/tags/oneclient-${pkgver}.tar.gz")
|
source=("$pkgname-$pkgver.tar.gz::${url}/archive/refs/tags/oneclient-${pkgver}.tar.gz")
|
||||||
sha256sums=('SKIP')
|
sha256sums=('SKIP')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
%global debug_package %{nil}
|
|
||||||
%global __strip /bin/true
|
|
||||||
%global _appname OneClient
|
|
||||||
%global _rpmfile %{_appname}_%{version}_linux_x86_64.rpm
|
|
||||||
|
|
||||||
Name: oneclient-bin
|
|
||||||
Version: 2.0.1
|
|
||||||
Release: 1%{?dist}
|
|
||||||
Summary: Next-generation open source Minecraft launcher (prebuilt)
|
|
||||||
|
|
||||||
License: GPL-3.0-only
|
|
||||||
URL: https://github.com/Polyfrost/OneLauncher
|
|
||||||
Source0: %{url}/releases/download/oneclient-%{version}/%{_rpmfile}
|
|
||||||
|
|
||||||
ExclusiveArch: x86_64
|
|
||||||
BuildRequires: bsdtar
|
|
||||||
Requires: gtk3
|
|
||||||
Requires: libayatana-appindicator-gtk3
|
|
||||||
Requires: libxdo
|
|
||||||
Requires: dbus-libs
|
|
||||||
Provides: oneclient = %{version}-%{release}
|
|
||||||
|
|
||||||
%description
|
|
||||||
OneClient is a next-generation open source Minecraft launcher.
|
|
||||||
This package ships the prebuilt binaries from the upstream release.
|
|
||||||
|
|
||||||
%prep
|
|
||||||
%setup -q -c -T
|
|
||||||
# Repackages the upstream release rpm; bsdtar reads its cpio payload directly.
|
|
||||||
bsdtar -xf %{SOURCE0}
|
|
||||||
|
|
||||||
%install
|
|
||||||
mkdir -p %{buildroot}
|
|
||||||
cp -a usr %{buildroot}/
|
|
||||||
ln -s oneclient_app %{buildroot}%{_bindir}/oneclient
|
|
||||||
|
|
||||||
%files
|
|
||||||
%{_bindir}/oneclient_app
|
|
||||||
%{_bindir}/oneclient
|
|
||||||
%{_datadir}/applications/*.desktop
|
|
||||||
%{_datadir}/icons/hicolor/*/apps/*
|
|
||||||
|
|
||||||
%changelog
|
|
||||||
* Wed Sep 09 2026 Polyfrost <contact@atmofrost.org> - 2.0.1-1
|
|
||||||
- See upstream release notes.
|
|
||||||
|
|
@ -1,117 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
BEGIN_RES = "# >>> CRT_RESOURCES >>>"
|
|
||||||
END_RES = "# <<< CRT_RESOURCES <<<"
|
|
||||||
BEGIN_SEC = "; >>> VC_REDIST_SECTION >>>"
|
|
||||||
END_SEC = "; <<< VC_REDIST_SECTION <<<"
|
|
||||||
|
|
||||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
||||||
DEFAULT_CARGO_TOML = REPO_ROOT / "packages" / "oneclient_app" / "Cargo.toml"
|
|
||||||
DEFAULT_OUT = REPO_ROOT / "target" / "crt"
|
|
||||||
|
|
||||||
VSWHERE = Path(
|
|
||||||
r"C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe"
|
|
||||||
)
|
|
||||||
CRT_GLOB = r"VC\Redist\MSVC\**\x64\Microsoft.VC*.CRT\*.dll"
|
|
||||||
|
|
||||||
SECTION_REPLACEMENT = """; >>> VC_REDIST_SECTION >>>
|
|
||||||
; <<< VC_REDIST_SECTION <<<"""
|
|
||||||
|
|
||||||
|
|
||||||
def region(begin: str, end: str) -> re.Pattern:
|
|
||||||
return re.compile(
|
|
||||||
r"^[ \t]*" + re.escape(begin) + r".*?^[ \t]*" + re.escape(end) + r"[ \t]*$",
|
|
||||||
re.DOTALL | re.MULTILINE,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
REGION_RES = region(BEGIN_RES, END_RES)
|
|
||||||
REGION_SEC = region(BEGIN_SEC, END_SEC)
|
|
||||||
|
|
||||||
|
|
||||||
def find_crt_dir() -> Path:
|
|
||||||
"""Locate the newest x64 Microsoft.VC*.CRT redist directory via vswhere."""
|
|
||||||
if not VSWHERE.is_file():
|
|
||||||
sys.exit(f"error: vswhere not found at {VSWHERE}; pass --crt-dir explicitly")
|
|
||||||
|
|
||||||
out = subprocess.run(
|
|
||||||
[str(VSWHERE), "-latest", "-products", "*", "-find", CRT_GLOB],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
check=True,
|
|
||||||
).stdout
|
|
||||||
dirs = {Path(line).parent for line in out.splitlines() if line.strip()}
|
|
||||||
if not dirs:
|
|
||||||
sys.exit(f"error: vswhere found no DLLs matching {CRT_GLOB}")
|
|
||||||
|
|
||||||
newest = sorted(dirs, key=lambda p: p.parts)[-1]
|
|
||||||
print(f"CRT redist dir: {newest}")
|
|
||||||
return newest
|
|
||||||
|
|
||||||
|
|
||||||
def collect(crt_dir: Path, out_dir: Path) -> list[Path]:
|
|
||||||
dlls = sorted(crt_dir.glob("*.dll"))
|
|
||||||
if not dlls:
|
|
||||||
sys.exit(f"error: no DLLs in {crt_dir}")
|
|
||||||
|
|
||||||
if out_dir.exists():
|
|
||||||
shutil.rmtree(out_dir)
|
|
||||||
out_dir.mkdir(parents=True)
|
|
||||||
|
|
||||||
copied = []
|
|
||||||
for dll in dlls:
|
|
||||||
dest = out_dir / dll.name
|
|
||||||
shutil.copy2(dll, dest)
|
|
||||||
copied.append(dest)
|
|
||||||
print(f" {dll.name} ({dest.stat().st_size} bytes)")
|
|
||||||
return copied
|
|
||||||
|
|
||||||
|
|
||||||
def patch(cargo_toml: Path, dlls: list[Path]) -> None:
|
|
||||||
text = cargo_toml.read_text(encoding="utf-8")
|
|
||||||
for pat, name in ((REGION_RES, "CRT_RESOURCES"), (REGION_SEC, "VC_REDIST_SECTION")):
|
|
||||||
if not pat.search(text):
|
|
||||||
sys.exit(f"error: {name} region not found in {cargo_toml}")
|
|
||||||
|
|
||||||
entries = "\n".join(
|
|
||||||
f' {{ src = "{str(d).replace(chr(92), chr(92) * 2)}", target = "{d.name}" }},'
|
|
||||||
for d in dlls
|
|
||||||
)
|
|
||||||
block = (
|
|
||||||
f" {BEGIN_RES}\n"
|
|
||||||
f" # Generated by distribution/windows/bundle-crt.py -- do not edit by hand.\n"
|
|
||||||
f"{entries}\n"
|
|
||||||
f" {END_RES}"
|
|
||||||
)
|
|
||||||
|
|
||||||
text = REGION_RES.sub(lambda _: block, text, count=1)
|
|
||||||
text = REGION_SEC.sub(lambda _: SECTION_REPLACEMENT, text, count=1)
|
|
||||||
cargo_toml.write_text(text, encoding="utf-8")
|
|
||||||
print(f"patched {cargo_toml} with {len(dlls)} app-local CRT DLLs")
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
ap = argparse.ArgumentParser(description=__doc__)
|
|
||||||
ap.add_argument("--cargo-toml", type=Path, default=DEFAULT_CARGO_TOML)
|
|
||||||
ap.add_argument("--out-dir", type=Path, default=DEFAULT_OUT)
|
|
||||||
ap.add_argument(
|
|
||||||
"--crt-dir",
|
|
||||||
type=Path,
|
|
||||||
help="Microsoft.VC*.CRT directory to copy from (default: ask vswhere)",
|
|
||||||
)
|
|
||||||
args = ap.parse_args()
|
|
||||||
|
|
||||||
crt_dir = args.crt_dir.resolve() if args.crt_dir else find_crt_dir()
|
|
||||||
dlls = collect(crt_dir, args.out_dir.resolve())
|
|
||||||
patch(args.cargo_toml.resolve(), dlls)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
Before Width: | Height: | Size: 44 KiB |
|
|
@ -1,108 +0,0 @@
|
||||||
# OneLauncher-Cracked 2.8.0 Update Implementation Plan
|
|
||||||
|
|
||||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
||||||
|
|
||||||
**Goal:** Rebase and upgrade OneLauncher-Cracked onto upstream OneLauncher 2.8.0 (`1d6ec598`), preserving all offline authentication bypasses, the local Yggdrasil skin server with authlib-injector, Forgejo auto-updater, and custom skin changer UI.
|
|
||||||
|
|
||||||
**Architecture:** Port 37 cracked subsystem files onto the clean upstream 2.8.0 codebase. Maintain strict isolation for cracked authentication and skin server logic while adopting upstream's new multi-instance management, package manager filters, and Freya improvements.
|
|
||||||
|
|
||||||
**Tech Stack:** Rust 2024, Freya (Dioxus + Skia), Tokio, Reqwest, RSA PKCS#1v15, Authlib-Injector, Forgejo API v1.
|
|
||||||
|
|
||||||
**Spec:** Direct human instruction: "update to version 2.8.0".
|
|
||||||
|
|
||||||
## Global Constraints
|
|
||||||
- Target version: `2.8.0-c1`.
|
|
||||||
- Offline authentication must require 0 Microsoft accounts.
|
|
||||||
- Local Yggdrasil skin server must eliminate multiplayer skin cloning via RSA-signed profile properties.
|
|
||||||
- Auto-updater must target Forgejo API at `https://git.oiupoyt.space/api/v1/repos/oiupoyt/OneLauncher-Cracked/releases/latest`.
|
|
||||||
- All workspace crates must pass tests and compilation without regressions.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 1: Project Metadata, Packaging & Workflows
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `Cargo.toml`
|
|
||||||
- Modify: `README.md`
|
|
||||||
- Modify: `.gitignore`
|
|
||||||
- Create: `.forgejo/workflows/release.yml`
|
|
||||||
- Create: `packages/oneclient_app/distribution/linux/oneclient_app.desktop`
|
|
||||||
|
|
||||||
- [ ] **Step 1: Update Cargo.toml metadata and dev profile**
|
|
||||||
- [ ] **Step 2: Update README.md with warning embed and Forgejo releases link**
|
|
||||||
- [ ] **Step 3: Add .forgejo release workflow and desktop file**
|
|
||||||
- [ ] **Step 4: Commit task 1**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 2: Port Offline Authentication Bypass (`oneclient_auth`)
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `packages/oneclient_auth/src/service.rs`
|
|
||||||
- Modify: `packages/oneclient_auth/src/store.rs`
|
|
||||||
- Modify: `packages/oneclient_auth/tests/offline.rs`
|
|
||||||
|
|
||||||
- [ ] **Step 1: In `service.rs`, remove `OfflineRequiresMicrosoft` check from `account_for_launch`**
|
|
||||||
- [ ] **Step 2: In `store.rs`, allow offline accounts without Microsoft accounts and auto-select default user**
|
|
||||||
- [ ] **Step 3: In `tests/offline.rs`, ensure tests assert offline launch succeeds without Microsoft**
|
|
||||||
- [ ] **Step 4: Run `cargo test -p oneclient_auth` and verify all tests pass**
|
|
||||||
- [ ] **Step 5: Commit task 2**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 3: Port Path Helpers, Assets & Offline Skin Engine (`oneclient_common` & `oneclient_core`)
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `packages/oneclient_common/src/paths.rs`
|
|
||||||
- Modify: `packages/oneclient_core/Cargo.toml`
|
|
||||||
- Copy assets: `packages/oneclient_core/assets/authlib-injector.jar`
|
|
||||||
- Copy assets: `packages/oneclient_core/assets/offlineskins-fabric.jar`
|
|
||||||
- Create: `packages/oneclient_core/src/game/skin_server.rs`
|
|
||||||
- Modify: `packages/oneclient_core/src/game/mod.rs`
|
|
||||||
- Modify: `packages/oneclient_core/src/game/launch.rs`
|
|
||||||
|
|
||||||
- [ ] **Step 1: Add skin paths and authlib-injector path to `oneclient_common/src/paths.rs`**
|
|
||||||
- [ ] **Step 2: Copy embedded jar assets and add dependencies to `oneclient_core/Cargo.toml`**
|
|
||||||
- [ ] **Step 3: Port `skin_server.rs` and register module in `game/mod.rs`**
|
|
||||||
- [ ] **Step 4: In `game/launch.rs`, inject authlib-injector `-javaagent`, `--userProperties`, and offline skin synchronization**
|
|
||||||
- [ ] **Step 5: Run `cargo test -p oneclient_core` and verify all tests pass**
|
|
||||||
- [ ] **Step 6: Commit task 3**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 4: Port UI Components, Skins Tab & Auto-Updater (`oneclient_app`)
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `packages/oneclient_app/src/constants.rs`
|
|
||||||
- Modify: `packages/oneclient_app/src/updater.rs`
|
|
||||||
- Modify: `packages/oneclient_app/src/components/avatar.rs`
|
|
||||||
- Modify: `packages/oneclient_app/src/components/player_model.rs`
|
|
||||||
- Modify: `packages/oneclient_app/src/components/navbar/app_navbar.rs`
|
|
||||||
- Create: `packages/oneclient_app/src/view/app/skins.rs`
|
|
||||||
- Create: `packages/oneclient_app/src/hooks/queries/skin.rs`
|
|
||||||
- Modify: `packages/oneclient_app/src/hooks/queries/mod.rs`
|
|
||||||
- Modify: `packages/oneclient_app/src/hooks/mod.rs`
|
|
||||||
- Modify: `packages/oneclient_app/src/view/onboarding/account.rs`
|
|
||||||
- Modify: `packages/oneclient_app/src/view/app/settings/accounts.rs`
|
|
||||||
- Modify: `packages/oneclient_app/src/layout/root_layout.rs`
|
|
||||||
- Modify: `packages/oneclient_app/src/routes.rs`
|
|
||||||
|
|
||||||
- [ ] **Step 1: Update `constants.rs` and `updater.rs` with Forgejo releases endpoint and `CrackedVersion` semver parser**
|
|
||||||
- [ ] **Step 2: Port skin queries in `hooks/queries/skin.rs` and wire into `hooks/mod.rs`**
|
|
||||||
- [ ] **Step 3: Port `skins.rs` view and register route in `routes.rs` and `root_layout.rs`**
|
|
||||||
- [ ] **Step 4: Add Skins pill button to `navbar_center` in `app_navbar.rs`**
|
|
||||||
- [ ] **Step 5: Port avatar persistent cache in `avatar.rs` and player model custom skin loading in `player_model.rs`**
|
|
||||||
- [ ] **Step 6: Port offline account inputs in onboarding (`onboarding/account.rs`) and settings (`settings/accounts.rs`)**
|
|
||||||
- [ ] **Step 7: Run `cargo test -p oneclient_app --lib` and verify all tests pass**
|
|
||||||
- [ ] **Step 8: Commit task 4**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 5: Verification, Packaging & Deployment
|
|
||||||
|
|
||||||
- [ ] **Step 1: Run full workspace `cargo check --workspace`**
|
|
||||||
- [ ] **Step 2: Build release binary locally**
|
|
||||||
- [ ] **Step 3: Package `.AppImage`, `.deb`, and `.tar.gz` for Linux**
|
|
||||||
- [ ] **Step 4: Merge `update/2.8.0` into `main`**
|
|
||||||
- [ ] **Step 5: Tag `v2.8.0-c1` and push branch and tag to Forgejo and GitHub**
|
|
||||||
- [ ] **Step 6: Publish Forgejo Release `v2.8.0-c1` with all binaries attached**
|
|
||||||
28
knope.toml
|
|
@ -1,28 +0,0 @@
|
||||||
[package]
|
|
||||||
versioned_files = ["Cargo.toml"]
|
|
||||||
changelog = "CHANGELOG.md"
|
|
||||||
|
|
||||||
[changes]
|
|
||||||
ignore_conventional_commits = true
|
|
||||||
|
|
||||||
[release_notes]
|
|
||||||
change_templates = [
|
|
||||||
"- $summary by [$pr_author_login](https://github.com/$pr_author_login)",
|
|
||||||
"- $summary",
|
|
||||||
]
|
|
||||||
|
|
||||||
[github]
|
|
||||||
owner = "Polyfrost"
|
|
||||||
repo = "OneLauncher"
|
|
||||||
|
|
||||||
[[workflows]]
|
|
||||||
name = "prepare-release"
|
|
||||||
|
|
||||||
[[workflows.steps]]
|
|
||||||
type = "PrepareRelease"
|
|
||||||
|
|
||||||
[[workflows]]
|
|
||||||
name = "document-change"
|
|
||||||
|
|
||||||
[[workflows.steps]]
|
|
||||||
type = "CreateChangeFile"
|
|
||||||
|
|
@ -29,33 +29,22 @@ chrono.workspace = true
|
||||||
oneclient_macro = { workspace = true, features = ["freya"] }
|
oneclient_macro = { workspace = true, features = ["freya"] }
|
||||||
|
|
||||||
polyio.workspace = true
|
polyio.workspace = true
|
||||||
rust-embed.workspace = true
|
rust-embed = { version = "=8.11.0", features = ["include-exclude", "compression", "debug-embed"] }
|
||||||
|
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
tracing-subscriber.workspace = true
|
tracing-subscriber.workspace = true
|
||||||
futures-util.workspace = true
|
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
uuid.workspace = true
|
uuid.workspace = true
|
||||||
bytes.workspace = true
|
bytes.workspace = true
|
||||||
sysinfo.workspace = true
|
sysinfo.workspace = true
|
||||||
|
|
||||||
notify.workspace = true
|
|
||||||
|
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
regex.workspace = true
|
regex.workspace = true
|
||||||
directories.workspace = true
|
|
||||||
dunce.workspace = true
|
|
||||||
percent-encoding.workspace = true
|
|
||||||
serde = { workspace = true, features = ["derive"] }
|
|
||||||
serde_json.workspace = true
|
|
||||||
reqwest.workspace = true
|
|
||||||
|
|
||||||
cargo-packager-updater = "=0.2.3"
|
cargo-packager-updater = "=0.2.3"
|
||||||
|
|
||||||
# rfd = "=0.17.2"
|
rfd = "=0.17.2"
|
||||||
# open = "=5.4.0"
|
open = "=5.4.0"
|
||||||
rfd.workspace = true
|
|
||||||
open.workspace = true
|
|
||||||
|
|
||||||
# clipboard image copy + decode for it
|
# clipboard image copy + decode for it
|
||||||
arboard.workspace = true
|
arboard.workspace = true
|
||||||
|
|
@ -63,27 +52,10 @@ image.workspace = true
|
||||||
|
|
||||||
freya-skia-safe.workspace = true
|
freya-skia-safe.workspace = true
|
||||||
freya.workspace = true
|
freya.workspace = true
|
||||||
freya-core.workspace = true
|
|
||||||
pulldown-cmark.workspace = true
|
|
||||||
# inlines svg logos embedded in markdown badges as data urls
|
|
||||||
base64.workspace = true
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
freya-testing.workspace = true
|
|
||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
mimalloc = "=0.1.52"
|
mimalloc = "=0.1.52"
|
||||||
|
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
|
||||||
libloading = "=0.8.9"
|
|
||||||
gtk-sys = "=0.18.2"
|
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
|
||||||
windows = { version = "=0.62.2", features = [
|
|
||||||
"Win32_Foundation",
|
|
||||||
"Win32_System_Registry",
|
|
||||||
] }
|
|
||||||
|
|
||||||
[target.'cfg(windows)'.build-dependencies]
|
[target.'cfg(windows)'.build-dependencies]
|
||||||
winresource = "=0.1.31"
|
winresource = "=0.1.31"
|
||||||
|
|
||||||
|
|
@ -91,7 +63,7 @@ winresource = "=0.1.31"
|
||||||
product-name = "OneClient"
|
product-name = "OneClient"
|
||||||
identifier = "org.polyfrost.OneClient"
|
identifier = "org.polyfrost.OneClient"
|
||||||
category = "Game"
|
category = "Game"
|
||||||
publisher = "Polyfrost Inc."
|
publisher = "Polyfrost"
|
||||||
copyright = "© 2026 Polyfrost Inc."
|
copyright = "© 2026 Polyfrost Inc."
|
||||||
homepage = "https://polyfrost.org/projects/oneclient"
|
homepage = "https://polyfrost.org/projects/oneclient"
|
||||||
long-description = "Next-generation open source Minecraft launcher"
|
long-description = "Next-generation open source Minecraft launcher"
|
||||||
|
|
@ -105,11 +77,6 @@ icons = [
|
||||||
"icons/icon.icns",
|
"icons/icon.icns",
|
||||||
"icons/icon.ico",
|
"icons/icon.ico",
|
||||||
]
|
]
|
||||||
resources = [
|
|
||||||
{ src = "icons/generated/Assets.car", target = "Assets.car" },
|
|
||||||
# >>> CRT_RESOURCES >>>
|
|
||||||
# <<< CRT_RESOURCES <<<
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.metadata.packager.macos]
|
[package.metadata.packager.macos]
|
||||||
minimum-system-version = "10.14"
|
minimum-system-version = "10.14"
|
||||||
|
|
@ -136,7 +103,6 @@ Section "-OneClientTauriMigration"
|
||||||
Delete /REBOOTOK "$INSTDIR\\OneClient.exe"
|
Delete /REBOOTOK "$INSTDIR\\OneClient.exe"
|
||||||
SectionEnd
|
SectionEnd
|
||||||
|
|
||||||
; >>> VC_REDIST_SECTION >>>
|
|
||||||
Section "-VCRedist"
|
Section "-VCRedist"
|
||||||
SetRegView 64
|
SetRegView 64
|
||||||
ReadRegDWORD $R0 HKLM "SOFTWARE\\Microsoft\\VisualStudio\\14.0\\VC\\Runtimes\\X64" "Installed"
|
ReadRegDWORD $R0 HKLM "SOFTWARE\\Microsoft\\VisualStudio\\14.0\\VC\\Runtimes\\X64" "Installed"
|
||||||
|
|
@ -148,13 +114,19 @@ Section "-VCRedist"
|
||||||
Goto vcredist_done
|
Goto vcredist_done
|
||||||
${EndIf}
|
${EndIf}
|
||||||
|
|
||||||
${If} $PassiveMode != 1
|
DetailPrint "Installing Microsoft Visual C++ Redistributable (x64)..."
|
||||||
MessageBox MB_OK|MB_ICONEXCLAMATION "This build of OneClient needs the Microsoft Visual C++ Redistributable (x64) 14.40 or newer. Install it from https://aka.ms/vs/17/release/vc_redist.x64.exe, then run this installer again."
|
nsis_tauri_utils::Download "https://aka.ms/vs/17/release/vc_redist.x64.exe" "$PLUGINSDIR\\vc_redist.x64.exe"
|
||||||
|
Pop $R2
|
||||||
|
${If} $R2 == 0
|
||||||
|
ExecWait '"$PLUGINSDIR\\vc_redist.x64.exe" /quiet /norestart' $R3
|
||||||
|
${If} $R3 == 3010
|
||||||
|
SetRebootFlag true
|
||||||
|
${EndIf}
|
||||||
|
${ElseIf} $PassiveMode != 1
|
||||||
|
MessageBox MB_OK|MB_ICONEXCLAMATION "Could not download the Microsoft Visual C++ Redistributable. OneClient may fail to start until you install it manually from https://aka.ms/vs/17/release/vc_redist.x64.exe"
|
||||||
${EndIf}
|
${EndIf}
|
||||||
|
|
||||||
vcredist_done:
|
vcredist_done:
|
||||||
SectionEnd
|
SectionEnd
|
||||||
; <<< VC_REDIST_SECTION <<<
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
[package.metadata.packager.deb]
|
[package.metadata.packager.deb]
|
||||||
|
|
@ -162,14 +134,12 @@ desktop-template = "distribution/linux/org.polyfrost.oneclient.template"
|
||||||
|
|
||||||
[package.metadata.packager.appimage]
|
[package.metadata.packager.appimage]
|
||||||
# desktop entry generated from the deb template above via the linux config
|
# desktop entry generated from the deb template above via the linux config
|
||||||
libs = ["libayatana-appindicator3.so.1"]
|
|
||||||
excluded-libraries = ["libwayland-*"]
|
|
||||||
|
|
||||||
[package.metadata.generate-rpm]
|
[package.metadata.generate-rpm]
|
||||||
auto-req = "builtin"
|
auto-req = "builtin"
|
||||||
assets = [
|
assets = [
|
||||||
{ source = "target/release/oneclient_app", dest = "/usr/bin/oneclient_app", mode = "755" },
|
{ source = "target/release/oneclient_app", dest = "/usr/bin/oneclient_app", mode = "755" },
|
||||||
{ source = "target/release/oneclient_app.desktop", dest = "/usr/share/applications/oneclient_app.desktop", mode = "644" },
|
{ source = "target/release/.cargo-packager/deb/*/data/usr/share/applications/oneclient_app.desktop", dest = "/usr/share/applications/", mode = "644" },
|
||||||
{ source = "icons/32x32.png", dest = "/usr/share/icons/hicolor/32x32/apps/oneclient_app.png", mode = "644" },
|
{ source = "icons/32x32.png", dest = "/usr/share/icons/hicolor/32x32/apps/oneclient_app.png", mode = "644" },
|
||||||
{ source = "icons/64x64.png", dest = "/usr/share/icons/hicolor/64x64/apps/oneclient_app.png", mode = "644" },
|
{ source = "icons/64x64.png", dest = "/usr/share/icons/hicolor/64x64/apps/oneclient_app.png", mode = "644" },
|
||||||
{ source = "icons/128x128.png", dest = "/usr/share/icons/hicolor/128x128/apps/oneclient_app.png", mode = "644" },
|
{ source = "icons/128x128.png", dest = "/usr/share/icons/hicolor/128x128/apps/oneclient_app.png", mode = "644" },
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 920 B After Width: | Height: | Size: 958 B |
|
Before Width: | Height: | Size: 390 KiB After Width: | Height: | Size: 619 KiB |
|
|
@ -1,3 +0,0 @@
|
||||||
<svg width="100%" height="100%" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M8 15V17M12 11V17M16 7V17M6.8 21H17.2C18.88 21 19.72 21 20.36 20.67C20.93 20.39 21.39 19.93 21.67 19.36C22 18.72 22 17.88 22 16.2V7.8C22 6.12 22 5.28 21.67 4.64C21.39 4.07 20.93 3.61 20.36 3.33C19.72 3 18.88 3 17.2 3H6.8C5.12 3 4.28 3 3.64 3.33C3.07 3.61 2.61 4.07 2.33 4.64C2 5.28 2 6.12 2 7.8V16.2C2 17.88 2 18.72 2.33 19.36C2.61 19.93 3.07 20.39 3.64 20.67C4.28 21 5.12 21 6.8 21Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 590 B |
|
|
@ -1 +0,0 @@
|
||||||
<svg role="img" viewBox="-11.2 -8.5 149 113" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="M81.15,0c-1.2376,2.1973-2.3489,4.4704-3.3591,6.794-9.5975-1.4396-19.3718-1.4396-28.9945,0-.985-2.3236-2.1216-4.5967-3.3591-6.794-9.0166,1.5407-17.8059,4.2431-26.1405,8.0568C2.779,32.5304-1.6914,56.3725.5312,79.8863c9.6732,7.1476,20.5083,12.603,32.0505,16.0884,2.6014-3.4854,4.8998-7.1981,6.8698-11.0623-3.738-1.3891-7.3497-3.1318-10.8098-5.1523.9092-.6567,1.7932-1.3386,2.6519-1.9953,20.281,9.547,43.7696,9.547,64.0758,0,.8587.7072,1.7427,1.3891,2.6519,1.9953-3.4601,2.0457-7.0718,3.7632-10.835,5.1776,1.97,3.8642,4.2683,7.5769,6.8698,11.0623,11.5419-3.4854,22.3769-8.9156,32.0509-16.0631,2.626-27.2771-4.496-50.9172-18.817-71.8548C98.9811,4.2684,90.1918,1.5659,81.1752.0505l-.0252-.0505ZM42.2802,65.4144c-6.2383,0-11.4159-5.6575-11.4159-12.6535s4.9755-12.6788,11.3907-12.6788,11.5169,5.708,11.4159,12.6788c-.101,6.9708-5.026,12.6535-11.3907,12.6535ZM84.3576,65.4144c-6.2637,0-11.3907-5.6575-11.3907-12.6535s4.9755-12.6788,11.3907-12.6788,11.4917,5.708,11.3906,12.6788c-.101,6.9708-5.026,12.6535-11.3906,12.6535Z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
|
@ -1 +0,0 @@
|
||||||
<svg role="img" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 678 B |
|
|
@ -1,7 +0,0 @@
|
||||||
<svg width="80" height="80" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M35.0354 10.6199H30.9125C26.1812 10.6199 21.8092 13.1414 19.4435 17.2347L10.2109 33.2097C9.89473 33.7567 9.62082 34.3208 9.38916 34.8972L22.5061 32.4833C27.8025 31.5085 31.9468 27.3686 32.9225 22.0775L35.0354 10.6199Z" fill="currentColor"/>
|
|
||||||
<path d="M9.37051 44.7052C9.60608 45.2979 9.8862 45.8775 10.2109 46.4393L19.4435 62.4143C21.8092 66.5076 26.1812 69.0292 30.9125 69.0292H35.0434L32.9225 57.5283C31.9468 52.2373 27.8025 48.0973 22.5061 47.1225L9.37051 44.7052Z" fill="currentColor"/>
|
|
||||||
<path d="M45.456 69.0292H49.3779C54.1092 69.0292 58.4812 66.5076 60.8469 62.4143L70.0795 46.4393C70.3966 45.8907 70.6712 45.3249 70.9033 44.7467L57.9933 47.1225C52.6968 48.0973 48.5526 52.2373 47.5769 57.5283L45.456 69.0292Z" fill="currentColor"/>
|
|
||||||
<path d="M70.8845 34.8557C70.6564 34.2937 70.3881 33.7436 70.0795 33.2097L60.8469 17.2347C58.4812 13.1414 54.1092 10.6199 49.3779 10.6199H45.4639L47.5769 22.0775C48.5526 27.3686 52.6968 31.5085 57.9933 32.4833L70.8845 34.8557Z" fill="currentColor"/>
|
|
||||||
<path d="M40.1206 11.766C40.1379 11.6124 40.3614 11.6124 40.3787 11.766L41.3999 20.8379C42.4408 30.0839 49.7344 37.3875 58.9875 38.4493L69.66 39.6741C69.8134 39.6917 69.8134 39.9142 69.66 39.9318L58.9875 41.1565C49.7345 42.2183 42.4408 49.5219 41.3999 58.768L40.3787 67.8398C40.3614 67.9935 40.1379 67.9935 40.1206 67.8398L39.1009 58.7822C38.0593 49.5296 30.7564 42.2232 21.4959 41.1688L10.6321 39.9318C10.4786 39.9143 10.4786 39.6915 10.6321 39.674L21.4958 38.437C30.7564 37.3826 38.0593 30.0762 39.1009 20.8236L40.1206 11.766Z" fill="currentColor"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.6 KiB |
|
|
@ -1,3 +0,0 @@
|
||||||
<svg width="100%" height="100%" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M21 21H4.6c-.56 0-.84 0-1.054-.109a1 1 0 0 1-.437-.437C3 20.2401 3 19.9601 3 19.4V3m17 5-3.9189 4.1827c-.1485.1585-.2227.2377-.3123.2787a.5.5 0 0 1-.2528.0433c-.0981-.0089-.1945-.0589-.3873-.159l-3.2574-1.6914c-.1928-.1001-.2892-.1501-.3873-.159a.5.5 0 0 0-.2528.0433c-.0896.041-.1638.1202-.3123.2787L7 15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 512 B |
|
|
@ -1,3 +0,0 @@
|
||||||
<svg width="100%" height="100%" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M16 17L21 12M21 12L16 7M21 12H9M9 21H7.8C6.12 21 5.28 21 4.64 20.67C4.07 20.39 3.61 19.93 3.33 19.36C3 18.72 3 17.88 3 16.2V7.8C3 6.12 3 5.28 3.33 4.64C3.61 4.07 4.07 3.61 4.64 3.33C5.28 3 6.12 3 7.8 3H9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 410 B |
|
|
@ -1,3 +0,0 @@
|
||||||
<svg width="100%" height="100%" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M19.2 12.63A7.2 7.2 0 1 1 11.37 4.8A5.6 5.6 0 0 0 19.2 12.63Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 270 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 5.9 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<svg width="156" height="32" viewBox="0 0.41 156 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="156" height="32" viewBox="0 0 156 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M10.9398 25.6841C9.60192 25.6841 8.3644 25.447 7.22718 24.9728C6.10671 24.4986 5.12003 23.8381 4.26712 22.9913C3.41424 22.1446 2.75365 21.1623 2.28539 20.0446C1.81714 18.9099 1.58301 17.6905 1.58301 16.3865C1.58301 15.0655 1.81714 13.8461 2.28539 12.7284C2.75365 11.6107 3.40587 10.6368 4.24206 9.80701C5.07821 8.96023 6.05654 8.3082 7.17703 7.85093C8.31422 7.37675 9.55174 7.13965 10.8896 7.13965C12.2108 7.13965 13.4316 7.37675 14.5521 7.85093C15.6893 8.3082 16.676 8.96023 17.5122 9.80701C18.365 10.6368 19.0256 11.6191 19.4939 12.7538C19.9621 13.8715 20.1963 15.0909 20.1963 16.4119C20.1963 17.7159 19.9621 18.9353 19.4939 20.07C19.0256 21.1877 18.3734 22.17 17.5372 23.0168C16.7011 23.8466 15.7144 24.4986 14.5772 24.9728C13.4567 25.447 12.2443 25.6841 10.9398 25.6841ZM10.8896 22.0514C11.9432 22.0514 12.863 21.8143 13.649 21.3401C14.4517 20.8659 15.0705 20.2054 15.5053 19.3587C15.9401 18.495 16.1576 17.5042 16.1576 16.3865C16.1576 15.5397 16.0321 14.7776 15.7813 14.1002C15.5304 13.4058 15.1709 12.8131 14.7026 12.3219C14.2344 11.8139 13.6741 11.4328 13.0219 11.1788C12.3864 10.9078 11.6757 10.7723 10.8896 10.7723C9.83606 10.7723 8.90791 11.0094 8.10518 11.4836C7.31917 11.9409 6.70876 12.5929 6.27395 13.4397C5.83913 14.2695 5.62175 15.2518 5.62175 16.3865C5.62175 17.2332 5.74716 18.0038 5.99802 18.6982C6.24886 19.3925 6.60007 19.9937 7.05159 20.5018C7.51986 20.9929 8.08009 21.374 8.73232 21.645C9.38451 21.9159 10.1036 22.0514 10.8896 22.0514ZM24.2015 25.3793V7.4445H26.9609L28.1399 11.0264V25.3793H24.2015ZM36.4181 25.3793L25.9825 11.8393L26.9609 7.4445L37.3963 20.9845L36.4181 25.3793ZM36.4181 25.3793L35.3644 21.7974V7.4445H39.3029V25.3793H36.4181ZM44.2267 25.3793V7.4445H48.1649V25.3793H44.2267ZM47.2368 25.3793V21.9244H56.9448V25.3793H47.2368ZM47.2368 17.8853V14.5574H56.0669V17.8853H47.2368ZM47.2368 10.8739V7.4445H56.8192V10.8739H47.2368Z" fill="currentColor" fill-opacity="0.85"/>
|
<path d="M10.9398 25.6841C9.60192 25.6841 8.3644 25.447 7.22718 24.9728C6.10671 24.4986 5.12003 23.8381 4.26712 22.9913C3.41424 22.1446 2.75365 21.1623 2.28539 20.0446C1.81714 18.9099 1.58301 17.6905 1.58301 16.3865C1.58301 15.0655 1.81714 13.8461 2.28539 12.7284C2.75365 11.6107 3.40587 10.6368 4.24206 9.80701C5.07821 8.96023 6.05654 8.3082 7.17703 7.85093C8.31422 7.37675 9.55174 7.13965 10.8896 7.13965C12.2108 7.13965 13.4316 7.37675 14.5521 7.85093C15.6893 8.3082 16.676 8.96023 17.5122 9.80701C18.365 10.6368 19.0256 11.6191 19.4939 12.7538C19.9621 13.8715 20.1963 15.0909 20.1963 16.4119C20.1963 17.7159 19.9621 18.9353 19.4939 20.07C19.0256 21.1877 18.3734 22.17 17.5372 23.0168C16.7011 23.8466 15.7144 24.4986 14.5772 24.9728C13.4567 25.447 12.2443 25.6841 10.9398 25.6841ZM10.8896 22.0514C11.9432 22.0514 12.863 21.8143 13.649 21.3401C14.4517 20.8659 15.0705 20.2054 15.5053 19.3587C15.9401 18.495 16.1576 17.5042 16.1576 16.3865C16.1576 15.5397 16.0321 14.7776 15.7813 14.1002C15.5304 13.4058 15.1709 12.8131 14.7026 12.3219C14.2344 11.8139 13.6741 11.4328 13.0219 11.1788C12.3864 10.9078 11.6757 10.7723 10.8896 10.7723C9.83606 10.7723 8.90791 11.0094 8.10518 11.4836C7.31917 11.9409 6.70876 12.5929 6.27395 13.4397C5.83913 14.2695 5.62175 15.2518 5.62175 16.3865C5.62175 17.2332 5.74716 18.0038 5.99802 18.6982C6.24886 19.3925 6.60007 19.9937 7.05159 20.5018C7.51986 20.9929 8.08009 21.374 8.73232 21.645C9.38451 21.9159 10.1036 22.0514 10.8896 22.0514ZM24.2015 25.3793V7.4445H26.9609L28.1399 11.0264V25.3793H24.2015ZM36.4181 25.3793L25.9825 11.8393L26.9609 7.4445L37.3963 20.9845L36.4181 25.3793ZM36.4181 25.3793L35.3644 21.7974V7.4445H39.3029V25.3793H36.4181ZM44.2267 25.3793V7.4445H48.1649V25.3793H44.2267ZM47.2368 25.3793V21.9244H56.9448V25.3793H47.2368ZM47.2368 17.8853V14.5574H56.0669V17.8853H47.2368ZM47.2368 10.8739V7.4445H56.8192V10.8739H47.2368Z" fill="currentColor" fill-opacity="0.85"/>
|
||||||
<path d="M69.2499 25.6588C67.9287 25.6588 66.6996 25.4302 65.5623 24.9729C64.4419 24.4987 63.4552 23.8382 62.6023 22.9915C61.7663 22.1447 61.1139 21.1624 60.6456 20.0447C60.1774 18.91 59.9434 17.6906 59.9434 16.3866C59.9434 15.0825 60.1774 13.8716 60.6456 12.7539C61.1139 11.6192 61.7663 10.6369 62.6023 9.80712C63.4386 8.97727 64.4168 8.33371 65.5373 7.87644C66.6746 7.40227 67.9037 7.16516 69.2248 7.16516C70.6296 7.16516 71.8756 7.3938 72.9625 7.85104C74.0663 8.30831 75.0279 8.93493 75.8473 9.7309L73.2134 12.4236C72.7452 11.9156 72.1766 11.5176 71.5076 11.2297C70.8555 10.9418 70.0946 10.7979 69.2248 10.7979C68.4556 10.7979 67.7448 10.9333 67.0927 11.2043C66.4572 11.4583 65.9053 11.8309 65.437 12.322C64.9854 12.8132 64.626 13.4059 64.3583 14.1003C64.1074 14.7946 63.9821 15.5567 63.9821 16.3866C63.9821 17.2334 64.1074 18.0039 64.3583 18.6983C64.626 19.3926 64.9854 19.9854 65.437 20.4765C65.9053 20.9676 66.4572 21.3487 67.0927 21.6197C67.7448 21.8907 68.4556 22.0261 69.2248 22.0261C70.1279 22.0261 70.914 21.8822 71.5828 21.5943C72.2518 21.3064 72.8288 20.9084 73.3137 20.4003L75.9729 23.0931C75.12 23.8891 74.1415 24.5156 73.0377 24.9729C71.9508 25.4302 70.6882 25.6588 69.2499 25.6588ZM79.6466 25.3794V7.44461H83.5851V25.3794H79.6466ZM82.6821 25.3794V21.8737H91.4367V25.3794H82.6821ZM95.2131 25.3794V7.44461H99.1515V25.3794H95.2131ZM104.067 25.3794V7.44461H108.006V25.3794H104.067ZM107.078 25.3794V21.9245H116.786V25.3794H107.078ZM107.078 17.8854V14.5575H115.908V17.8854H107.078ZM107.078 10.874V7.44461H116.66V10.874H107.078ZM121.006 25.3794V7.44461H123.765L124.944 11.0265V25.3794H121.006ZM133.222 25.3794L122.787 11.8394L123.765 7.44461L134.201 20.9846L133.222 25.3794ZM133.222 25.3794L132.169 21.7975V7.44461H136.107V25.3794H133.222ZM145.371 25.3794V7.69862H149.309V25.3794H145.371ZM139.827 10.9249V7.44461H154.853V10.9249H139.827Z" fill="currentColor"/>
|
<path d="M69.2499 25.6588C67.9287 25.6588 66.6996 25.4302 65.5623 24.9729C64.4419 24.4987 63.4552 23.8382 62.6023 22.9915C61.7663 22.1447 61.1139 21.1624 60.6456 20.0447C60.1774 18.91 59.9434 17.6906 59.9434 16.3866C59.9434 15.0825 60.1774 13.8716 60.6456 12.7539C61.1139 11.6192 61.7663 10.6369 62.6023 9.80712C63.4386 8.97727 64.4168 8.33371 65.5373 7.87644C66.6746 7.40227 67.9037 7.16516 69.2248 7.16516C70.6296 7.16516 71.8756 7.3938 72.9625 7.85104C74.0663 8.30831 75.0279 8.93493 75.8473 9.7309L73.2134 12.4236C72.7452 11.9156 72.1766 11.5176 71.5076 11.2297C70.8555 10.9418 70.0946 10.7979 69.2248 10.7979C68.4556 10.7979 67.7448 10.9333 67.0927 11.2043C66.4572 11.4583 65.9053 11.8309 65.437 12.322C64.9854 12.8132 64.626 13.4059 64.3583 14.1003C64.1074 14.7946 63.9821 15.5567 63.9821 16.3866C63.9821 17.2334 64.1074 18.0039 64.3583 18.6983C64.626 19.3926 64.9854 19.9854 65.437 20.4765C65.9053 20.9676 66.4572 21.3487 67.0927 21.6197C67.7448 21.8907 68.4556 22.0261 69.2248 22.0261C70.1279 22.0261 70.914 21.8822 71.5828 21.5943C72.2518 21.3064 72.8288 20.9084 73.3137 20.4003L75.9729 23.0931C75.12 23.8891 74.1415 24.5156 73.0377 24.9729C71.9508 25.4302 70.6882 25.6588 69.2499 25.6588ZM79.6466 25.3794V7.44461H83.5851V25.3794H79.6466ZM82.6821 25.3794V21.8737H91.4367V25.3794H82.6821ZM95.2131 25.3794V7.44461H99.1515V25.3794H95.2131ZM104.067 25.3794V7.44461H108.006V25.3794H104.067ZM107.078 25.3794V21.9245H116.786V25.3794H107.078ZM107.078 17.8854V14.5575H115.908V17.8854H107.078ZM107.078 10.874V7.44461H116.66V10.874H107.078ZM121.006 25.3794V7.44461H123.765L124.944 11.0265V25.3794H121.006ZM133.222 25.3794L122.787 11.8394L123.765 7.44461L134.201 20.9846L133.222 25.3794ZM133.222 25.3794L132.169 21.7975V7.44461H136.107V25.3794H133.222ZM145.371 25.3794V7.69862H149.309V25.3794H145.371ZM139.827 10.9249V7.44461H154.853V10.9249H139.827Z" fill="currentColor"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 847 B After Width: | Height: | Size: 848 B |
|
|
@ -1,19 +1,9 @@
|
||||||
fn main() {
|
fn main() {
|
||||||
|
// Embeds the app icon into the Windows executable.
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
let mut res = winresource::WindowsResource::new();
|
let mut res = winresource::WindowsResource::new();
|
||||||
res.set_icon("icons/icon.ico");
|
res.set_icon("icons/icon.ico");
|
||||||
|
|
||||||
let version = env!("CARGO_PKG_VERSION");
|
|
||||||
res.set("CompanyName", "Polyfrost Inc.");
|
|
||||||
res.set("ProductName", "OneClient");
|
|
||||||
res.set("FileDescription", "OneClient");
|
|
||||||
res.set("LegalCopyright", "© 2026 Polyfrost Inc.");
|
|
||||||
res.set("OriginalFilename", "oneclient_app.exe");
|
|
||||||
res.set("InternalName", "oneclient_app");
|
|
||||||
res.set("FileVersion", version);
|
|
||||||
res.set("ProductVersion", version);
|
|
||||||
|
|
||||||
res.compile()
|
res.compile()
|
||||||
.expect("failed to embed Windows icon resource");
|
.expect("failed to embed Windows icon resource");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,18 @@
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleIconName</key>
|
<!-- registers our uri handler on macos -->
|
||||||
|
<key>CFBundleURLTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
<string>OneClient</string>
|
<string>OneClient</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>oneclient</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
<!-- allows us to request camera permissions (for things like voice chat) -->
|
<!-- allows us to request camera permissions (for things like voice chat) -->
|
||||||
<key>NSCameraUsageDescription</key>
|
<key>NSCameraUsageDescription</key>
|
||||||
<string>A Minecraft mod is requesting access to your camera.</string>
|
<string>A Minecraft mod is requesting access to your camera.</string>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 20 KiB |
|
|
@ -1,11 +0,0 @@
|
||||||
[Desktop Entry]
|
|
||||||
Name=OneClient
|
|
||||||
Comment=Next-generation open source Minecraft launcher
|
|
||||||
Exec=/usr/bin/oneclient_app
|
|
||||||
Icon=oneclient_app
|
|
||||||
Type=Application
|
|
||||||
Terminal=false
|
|
||||||
StartupNotify=true
|
|
||||||
Categories=Game;ActionGame;AdventureGame;Simulation;
|
|
||||||
Keywords=mc;minecraft;game;
|
|
||||||
StartupWMClass=oneclient_app
|
|
||||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 840 B After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2.7 KiB |
|
|
@ -1,7 +0,0 @@
|
||||||
<svg width="197" height="187" viewBox="0 0 197 187" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M82.1674 0H68.9678C53.8201 0 39.8231 8.07288 32.2493 21.1777L2.6905 72.3224C1.67829 74.0738 0.801358 75.8795 0.0597031 77.725L42.054 69.9967C59.011 66.876 72.2789 53.6217 75.4028 36.6822L82.1674 0Z" fill="#0048C5"/>
|
|
||||||
<path d="M0 109.126C0.754162 111.023 1.651 112.879 2.6905 114.678L32.2493 165.822C39.8231 178.927 53.8201 187 68.9678 187H82.1929L75.4028 150.179C72.2789 133.24 59.011 119.986 42.0541 116.865L0 109.126Z" fill="#0048C5"/>
|
|
||||||
<path d="M115.529 187H128.085C143.233 187 157.23 178.927 164.804 165.822L194.363 114.678C195.378 112.921 196.257 111.11 197 109.259L155.668 116.865C138.711 119.986 125.443 133.24 122.319 150.179L115.529 187Z" fill="#0048C5"/>
|
|
||||||
<path d="M196.94 77.592C196.209 75.7927 195.35 74.0317 194.363 72.3224L164.804 21.1777C157.23 8.07287 143.233 0 128.085 0H115.555L122.319 36.6822C125.443 53.6217 138.711 66.876 155.668 69.9967L196.94 77.592Z" fill="#0048C5"/>
|
|
||||||
<path d="M98.4478 3.6693C98.5032 3.17751 99.2186 3.17751 99.2739 3.6693L102.544 32.7134C105.876 62.3151 129.227 85.6977 158.851 89.0973L193.019 93.0183C193.51 93.0746 193.51 93.787 193.019 93.8433L158.851 97.7644C129.227 101.164 105.876 124.547 102.544 154.148L99.2739 183.192C99.2186 183.684 98.5032 183.684 98.4478 183.192L95.1833 154.194C91.8485 124.571 68.4679 101.18 38.8198 97.8037L4.03907 93.8434C3.5476 93.7874 3.5476 93.0742 4.03907 93.0182L38.8198 89.0579C68.4679 85.6821 91.8485 62.2903 95.1833 32.6676L98.4478 3.6693Z" fill="#0A5BE8"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB |
|
|
@ -1,38 +0,0 @@
|
||||||
{
|
|
||||||
"fill": "automatic",
|
|
||||||
"groups": [
|
|
||||||
{
|
|
||||||
"layers": [
|
|
||||||
{
|
|
||||||
"blend-mode": "normal",
|
|
||||||
"fill": "automatic",
|
|
||||||
"glass": true,
|
|
||||||
"hidden": false,
|
|
||||||
"image-name": "oneclient-mark.svg",
|
|
||||||
"name": "oneclient-mark",
|
|
||||||
"position": {
|
|
||||||
"scale": 3.6,
|
|
||||||
"translation-in-points": [
|
|
||||||
0,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"shadow": {
|
|
||||||
"kind": "neutral",
|
|
||||||
"opacity": 0.5
|
|
||||||
},
|
|
||||||
"translucency": {
|
|
||||||
"enabled": true,
|
|
||||||
"value": 0.5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"supported-platforms": {
|
|
||||||
"circles": [
|
|
||||||
"watchOS"
|
|
||||||
],
|
|
||||||
"squares": "shared"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
|
@ -7,7 +7,7 @@ impl AppAssets {
|
||||||
AppAssets::get(path).map(|file| match file.data {
|
AppAssets::get(path).map(|file| match file.data {
|
||||||
std::borrow::Cow::Borrowed(slice) => bytes::Bytes::copy_from_slice(slice),
|
std::borrow::Cow::Borrowed(slice) => bytes::Bytes::copy_from_slice(slice),
|
||||||
std::borrow::Cow::Owned(vec) => {
|
std::borrow::Cow::Owned(vec) => {
|
||||||
tracing::debug!("Cloning asset data for '{}'", path);
|
tracing::warn!("Cloning asset data for '{}'", path);
|
||||||
bytes::Bytes::from(vec)
|
bytes::Bytes::from(vec)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
|
||||||
pub struct Cli {
|
|
||||||
pub launch: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Cli {
|
|
||||||
fn from_args(args: impl IntoIterator<Item = String>) -> Self {
|
|
||||||
let mut cli = Self::default();
|
|
||||||
let mut args = args.into_iter();
|
|
||||||
|
|
||||||
while let Some(arg) = args.next() {
|
|
||||||
if let Some(value) = arg.strip_prefix("--launch=") {
|
|
||||||
cli.launch = non_empty(value.to_string());
|
|
||||||
} else if arg == "--launch" {
|
|
||||||
cli.launch = args.next().and_then(non_empty);
|
|
||||||
} else if let Some(folder) = crate::protocol::parse_launch_url(&arg) {
|
|
||||||
cli.launch = Some(folder);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cli
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn non_empty(value: String) -> Option<String> {
|
|
||||||
let trimmed = value.trim();
|
|
||||||
(!trimmed.is_empty()).then(|| trimmed.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn parse() -> Cli {
|
|
||||||
Cli::from_args(std::env::args().skip(1))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
fn parse(args: &[&str]) -> Cli {
|
|
||||||
Cli::from_args(args.iter().map(|s| (*s).to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn no_arguments_is_an_ordinary_start() {
|
|
||||||
assert_eq!(parse(&[]).launch, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn both_spellings_carry_the_folder() {
|
|
||||||
assert_eq!(
|
|
||||||
parse(&["--launch", "fabric-1-20"]).launch.as_deref(),
|
|
||||||
Some("fabric-1-20")
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
parse(&["--launch=fabric-1-20"]).launch.as_deref(),
|
|
||||||
Some("fabric-1-20")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_blank_folder_is_no_request_at_all() {
|
|
||||||
assert_eq!(parse(&["--launch", " "]).launch, None);
|
|
||||||
assert_eq!(parse(&["--launch="]).launch, None);
|
|
||||||
assert_eq!(parse(&["--launch"]).launch, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn spaces_survive_the_round_trip() {
|
|
||||||
assert_eq!(
|
|
||||||
parse(&["--launch", "My Pack (1.8.9)"]).launch.as_deref(),
|
|
||||||
Some("My Pack (1.8.9)"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn unknown_flags_are_ignored_not_fatal() {
|
|
||||||
assert_eq!(
|
|
||||||
parse(&["--verbose", "--launch", "x", "leftover"])
|
|
||||||
.launch
|
|
||||||
.as_deref(),
|
|
||||||
Some("x")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_launch_url_is_accepted_as_a_bare_argument() {
|
|
||||||
let url = crate::protocol::launch_url("26.1.2 Fabric");
|
|
||||||
assert_eq!(parse(&[&url]).launch.as_deref(), Some("26.1.2 Fabric"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_url_that_is_not_ours_is_not_a_launch() {
|
|
||||||
assert_eq!(parse(&["https://polyfrost.org"]).launch, None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -6,6 +6,7 @@ use freya::{
|
||||||
use oneclient_auth::MinecraftAccount;
|
use oneclient_auth::MinecraftAccount;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
ui::{divider},
|
||||||
Route,
|
Route,
|
||||||
components::{Avatar, Button, Icon, IconType, OverlayPopup},
|
components::{Avatar, Button, Icon, IconType, OverlayPopup},
|
||||||
hooks::{
|
hooks::{
|
||||||
|
|
@ -13,7 +14,6 @@ use crate::{
|
||||||
use_accounts, use_current_account, use_dispatch, use_set_default_account,
|
use_accounts, use_current_account, use_dispatch, use_set_default_account,
|
||||||
},
|
},
|
||||||
theme::colors,
|
theme::colors,
|
||||||
ui::divider,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
|
|
@ -54,6 +54,7 @@ impl Component for AccountPanel {
|
||||||
});
|
});
|
||||||
let progress = intro.read().value();
|
let progress = intro.read().value();
|
||||||
|
|
||||||
|
// active account first, then the rest
|
||||||
let mut ordered: Vec<MinecraftAccount> = Vec::new();
|
let mut ordered: Vec<MinecraftAccount> = Vec::new();
|
||||||
if let Some(account) = default_account.clone() {
|
if let Some(account) = default_account.clone() {
|
||||||
ordered.push(account);
|
ordered.push(account);
|
||||||
|
|
@ -91,7 +92,7 @@ impl Component for AccountPanel {
|
||||||
.opacity(progress)
|
.opacity(progress)
|
||||||
.margin(Gaps::new((1.0 - progress) * -8.0, 0., 0., 0.))
|
.margin(Gaps::new((1.0 - progress) * -8.0, 0., 0., 0.))
|
||||||
.background(colors::page_elevated().with_a(220))
|
.background(colors::page_elevated().with_a(220))
|
||||||
.backdrop_blur(12.)
|
.blur(12.)
|
||||||
.corner_radius(CornerRadius::new_all(12.))
|
.corner_radius(CornerRadius::new_all(12.))
|
||||||
.border(
|
.border(
|
||||||
Border::new()
|
Border::new()
|
||||||
|
|
@ -106,20 +107,12 @@ impl Component for AccountPanel {
|
||||||
0.,
|
0.,
|
||||||
Color::from_argb(120, 0, 0, 0),
|
Color::from_argb(120, 0, 0, 0),
|
||||||
)))
|
)))
|
||||||
.child(
|
|
||||||
rect()
|
|
||||||
.horizontal()
|
|
||||||
.width(Size::fill())
|
|
||||||
.cross_align(Alignment::Center)
|
|
||||||
.spacing(6.)
|
|
||||||
.child(BackToControlCenter)
|
|
||||||
.child(
|
.child(
|
||||||
label()
|
label()
|
||||||
.text("Accounts")
|
.text("Accounts")
|
||||||
.font_size(18.)
|
.font_size(18.)
|
||||||
.font_weight(FontWeight::MEDIUM)
|
.font_weight(FontWeight::MEDIUM)
|
||||||
.a11y_role(AccessibilityRole::TitleBar),
|
.a11y_role(AccessibilityRole::TitleBar),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.child(divider())
|
.child(divider())
|
||||||
.child(rows)
|
.child(rows)
|
||||||
|
|
@ -128,42 +121,6 @@ impl Component for AccountPanel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
|
||||||
struct BackToControlCenter;
|
|
||||||
|
|
||||||
impl Component for BackToControlCenter {
|
|
||||||
fn render(&self) -> impl IntoElement {
|
|
||||||
let dispatch = use_dispatch();
|
|
||||||
let mut hovered = use_state(|| false);
|
|
||||||
|
|
||||||
let back = move |_| {
|
|
||||||
dispatch.close_account_switcher();
|
|
||||||
dispatch.open_control_center();
|
|
||||||
};
|
|
||||||
|
|
||||||
rect()
|
|
||||||
.width(Size::px(28.))
|
|
||||||
.height(Size::px(28.))
|
|
||||||
.center()
|
|
||||||
.corner_radius(CornerRadius::new_all(8.))
|
|
||||||
.background(Color::RED.with_a(0))
|
|
||||||
.maybe(*hovered.read(), |el| {
|
|
||||||
el.background(colors::ghost_overlay_hover())
|
|
||||||
})
|
|
||||||
.cursor(CursorIcon::Pointer)
|
|
||||||
.a11y_role(AccessibilityRole::Button)
|
|
||||||
.a11y_alt("Back to control center")
|
|
||||||
.on_pointer_enter(move |_| hovered.set(true))
|
|
||||||
.on_pointer_leave(move |_| hovered.set(false))
|
|
||||||
.on_press(back)
|
|
||||||
.child(
|
|
||||||
Icon::new(IconType::ArrowLeft)
|
|
||||||
.size(18.)
|
|
||||||
.color(colors::fg_secondary()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct AccountRow {
|
struct AccountRow {
|
||||||
account: MinecraftAccount,
|
account: MinecraftAccount,
|
||||||
active: bool,
|
active: bool,
|
||||||
|
|
@ -223,9 +180,16 @@ impl Component for AccountRow {
|
||||||
.maybe(*hovered.read() && !active, |el| {
|
.maybe(*hovered.read() && !active, |el| {
|
||||||
el.background(colors::ghost_overlay_hover())
|
el.background(colors::ghost_overlay_hover())
|
||||||
})
|
})
|
||||||
.cursor((!active).then_some(CursorIcon::Pointer))
|
.on_pointer_enter(move |_| {
|
||||||
.on_pointer_enter(move |_| hovered.set(true))
|
hovered.set(true);
|
||||||
.on_pointer_leave(move |_| hovered.set(false))
|
if !active {
|
||||||
|
Cursor::set(CursorIcon::Pointer);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on_pointer_leave(move |_| {
|
||||||
|
hovered.set(false);
|
||||||
|
Cursor::set(CursorIcon::default());
|
||||||
|
})
|
||||||
.on_press(switch)
|
.on_press(switch)
|
||||||
.child(
|
.child(
|
||||||
Avatar::new(id.to_string())
|
Avatar::new(id.to_string())
|
||||||
|
|
@ -271,7 +235,7 @@ impl Component for Footer {
|
||||||
let dispatch = use_dispatch();
|
let dispatch = use_dispatch();
|
||||||
let open_accounts = move |_| {
|
let open_accounts = move |_| {
|
||||||
dispatch.close_account_switcher();
|
dispatch.close_account_switcher();
|
||||||
let _ = RouterContext::get().push(Route::SettingsAccounts {});
|
let _ = RouterContext::get().push(Route::Accounts {});
|
||||||
};
|
};
|
||||||
|
|
||||||
Button::new()
|
Button::new()
|
||||||
|
|
|
||||||
|
|
@ -4,16 +4,12 @@ use freya::router::RouterContext;
|
||||||
|
|
||||||
use crate::components::{Button, Icon, IconType};
|
use crate::components::{Button, Icon, IconType};
|
||||||
use oneclient_common::parse_mc_version;
|
use oneclient_common::parse_mc_version;
|
||||||
use oneclient_core::clusters::ClusterKind;
|
|
||||||
|
|
||||||
use crate::hooks::{
|
use crate::hooks::{settled_or_loading, use_active_cluster_id, use_clusters, use_dispatch, use_game_snapshot, use_launcher, use_version_metadata};
|
||||||
settled_or_loading, use_active_cluster_id, use_clusters, use_dispatch, use_game_snapshot,
|
|
||||||
use_launcher, use_version_metadata,
|
|
||||||
};
|
|
||||||
use crate::routes::Route;
|
use crate::routes::Route;
|
||||||
use crate::theme::colors;
|
use crate::theme::colors;
|
||||||
use crate::utils::sort_clusters_for_home;
|
use crate::utils::sort_clusters_for_home;
|
||||||
use crate::view::app::{launch_button_state, launch_syncing, shared_dir_taken};
|
use crate::view::app::launch_button_state;
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
pub struct ActiveClusterPanel;
|
pub struct ActiveClusterPanel;
|
||||||
|
|
@ -25,6 +21,7 @@ impl Component for ActiveClusterPanel {
|
||||||
let dispatch = use_dispatch();
|
let dispatch = use_dispatch();
|
||||||
let game = use_game_snapshot();
|
let game = use_game_snapshot();
|
||||||
let launcher = use_launcher();
|
let launcher = use_launcher();
|
||||||
|
let syncing = launcher.fetching || launcher.syncing_bundles;
|
||||||
|
|
||||||
let clusters = settled_or_loading(&clusters_query).unwrap_or_default();
|
let clusters = settled_or_loading(&clusters_query).unwrap_or_default();
|
||||||
|
|
||||||
|
|
@ -75,24 +72,11 @@ impl Component for ActiveClusterPanel {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
let version = format!("{} {}", cluster.mc_version, cluster.mc_loader);
|
let title = format!("{} {}", cluster.mc_version, cluster.mc_loader);
|
||||||
let (title, subtitle) = if cluster.user_created && cluster.kind != ClusterKind::OneClient {
|
let subtitle = metadata
|
||||||
(cluster.name.clone(), version)
|
.map(|m| m.name)
|
||||||
} else {
|
.unwrap_or_else(|| cluster.name.clone());
|
||||||
let subtitle = match metadata {
|
|
||||||
Some(m) => format!("OneClient · {}", m.name),
|
|
||||||
None => "OneClient".to_string(),
|
|
||||||
};
|
|
||||||
let title = if cluster.user_created {
|
|
||||||
cluster.name.clone()
|
|
||||||
} else {
|
|
||||||
version
|
|
||||||
};
|
|
||||||
(title, subtitle)
|
|
||||||
};
|
|
||||||
let cluster_id = cluster.id;
|
let cluster_id = cluster.id;
|
||||||
let syncing = launch_syncing(&launcher, cluster.uses_bundles());
|
|
||||||
let shared_dir_taken = shared_dir_taken(&game, &sorted, cluster_id);
|
|
||||||
|
|
||||||
rect()
|
rect()
|
||||||
.vertical()
|
.vertical()
|
||||||
|
|
@ -104,9 +88,6 @@ impl Component for ActiveClusterPanel {
|
||||||
.child(
|
.child(
|
||||||
label()
|
label()
|
||||||
.text(title)
|
.text(title)
|
||||||
.width(Size::fill())
|
|
||||||
.max_lines(1)
|
|
||||||
.text_overflow(TextOverflow::Ellipsis)
|
|
||||||
.font_size(56.)
|
.font_size(56.)
|
||||||
.line_height(1.1)
|
.line_height(1.1)
|
||||||
.font_weight(FontWeight::BOLD)
|
.font_weight(FontWeight::BOLD)
|
||||||
|
|
@ -128,7 +109,7 @@ impl Component for ActiveClusterPanel {
|
||||||
.child(launch_button(
|
.child(launch_button(
|
||||||
cluster_id,
|
cluster_id,
|
||||||
dispatch,
|
dispatch,
|
||||||
launch_button_state(&game, cluster_id, syncing, shared_dir_taken),
|
launch_button_state(&game, cluster_id, syncing),
|
||||||
))
|
))
|
||||||
.child(cluster_settings_button(cluster_id)),
|
.child(cluster_settings_button(cluster_id)),
|
||||||
)
|
)
|
||||||
|
|
@ -160,7 +141,6 @@ fn cluster_settings_button(cluster_id: i64) -> impl IntoElement {
|
||||||
Button::new()
|
Button::new()
|
||||||
.ghost()
|
.ghost()
|
||||||
.icon()
|
.icon()
|
||||||
.tooltip("Cluster settings")
|
|
||||||
.on_press(move |_| {
|
.on_press(move |_| {
|
||||||
let _ = RouterContext::get().push(Route::ClusterOverview { cluster_id });
|
let _ = RouterContext::get().push(Route::ClusterOverview { cluster_id });
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
use freya::elements::image::{AspectRatio, ImageCover, ImageHandle, image};
|
|
||||||
use freya::prelude::*;
|
|
||||||
|
|
||||||
use super::local_image::decode;
|
|
||||||
use crate::AppAssets;
|
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
|
||||||
pub struct AssetImage {
|
|
||||||
path: &'static str,
|
|
||||||
size: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AssetImage {
|
|
||||||
pub fn new(path: &'static str, size: f32) -> Self {
|
|
||||||
Self { path, size }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component for AssetImage {
|
|
||||||
fn render(&self) -> impl IntoElement {
|
|
||||||
let path = self.path;
|
|
||||||
let size = self.size;
|
|
||||||
|
|
||||||
let handle: Option<ImageHandle> =
|
|
||||||
use_hook(move || AppAssets::get_bytes(path).as_ref().and_then(decode));
|
|
||||||
|
|
||||||
rect()
|
|
||||||
.key(path)
|
|
||||||
.width(Size::px(size))
|
|
||||||
.height(Size::px(size))
|
|
||||||
.center()
|
|
||||||
.maybe_child(handle.map(|handle| {
|
|
||||||
image(handle)
|
|
||||||
.width(Size::fill())
|
|
||||||
.height(Size::fill())
|
|
||||||
.aspect_ratio(AspectRatio::Min)
|
|
||||||
.image_cover(ImageCover::Center)
|
|
||||||
.into_element()
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,3 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::sync::{Mutex, OnceLock};
|
|
||||||
|
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use freya::elements::image::{ImageHandle, image};
|
use freya::elements::image::{ImageHandle, image};
|
||||||
use freya::engine::prelude::{Paint, SkData, SkImage, raster_n32_premul};
|
use freya::engine::prelude::{Paint, SkData, SkImage, raster_n32_premul};
|
||||||
|
|
@ -13,8 +10,6 @@ const FACE: f32 = 8.;
|
||||||
const HEAD_FACE: (f32, f32) = (8., 8.);
|
const HEAD_FACE: (f32, f32) = (8., 8.);
|
||||||
const HEAD_OVERLAY: (f32, f32) = (40., 8.);
|
const HEAD_OVERLAY: (f32, f32) = (40., 8.);
|
||||||
|
|
||||||
static AVATAR_HEAD_CACHE: OnceLock<Mutex<HashMap<usize, ImageHandle>>> = OnceLock::new();
|
|
||||||
|
|
||||||
#[derive(PartialEq, Clone)]
|
#[derive(PartialEq, Clone)]
|
||||||
pub struct Avatar {
|
pub struct Avatar {
|
||||||
uuid: String,
|
uuid: String,
|
||||||
|
|
@ -39,13 +34,22 @@ impl LayoutExt for Avatar {
|
||||||
impl ContainerSizeExt for Avatar {}
|
impl ContainerSizeExt for Avatar {}
|
||||||
|
|
||||||
impl Component for Avatar {
|
impl Component for Avatar {
|
||||||
fn render_key(&self) -> DiffKey {
|
|
||||||
DiffKey::from(&self.uuid)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render(&self) -> impl IntoElement {
|
fn render(&self) -> impl IntoElement {
|
||||||
let (skin_bytes, _is_slim) = use_player_skin(self.uuid.clone());
|
let (skin_bytes, _is_slim) = use_player_skin(self.uuid.clone());
|
||||||
let head = get_or_compose_head(&skin_bytes);
|
|
||||||
|
let mut cache = use_state(|| None::<(usize, ImageHandle)>);
|
||||||
|
let src_ptr = skin_bytes.as_ptr() as usize;
|
||||||
|
let cached = cache.read().clone();
|
||||||
|
let head = match cached {
|
||||||
|
Some((ptr, holder)) if ptr == src_ptr => Some(holder),
|
||||||
|
_ => {
|
||||||
|
let holder = compose_head(&skin_bytes);
|
||||||
|
if let Some(holder) = &holder {
|
||||||
|
cache.set(Some((src_ptr, holder.clone())));
|
||||||
|
}
|
||||||
|
holder
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
rect()
|
rect()
|
||||||
.width(Size::px(AVATAR_SIZE))
|
.width(Size::px(AVATAR_SIZE))
|
||||||
|
|
@ -63,23 +67,6 @@ impl Component for Avatar {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_or_compose_head(skin_bytes: &Bytes) -> Option<ImageHandle> {
|
|
||||||
if skin_bytes.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let src_ptr = skin_bytes.as_ptr() as usize;
|
|
||||||
let cache = AVATAR_HEAD_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
|
|
||||||
if let Some(handle) = cache.lock().ok().and_then(|g| g.get(&src_ptr).cloned()) {
|
|
||||||
return Some(handle);
|
|
||||||
}
|
|
||||||
|
|
||||||
let composed = compose_head(skin_bytes)?;
|
|
||||||
if let Ok(mut guard) = cache.lock() {
|
|
||||||
guard.insert(src_ptr, composed.clone());
|
|
||||||
}
|
|
||||||
Some(composed)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compose_head(skin_bytes: &Bytes) -> Option<ImageHandle> {
|
fn compose_head(skin_bytes: &Bytes) -> Option<ImageHandle> {
|
||||||
let data = unsafe { SkData::new_bytes(skin_bytes) };
|
let data = unsafe { SkData::new_bytes(skin_bytes) };
|
||||||
let skin = SkImage::from_encoded(data)?;
|
let skin = SkImage::from_encoded(data)?;
|
||||||
|
|
|
||||||
|
|
@ -1,528 +0,0 @@
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
use freya::prelude::*;
|
|
||||||
use freya::router::RouterContext;
|
|
||||||
use notify::{Event, EventKind};
|
|
||||||
use oneclient_content::modpacks::BlockedFile;
|
|
||||||
|
|
||||||
use crate::components::{Button, Icon, IconType, OverlayPopup};
|
|
||||||
use crate::hooks::{use_dispatch, use_folder_watch, use_notifications_snapshot};
|
|
||||||
use crate::notifications::BlockedDownloads;
|
|
||||||
use crate::routes::Route;
|
|
||||||
use crate::theme::colors;
|
|
||||||
use crate::ui::border_all_color;
|
|
||||||
|
|
||||||
const CARD_BG: Color = Color::from_rgb(26, 34, 41);
|
|
||||||
const DIALOG_W: f32 = 560.;
|
|
||||||
const DIALOG_PAD: f32 = 22.;
|
|
||||||
const LIST_MAX_H: f32 = 300.;
|
|
||||||
const ROW_H: f32 = 52.;
|
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
|
||||||
pub struct BlockedDownloadsPopup;
|
|
||||||
|
|
||||||
impl Component for BlockedDownloadsPopup {
|
|
||||||
fn render(&self) -> impl IntoElement {
|
|
||||||
let snapshot = use_notifications_snapshot();
|
|
||||||
let dispatch = use_dispatch();
|
|
||||||
let router = RouterContext::get();
|
|
||||||
|
|
||||||
use_side_effect_with_deps(&snapshot.open_cluster, move |requested| {
|
|
||||||
if requested.is_none() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if let Some(cluster_id) = dispatch.take_open_cluster() {
|
|
||||||
let _ = router.push(Route::ClusterOverview { cluster_id });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
match snapshot.blocked_downloads {
|
|
||||||
Some(blocked) => BlockedDownloadsDialog { blocked }.into_element(),
|
|
||||||
None => rect().into_element(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn downloads_dir() -> Option<PathBuf> {
|
|
||||||
directories::UserDirs::new().and_then(|dirs| dirs.download_dir().map(Path::to_path_buf))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn touches_file(_root: &Path, event: &Event) -> bool {
|
|
||||||
matches!(event.kind, EventKind::Create(_) | EventKind::Modify(_))
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExtraFolder = State<Option<(i64, PathBuf)>>;
|
|
||||||
|
|
||||||
fn bump(mut tick: State<u64>) {
|
|
||||||
let next = *tick.peek() + 1;
|
|
||||||
tick.set(next);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
|
||||||
struct ExtraFolderWatch {
|
|
||||||
folder: PathBuf,
|
|
||||||
tick: State<u64>,
|
|
||||||
key: DiffKey,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl KeyExt for ExtraFolderWatch {
|
|
||||||
fn write_key(&mut self) -> &mut DiffKey {
|
|
||||||
&mut self.key
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component for ExtraFolderWatch {
|
|
||||||
fn render(&self) -> impl IntoElement {
|
|
||||||
let tick = self.tick;
|
|
||||||
use_folder_watch(Some(self.folder.clone()), false, touches_file, move || {
|
|
||||||
bump(tick)
|
|
||||||
});
|
|
||||||
rect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_key(&self) -> DiffKey {
|
|
||||||
self.key.clone().or(self.default_key())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
|
||||||
struct BlockedDownloadsDialog {
|
|
||||||
blocked: BlockedDownloads,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component for BlockedDownloadsDialog {
|
|
||||||
fn render(&self) -> impl IntoElement {
|
|
||||||
let dispatch = use_dispatch();
|
|
||||||
let downloads = use_hook(downloads_dir);
|
|
||||||
let tick = use_state(|| 0u64);
|
|
||||||
let extra: ExtraFolder = use_state(|| None);
|
|
||||||
|
|
||||||
use_folder_watch(downloads.clone(), false, touches_file, move || bump(tick));
|
|
||||||
|
|
||||||
let cluster_id = self.blocked.cluster_id;
|
|
||||||
let chosen = extra
|
|
||||||
.read()
|
|
||||||
.as_ref()
|
|
||||||
.filter(|(owner, _)| *owner == cluster_id)
|
|
||||||
.map(|(_, folder)| folder.clone());
|
|
||||||
|
|
||||||
let deps = (
|
|
||||||
*tick.read(),
|
|
||||||
cluster_id,
|
|
||||||
self.blocked.remaining(),
|
|
||||||
chosen.clone(),
|
|
||||||
);
|
|
||||||
{
|
|
||||||
let dispatch = dispatch.clone();
|
|
||||||
let downloads = downloads.clone();
|
|
||||||
use_side_effect_with_deps(&deps, move |(_, cluster_id, files, chosen)| {
|
|
||||||
if files.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let locations: Vec<PathBuf> =
|
|
||||||
downloads.iter().chain(chosen.iter()).cloned().collect();
|
|
||||||
if !locations.is_empty() {
|
|
||||||
dispatch.scan_blocked_downloads(*cluster_id, locations, files.clone());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let close = dispatch.clone();
|
|
||||||
OverlayPopup::new()
|
|
||||||
.on_close(move |_| close.close_blocked_downloads())
|
|
||||||
.child(
|
|
||||||
rect()
|
|
||||||
.width(Size::window_percent(100.))
|
|
||||||
.height(Size::window_percent(100.))
|
|
||||||
.center()
|
|
||||||
.child(dialog(
|
|
||||||
&self.blocked,
|
|
||||||
downloads.as_deref(),
|
|
||||||
chosen.as_deref(),
|
|
||||||
extra,
|
|
||||||
dispatch,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.maybe_child(chosen.map(|folder| {
|
|
||||||
let key = folder.to_string_lossy().into_owned();
|
|
||||||
ExtraFolderWatch {
|
|
||||||
folder,
|
|
||||||
tick,
|
|
||||||
key: DiffKey::None,
|
|
||||||
}
|
|
||||||
.key(key)
|
|
||||||
.into_element()
|
|
||||||
}))
|
|
||||||
.into_element()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn folder_label(path: &Path) -> String {
|
|
||||||
path.file_name()
|
|
||||||
.map(|name| name.to_string_lossy().into_owned())
|
|
||||||
.unwrap_or_else(|| path.display().to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn watched_text(downloads: Option<&Path>, chosen: Option<&Path>) -> String {
|
|
||||||
match (downloads, chosen) {
|
|
||||||
(Some(_), Some(chosen)) => format!("Downloads and {}", chosen.display()),
|
|
||||||
(Some(downloads), None) => downloads.display().to_string(),
|
|
||||||
(None, Some(chosen)) => chosen.display().to_string(),
|
|
||||||
(None, None) => "Nothing yet. Add a folder or use Browse.".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn pick_extra_folder(cluster_id: i64, start: Option<PathBuf>, mut extra: ExtraFolder) {
|
|
||||||
spawn(async move {
|
|
||||||
let mut picker = rfd::AsyncFileDialog::new().set_title("Pick the folder you download into");
|
|
||||||
if let Some(start) = start {
|
|
||||||
picker = picker.set_directory(start);
|
|
||||||
}
|
|
||||||
if let Some(handle) = picker.pick_folder().await {
|
|
||||||
extra.set(Some((cluster_id, handle.path().to_path_buf())));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn folder_row(
|
|
||||||
cluster_id: i64,
|
|
||||||
downloads: Option<&Path>,
|
|
||||||
chosen: Option<&Path>,
|
|
||||||
extra: ExtraFolder,
|
|
||||||
) -> impl IntoElement {
|
|
||||||
let start = chosen
|
|
||||||
.map(Path::to_path_buf)
|
|
||||||
.or_else(|| downloads.map(Path::to_path_buf));
|
|
||||||
let has_extra = chosen.is_some();
|
|
||||||
|
|
||||||
rect()
|
|
||||||
.vertical()
|
|
||||||
.width(Size::fill())
|
|
||||||
.spacing(6.)
|
|
||||||
.child(
|
|
||||||
rect()
|
|
||||||
.horizontal()
|
|
||||||
.width(Size::fill())
|
|
||||||
.content(Content::Flex)
|
|
||||||
.cross_align(Alignment::Center)
|
|
||||||
.padding(Gaps::new_symmetric(8., 12.))
|
|
||||||
.spacing(10.)
|
|
||||||
.corner_radius(CornerRadius::new_all(10.))
|
|
||||||
.background(colors::component_bg())
|
|
||||||
.border(border_all_color(1., colors::component_border()))
|
|
||||||
.child(
|
|
||||||
Icon::new(IconType::FolderCheck)
|
|
||||||
.size(16.)
|
|
||||||
.color(colors::fg_secondary()),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
rect()
|
|
||||||
.vertical()
|
|
||||||
.width(Size::flex(1.))
|
|
||||||
.spacing(2.)
|
|
||||||
.child(
|
|
||||||
label()
|
|
||||||
.text("Watching")
|
|
||||||
.font_size(11.)
|
|
||||||
.color(colors::fg_secondary()),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
label()
|
|
||||||
.text(watched_text(downloads, chosen))
|
|
||||||
.font_size(12.5)
|
|
||||||
.max_lines(2)
|
|
||||||
.color(colors::fg_primary()),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
Button::new()
|
|
||||||
.secondary()
|
|
||||||
.small()
|
|
||||||
.on_press(move |_| pick_extra_folder(cluster_id, start.clone(), extra))
|
|
||||||
.text(if has_extra {
|
|
||||||
"Change folder"
|
|
||||||
} else {
|
|
||||||
"Add folder"
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.maybe_child(has_extra.then(|| {
|
|
||||||
let mut extra = extra;
|
|
||||||
Button::new()
|
|
||||||
.ghost()
|
|
||||||
.small()
|
|
||||||
.on_press(move |_| extra.set(None))
|
|
||||||
.text("Remove")
|
|
||||||
.into_element()
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
label()
|
|
||||||
.text(
|
|
||||||
"Your browser saves to its own download folder. To save somewhere else, \
|
|
||||||
set it to ask where to save each file.",
|
|
||||||
)
|
|
||||||
.font_size(11.)
|
|
||||||
.max_lines(2)
|
|
||||||
.color(colors::fg_secondary()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dialog(
|
|
||||||
blocked: &BlockedDownloads,
|
|
||||||
downloads: Option<&Path>,
|
|
||||||
chosen: Option<&Path>,
|
|
||||||
extra: ExtraFolder,
|
|
||||||
dispatch: crate::Actions,
|
|
||||||
) -> impl IntoElement {
|
|
||||||
let count = blocked.files.len();
|
|
||||||
let added = blocked.added.len().min(count);
|
|
||||||
let done = added == count;
|
|
||||||
let plural = crate::utils::plural(count as i64);
|
|
||||||
let where_to = match (downloads, chosen) {
|
|
||||||
(_, Some(chosen)) => format!(
|
|
||||||
"Download each one from its page and it will be picked up from Downloads or {} automatically.",
|
|
||||||
folder_label(chosen)
|
|
||||||
),
|
|
||||||
(Some(_), None) => {
|
|
||||||
"Download each one from its page and it will be picked up from your Downloads folder automatically."
|
|
||||||
.to_string()
|
|
||||||
}
|
|
||||||
(None, None) => {
|
|
||||||
"Download each one from its page, then add the folder you saved them to or use Browse."
|
|
||||||
.to_string()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let subtitle = if done {
|
|
||||||
format!(
|
|
||||||
"All {count} file{plural} are in {}. It is ready to play.",
|
|
||||||
blocked.cluster_name
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
format!(
|
|
||||||
"CurseForge does not let launchers download {count} file{plural} that {} needs. {where_to}",
|
|
||||||
blocked.cluster_name
|
|
||||||
)
|
|
||||||
};
|
|
||||||
let title = if done {
|
|
||||||
"Manual downloads added".to_string()
|
|
||||||
} else if added > 0 {
|
|
||||||
format!("Some files need a manual download ({added} of {count} added)")
|
|
||||||
} else {
|
|
||||||
"Some files need a manual download".to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
let cluster_id = blocked.cluster_id;
|
|
||||||
let files = blocked.remaining();
|
|
||||||
let browse = dispatch.clone();
|
|
||||||
let close = dispatch.clone();
|
|
||||||
let browse_start = chosen
|
|
||||||
.map(Path::to_path_buf)
|
|
||||||
.or_else(|| downloads.map(Path::to_path_buf));
|
|
||||||
|
|
||||||
rect()
|
|
||||||
.vertical()
|
|
||||||
.width(Size::px(DIALOG_W))
|
|
||||||
.height(Size::Inner)
|
|
||||||
.max_width(Size::window_percent(95.))
|
|
||||||
.max_height(Size::window_percent(85.))
|
|
||||||
.overflow(Overflow::Clip)
|
|
||||||
.corner_radius(CornerRadius::new_all(16.))
|
|
||||||
.background(CARD_BG)
|
|
||||||
.border(border_all_color(1., colors::component_border()))
|
|
||||||
.shadow(Shadow::from((
|
|
||||||
0.,
|
|
||||||
18.,
|
|
||||||
52.,
|
|
||||||
0.,
|
|
||||||
Color::from_argb(150, 0, 0, 0),
|
|
||||||
)))
|
|
||||||
.child(
|
|
||||||
rect()
|
|
||||||
.vertical()
|
|
||||||
.width(Size::fill())
|
|
||||||
.height(Size::Inner)
|
|
||||||
.padding(Gaps::new_all(DIALOG_PAD))
|
|
||||||
.spacing(14.)
|
|
||||||
.child(
|
|
||||||
rect()
|
|
||||||
.vertical()
|
|
||||||
.width(Size::fill())
|
|
||||||
.spacing(3.)
|
|
||||||
.child(
|
|
||||||
label()
|
|
||||||
.text(title)
|
|
||||||
.font_size(17.)
|
|
||||||
.font_weight(FontWeight::SEMI_BOLD)
|
|
||||||
.color(colors::fg_primary()),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
label()
|
|
||||||
.text(subtitle)
|
|
||||||
.font_size(12.5)
|
|
||||||
.max_lines(4)
|
|
||||||
.color(colors::fg_secondary()),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.maybe_child(
|
|
||||||
(!done)
|
|
||||||
.then(|| folder_row(cluster_id, downloads, chosen, extra).into_element()),
|
|
||||||
)
|
|
||||||
.child(file_list(blocked))
|
|
||||||
.child(if done {
|
|
||||||
let close = close.clone();
|
|
||||||
rect()
|
|
||||||
.horizontal()
|
|
||||||
.width(Size::fill())
|
|
||||||
.main_align(Alignment::End)
|
|
||||||
.child(
|
|
||||||
Button::new()
|
|
||||||
.primary()
|
|
||||||
.on_press(move |_| close.close_blocked_downloads())
|
|
||||||
.child(Icon::new(IconType::Check).size(15.))
|
|
||||||
.text("Done"),
|
|
||||||
)
|
|
||||||
.into_element()
|
|
||||||
} else {
|
|
||||||
footer(cluster_id, files, browse_start, browse, close).into_element()
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn footer(
|
|
||||||
cluster_id: i64,
|
|
||||||
files: Vec<BlockedFile>,
|
|
||||||
browse_start: Option<PathBuf>,
|
|
||||||
browse: crate::Actions,
|
|
||||||
close: crate::Actions,
|
|
||||||
) -> impl IntoElement {
|
|
||||||
rect()
|
|
||||||
.horizontal()
|
|
||||||
.width(Size::fill())
|
|
||||||
.cross_align(Alignment::Center)
|
|
||||||
.main_align(Alignment::End)
|
|
||||||
.spacing(8.)
|
|
||||||
.child(
|
|
||||||
Button::new()
|
|
||||||
.ghost()
|
|
||||||
.on_press(move |_| close.close_blocked_downloads())
|
|
||||||
.text("Skip for now"),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
Button::new()
|
|
||||||
.primary()
|
|
||||||
.on_press(move |_| {
|
|
||||||
let dispatch = browse.clone();
|
|
||||||
let files = files.clone();
|
|
||||||
let start = browse_start.clone();
|
|
||||||
spawn(async move {
|
|
||||||
let mut picker =
|
|
||||||
rfd::AsyncFileDialog::new().set_title("Pick the downloaded files");
|
|
||||||
if let Some(start) = start {
|
|
||||||
picker = picker.set_directory(start);
|
|
||||||
}
|
|
||||||
if let Some(handles) = picker.pick_files().await {
|
|
||||||
let picked = handles
|
|
||||||
.iter()
|
|
||||||
.map(|handle| handle.path().to_path_buf())
|
|
||||||
.collect();
|
|
||||||
dispatch.scan_blocked_downloads(cluster_id, picked, files);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.child(Icon::new(IconType::Folder).size(15.))
|
|
||||||
.text("Browse"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn file_list(blocked: &BlockedDownloads) -> impl IntoElement {
|
|
||||||
ScrollView::new()
|
|
||||||
.width(Size::fill())
|
|
||||||
.height(Size::Inner)
|
|
||||||
.max_height(Size::px(LIST_MAX_H))
|
|
||||||
.child(
|
|
||||||
rect().vertical().width(Size::fill()).spacing(6.).children(
|
|
||||||
blocked
|
|
||||||
.files
|
|
||||||
.iter()
|
|
||||||
.map(|file| file_row(file, blocked.is_added(file)).into_element()),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn file_row(file: &BlockedFile, added: bool) -> impl IntoElement {
|
|
||||||
let page = file.page_url.clone();
|
|
||||||
let (background, border) = if added {
|
|
||||||
(colors::success().with_a(28), colors::success().with_a(120))
|
|
||||||
} else {
|
|
||||||
(colors::component_bg(), colors::component_border())
|
|
||||||
};
|
|
||||||
|
|
||||||
let trailing = if added {
|
|
||||||
rect()
|
|
||||||
.horizontal()
|
|
||||||
.cross_align(Alignment::Center)
|
|
||||||
.spacing(6.)
|
|
||||||
.padding(Gaps::new_symmetric(0., 4.))
|
|
||||||
.child(
|
|
||||||
Icon::new(IconType::CheckCircle)
|
|
||||||
.size(14.)
|
|
||||||
.color(colors::success()),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
label()
|
|
||||||
.text("Added")
|
|
||||||
.font_size(12.)
|
|
||||||
.font_weight(FontWeight::SEMI_BOLD)
|
|
||||||
.color(colors::success()),
|
|
||||||
)
|
|
||||||
.into_element()
|
|
||||||
} else {
|
|
||||||
Button::new()
|
|
||||||
.secondary()
|
|
||||||
.small()
|
|
||||||
.enabled(page.is_some())
|
|
||||||
.on_press(move |_| {
|
|
||||||
if let Some(url) = &page {
|
|
||||||
crate::platform::open_url(url);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.child(Icon::new(IconType::LinkExternal01).size(13.))
|
|
||||||
.text("Open page")
|
|
||||||
.into_element()
|
|
||||||
};
|
|
||||||
|
|
||||||
rect()
|
|
||||||
.key(file.sha1.clone())
|
|
||||||
.horizontal()
|
|
||||||
.width(Size::fill())
|
|
||||||
.height(Size::px(ROW_H))
|
|
||||||
.content(Content::Flex)
|
|
||||||
.cross_align(Alignment::Center)
|
|
||||||
.padding(Gaps::new_symmetric(0., 12.))
|
|
||||||
.spacing(10.)
|
|
||||||
.corner_radius(CornerRadius::new_all(10.))
|
|
||||||
.background(background)
|
|
||||||
.border(border_all_color(1., border))
|
|
||||||
.child(
|
|
||||||
rect()
|
|
||||||
.vertical()
|
|
||||||
.width(Size::flex(1.))
|
|
||||||
.spacing(2.)
|
|
||||||
.child(
|
|
||||||
label()
|
|
||||||
.text(file.project_name.clone())
|
|
||||||
.font_size(13.)
|
|
||||||
.font_weight(FontWeight::SEMI_BOLD)
|
|
||||||
.max_lines(1)
|
|
||||||
.color(colors::fg_primary()),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
label()
|
|
||||||
.text(file.file_name.clone())
|
|
||||||
.font_size(11.)
|
|
||||||
.max_lines(1)
|
|
||||||
.color(colors::fg_secondary()),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(trailing)
|
|
||||||
}
|
|
||||||
|
|
@ -1,157 +0,0 @@
|
||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
use freya::prelude::*;
|
|
||||||
|
|
||||||
use crate::components::{Button, Icon, IconType, OverlayPopup, toggle_controlled};
|
|
||||||
use crate::hooks::{use_dispatch, use_notifications_snapshot};
|
|
||||||
use crate::theme::colors;
|
|
||||||
use crate::ui::border_all_color;
|
|
||||||
|
|
||||||
const CARD_BG: Color = Color::from_rgb(26, 34, 41);
|
|
||||||
const DIALOG_W: f32 = 460.;
|
|
||||||
const LIST_MAX_H: f32 = 306.;
|
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
|
||||||
pub struct BundleChoicesPopup;
|
|
||||||
|
|
||||||
impl Component for BundleChoicesPopup {
|
|
||||||
fn render(&self) -> impl IntoElement {
|
|
||||||
let snapshot = use_notifications_snapshot();
|
|
||||||
let dispatch = use_dispatch();
|
|
||||||
let mut chosen = use_state(HashSet::<String>::new);
|
|
||||||
|
|
||||||
let Some(choices) = snapshot.bundle_choices.clone() else {
|
|
||||||
return rect().into_element();
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut list = rect().vertical().width(Size::fill()).spacing(6.);
|
|
||||||
for (name, title) in &choices.bundles {
|
|
||||||
let key = name.clone();
|
|
||||||
let on_toggle: EventHandler<()> = (move |()| {
|
|
||||||
let mut next = chosen.read().clone();
|
|
||||||
if !next.remove(&key) {
|
|
||||||
next.insert(key.clone());
|
|
||||||
}
|
|
||||||
chosen.set(next);
|
|
||||||
})
|
|
||||||
.into();
|
|
||||||
list = list.child(
|
|
||||||
rect()
|
|
||||||
.key(name.clone())
|
|
||||||
.horizontal()
|
|
||||||
.width(Size::fill())
|
|
||||||
.content(Content::Flex)
|
|
||||||
.cross_align(Alignment::Center)
|
|
||||||
.padding(Gaps::new_symmetric(8., 10.))
|
|
||||||
.corner_radius(CornerRadius::new_all(8.))
|
|
||||||
.background(colors::component_bg())
|
|
||||||
.child(
|
|
||||||
label()
|
|
||||||
.text(title.clone())
|
|
||||||
.font_size(13.)
|
|
||||||
.max_lines(1)
|
|
||||||
.width(Size::flex(1.0))
|
|
||||||
.color(colors::fg_primary()),
|
|
||||||
)
|
|
||||||
.child(toggle_controlled(
|
|
||||||
choices.held.contains(name) != chosen.read().contains(name),
|
|
||||||
on_toggle,
|
|
||||||
)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let names: Vec<String> = choices
|
|
||||||
.bundles
|
|
||||||
.iter()
|
|
||||||
.map(|(name, _)| name.clone())
|
|
||||||
.collect();
|
|
||||||
let held = choices.held.clone();
|
|
||||||
let later = dispatch.clone();
|
|
||||||
let close = dispatch.clone();
|
|
||||||
|
|
||||||
OverlayPopup::new()
|
|
||||||
.on_close(move |_| {
|
|
||||||
chosen.set(HashSet::new());
|
|
||||||
close.close_bundle_choices(None);
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
rect()
|
|
||||||
.width(Size::window_percent(100.))
|
|
||||||
.height(Size::window_percent(100.))
|
|
||||||
.center()
|
|
||||||
.child(
|
|
||||||
rect()
|
|
||||||
.vertical()
|
|
||||||
.width(Size::px(DIALOG_W))
|
|
||||||
.height(Size::Inner)
|
|
||||||
.max_width(Size::window_percent(95.))
|
|
||||||
.max_height(Size::window_percent(85.))
|
|
||||||
.padding(Gaps::new_all(22.))
|
|
||||||
.spacing(14.)
|
|
||||||
.corner_radius(CornerRadius::new_all(16.))
|
|
||||||
.background(CARD_BG)
|
|
||||||
.border(border_all_color(1., colors::component_border()))
|
|
||||||
.child(
|
|
||||||
label()
|
|
||||||
.text("Choose your bundles")
|
|
||||||
.font_size(17.)
|
|
||||||
.font_weight(FontWeight::SEMI_BOLD)
|
|
||||||
.color(colors::fg_primary()),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
label()
|
|
||||||
.text(format!(
|
|
||||||
"{} has optional bundles you have not chosen yet. Turn on the ones you want, turning one off removes its mods.",
|
|
||||||
choices.cluster_name
|
|
||||||
))
|
|
||||||
.font_size(12.5)
|
|
||||||
.max_lines(4)
|
|
||||||
.color(colors::fg_secondary()),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
ScrollView::new()
|
|
||||||
.width(Size::fill())
|
|
||||||
.height(Size::Inner)
|
|
||||||
.max_height(Size::px(LIST_MAX_H))
|
|
||||||
.child(list),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
rect()
|
|
||||||
.horizontal()
|
|
||||||
.width(Size::fill())
|
|
||||||
.main_align(Alignment::End)
|
|
||||||
.spacing(8.)
|
|
||||||
.child(
|
|
||||||
Button::new()
|
|
||||||
.ghost()
|
|
||||||
.on_press(move |_| {
|
|
||||||
chosen.set(HashSet::new());
|
|
||||||
later.close_bundle_choices(None);
|
|
||||||
})
|
|
||||||
.text("Ask me later"),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
Button::new()
|
|
||||||
.primary()
|
|
||||||
.on_press(move |_| {
|
|
||||||
let flipped = chosen.read().clone();
|
|
||||||
let picked = names
|
|
||||||
.iter()
|
|
||||||
.filter(|name| {
|
|
||||||
held.contains(*name)
|
|
||||||
!= flipped.contains(*name)
|
|
||||||
})
|
|
||||||
.cloned()
|
|
||||||
.collect();
|
|
||||||
chosen.set(HashSet::new());
|
|
||||||
dispatch.close_bundle_choices(Some(picked));
|
|
||||||
})
|
|
||||||
.child(Icon::new(IconType::Check).size(15.))
|
|
||||||
.text("Confirm & launch"),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.into_element()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -4,7 +4,7 @@ use std::borrow::Cow;
|
||||||
use freya::prelude::*;
|
use freya::prelude::*;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
components::{Icon, IconTint, IconType, TooltipPlacement, tooltip::use_tooltip_anchor},
|
components::{Icon, IconType},
|
||||||
theme::colors,
|
theme::colors,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -26,6 +26,7 @@ pub enum ButtonSize {
|
||||||
Icon,
|
Icon,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// How much a disabled button is faded out.
|
||||||
const DISABLED_OPACITY: f32 = 0.5;
|
const DISABLED_OPACITY: f32 = 0.5;
|
||||||
|
|
||||||
struct ButtonColors {
|
struct ButtonColors {
|
||||||
|
|
@ -53,10 +54,7 @@ pub struct Button {
|
||||||
enabled: bool,
|
enabled: bool,
|
||||||
focusable: bool,
|
focusable: bool,
|
||||||
alt: Option<Box<str>>,
|
alt: Option<Box<str>>,
|
||||||
tooltip: Option<Box<str>>,
|
|
||||||
tooltip_placement: TooltipPlacement,
|
|
||||||
on_press: Option<EventHandler<Event<PressEventData>>>,
|
on_press: Option<EventHandler<Event<PressEventData>>>,
|
||||||
overflow: Overflow,
|
|
||||||
|
|
||||||
elements: Vec<Element>,
|
elements: Vec<Element>,
|
||||||
key: DiffKey,
|
key: DiffKey,
|
||||||
|
|
@ -114,11 +112,8 @@ impl Button {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
focusable: true,
|
focusable: true,
|
||||||
alt: None,
|
alt: None,
|
||||||
tooltip: None,
|
|
||||||
tooltip_placement: TooltipPlacement::default(),
|
|
||||||
on_press: None,
|
on_press: None,
|
||||||
elements: Vec::new(),
|
elements: Vec::new(),
|
||||||
overflow: Overflow::Clip,
|
|
||||||
key: DiffKey::None,
|
key: DiffKey::None,
|
||||||
cursor_icon: CursorIcon::Pointer,
|
cursor_icon: CursorIcon::Pointer,
|
||||||
}
|
}
|
||||||
|
|
@ -158,11 +153,6 @@ impl Button {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn overflow(mut self, overflow: Overflow) -> Self {
|
|
||||||
self.overflow = overflow;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn font_size(mut self, font_size: impl Into<f32>) -> Self {
|
pub fn font_size(mut self, font_size: impl Into<f32>) -> Self {
|
||||||
self.text_style.font_size = Some(font_size.into().into());
|
self.text_style.font_size = Some(font_size.into().into());
|
||||||
self
|
self
|
||||||
|
|
@ -208,26 +198,13 @@ impl Button {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Accessible name for icon-only buttons which have no label to read out
|
/// The name a button carrying only an icon goes by, since there is no label
|
||||||
|
/// for accessibility to read out.
|
||||||
pub fn alt(mut self, alt: impl Into<Box<str>>) -> Self {
|
pub fn alt(mut self, alt: impl Into<Box<str>>) -> Self {
|
||||||
self.alt = Some(alt.into());
|
self.alt = Some(alt.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tooltip(mut self, tooltip: impl Into<Box<str>>) -> Self {
|
|
||||||
let tooltip = tooltip.into();
|
|
||||||
if self.alt.is_none() {
|
|
||||||
self.alt = Some(tooltip.clone());
|
|
||||||
}
|
|
||||||
self.tooltip = Some(tooltip);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tooltip_placement(mut self, placement: TooltipPlacement) -> Self {
|
|
||||||
self.tooltip_placement = placement;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cursor_icon(mut self, cursor_icon: CursorIcon) -> Self {
|
pub fn cursor_icon(mut self, cursor_icon: CursorIcon) -> Self {
|
||||||
self.cursor_icon = cursor_icon;
|
self.cursor_icon = cursor_icon;
|
||||||
self
|
self
|
||||||
|
|
@ -258,8 +235,6 @@ impl LayoutExt for Button {
|
||||||
|
|
||||||
impl ContainerExt for Button {}
|
impl ContainerExt for Button {}
|
||||||
|
|
||||||
impl DecorationExt for Button {}
|
|
||||||
|
|
||||||
impl StyleExt for Button {
|
impl StyleExt for Button {
|
||||||
fn get_style(&mut self) -> &mut StyleState {
|
fn get_style(&mut self) -> &mut StyleState {
|
||||||
&mut self.style
|
&mut self.style
|
||||||
|
|
@ -274,12 +249,14 @@ impl Component for Button {
|
||||||
let a11y_id = use_a11y();
|
let a11y_id = use_a11y();
|
||||||
let focus = use_focus(a11y_id);
|
let focus = use_focus(a11y_id);
|
||||||
|
|
||||||
let tooltip = use_tooltip_anchor(self.tooltip.clone(), self.tooltip_placement);
|
|
||||||
|
|
||||||
let enabled = use_reactive(&self.enabled);
|
let enabled = use_reactive(&self.enabled);
|
||||||
|
|
||||||
let cursor_icon = self.cursor_icon;
|
let cursor_icon = self.cursor_icon;
|
||||||
|
|
||||||
|
use_drop(move || {
|
||||||
|
Cursor::set(CursorIcon::default());
|
||||||
|
});
|
||||||
|
|
||||||
let palette = variant_colors(self.variant);
|
let palette = variant_colors(self.variant);
|
||||||
|
|
||||||
let background = if !enabled() {
|
let background = if !enabled() {
|
||||||
|
|
@ -298,12 +275,6 @@ impl Component for Button {
|
||||||
palette.disabled_foreground
|
palette.disabled_foreground
|
||||||
};
|
};
|
||||||
|
|
||||||
// Child icons would otherwise inherit this through a `styled` event that lands a couple of
|
|
||||||
// frames after the button paints, so hand it straight to them instead
|
|
||||||
let mut icon_tint = use_state(|| foreground);
|
|
||||||
icon_tint.set_if_modified(foreground);
|
|
||||||
use_hook(|| provide_context(IconTint(icon_tint)));
|
|
||||||
|
|
||||||
let border_color = if !enabled() {
|
let border_color = if !enabled() {
|
||||||
palette.border
|
palette.border
|
||||||
} else if pressing() {
|
} else if pressing() {
|
||||||
|
|
@ -323,7 +294,7 @@ impl Component for Button {
|
||||||
.a11y_focusable(enabled() && self.focusable)
|
.a11y_focusable(enabled() && self.focusable)
|
||||||
.a11y_role(AccessibilityRole::Button)
|
.a11y_role(AccessibilityRole::Button)
|
||||||
.map(self.alt.clone(), |rect, alt| rect.a11y_alt(alt))
|
.map(self.alt.clone(), |rect, alt| rect.a11y_alt(alt))
|
||||||
.overflow(self.overflow)
|
.overflow(Overflow::Clip)
|
||||||
.layout(self.layout.clone())
|
.layout(self.layout.clone())
|
||||||
.text_style(self.text_style.clone())
|
.text_style(self.text_style.clone())
|
||||||
.children(self.elements.clone());
|
.children(self.elements.clone());
|
||||||
|
|
@ -348,23 +319,30 @@ impl Component for Button {
|
||||||
if enabled() {
|
if enabled() {
|
||||||
rect = rect
|
rect = rect
|
||||||
.on_pointer_down(move |_| pressing.set(true))
|
.on_pointer_down(move |_| pressing.set(true))
|
||||||
.on_global_pointer_up(move |_| pressing.set(false))
|
.on_global_pointer_press(move |_| pressing.set(false))
|
||||||
.on_pointer_over(move |_| hovering.set(true))
|
.on_pointer_over(move |_| hovering.set(true))
|
||||||
.on_pointer_out(move |_| {
|
.on_pointer_out(move |_| {
|
||||||
hovering.set(false);
|
hovering.set(false);
|
||||||
pressing.set(false);
|
pressing.set(false);
|
||||||
})
|
})
|
||||||
.cursor(cursor_icon)
|
.on_pointer_enter(move |_| {
|
||||||
|
Cursor::set(cursor_icon);
|
||||||
|
})
|
||||||
|
.on_pointer_leave(move |_| {
|
||||||
|
Cursor::set(CursorIcon::default());
|
||||||
|
})
|
||||||
.map(on_press.clone(), |rect, handler| {
|
.map(on_press.clone(), |rect, handler| {
|
||||||
rect.on_press(move |event: Event<PressEventData>| {
|
rect.on_all_press(move |event: Event<PressEventData>| {
|
||||||
handler.call(event);
|
handler.call(event);
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
rect = rect.cursor(CursorIcon::NotAllowed);
|
rect = rect
|
||||||
|
.on_pointer_enter(move |_| Cursor::set(CursorIcon::NotAllowed))
|
||||||
|
.on_pointer_leave(move |_| Cursor::set(CursorIcon::default()));
|
||||||
}
|
}
|
||||||
|
|
||||||
tooltip.attach(rect)
|
rect
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_key(&self) -> DiffKey {
|
fn render_key(&self) -> DiffKey {
|
||||||
|
|
@ -473,36 +451,9 @@ pub fn open_folder_button(folder: std::path::PathBuf) -> Button {
|
||||||
Button::new()
|
Button::new()
|
||||||
.secondary()
|
.secondary()
|
||||||
.icon()
|
.icon()
|
||||||
.tooltip("Open folder")
|
|
||||||
.on_press(move |_| {
|
.on_press(move |_| {
|
||||||
std::fs::create_dir_all(&folder).ok();
|
std::fs::create_dir_all(&folder).ok();
|
||||||
crate::platform::open_path(&folder.to_string_lossy());
|
crate::platform::open_url(&folder.to_string_lossy());
|
||||||
})
|
})
|
||||||
.child(Icon::new(IconType::Folder).size(16.))
|
.child(Icon::new(IconType::Folder).size(16.))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn copy_button(text: String, copied_body: &'static str, dispatch: crate::Actions) -> Button {
|
|
||||||
Button::new()
|
|
||||||
.secondary()
|
|
||||||
.on_press(move |_| {
|
|
||||||
if let Err(err) = freya::text_edit::Clipboard::set(text.clone()) {
|
|
||||||
tracing::warn!("clipboard copy failed: {err:?}");
|
|
||||||
dispatch
|
|
||||||
.notify("Copy failed")
|
|
||||||
.body("Could not copy to the clipboard.")
|
|
||||||
.error()
|
|
||||||
.toast_only()
|
|
||||||
.send();
|
|
||||||
} else {
|
|
||||||
dispatch
|
|
||||||
.notify("Copied to clipboard")
|
|
||||||
.body(copied_body)
|
|
||||||
.info()
|
|
||||||
.icon(IconType::ClipboardCheck)
|
|
||||||
.toast_only()
|
|
||||||
.send();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.child(Icon::new(IconType::Copy01).size(14.))
|
|
||||||
.text("Copy")
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use freya::engine::prelude::{Paint, PaintStyle, PathBuilder, SkColor, SkRect};
|
||||||
use freya::prelude::*;
|
use freya::prelude::*;
|
||||||
|
|
||||||
use crate::theme::colors;
|
use crate::theme::colors;
|
||||||
use crate::utils::format_duration;
|
use crate::utils::format_duration_hm;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub enum ValueUnit {
|
pub enum ValueUnit {
|
||||||
|
|
@ -13,7 +13,7 @@ pub enum ValueUnit {
|
||||||
impl ValueUnit {
|
impl ValueUnit {
|
||||||
pub fn format(self, value: i64) -> String {
|
pub fn format(self, value: i64) -> String {
|
||||||
match self {
|
match self {
|
||||||
ValueUnit::Duration => format_duration(value),
|
ValueUnit::Duration => format_duration_hm(value),
|
||||||
ValueUnit::Count => format!("{value}×"),
|
ValueUnit::Count => format!("{value}×"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -22,6 +22,7 @@ impl ValueUnit {
|
||||||
const DEFAULT_HEIGHT: f32 = 150.;
|
const DEFAULT_HEIGHT: f32 = 150.;
|
||||||
const Y_AXIS_WIDTH: f32 = 44.;
|
const Y_AXIS_WIDTH: f32 = 44.;
|
||||||
|
|
||||||
|
/// Slice colors, cycled when there are more slices than entries.
|
||||||
pub const SLICE_COLORS: [Color; 10] = [
|
pub const SLICE_COLORS: [Color; 10] = [
|
||||||
Color::from_rgb(43, 75, 255),
|
Color::from_rgb(43, 75, 255),
|
||||||
Color::from_rgb(27, 217, 106),
|
Color::from_rgb(27, 217, 106),
|
||||||
|
|
@ -40,9 +41,11 @@ pub fn slice_color(i: usize) -> Color {
|
||||||
}
|
}
|
||||||
|
|
||||||
const PIE_SIZE: f32 = 190.;
|
const PIE_SIZE: f32 = 190.;
|
||||||
/// Fraction of the radius
|
/// Inner hole as a fraction of the radius, making it a donut.
|
||||||
const PIE_HOLE: f32 = 0.58;
|
const PIE_HOLE: f32 = 0.58;
|
||||||
|
|
||||||
|
/// A donut chart. Hovering a slice reports it via `on_hover` so callers can
|
||||||
|
/// sync an external legend; the hovered slice is also expanded slightly.
|
||||||
pub struct PieChart {
|
pub struct PieChart {
|
||||||
values: Vec<i64>,
|
values: Vec<i64>,
|
||||||
labels: Vec<String>,
|
labels: Vec<String>,
|
||||||
|
|
@ -67,6 +70,7 @@ impl PieChart {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Shares the hovered slice index with the caller (e.g. to highlight a legend row).
|
||||||
pub fn hovered(mut self, hovered: State<Option<usize>>) -> Self {
|
pub fn hovered(mut self, hovered: State<Option<usize>>) -> Self {
|
||||||
self.hovered = Some(hovered);
|
self.hovered = Some(hovered);
|
||||||
self
|
self
|
||||||
|
|
@ -93,7 +97,7 @@ impl Component for PieChart {
|
||||||
let values = self.values.clone();
|
let values = self.values.clone();
|
||||||
let hit_values = self.values.clone();
|
let hit_values = self.values.clone();
|
||||||
|
|
||||||
let render_cb = RenderCallback::new(move |ctx: &mut FillRenderContext| {
|
let render_cb = RenderCallback::new(move |ctx: &mut CanvasContext| {
|
||||||
let (w, h) = (ctx.size.width, ctx.size.height);
|
let (w, h) = (ctx.size.width, ctx.size.height);
|
||||||
if w <= 0.0 || h <= 0.0 {
|
if w <= 0.0 || h <= 0.0 {
|
||||||
return;
|
return;
|
||||||
|
|
@ -110,6 +114,7 @@ impl Component for PieChart {
|
||||||
paint.set_anti_alias(true);
|
paint.set_anti_alias(true);
|
||||||
paint.set_style(PaintStyle::Fill);
|
paint.set_style(PaintStyle::Fill);
|
||||||
|
|
||||||
|
// With nothing recorded, draw an empty track so the ring is still there.
|
||||||
if total <= 0 {
|
if total <= 0 {
|
||||||
paint.set_color(SkColor::from(colors::component_bg_hover()));
|
paint.set_color(SkColor::from(colors::component_bg_hover()));
|
||||||
ctx.canvas.draw_circle((cx, cy), radius - 5.0, &paint);
|
ctx.canvas.draw_circle((cx, cy), radius - 5.0, &paint);
|
||||||
|
|
@ -125,6 +130,7 @@ impl Component for PieChart {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let sweep = (v as f32 / total as f32) * 360.0;
|
let sweep = (v as f32 / total as f32) * 360.0;
|
||||||
|
// Grow the hovered slice a touch, and dim the rest.
|
||||||
let is_active = Some(i) == active;
|
let is_active = Some(i) == active;
|
||||||
let r = if is_active { radius } else { radius - 5.0 };
|
let r = if is_active { radius } else { radius - 5.0 };
|
||||||
let color = slice_color(i);
|
let color = slice_color(i);
|
||||||
|
|
@ -145,6 +151,7 @@ impl Component for PieChart {
|
||||||
start += sweep;
|
start += sweep;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Punch the donut hole using the card background.
|
||||||
paint.set_color(SkColor::from(colors::page_elevated()));
|
paint.set_color(SkColor::from(colors::page_elevated()));
|
||||||
ctx.canvas
|
ctx.canvas
|
||||||
.draw_circle((cx, cy), (radius - 5.0) * PIE_HOLE, &paint);
|
.draw_circle((cx, cy), (radius - 5.0) * PIE_HOLE, &paint);
|
||||||
|
|
@ -182,13 +189,9 @@ impl Component for PieChart {
|
||||||
.height(Size::fill())
|
.height(Size::fill())
|
||||||
.position(Position::new_absolute())
|
.position(Position::new_absolute())
|
||||||
.interactive(false)
|
.interactive(false)
|
||||||
// Childless rect auto-sizes to 0x0 without an explicit size
|
// The canvas defaults to auto-sizing and has no children, so
|
||||||
.child(
|
// without an explicit size it measures 0x0 and draws nothing.
|
||||||
rect()
|
.child(canvas(render_cb).width(Size::fill()).height(Size::fill())),
|
||||||
.width(Size::fill())
|
|
||||||
.height(Size::fill())
|
|
||||||
.background(render_cb),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
rect()
|
rect()
|
||||||
|
|
@ -196,9 +199,11 @@ impl Component for PieChart {
|
||||||
.height(Size::fill())
|
.height(Size::fill())
|
||||||
.position(Position::new_absolute())
|
.position(Position::new_absolute())
|
||||||
.interactive(false)
|
.interactive(false)
|
||||||
// Sibling absolute rects on an equal layer paint in undefined order
|
// Sibling absolute rects on an equal layer paint in undefined
|
||||||
|
// order, so pin the readout above the canvas.
|
||||||
.layer(Layer::Relative(1))
|
.layer(Layer::Relative(1))
|
||||||
.center()
|
.center()
|
||||||
|
// Keeps the readout inside the donut hole (Gaps is vertical, horizontal).
|
||||||
.padding(Gaps::new_symmetric(0., size * 0.25))
|
.padding(Gaps::new_symmetric(0., size * 0.25))
|
||||||
.child(
|
.child(
|
||||||
rect()
|
rect()
|
||||||
|
|
@ -230,6 +235,7 @@ impl Component for PieChart {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Maps a pointer position inside the chart box to a slice index.
|
||||||
fn slice_at(values: &[i64], x: f32, y: f32, size: f32) -> Option<usize> {
|
fn slice_at(values: &[i64], x: f32, y: f32, size: f32) -> Option<usize> {
|
||||||
let total: i64 = values.iter().sum();
|
let total: i64 = values.iter().sum();
|
||||||
if total <= 0 {
|
if total <= 0 {
|
||||||
|
|
@ -245,7 +251,7 @@ fn slice_at(values: &[i64], x: f32, y: f32, size: f32) -> Option<usize> {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clockwise from 12 o'clock matching the drawing order
|
// Angle measured clockwise from 12 o'clock, matching the drawing order.
|
||||||
let mut angle = dy.atan2(dx).to_degrees() + 90.0;
|
let mut angle = dy.atan2(dx).to_degrees() + 90.0;
|
||||||
if angle < 0.0 {
|
if angle < 0.0 {
|
||||||
angle += 360.0;
|
angle += 360.0;
|
||||||
|
|
@ -269,7 +275,6 @@ fn slice_at(values: &[i64], x: f32, y: f32, size: f32) -> Option<usize> {
|
||||||
pub struct BarChart {
|
pub struct BarChart {
|
||||||
values: Vec<i64>,
|
values: Vec<i64>,
|
||||||
labels: Vec<String>,
|
labels: Vec<String>,
|
||||||
readout_labels: Option<Vec<String>>,
|
|
||||||
highlight: Option<usize>,
|
highlight: Option<usize>,
|
||||||
unit: ValueUnit,
|
unit: ValueUnit,
|
||||||
height: f32,
|
height: f32,
|
||||||
|
|
@ -281,7 +286,6 @@ impl BarChart {
|
||||||
Self {
|
Self {
|
||||||
values,
|
values,
|
||||||
labels,
|
labels,
|
||||||
readout_labels: None,
|
|
||||||
highlight: None,
|
highlight: None,
|
||||||
unit: ValueUnit::Duration,
|
unit: ValueUnit::Duration,
|
||||||
height: DEFAULT_HEIGHT,
|
height: DEFAULT_HEIGHT,
|
||||||
|
|
@ -289,11 +293,6 @@ impl BarChart {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn readout_labels(mut self, labels: Vec<String>) -> Self {
|
|
||||||
self.readout_labels = Some(labels);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn highlight(mut self, highlight: Option<usize>) -> Self {
|
pub fn highlight(mut self, highlight: Option<usize>) -> Self {
|
||||||
self.highlight = highlight;
|
self.highlight = highlight;
|
||||||
self
|
self
|
||||||
|
|
@ -316,9 +315,7 @@ impl BarChart {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn readout_name(&self, i: usize) -> String {
|
fn readout_name(&self, i: usize) -> String {
|
||||||
self.readout_labels
|
self.labels
|
||||||
.as_ref()
|
|
||||||
.unwrap_or(&self.labels)
|
|
||||||
.get(i)
|
.get(i)
|
||||||
.cloned()
|
.cloned()
|
||||||
.filter(|s| !s.is_empty())
|
.filter(|s| !s.is_empty())
|
||||||
|
|
@ -357,9 +354,13 @@ impl Component for BarChart {
|
||||||
for (i, &v) in self.values.iter().enumerate() {
|
for (i, &v) in self.values.iter().enumerate() {
|
||||||
let frac = if max > 0 { v as f32 / max as f32 } else { 0.0 };
|
let frac = if max > 0 { v as f32 / max as f32 } else { 0.0 };
|
||||||
let h = if v > 0 { (frac * height).max(4.0) } else { 0.0 };
|
let h = if v > 0 { (frac * height).max(4.0) } else { 0.0 };
|
||||||
let color = match active {
|
let is_active = Some(i) == focus;
|
||||||
Some(a) if a != i => colors::component_bg_hover(),
|
let color = if is_active {
|
||||||
_ => colors::brand(),
|
colors::brand()
|
||||||
|
} else if active.is_some() {
|
||||||
|
colors::component_bg_hover().with_a(150)
|
||||||
|
} else {
|
||||||
|
colors::component_bg_hover()
|
||||||
};
|
};
|
||||||
|
|
||||||
bars = bars.child(
|
bars = bars.child(
|
||||||
|
|
@ -367,13 +368,22 @@ impl Component for BarChart {
|
||||||
.width(Size::flex(1.0))
|
.width(Size::flex(1.0))
|
||||||
.height(Size::fill())
|
.height(Size::fill())
|
||||||
.main_align(Alignment::End)
|
.main_align(Alignment::End)
|
||||||
.cursor(CursorIcon::Pointer)
|
.on_pointer_enter(move |_| {
|
||||||
.on_pointer_enter(move |_| *hovered.write() = Some(i))
|
*hovered.write() = Some(i);
|
||||||
|
Cursor::set(CursorIcon::Pointer);
|
||||||
|
})
|
||||||
|
.on_pointer_leave(|_| Cursor::set(CursorIcon::default()))
|
||||||
.child(
|
.child(
|
||||||
rect()
|
rect()
|
||||||
.width(Size::fill())
|
.width(Size::fill())
|
||||||
.height(Size::px(h))
|
.height(Size::px(h))
|
||||||
.corner_radius(CornerRadius::new(4., 4., 0., 0.))
|
.corner_radius(CornerRadius {
|
||||||
|
top_left: 4.,
|
||||||
|
top_right: 4.,
|
||||||
|
bottom_right: 0.,
|
||||||
|
bottom_left: 0.,
|
||||||
|
smoothing: 0.,
|
||||||
|
})
|
||||||
.background(color),
|
.background(color),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -391,13 +401,14 @@ impl Component for BarChart {
|
||||||
label_row = label_row.child(
|
label_row = label_row.child(
|
||||||
rect()
|
rect()
|
||||||
.width(Size::flex(1.0))
|
.width(Size::flex(1.0))
|
||||||
.horizontal()
|
|
||||||
.main_align(Alignment::Center)
|
.main_align(Alignment::Center)
|
||||||
.child(
|
.child(
|
||||||
label()
|
label()
|
||||||
.text(if shown { text.clone() } else { String::new() })
|
.text(if shown { text.clone() } else { String::new() })
|
||||||
.font_size(10.)
|
.font_size(10.)
|
||||||
.max_lines(1)
|
.max_lines(1)
|
||||||
|
.width(Size::fill())
|
||||||
|
.text_align(TextAlign::Center)
|
||||||
.color(colors::fg_secondary()),
|
.color(colors::fg_secondary()),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -446,13 +457,15 @@ impl Component for BarChart {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Bars to skip between shown labels derived from measured plot width
|
/// How many bars to skip between shown labels so text never overlaps.
|
||||||
|
/// Derived from the measured plot width: a wider chart (e.g. maximised window)
|
||||||
|
/// fits more labels and returns a smaller stride.
|
||||||
fn label_stride(labels: &[String], plot_width: f32, gap: f32) -> usize {
|
fn label_stride(labels: &[String], plot_width: f32, gap: f32) -> usize {
|
||||||
let n = labels.len();
|
let n = labels.len();
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
// Before the first measurement thin conservatively to avoid an overlap flash
|
// Before the first measurement, thin conservatively to avoid an overlap flash.
|
||||||
if plot_width <= 0.0 {
|
if plot_width <= 0.0 {
|
||||||
return (n / 12).max(1);
|
return (n / 12).max(1);
|
||||||
}
|
}
|
||||||
|
|
@ -463,8 +476,8 @@ fn label_stride(labels: &[String], plot_width: f32, gap: f32) -> usize {
|
||||||
.max()
|
.max()
|
||||||
.unwrap_or(0)
|
.unwrap_or(0)
|
||||||
.max(1);
|
.max(1);
|
||||||
// ~6.2px per char at font_size 10
|
// ~6.2px per char at font_size 10, plus breathing room between labels.
|
||||||
let slot_px = max_chars as f32 * 6.2 + gap.max(8.0) + 12.0;
|
let slot_px = max_chars as f32 * 6.2 + gap.max(8.0) + 8.0;
|
||||||
let max_labels = (plot_width / slot_px).floor().max(1.0) as usize;
|
let max_labels = (plot_width / slot_px).floor().max(1.0) as usize;
|
||||||
|
|
||||||
if n <= max_labels {
|
if n <= max_labels {
|
||||||
|
|
@ -496,7 +509,7 @@ fn gridlines(height: f32) -> Element {
|
||||||
|
|
||||||
fn y_axis(max: i64, unit: ValueUnit, height: f32) -> Element {
|
fn y_axis(max: i64, unit: ValueUnit, height: f32) -> Element {
|
||||||
let fmt = |v: i64| match unit {
|
let fmt = |v: i64| match unit {
|
||||||
ValueUnit::Duration => format_duration(v),
|
ValueUnit::Duration => format_duration_hm(v),
|
||||||
ValueUnit::Count => v.to_string(),
|
ValueUnit::Count => v.to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,133 +0,0 @@
|
||||||
use freya::{
|
|
||||||
animation::{AnimColor, AnimNum, AnimatedValue, Ease, Function, use_animation_transition},
|
|
||||||
prelude::*,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
components::{Icon, IconType},
|
|
||||||
theme::colors,
|
|
||||||
ui,
|
|
||||||
};
|
|
||||||
|
|
||||||
const BOX_SIZE: f32 = 18.;
|
|
||||||
const MARK_SIZE: f32 = 12.;
|
|
||||||
const TIME: u64 = 180;
|
|
||||||
|
|
||||||
pub fn checkbox_labeled(value: State<bool>, text: impl Into<Box<str>>) -> impl IntoElement {
|
|
||||||
let mut v = value;
|
|
||||||
Check {
|
|
||||||
value,
|
|
||||||
label: Some(text.into()),
|
|
||||||
on_press: (move |()| v.toggle()).into(),
|
|
||||||
disabled: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
|
||||||
struct Check {
|
|
||||||
value: State<bool>,
|
|
||||||
label: Option<Box<str>>,
|
|
||||||
on_press: EventHandler<()>,
|
|
||||||
disabled: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component for Check {
|
|
||||||
fn render(&self) -> impl IntoElement {
|
|
||||||
let value = self.value;
|
|
||||||
let on_press = self.on_press.clone();
|
|
||||||
let checked = *value.read();
|
|
||||||
let disabled = self.disabled;
|
|
||||||
|
|
||||||
let a11y_id = use_a11y();
|
|
||||||
let focus = use_focus(a11y_id);
|
|
||||||
|
|
||||||
let box_border = if focus().is_focused() && !disabled {
|
|
||||||
colors::fg_primary()
|
|
||||||
} else if checked && !disabled {
|
|
||||||
colors::brand()
|
|
||||||
} else {
|
|
||||||
colors::component_border()
|
|
||||||
};
|
|
||||||
|
|
||||||
let background = use_animation_transition(value, |_, checked| {
|
|
||||||
if checked {
|
|
||||||
AnimColor::new(colors::component_bg(), colors::brand())
|
|
||||||
} else {
|
|
||||||
AnimColor::new(colors::brand(), colors::component_bg())
|
|
||||||
}
|
|
||||||
.time(TIME)
|
|
||||||
.function(Function::Expo)
|
|
||||||
.ease(Ease::Out)
|
|
||||||
});
|
|
||||||
|
|
||||||
let mark = use_animation_transition(value, |_, checked| {
|
|
||||||
let scale = AnimNum::new(0.5, 1.)
|
|
||||||
.time(TIME)
|
|
||||||
.function(Function::Expo)
|
|
||||||
.ease(Ease::Out);
|
|
||||||
let opacity = AnimNum::new(0., 1.)
|
|
||||||
.time(TIME)
|
|
||||||
.function(Function::Expo)
|
|
||||||
.ease(Ease::Out);
|
|
||||||
|
|
||||||
if checked {
|
|
||||||
(scale, opacity)
|
|
||||||
} else {
|
|
||||||
(scale.into_reversed(), opacity.into_reversed())
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let (mark_scale, mark_opacity) = {
|
|
||||||
let mark = mark.read();
|
|
||||||
(mark.0.value(), mark.1.value())
|
|
||||||
};
|
|
||||||
|
|
||||||
rect()
|
|
||||||
.horizontal()
|
|
||||||
.cross_align(Alignment::Center)
|
|
||||||
.spacing(8.)
|
|
||||||
.a11y_id(a11y_id)
|
|
||||||
.a11y_focusable(!disabled)
|
|
||||||
.a11y_role(AccessibilityRole::CheckBox)
|
|
||||||
.maybe(!disabled, |el| {
|
|
||||||
el.cursor(CursorIcon::Pointer).on_press(move |_| {
|
|
||||||
on_press.call(());
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
rect()
|
|
||||||
.width(Size::px(BOX_SIZE))
|
|
||||||
.height(Size::px(BOX_SIZE))
|
|
||||||
.corner_radius(CornerRadius::new_all(5.))
|
|
||||||
.main_align(Alignment::Center)
|
|
||||||
.cross_align(Alignment::Center)
|
|
||||||
.background(if disabled {
|
|
||||||
colors::component_bg_disabled()
|
|
||||||
} else {
|
|
||||||
background.read().value()
|
|
||||||
})
|
|
||||||
.border(ui::border_all_color(1., box_border))
|
|
||||||
.maybe_child((checked || mark_opacity > 0.).then(|| {
|
|
||||||
rect().opacity(mark_opacity).scale(mark_scale).child(
|
|
||||||
Icon::new(IconType::Check)
|
|
||||||
.size(MARK_SIZE)
|
|
||||||
.color(if disabled {
|
|
||||||
colors::fg_secondary()
|
|
||||||
} else {
|
|
||||||
Color::WHITE
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.maybe_child(self.label.as_ref().map(|text| {
|
|
||||||
label()
|
|
||||||
.text(text.to_string())
|
|
||||||
.font_size(12.)
|
|
||||||
.color(if disabled {
|
|
||||||
colors::fg_secondary()
|
|
||||||
} else {
|
|
||||||
colors::fg_primary()
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -4,9 +4,7 @@ use freya::{prelude::*, router::RouterContext};
|
||||||
use oneclient_content::packages::{CachedPackageMeta, ProviderId};
|
use oneclient_content::packages::{CachedPackageMeta, ProviderId};
|
||||||
|
|
||||||
use crate::components::{Button, Icon, IconType, OverlayPopup, ScrollArea, TabBar, TabItem};
|
use crate::components::{Button, Icon, IconType, OverlayPopup, ScrollArea, TabBar, TabItem};
|
||||||
use crate::hooks::{
|
use crate::hooks::{package_meta_batch, use_dispatch, use_notifications_snapshot, use_package_meta_batch};
|
||||||
package_meta_batch, use_dispatch, use_notifications_snapshot, use_package_meta_batch,
|
|
||||||
};
|
|
||||||
use crate::notifications::{ClusterUpdateItem, ClusterUpdateSummary};
|
use crate::notifications::{ClusterUpdateItem, ClusterUpdateSummary};
|
||||||
use crate::routes::Route;
|
use crate::routes::Route;
|
||||||
use crate::theme::colors;
|
use crate::theme::colors;
|
||||||
|
|
@ -23,32 +21,25 @@ enum UpdateTab {
|
||||||
Updates,
|
Updates,
|
||||||
Additions,
|
Additions,
|
||||||
Removals,
|
Removals,
|
||||||
Optional,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UpdateTab {
|
impl UpdateTab {
|
||||||
const ALL: [UpdateTab; 4] = [
|
const ALL: [UpdateTab; 3] = [UpdateTab::Updates, UpdateTab::Additions, UpdateTab::Removals];
|
||||||
UpdateTab::Updates,
|
|
||||||
UpdateTab::Additions,
|
|
||||||
UpdateTab::Removals,
|
|
||||||
UpdateTab::Optional,
|
|
||||||
];
|
|
||||||
|
|
||||||
fn label(self) -> &'static str {
|
fn label(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
UpdateTab::Updates => "Updated",
|
UpdateTab::Updates => "Updated",
|
||||||
UpdateTab::Additions => "Added",
|
UpdateTab::Additions => "Added",
|
||||||
UpdateTab::Removals => "Removed",
|
UpdateTab::Removals => "Removed",
|
||||||
UpdateTab::Optional => "Optional",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Empty-state copy; the tab labels don't read as sentences on their own.
|
||||||
fn empty_text(self) -> &'static str {
|
fn empty_text(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
UpdateTab::Updates => "Nothing was updated.",
|
UpdateTab::Updates => "Nothing was updated.",
|
||||||
UpdateTab::Additions => "Nothing was added.",
|
UpdateTab::Additions => "Nothing was added.",
|
||||||
UpdateTab::Removals => "Nothing was removed.",
|
UpdateTab::Removals => "Nothing was removed.",
|
||||||
UpdateTab::Optional => "Nothing optional was offered.",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,7 +48,6 @@ impl UpdateTab {
|
||||||
UpdateTab::Updates => IconType::RefreshCw01,
|
UpdateTab::Updates => IconType::RefreshCw01,
|
||||||
UpdateTab::Additions => IconType::Plus,
|
UpdateTab::Additions => IconType::Plus,
|
||||||
UpdateTab::Removals => IconType::Trash01,
|
UpdateTab::Removals => IconType::Trash01,
|
||||||
UpdateTab::Optional => IconType::Plus,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -66,7 +56,6 @@ impl UpdateTab {
|
||||||
UpdateTab::Updates => colors::brand(),
|
UpdateTab::Updates => colors::brand(),
|
||||||
UpdateTab::Additions => colors::success(),
|
UpdateTab::Additions => colors::success(),
|
||||||
UpdateTab::Removals => colors::danger(),
|
UpdateTab::Removals => colors::danger(),
|
||||||
UpdateTab::Optional => colors::brand(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -75,11 +64,11 @@ impl UpdateTab {
|
||||||
UpdateTab::Updates => &summary.updated,
|
UpdateTab::Updates => &summary.updated,
|
||||||
UpdateTab::Additions => &summary.added,
|
UpdateTab::Additions => &summary.added,
|
||||||
UpdateTab::Removals => &summary.removed,
|
UpdateTab::Removals => &summary.removed,
|
||||||
UpdateTab::Optional => &summary.optional,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One cluster's resolved package names for the active tab.
|
||||||
struct ClusterGroup {
|
struct ClusterGroup {
|
||||||
cluster_id: i64,
|
cluster_id: i64,
|
||||||
cluster_name: String,
|
cluster_name: String,
|
||||||
|
|
@ -97,24 +86,16 @@ impl Component for ClusterUpdatePopup {
|
||||||
|
|
||||||
let summaries = snapshot.cluster_update.clone();
|
let summaries = snapshot.cluster_update.clone();
|
||||||
|
|
||||||
// Hooks must run unconditionally so query every provider before the early return below
|
// Resolve pretty display names through the package-meta cache. Hooks
|
||||||
|
// must run unconditionally, so gather ids (empty when no summary) and
|
||||||
|
// query every remote provider before the early return below.
|
||||||
let all_items: Vec<&ClusterUpdateItem> = summaries
|
let all_items: Vec<&ClusterUpdateItem> = summaries
|
||||||
.iter()
|
.iter()
|
||||||
.flatten()
|
.flatten()
|
||||||
.flat_map(|s| {
|
.flat_map(|s| s.updated.iter().chain(&s.added).chain(&s.removed))
|
||||||
s.updated
|
|
||||||
.iter()
|
|
||||||
.chain(&s.added)
|
|
||||||
.chain(&s.removed)
|
|
||||||
.chain(&s.optional)
|
|
||||||
})
|
|
||||||
.collect();
|
.collect();
|
||||||
let mut meta = MetaMap::new();
|
let mut meta = MetaMap::new();
|
||||||
for provider in ProviderId::REMOTE_PROVIDERS
|
for provider in ProviderId::REMOTE_PROVIDERS.iter().copied() {
|
||||||
.iter()
|
|
||||||
.copied()
|
|
||||||
.chain([ProviderId::Local])
|
|
||||||
{
|
|
||||||
let ids: Vec<String> = all_items
|
let ids: Vec<String> = all_items
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|i| i.provider == provider)
|
.filter(|i| i.provider == provider)
|
||||||
|
|
@ -189,7 +170,8 @@ fn content(
|
||||||
let total: usize = summaries.iter().map(|s| s.total()).sum();
|
let total: usize = summaries.iter().map(|s| s.total()).sum();
|
||||||
let active_tab = *active.read();
|
let active_tab = *active.read();
|
||||||
|
|
||||||
// A batch sync can touch several clusters single-cluster keeps the footer shortcut
|
// A batch sync can touch several clusters; a single-cluster update keeps the
|
||||||
|
// plain wording and the "Open cluster" shortcut in the footer.
|
||||||
let single = match summaries {
|
let single = match summaries {
|
||||||
[only] => Some((only.cluster_id, only.cluster_name.clone())),
|
[only] => Some((only.cluster_id, only.cluster_name.clone())),
|
||||||
_ => None,
|
_ => None,
|
||||||
|
|
@ -207,7 +189,8 @@ fn content(
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Every category is shown even at zero so the modal shape stays stable
|
// Tab row: every category always shown with a count pill, even at zero, so
|
||||||
|
// the modal shape stays stable across clusters.
|
||||||
let tabs = TabBar::new()
|
let tabs = TabBar::new()
|
||||||
.width(Size::fill())
|
.width(Size::fill())
|
||||||
.height(Size::px(30.))
|
.height(Size::px(30.))
|
||||||
|
|
@ -220,7 +203,8 @@ fn content(
|
||||||
.on_press(move |_| *set.write() = tab)
|
.on_press(move |_| *set.write() = tab)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Clusters empty in the active category drop out avoiding empty section headers
|
// Clusters with nothing in the active category drop out, so the list never
|
||||||
|
// shows an empty section header.
|
||||||
let groups: Vec<ClusterGroup> = summaries
|
let groups: Vec<ClusterGroup> = summaries
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|summary| {
|
.filter_map(|summary| {
|
||||||
|
|
@ -360,8 +344,14 @@ fn change_list(
|
||||||
scroll.into_element()
|
scroll.into_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Multi-cluster stand-in for the footer's "Open cluster" shortcut
|
/// Section header naming the cluster a run of changes belongs to. Pressing it
|
||||||
fn cluster_header(group: &ClusterGroup, first: bool, dispatch: crate::Actions) -> impl IntoElement {
|
/// jumps straight to that cluster, which is the multi-cluster stand-in for the
|
||||||
|
/// footer's "Open cluster" shortcut.
|
||||||
|
fn cluster_header(
|
||||||
|
group: &ClusterGroup,
|
||||||
|
first: bool,
|
||||||
|
dispatch: crate::Actions,
|
||||||
|
) -> impl IntoElement {
|
||||||
let cluster_id = group.cluster_id;
|
let cluster_id = group.cluster_id;
|
||||||
let count = group.names.len();
|
let count = group.names.len();
|
||||||
|
|
||||||
|
|
@ -375,7 +365,8 @@ fn cluster_header(group: &ClusterGroup, first: bool, dispatch: crate::Actions) -
|
||||||
.margin(Gaps::new(if first { 0. } else { 8. }, 0., 3., 0.))
|
.margin(Gaps::new(if first { 0. } else { 8. }, 0., 3., 0.))
|
||||||
.corner_radius(CornerRadius::new_all(8.))
|
.corner_radius(CornerRadius::new_all(8.))
|
||||||
.background(colors::component_bg())
|
.background(colors::component_bg())
|
||||||
.cursor(CursorIcon::Pointer)
|
.on_pointer_enter(|_| Cursor::set(CursorIcon::Pointer))
|
||||||
|
.on_pointer_leave(|_| Cursor::set(CursorIcon::default()))
|
||||||
.on_press(move |_| open_cluster(&dispatch, cluster_id))
|
.on_press(move |_| open_cluster(&dispatch, cluster_id))
|
||||||
.child(
|
.child(
|
||||||
label()
|
label()
|
||||||
|
|
|
||||||